diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index 757c4b1d353..3d560b4f630 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -264,6 +264,13 @@ jobs: #if: ${{ always() }} run: moodle-plugin-ci phpunit + - name: JS unit tests + if: ${{matrix.moodle-branch == 'MOODLE_502_STABLE'}} + run: | + cd plugin/tests/jest + npm install + npm test + - name: Behat features if: ${{ always() }} run: moodle-plugin-ci behat --profile chrome --auto-rerun 12 diff --git a/.gitignore b/.gitignore index 61b44de1563..586d202ee35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ site/* +tests/jest/node_modules/* # Ignore macOS .DS_Store files -.DS_Store +.DS_Store # Ignore eclipse project files .project diff --git a/amd/build/metadata/container.min.js b/amd/build/metadata/container.min.js new file mode 100644 index 00000000000..c4ec87207f6 --- /dev/null +++ b/amd/build/metadata/container.min.js @@ -0,0 +1,11 @@ +define("qtype_stack/metadata/container",["exports","core/reactive","qtype_stack/metadata/metadata","core_form/events"],(function(_exports,_reactive,_metadata,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-container",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",UPDATEJSON:"#stack-metadata-update",UPDATEINPUTS:"#stack-metadata-update-inputs",ADDITEM:'[name="smd_add"]',DELETEITEM:'[name="smd_delete"]',MAKECONTRIBUTOR:"#stack-metadata-make-contributor",MAKECREATOR:"#stack-metadata-make-creator",REVERT:"#stack-metadata-revert",FORMJSON:'input[name="metadata"]',JSONINPUT:"#id_metadata_json",REQUIREDINPUTS:'#qtype-stack-metadata-content input[aria-required="true"]',ALLINPUTS:'#qtype-stack-metadata-content [id^="smdi"]'},_metadata.metadata.container=this}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}async stateReady(state){await this.reloadContainerComponent({state:state})}getWatchers(){return[{watch:"state:updated",handler:this.reloadContainerComponent}]}createDataElement(required,id,tag,value){return{required:required,element:{value:value,wrapperid:"fitem_smdi_"+id+"_"+tag,id:"smdi_"+id+"_"+tag,name:"smdi_"+id+"_"+tag,iderror:"smde_"+id+"_"+tag+"_error"}}}async reloadContainerComponent(_ref){let{state:state}=_ref;const data={creator:{},contributor:[],language:[],license:this.createDataElement(!0,0,"license_value",state.license.value),isPartOf:this.createDataElement(!1,0,"isPartOf_value",state.isPartOf.value),scope:[],freeform:this.createDataElement(!1,0,"freeform_value",state.freeform.value||"{}")};data.license.element.options=JSON.parse(JSON.stringify(_metadata.metadata.lib.licenses));const selectedLicense=state.license.value;let selectedOption=data.license.element.options.find((op=>op.value===selectedLicense));selectedOption?selectedOption.selected=!0:data.license.element.options.push({value:state.license.value,text:state.license.value,selected:!0}),data.license.element.tags="[]",data.license.element.ajax="",data.license.element.placeholder=_metadata.metadata.lib.placeholder,data.license.element.noselectionstring="",data.license.element.showsuggestions="true",data.license.element.casesensitive="false",state.language.forEach((language=>{const element={id:language.id,lang:this.createDataElement(!0,language.id,"language_value",language.value)};data.language.push({...element})})),state.contributor.forEach((contributor=>{const element={firstname:this.createDataElement(!1,contributor.id,"contributor_firstName",contributor.firstName),lastname:this.createDataElement(!1,contributor.id,"contributor_lastName",contributor.lastName),institution:this.createDataElement(!1,contributor.id,"contributor_institution",contributor.institution),year:this.createDataElement(!1,contributor.id,"contributor_year",contributor.year),id:contributor.id};data.contributor.push({...element})}));const scopeHolder={};state.additional.forEach((additional=>{const element={property:this.createDataElement(!0,additional.id,"additional_property",additional.property),qualifier:this.createDataElement(!1,additional.id,"additional_qualifier",additional.qualifier),value:this.createDataElement(!1,additional.id,"additional_value",additional.value),id:additional.id};scopeHolder[additional.scope]||(scopeHolder[additional.scope]=[]),scopeHolder[additional.scope].push(element)}));for(const scope in scopeHolder){const current={name:scope,firstProp:scopeHolder[scope][0].id,properties:scopeHolder[scope],input:this.createDataElement(!0,scopeHolder[scope][0].id,"additional_scope",scope)};data.scope.push(current)}data.creator={firstname:this.createDataElement(!1,0,"creator_firstName",state.creator.firstName),lastname:this.createDataElement(!1,0,"creator_lastName",state.creator.lastName),institution:this.createDataElement(!1,0,"creator_institution",state.creator.institution),year:this.createDataElement(!1,0,"creator_year",state.creator.year)},data.json={required:!0,element:{value:_metadata.metadata.jsonStringify(state,4),attributes:'rows="10"',wrapperid:"fitem_metadata_json",id:"id_metadata_json",name:"metadata_json"}};const metadataContainer=this.getElement(this.selectors.METADATACONTAINER);if(!metadataContainer)throw new Error("Missing metadata container.");await this.renderComponent(metadataContainer,"qtype_stack/metadata/metadatacontent",data),this.addEventListener(this.getElement(this.selectors.UPDATEJSON),"click",this.update);const addButtons=this.getElements(this.selectors.ADDITEM);for(const addButton of addButtons)this.addEventListener(addButton,"click",this.addItem);const deleteButtons=this.getElements(this.selectors.DELETEITEM);for(const deleteButton of deleteButtons)this.addEventListener(deleteButton,"click",this.deleteItem);if(this.addEventListener(this.getElement(this.selectors.UPDATEINPUTS),"click",this.updateInputs),this.addEventListener(this.getElement(this.selectors.MAKECREATOR),"click",this.makeCreator),this.addEventListener(this.getElement(this.selectors.MAKECONTRIBUTOR),"click",this.makeContributor),this.addEventListener(this.getElement(this.selectors.REVERT),"click",this.revert),_metadata.metadata.lib.brokenMetadata){var _document$querySelect;const jsonElement=this.getElement(this.selectors.JSONINPUT);jsonElement.value=null!==(_document$querySelect=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect?_document$querySelect:"",(0,_events.notifyFieldValidationFailure)(jsonElement,_metadata.metadata.lib.brokenMetadata),delete _metadata.metadata.lib.brokenMetadata}}async update(){if(!(arguments.length>0&&void 0!==arguments[0])||arguments[0]){const requiredElements=this.getElements(this.selectors.REQUIREDINPUTS);let isError=!1;for(const element of requiredElements)""===element.value?(isError=!0,(0,_events.notifyFieldValidationFailure)(element,"Required")):element.classList.contains("is-invalid")&&(0,_events.notifyFieldValidationFailure)(element,"");const freeformElement=this.getElement("#smdi_0_freeform_value");if(freeformElement&&""!==freeformElement.value.trim())try{JSON.parse(freeformElement.value),(0,_events.notifyFieldValidationFailure)(freeformElement,"")}catch(e){(0,_events.notifyFieldValidationFailure)(freeformElement,e.message),isError=!0}if(isError)return!1}let inputElements=this.getElements(this.selectors.ALLINPUTS);inputElements=Array.from(inputElements).map((el=>[el.id,el.value]));try{await this.reactive.dispatch("updateAll",inputElements)}catch(e){const addIds=e.split(",");for(const id of addIds){const element=this.getElement('#qtype-stack-metadata-content [id="smdi_'+id+'_additional_qualifier"]');(0,_events.notifyFieldValidationFailure)(element,"Required")}return!1}return!0}async addItem(event){if(await this.update(!0)){const parts=event.target.id.split("_");this.reactive.dispatch("addItem",parts[1],parts[2])}}async deleteItem(event){if(await this.update(!1)){const parts=event.target.id.split("_");this.reactive.dispatch("deleteRow",parts[1],parts[2])}}updateInputs(){const jsonElement=this.getElement(this.selectors.JSONINPUT);let data=null;try{data=_metadata.metadata.jsonToState(jsonElement.value),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return void(0,_events.notifyFieldValidationFailure)(jsonElement,e.message)}jsonElement.value=_metadata.metadata.jsonStringify(data,4),this.reactive.dispatch("updateFromJson",data)}async makeContributor(){await this.update(!1)&&this.reactive.dispatch("addItem","contributor","user")}makeCreator(){this.getElement("#smdi_0_creator_firstName").value=_metadata.metadata.lib.user.firstname,this.getElement("#smdi_0_creator_lastName").value=_metadata.metadata.lib.user.lastname,this.getElement("#smdi_0_creator_institution").value=_metadata.metadata.lib.user.institution,this.getElement("#smdi_0_creator_year").value=(new Date).getFullYear()}revert(){var _document$querySelect2;const jsonElement=this.getElement(this.selectors.JSONINPUT);let previousdataJSON=null!==(_document$querySelect2=document.querySelector(this.selectors.FORMJSON).value)&&void 0!==_document$querySelect2?_document$querySelect2:null,previousdata=null;try{previousdata=_metadata.metadata.jsonToState(previousdataJSON),(0,_events.notifyFieldValidationFailure)(jsonElement,"")}catch(e){return(0,_events.notifyFieldValidationFailure)(jsonElement,e.message),jsonElement.value=previousdataJSON,_metadata.metadata.lib.brokenMetadata=e.message,void this.reactive.dispatch("updateFromJson",_metadata.metadata.jsonToState("{}"))}jsonElement.value=_metadata.metadata.jsonStringify(previousdata,4),this.reactive.dispatch("updateFromJson",previousdata)}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=container.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/container.min.js.map b/amd/build/metadata/container.min.js.map new file mode 100644 index 00000000000..34716fce986 --- /dev/null +++ b/amd/build/metadata/container.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"container.min.js","sources":["../../src/metadata/container.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport {notifyFieldValidationFailure} from 'core_form/events';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-container';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n UPDATEJSON: `#stack-metadata-update`,\n UPDATEINPUTS: `#stack-metadata-update-inputs`,\n ADDITEM: `[name=\"smd_add\"]`,\n DELETEITEM: `[name=\"smd_delete\"]`,\n MAKECONTRIBUTOR: `#stack-metadata-make-contributor`,\n MAKECREATOR: `#stack-metadata-make-creator`,\n REVERT: `#stack-metadata-revert`,\n FORMJSON: 'input[name=\"metadata\"]',\n JSONINPUT: '#id_metadata_json',\n REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required=\"true\"]',\n ALLINPUTS: '#qtype-stack-metadata-content [id^=\"smdi\"]',\n };\n metadata.container = this;\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n\n /**\n * Initial state ready method.\n *\n * @param {object} state the initial state\n */\n async stateReady(state) {\n await this.reloadContainerComponent({state});\n }\n\n /**\n * Set to refresh display on any state change.\n *\n * @returns {object} watchers\n */\n getWatchers() {\n return [\n {watch: `state:updated`, handler: this.reloadContainerComponent},\n ];\n }\n\n /**\n * Converts field information into element suitable for feeding into Mustache templates.\n *\n * @param {bool} required\n * @param {mixed} id to link to state\n * @param {string} tag type of field\n * @param {mixed} value of element\n * @returns {object}\n */\n createDataElement(required, id, tag, value) {\n return {\n required: required,\n element: {\n value: value,\n wrapperid: 'fitem_smdi_' + id + '_' + tag,\n id: 'smdi_' + id + '_' + tag,\n name: 'smdi_' + id + '_' + tag,\n iderror: 'smde_' + id + '_' + tag + '_error'\n }\n };\n }\n\n async reloadContainerComponent({state}) {\n // Mustache data is not fully compatible with state object so we need to convert it\n // into a plain object.\n const data = {\n creator: {},\n contributor: [],\n language: [],\n license: this.createDataElement(true, 0, 'license_value', state.license.value),\n isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value),\n scope: [],\n freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'),\n };\n\n // Need to copy licenses list as we modify to mark as selected.\n data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses));\n const selectedLicense = state.license.value;\n let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense);\n if (selectedOption) {\n selectedOption.selected = true;\n } else {\n data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true});\n }\n data.license.element.tags = '[]';\n data.license.element.ajax = '';\n data.license.element.placeholder = metadata.lib.placeholder;\n data.license.element.noselectionstring = '';\n data.license.element.showsuggestions = 'true';\n data.license.element.casesensitive = 'false';\n\n state.language.forEach(language => {\n const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) };\n data.language.push({...element});\n });\n\n state.contributor.forEach(contributor => {\n const element = {\n firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName),\n lastname: this.createDataElement(false, contributor.id, 'contributor_lastName', contributor.lastName),\n institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution),\n year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year),\n id: contributor.id,\n };\n data.contributor.push({...element});\n });\n\n const scopeHolder = {};\n // Rearrange additional metadata by scope.\n state.additional.forEach(additional => {\n const element = {\n property: this.createDataElement(true, additional.id, 'additional_property', additional.property),\n qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier),\n value: this.createDataElement(false, additional.id, 'additional_value', additional.value),\n id: additional.id,\n };\n if (!scopeHolder[additional.scope]) {\n scopeHolder[additional.scope] = [];\n }\n scopeHolder[additional.scope].push(element);\n });\n for (const scope in scopeHolder) {\n const current = {\n name: scope,\n firstProp: scopeHolder[scope][0].id,\n properties: scopeHolder[scope],\n input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope)\n };\n data.scope.push(current);\n }\n\n data.creator = {\n firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName),\n lastname: this.createDataElement(false, 0, 'creator_lastName', state.creator.lastName),\n institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution),\n year: this.createDataElement(false, 0, 'creator_year', state.creator.year),\n };\n\n data.json = {\n required: true,\n element: {\n value: metadata.jsonStringify(state, 4),\n attributes: 'rows=\"10\"',\n wrapperid: 'fitem_metadata_json',\n id: 'id_metadata_json',\n name: 'metadata_json',\n }\n };\n\n // To render a child component we need a container.\n const metadataContainer = this.getElement(this.selectors.METADATACONTAINER);\n if (!metadataContainer) {\n throw new Error('Missing metadata container.');\n }\n\n await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data);\n\n // Add all the event listeners as all elements have been destroyed and rebuilt.\n this.addEventListener(\n this.getElement(this.selectors.UPDATEJSON),\n 'click',\n this.update\n );\n const addButtons = this.getElements(this.selectors.ADDITEM);\n for (const addButton of addButtons) {\n this.addEventListener(\n addButton,\n 'click',\n this.addItem\n );\n }\n const deleteButtons = this.getElements(this.selectors.DELETEITEM);\n for (const deleteButton of deleteButtons) {\n this.addEventListener(\n deleteButton,\n 'click',\n this.deleteItem\n );\n }\n this.addEventListener(\n this.getElement(this.selectors.UPDATEINPUTS),\n 'click',\n this.updateInputs\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKECREATOR),\n 'click',\n this.makeCreator\n );\n this.addEventListener(\n this.getElement(this.selectors.MAKECONTRIBUTOR),\n 'click',\n this.makeContributor\n );\n this.addEventListener(\n this.getElement(this.selectors.REVERT),\n 'click',\n this.revert\n );\n\n // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup.\n // We load in the original un-prettified JSON and display error message, giving user chance to edit.\n // After this, though, they'll need to sort it out - if we're back here again then we'll use\n // JSON created from current content of state.\n if (metadata.lib.brokenMetadata) {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? '';\n notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata);\n delete metadata.lib.brokenMetadata;\n }\n }\n\n /**\n * Updates state based on contents of inputs.\n *\n * @param {bool} mustValidate Do we want validation to occur?\n * We check when explicitly asked for and when attempting to close the modal other than by cancel.\n * We don't check when e.g. adding a contributor. This means state can be invalid but we only\n * update the edit form entry after successful validation on modal close.\n * @returns {bool} Returns false on validation error.\n */\n async update(mustValidate = true) {\n if (mustValidate) {\n // TO-DO Do we need other validation and/or different required fields.\n const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS);\n let isError = false;\n for (const element of requiredElements) {\n if (element.value === '') {\n isError = true;\n notifyFieldValidationFailure(element, 'Required');\n } else if (element.classList.contains('is-invalid')) {\n // Reset warning as field no longer empty.\n notifyFieldValidationFailure(element, '');\n }\n }\n // Validate freeform JSON if non-empty.\n const freeformElement = this.getElement('#smdi_0_freeform_value');\n if (freeformElement && freeformElement.value.trim() !== '') {\n try {\n JSON.parse(freeformElement.value);\n notifyFieldValidationFailure(freeformElement, '');\n } catch(e) {\n notifyFieldValidationFailure(freeformElement, e.message);\n isError = true;\n }\n }\n if (isError) {\n return false;\n }\n }\n // Elements have ids in form smdi_id_category_field e.g. smdi_1_contributor_year.\n // id is category entry id in state. 0 is used for single elements e.g. license.\n // Multi-elements begin counting from 1.\n let inputElements = this.getElements(this.selectors.ALLINPUTS);\n inputElements = Array.from(inputElements).map((el) => [el.id, el.value]);\n try {\n await this.reactive.dispatch('updateAll', inputElements);\n } catch (e) {\n const addIds = e.split(',');\n for (const id of addIds) {\n const element = this.getElement('#qtype-stack-metadata-content [id=\"smdi_' + id + '_additional_qualifier\"]');\n notifyFieldValidationFailure(element, 'Required');\n }\n return false;\n }\n return true;\n }\n\n /**\n * Add a new row to modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display to show the new row.\n *\n * @param {*} event\n */\n async addItem(event) {\n const result = await this.update(true);\n if (result) {\n const parts = event.target.id.split('_');\n this.reactive.dispatch('addItem', parts[1], parts[2]);\n }\n }\n /**\n * Delete a row from modal form.\n * We have to update state from the input fields first or any changes will\n * be wiped when we refresh the display toremove the row\n *\n * @param {*} event\n */\n async deleteItem(event) {\n const result = await this.update(false);\n if (result) {\n const parts = event.target.id.split('_');\n this.reactive.dispatch('deleteRow', parts[1], parts[2]);\n }\n }\n\n /**\n * Update state from the currently entered JSON if JSON is valid.\n */\n updateInputs() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let data = null;\n try {\n data = metadata.jsonToState(jsonElement.value);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n return;\n }\n jsonElement.value = metadata.jsonStringify(data, 4);\n this.reactive.dispatch('updateFromJson', data);\n }\n\n /**\n * Add the current user as a contributor.\n */\n async makeContributor() {\n const result = await this.update(false);\n if (result) {\n this.reactive.dispatch('addItem', 'contributor', 'user');\n }\n }\n\n /**\n * Make current user the creator.\n */\n makeCreator() {\n this.getElement('#smdi_0_creator_firstName').value = metadata.lib.user.firstname;\n this.getElement('#smdi_0_creator_lastName').value = metadata.lib.user.lastname;\n this.getElement('#smdi_0_creator_institution').value = metadata.lib.user.institution;\n this.getElement('#smdi_0_creator_year').value = new Date().getFullYear();\n }\n\n /**\n * Return JSON to the current version on the edit form. This will be either the saved\n * version from the question or the update from a previous close and validate of the metadata modal.\n * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure\n * in metadata.js.\n */\n revert() {\n const jsonElement = this.getElement(this.selectors.JSONINPUT);\n let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null;\n let previousdata = null;\n try {\n previousdata = metadata.jsonToState(previousdataJSON);\n notifyFieldValidationFailure(jsonElement, '');\n } catch (e) {\n notifyFieldValidationFailure(jsonElement, e.message);\n jsonElement.value = previousdataJSON;\n metadata.lib.brokenMetadata = e.message;\n this.reactive.dispatch('updateFromJson', metadata.jsonToState('{}'));\n return;\n }\n jsonElement.value = metadata.jsonStringify(previousdata, 4);\n this.reactive.dispatch('updateFromJson', previousdata);\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","UPDATEJSON","UPDATEINPUTS","ADDITEM","DELETEITEM","MAKECONTRIBUTOR","MAKECREATOR","REVERT","FORMJSON","JSONINPUT","REQUIREDINPUTS","ALLINPUTS","container","this","target","element","document","querySelector","reactive","metadata","state","reloadContainerComponent","getWatchers","watch","handler","createDataElement","required","id","tag","value","wrapperid","iderror","data","creator","contributor","language","license","isPartOf","scope","freeform","options","JSON","parse","stringify","lib","licenses","selectedLicense","selectedOption","find","op","selected","push","text","tags","ajax","placeholder","noselectionstring","showsuggestions","casesensitive","forEach","lang","firstname","firstName","lastname","lastName","institution","year","scopeHolder","additional","property","qualifier","current","firstProp","properties","input","json","jsonStringify","attributes","metadataContainer","getElement","Error","renderComponent","addEventListener","update","addButtons","getElements","addButton","addItem","deleteButtons","deleteButton","deleteItem","updateInputs","makeCreator","makeContributor","revert","brokenMetadata","jsonElement","requiredElements","isError","classList","contains","freeformElement","trim","e","message","inputElements","Array","from","map","el","dispatch","addIds","split","event","parts","jsonToState","user","Date","getFullYear","previousdataJSON","previousdata"],"mappings":";;;;;;;;uBA2B6BA,wBACzBC,cACSC,KAAO,gCACPC,UAAY,CACbC,sDACAC,oCACAC,6CACAC,2BACAC,iCACAC,mDACAC,2CACAC,gCACAC,SAAU,yBACVC,UAAW,oBACXC,eAAgB,4DAChBC,UAAW,iEAENC,UAAYC,iBAUbC,OAAQf,kBACT,IAAIc,KAAK,CACZE,QAASC,SAASC,cAAcH,QAChCI,SAAUC,mBACVpB,UAAAA,6BASSqB,aACPP,KAAKQ,yBAAyB,CAACD,MAAAA,QAQzCE,oBACW,CACH,CAACC,sBAAwBC,QAASX,KAAKQ,2BAa/CI,kBAAkBC,SAAUC,GAAIC,IAAKC,aAC1B,CACHH,SAAUA,SACVX,QAAS,CACLc,MAAOA,MACPC,UAAW,cAAgBH,GAAK,IAAMC,IACtCD,GAAI,QAAUA,GAAK,IAAMC,IACzB9B,KAAM,QAAU6B,GAAK,IAAMC,IAC3BG,QAAS,QAAUJ,GAAK,IAAMC,IAAM,oDAKjBR,MAACA,kBAGtBY,KAAO,CACTC,QAAS,GACTC,YAAa,GACbC,SAAU,GACVC,QAASvB,KAAKY,mBAAkB,EAAM,EAAG,gBAAiBL,MAAMgB,QAAQP,OACxEQ,SAAUxB,KAAKY,mBAAkB,EAAO,EAAG,iBAAkBL,MAAMiB,SAASR,OAC5ES,MAAO,GACPC,SAAU1B,KAAKY,mBAAkB,EAAO,EAAG,iBAAkBL,MAAMmB,SAASV,OAAS,OAIzFG,KAAKI,QAAQrB,QAAQyB,QAAUC,KAAKC,MAAMD,KAAKE,UAAUxB,mBAASyB,IAAIC,iBAChEC,gBAAkB1B,MAAMgB,QAAQP,UAClCkB,eAAiBf,KAAKI,QAAQrB,QAAQyB,QAAQQ,MAAMC,IAAOA,GAAGpB,QAAUiB,kBACxEC,eACAA,eAAeG,UAAW,EAE1BlB,KAAKI,QAAQrB,QAAQyB,QAAQW,KAAK,CAACtB,MAAOT,MAAMgB,QAAQP,MAAOuB,KAAMhC,MAAMgB,QAAQP,MAAOqB,UAAU,IAExGlB,KAAKI,QAAQrB,QAAQsC,KAAO,KAC5BrB,KAAKI,QAAQrB,QAAQuC,KAAO,GAC5BtB,KAAKI,QAAQrB,QAAQwC,YAAcpC,mBAASyB,IAAIW,YAChDvB,KAAKI,QAAQrB,QAAQyC,kBAAoB,GACzCxB,KAAKI,QAAQrB,QAAQ0C,gBAAkB,OACvCzB,KAAKI,QAAQrB,QAAQ2C,cAAgB,QAErCtC,MAAMe,SAASwB,SAAQxB,iBACbpB,QAAU,CAAEY,GAAIQ,SAASR,GAAIiC,KAAM/C,KAAKY,mBAAkB,EAAMU,SAASR,GAAI,iBAAkBQ,SAASN,QAC9GG,KAAKG,SAASgB,KAAK,IAAIpC,aAG3BK,MAAMc,YAAYyB,SAAQzB,oBACfnB,QAAU,CACb8C,UAAWhD,KAAKY,mBAAkB,EAAOS,YAAYP,GAAI,wBAAyBO,YAAY4B,WAC9FC,SAAUlD,KAAKY,mBAAkB,EAAOS,YAAYP,GAAI,uBAAwBO,YAAY8B,UAC5FC,YAAapD,KAAKY,mBAAkB,EAAOS,YAAYP,GAAI,0BAA2BO,YAAY+B,aAClGC,KAAMrD,KAAKY,mBAAkB,EAAOS,YAAYP,GAAI,mBAAoBO,YAAYgC,MACpFvC,GAAIO,YAAYP,IAEpBK,KAAKE,YAAYiB,KAAK,IAAIpC,mBAGxBoD,YAAc,GAEpB/C,MAAMgD,WAAWT,SAAQS,mBACfrD,QAAU,CACZsD,SAAUxD,KAAKY,mBAAkB,EAAM2C,WAAWzC,GAAI,sBAAuByC,WAAWC,UACxFC,UAAWzD,KAAKY,mBAAkB,EAAO2C,WAAWzC,GAAI,uBAAwByC,WAAWE,WAC3FzC,MAAOhB,KAAKY,mBAAkB,EAAO2C,WAAWzC,GAAI,mBAAoByC,WAAWvC,OACnFF,GAAIyC,WAAWzC,IAEdwC,YAAYC,WAAW9B,SACxB6B,YAAYC,WAAW9B,OAAS,IAEpC6B,YAAYC,WAAW9B,OAAOa,KAAKpC,gBAElC,MAAMuB,SAAS6B,YAAa,OACvBI,QAAU,CACZzE,KAAMwC,MACNkC,UAAWL,YAAY7B,OAAO,GAAGX,GACjC8C,WAAYN,YAAY7B,OACxBoC,MAAO7D,KAAKY,mBAAkB,EAAM0C,YAAY7B,OAAO,GAAGX,GAAI,mBAAoBW,QAEtFN,KAAKM,MAAMa,KAAKoB,SAGpBvC,KAAKC,QAAU,CACX4B,UAAWhD,KAAKY,mBAAkB,EAAO,EAAG,oBAAqBL,MAAMa,QAAQ6B,WAC/EC,SAAUlD,KAAKY,mBAAkB,EAAO,EAAG,mBAAoBL,MAAMa,QAAQ+B,UAC7EC,YAAapD,KAAKY,mBAAkB,EAAO,EAAG,sBAAuBL,MAAMa,QAAQgC,aACnFC,KAAMrD,KAAKY,mBAAkB,EAAO,EAAG,eAAgBL,MAAMa,QAAQiC,OAGzElC,KAAK2C,KAAO,CACRjD,UAAU,EACVX,QAAS,CACLc,MAAOV,mBAASyD,cAAcxD,MAAO,GACrCyD,WAAY,YACZ/C,UAAW,sBACXH,GAAI,mBACJ7B,KAAM,wBAKRgF,kBAAoBjE,KAAKkE,WAAWlE,KAAKd,UAAUC,uBACpD8E,wBACK,IAAIE,MAAM,qCAGdnE,KAAKoE,gBAAgBH,kBAAmB,uCAAwC9C,WAGjFkD,iBACDrE,KAAKkE,WAAWlE,KAAKd,UAAUE,YAC/B,QACAY,KAAKsE,cAEHC,WAAavE,KAAKwE,YAAYxE,KAAKd,UAAUI,aAC9C,MAAMmF,aAAaF,gBACfF,iBACDI,UACA,QACAzE,KAAK0E,eAGPC,cAAgB3E,KAAKwE,YAAYxE,KAAKd,UAAUK,gBACjD,MAAMqF,gBAAgBD,mBAClBN,iBACDO,aACA,QACA5E,KAAK6E,oBAGRR,iBACDrE,KAAKkE,WAAWlE,KAAKd,UAAUG,cAC/B,QACAW,KAAK8E,mBAEJT,iBACDrE,KAAKkE,WAAWlE,KAAKd,UAAUO,aAC/B,QACAO,KAAK+E,kBAEJV,iBACDrE,KAAKkE,WAAWlE,KAAKd,UAAUM,iBAC/B,QACAQ,KAAKgF,sBAEJX,iBACDrE,KAAKkE,WAAWlE,KAAKd,UAAUQ,QAC/B,QACAM,KAAKiF,QAOL3E,mBAASyB,IAAImD,eAAgB,iCACvBC,YAAcnF,KAAKkE,WAAWlE,KAAKd,UAAUU,WACnDuF,YAAYnE,oCAAQb,SAASC,cAAcJ,KAAKd,UAAUS,UAAUqB,6DAAS,4CAChDmE,YAAa7E,mBAASyB,IAAImD,uBAChD5E,mBAASyB,IAAImD,6FAcN,OAERE,iBAAmBpF,KAAKwE,YAAYxE,KAAKd,UAAUW,oBACrDwF,SAAU,MACT,MAAMnF,WAAWkF,iBACI,KAAlBlF,QAAQc,OACRqE,SAAU,2CACmBnF,QAAS,aAC/BA,QAAQoF,UAAUC,SAAS,wDAELrF,QAAS,UAIxCsF,gBAAkBxF,KAAKkE,WAAW,6BACpCsB,iBAAoD,KAAjCA,gBAAgBxE,MAAMyE,WAErC7D,KAAKC,MAAM2D,gBAAgBxE,gDACEwE,gBAAiB,IAChD,MAAME,4CACyBF,gBAAiBE,EAAEC,SAChDN,SAAU,KAGdA,eACO,MAMXO,cAAgB5F,KAAKwE,YAAYxE,KAAKd,UAAUY,WACpD8F,cAAgBC,MAAMC,KAAKF,eAAeG,KAAKC,IAAO,CAACA,GAAGlF,GAAIkF,GAAGhF,mBAEvDhB,KAAKK,SAAS4F,SAAS,YAAaL,eAC5C,MAAOF,SACCQ,OAASR,EAAES,MAAM,SAClB,MAAMrF,MAAMoF,OAAQ,OACfhG,QAAUF,KAAKkE,WAAW,2CAA6CpD,GAAK,oEACrDZ,QAAS,mBAEnC,SAEJ,gBAUGkG,gBACWpG,KAAKsE,QAAO,GACrB,OACF+B,MAAQD,MAAMnG,OAAOa,GAAGqF,MAAM,UAC/B9F,SAAS4F,SAAS,UAAWI,MAAM,GAAIA,MAAM,sBAUzCD,gBACQpG,KAAKsE,QAAO,GACrB,OACF+B,MAAQD,MAAMnG,OAAOa,GAAGqF,MAAM,UAC/B9F,SAAS4F,SAAS,YAAaI,MAAM,GAAIA,MAAM,KAO5DvB,qBACUK,YAAcnF,KAAKkE,WAAWlE,KAAKd,UAAUU,eAC/CuB,KAAO,SAEPA,KAAOb,mBAASgG,YAAYnB,YAAYnE,gDACXmE,YAAa,IAC5C,MAAOO,uDACwBP,YAAaO,EAAEC,SAGhDR,YAAYnE,MAAQV,mBAASyD,cAAc5C,KAAM,QAC5Cd,SAAS4F,SAAS,iBAAkB9E,oCAOpBnB,KAAKsE,QAAO,SAExBjE,SAAS4F,SAAS,UAAW,cAAe,QAOzDlB,mBACSb,WAAW,6BAA6BlD,MAAQV,mBAASyB,IAAIwE,KAAKvD,eAClEkB,WAAW,4BAA4BlD,MAAQV,mBAASyB,IAAIwE,KAAKrD,cACjEgB,WAAW,+BAA+BlD,MAAQV,mBAASyB,IAAIwE,KAAKnD,iBACpEc,WAAW,wBAAwBlD,OAAQ,IAAIwF,MAAOC,cAS/DxB,0CACUE,YAAcnF,KAAKkE,WAAWlE,KAAKd,UAAUU,eAC/C8G,gDAAmBvG,SAASC,cAAcJ,KAAKd,UAAUS,UAAUqB,+DAAS,KAC5E2F,aAAe,SAEfA,aAAerG,mBAASgG,YAAYI,2DACPvB,YAAa,IAC5C,MAAOO,kDACwBP,YAAaO,EAAEC,SAC5CR,YAAYnE,MAAQ0F,oCACX3E,IAAImD,eAAiBQ,EAAEC,kBAC3BtF,SAAS4F,SAAS,iBAAkB3F,mBAASgG,YAAY,OAGlEnB,YAAYnE,MAAQV,mBAASyD,cAAc4C,aAAc,QACpDtG,SAAS4F,SAAS,iBAAkBU"} \ No newline at end of file diff --git a/amd/build/metadata/events.min.js b/amd/build/metadata/events.min.js new file mode 100644 index 00000000000..8ec91e58ef1 --- /dev/null +++ b/amd/build/metadata/events.min.js @@ -0,0 +1,11 @@ +define("qtype_stack/metadata/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyQtypeStackStateUpdated=_exports.eventTypes=void 0; +/** + * Javascript events for STACK metadata. + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +const eventTypes={qtypeStackStateUpdated:"qtype_stack/stateUpdated"};_exports.eventTypes=eventTypes;_exports.notifyQtypeStackStateUpdated=(detail,container)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.qtypeStackStateUpdated,detail,container)})); + +//# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/events.min.js.map b/amd/build/metadata/events.min.js.map new file mode 100644 index 00000000000..16c72f7de9c --- /dev/null +++ b/amd/build/metadata/events.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"events.min.js","sources":["../../src/metadata/events.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Javascript events for STACK metadata.\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\n/**\n * Events for STACK metadata\n *\n * @constant\n * @property {String} qtypeStackStateUpdated See {@link event:qtypeStackStateUpdated}\n */\nexport const eventTypes = {\n /**\n * Event triggered when the activity reactive state is updated.\n *\n * @event qtypeStackStateUpdated\n * @type {CustomEvent}\n * @property {Array} nodes The list of parent nodes which were updated\n */\n qtypeStackStateUpdated: 'qtype_stack/stateUpdated',\n};\n\n/**\n * Trigger an event to indicate that the activity state is updated.\n *\n * @method qtypeStackStateUpdated\n * @param {object} detail the full state\n * @param {HTMLElement} container the custom event target (document if none provided)\n * @returns {CustomEvent}\n * @fires qtypeStackStateUpdated\n */\nexport const notifyQtypeStackStateUpdated = (detail, container) => {\n return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container);\n};"],"names":["eventTypes","qtypeStackStateUpdated","detail","container"],"mappings":";;;;;;;;MA+BaA,WAAa,CAQtBC,uBAAwB,iGAYgB,CAACC,OAAQC,aAC1C,mCAAcH,WAAWC,uBAAwBC,OAAQC"} \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js b/amd/build/metadata/metadata.min.js new file mode 100644 index 00000000000..bda54ab929b --- /dev/null +++ b/amd/build/metadata/metadata.min.js @@ -0,0 +1,3 @@ +define("qtype_stack/metadata/metadata",["exports","core/reactive","qtype_stack/metadata/mutations","qtype_stack/metadata/events"],(function(_exports,_reactive,_mutations,_events){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.metadata=void 0;class StackMetadata extends _reactive.Reactive{constructor(){var obj,key,value;super(...arguments),value={languages:["en"],user:{firstname:"",lastname:"",institution:"",year:""},licenses:[{value:"unknown",text:"unknown"}],placeholder:""},(key="lib")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}loadState(){var _metadata$value;let metadata=document.querySelector('input[name="metadata"]');const metadataJSON=null!==(_metadata$value=metadata.value)&&void 0!==_metadata$value?_metadata$value:null;try{this.lib=JSON.parse(metadata.dataset.lib),this.lib.user.year=(new Date).getFullYear();let languages=new Set(this.lib.languages);this.lib.languages=languages.difference(new Set([null,void 0,""])),this.lib.languages=Array.from(this.lib.languages)}catch(e){}try{metadata=this.jsonToState(metadataJSON)}catch(e){this.lib.brokenMetadata=e.message,metadata=this.jsonToState("{}")}metadata.metadataTicker={value:1},this.setInitialState(metadata)}replacer(key,value){const languages=[],additional={};switch(key){case"metadataTicker":case"id":return;case"language":for(const lang of value)languages.push(lang.value);return languages;case"license":case"isPartOf":return value.value;case"freeform":if(!value.value)return"{}";try{return value.value}catch(e){return}case"additional":for(const item of value){item.scope in additional==!1&&(additional[item.scope]={}),item.property in additional[item.scope]==!1&&item.qualifier&&(additional[item.scope][item.property]={});let currentValue=null;currentValue=""===item.qualifier?additional[item.scope][item.property]:additional[item.scope][item.property][item.qualifier];let value=null;value=currentValue?Array.isArray(currentValue)?currentValue.concat([item.value]):[currentValue,item.value]:item.value,""===item.qualifier?additional[item.scope][item.property]=value:additional[item.scope][item.property][item.qualifier]=value}return JSON.stringify(additional);default:return value}}jsonStringify(state,spacing){let output=JSON.stringify(state,this.replacer);return output=JSON.parse(output),output.freeform=JSON.parse(output.freeform),output.additional=JSON.parse(output.additional),output=JSON.stringify(output,null,spacing),output}reviver(key,value){const holder=[];let id=1;switch(key){case"contributor":for(const current of value)current.id=id,holder.push(current),id++;return holder;case"language":for(const lang of value)holder.push({id:id,value:lang}),id++;return holder;case"license":case"isPartOf":return{value:value};case"freeform":return{value:JSON.stringify(value)};default:return value}}jsonToState(data){data=JSON.parse(data);for(let property in data)data[property]=this.reviver(property,data[property]);const contribFields=["id","firstName","lastName","institution","year"],standardFields=["id","value"];(data=this.stripFields(data,["creator","contributor","language","license","isPartOf","additional","freeform"])).creator=this.tidyObject(data.creator,["firstName","lastName","institution","year"]),data.contributor=Array.isArray(data.contributor)?data.contributor:[];const contribHolder=[];for(let contrib of data.contributor)contrib=this.tidyObject(contrib,contribFields),contribHolder.push(contrib);data.contributor=contribHolder,data.language=Array.isArray(data.language)?data.language:[];const langHolder=[];for(let lang of data.language)lang=this.tidyObject(lang,standardFields),langHolder.push(lang);data.language=langHolder,data.isPartOf=this.tidyObject(data.isPartOf,standardFields),data.license=this.tidyObject(data.license,standardFields);const addHolder=[];let addId=1;for(const addScope in data.additional)for(const addProperty in data.additional[addScope])if(data.additional[addScope][addProperty]&&"object"==typeof data.additional[addScope][addProperty]&&!Array.isArray(data.additional[addScope][addProperty]))for(const addQualifier in data.additional[addScope][addProperty]){let values=data.additional[addScope][addProperty][addQualifier];values=Array.isArray(values)?values:[values];for(const value of values){const add={id:addId,scope:addScope,property:addProperty,qualifier:addQualifier,value:value};addHolder.push(add),addId++}}else{let values=data.additional[addScope][addProperty];values=Array.isArray(values)?values:[values];for(const value of values){const add={id:addId,scope:addScope,property:addProperty,qualifier:"",value:value};addHolder.push(add),addId++}}return data.additional=addHolder,data.freeform=this.tidyObject(data.freeform,standardFields),data}stripFields(obj,fields){const result={};for(const suppliedField in obj)fields.includes(suppliedField)&&(result[suppliedField]=obj[suppliedField]);return result}addFields(obj,fields){for(const field of fields)Object.hasOwn(obj,field)?obj[field]=String(obj[field]):obj[field]="";return obj}tidyObject(obj,fields){return obj=obj&&"object"==typeof obj?obj:{},obj=this.stripFields(obj,fields),obj=this.addFields(obj,fields)}}const metadata=new StackMetadata({name:"qtype_stack_metadata",eventName:_events.eventTypes.qtypeStackStateUpdated,eventDispatch:_events.notifyQtypeStackStateUpdated,mutations:_mutations.mutations});_exports.metadata=metadata})); + +//# sourceMappingURL=metadata.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadata.min.js.map b/amd/build/metadata/metadata.min.js.map new file mode 100644 index 00000000000..098ad4058a1 --- /dev/null +++ b/amd/build/metadata/metadata.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadata.min.js","sources":["../../src/metadata/metadata.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Metadata entry reactive component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {Reactive} from 'core/reactive';\nimport {mutations} from 'qtype_stack/metadata/mutations';\nimport {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events';\n\nclass StackMetadata extends Reactive {\n // Default and config data passed through from Moodle.\n lib = {\n languages: ['en'],\n user: {\n firstname: '',\n lastname: '',\n institution: '',\n year: ''\n },\n licenses: [{value: 'unknown', text: 'unknown'}],\n placeholder: ''\n };\n\n /**\n * Load initial value of state from value on form.\n */\n loadState() {\n let metadata = document.querySelector('input[name=\"metadata\"]');\n const metadataJSON = metadata.value ?? null;\n try {\n this.lib = JSON.parse(metadata.dataset.lib);\n this.lib.user.year = new Date().getFullYear();\n // Weed out duplicates and falsy values.\n let languages = new Set(this.lib.languages);\n this.lib.languages = languages.difference(new Set([null, undefined, \"\"]));\n this.lib.languages = Array.from(this.lib.languages);\n } catch (e) {\n // Lib will be set to defaults.\n }\n try {\n metadata = this.jsonToState(metadataJSON);\n } catch (e) {\n // If the saved data is broken, show empty inputs and save error message for display in modal.\n this.lib.brokenMetadata = e.message;\n metadata = this.jsonToState('{}');\n }\n metadata.metadataTicker = {value: 1};\n this.setInitialState(metadata);\n }\n\n /**\n * Replacer function for JSON stringify of state.\n * Removed unwanted properties and converts some objects to plain values.\n *\n * @param {*} key\n * @param {*} value\n * @returns\n */\n replacer(key, value) {\n const languages = [];\n const additional = {};\n switch(key) {\n case 'metadataTicker':\n return undefined;\n case 'id':\n return undefined;\n case 'language':\n for (const lang of value) {\n languages.push(lang.value);\n }\n return languages;\n case 'license':\n case 'isPartOf':\n return value.value;\n case 'freeform':\n if (!value.value) {\n return '{}';\n }\n try {\n // If we parse the value here then the replacer\n // works on it recursively which gets messed up\n // if someone uses property values to match our own.\n return value.value;\n } catch(e) {\n return undefined;\n }\n case 'additional':\n for (const item of value) {\n if (item.scope in additional === false) {\n additional[item.scope] = {};\n }\n if (item.property in additional[item.scope] === false && item.qualifier) {\n additional[item.scope][item.property] = {};\n }\n let currentValue = null;\n if (item.qualifier === '') {\n currentValue = additional[item.scope][item.property];\n } else {\n currentValue = additional[item.scope][item.property][item.qualifier];\n }\n let value = null;\n // If we have multiple values, we need to convert to an array.\n if (!currentValue) {\n value = item.value;\n } else if (!Array.isArray(currentValue)) {\n value = [currentValue, item.value];\n } else {\n value = currentValue.concat([item.value]);\n }\n if (item.qualifier === '') {\n additional[item.scope][item.property] = value;\n } else {\n additional[item.scope][item.property][item.qualifier] = value;\n }\n }\n return JSON.stringify(additional);\n default:\n return value;\n }\n\n }\n\n /**\n * Convert state into a JSON string.\n * We can't simply stringify because of the freeform and additional properties.\n * We have to deal with them as strings before this to prevent the replacer\n * acting recursively.\n *\n * @param {*} state\n * @param {*} spacing\n * @returns\n */\n jsonStringify(state, spacing) {\n let output = JSON.stringify(state, this.replacer);\n output = JSON.parse(output);\n output.freeform = JSON.parse(output.freeform);\n output.additional = JSON.parse(output.additional);\n output = JSON.stringify(output, null, spacing);\n return output;\n }\n\n /**\n * Reviver function for JSON parsing to feed into state.\n * Adds id values and converts strings to obj.value.\n *\n * @param {*} key\n * @param {*} value\n * @returns\n */\n reviver(key, value) {\n const holder = [];\n let id = 1;\n switch(key) {\n case 'contributor':\n for (const current of value) {\n current.id = id;\n holder.push(current);\n id++;\n }\n return holder;\n case 'language':\n for (const lang of value) {\n holder.push({id: id, value: lang});\n id++;\n }\n return holder;\n case 'license':\n case 'isPartOf':\n return {value: value};\n case 'freeform':\n return {value: JSON.stringify(value)};\n default:\n return value;\n }\n }\n\n /**\n * Convert JSON to state format ready for updateFromJson mutation.\n * Strips out extraneous fields; adds in missing fields with blank values.\n *\n * @param {*} data\n * @returns\n */\n jsonToState(data) {\n data = JSON.parse(data);\n for (let property in data) {\n // We use the reviver once rather than recursively as it works from the bottom\n // up and that causes issues if someone has re-used one of our property names.\n data[property] = this.reviver(property, data[property]);\n }\n const fields = ['creator', 'contributor', 'language', 'license', 'isPartOf', 'additional', 'freeform'];\n data = this.stripFields(data, fields);\n const creatorFields = ['firstName', 'lastName', 'institution', 'year'];\n const contribFields = ['id', 'firstName', 'lastName', 'institution', 'year'];\n const standardFields = ['id', 'value'];\n\n data.creator = this.tidyObject(data.creator, creatorFields);\n data.contributor = (Array.isArray(data.contributor)) ? data.contributor : [];\n const contribHolder = [];\n for (let contrib of data.contributor) {\n contrib = this.tidyObject(contrib, contribFields);\n contribHolder.push(contrib);\n }\n data.contributor = contribHolder;\n data.language = (Array.isArray(data.language)) ? data.language : [];\n const langHolder = [];\n for (let lang of data.language) {\n lang = this.tidyObject(lang, standardFields);\n langHolder.push(lang);\n }\n data.language = langHolder;\n data.isPartOf = this.tidyObject(data.isPartOf, standardFields);\n data.license = this.tidyObject(data.license, standardFields);\n const addHolder = [];\n let addId = 1;\n for (const addScope in data.additional) {\n for (const addProperty in data.additional[addScope]) {\n if (\n data.additional[addScope][addProperty] &&\n typeof data.additional[addScope][addProperty] === 'object' &&\n !Array.isArray(data.additional[addScope][addProperty])\n ) {\n for (const addQualifier in data.additional[addScope][addProperty]) {\n let values = data.additional[addScope][addProperty][addQualifier];\n values = (Array.isArray(values)) ? values : [values];\n for (const value of values) {\n const add = {\n id: addId,\n scope: addScope,\n property: addProperty,\n qualifier: addQualifier,\n value: value\n };\n addHolder.push(add);\n addId++;\n }\n }\n } else {\n let values = data.additional[addScope][addProperty];\n values = (Array.isArray(values)) ? values : [values];\n for (const value of values) {\n const add = {\n id: addId,\n scope: addScope,\n property: addProperty,\n qualifier: '',\n value: value\n };\n addHolder.push(add);\n addId++;\n }\n }\n }\n }\n data.additional = addHolder;\n data.freeform = this.tidyObject(data.freeform, standardFields);\n\n return data;\n }\n\n /**\n * Remove any properties from an object that are not in a supplied array of property names.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns {object}\n */\n stripFields(obj, fields) {\n const result = {};\n for (const suppliedField in obj) {\n if (fields.includes(suppliedField)) {\n result[suppliedField] = obj[suppliedField];\n }\n }\n return result;\n }\n\n /**\n * Add any missing properties to an object from a supplied array of field names and set to ''.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns\n */\n addFields(obj, fields) {\n for (const field of fields) {\n if (!Object.hasOwn(obj, field)) {\n obj[field] = '';\n } else {\n obj[field] = String(obj[field]);\n }\n }\n return obj;\n }\n\n /**\n * Set properties of an object to those from a supplied array of field names.\n *\n * @param {object} obj\n * @param {array} fields\n * @returns\n */\n tidyObject(obj, fields) {\n obj = (obj && typeof obj === 'object') ? obj : {};\n obj = this.stripFields(obj, fields);\n obj = this.addFields(obj, fields);\n return obj;\n }\n}\n\n/**\n * The metadata state instance.\n */\nexport const metadata = new StackMetadata({\n name: 'qtype_stack_metadata',\n eventName: eventTypes.qtypeStackStateUpdated,\n eventDispatch: notifyQtypeStackStateUpdated,\n mutations,\n});\n\n\n"],"names":["StackMetadata","Reactive","languages","user","firstname","lastname","institution","year","licenses","value","text","placeholder","loadState","metadata","document","querySelector","metadataJSON","lib","JSON","parse","dataset","Date","getFullYear","Set","this","difference","undefined","Array","from","e","jsonToState","brokenMetadata","message","metadataTicker","setInitialState","replacer","key","additional","lang","push","item","scope","property","qualifier","currentValue","isArray","concat","stringify","jsonStringify","state","spacing","output","freeform","reviver","holder","id","current","data","contribFields","standardFields","stripFields","creator","tidyObject","contributor","contribHolder","contrib","language","langHolder","isPartOf","license","addHolder","addId","addScope","addProperty","addQualifier","values","add","obj","fields","result","suppliedField","includes","addFields","field","Object","hasOwn","String","name","eventName","eventTypes","qtypeStackStateUpdated","eventDispatch","notifyQtypeStackStateUpdated","mutations"],"mappings":"0QA2BMA,sBAAsBC,6EAElB,CACFC,UAAW,CAAC,MACZC,KAAM,CACFC,UAAW,GACXC,SAAU,GACVC,YAAa,GACbC,KAAM,IAEVC,SAAU,CAAC,CAACC,MAAO,UAAWC,KAAM,YACpCC,YAAa,kIAMjBC,oCACQC,SAAWC,SAASC,cAAc,gCAChCC,qCAAeH,SAASJ,iDAAS,cAE9BQ,IAAMC,KAAKC,MAAMN,SAASO,QAAQH,UAClCA,IAAId,KAAKI,MAAO,IAAIc,MAAOC,kBAE5BpB,UAAY,IAAIqB,IAAIC,KAAKP,IAAIf,gBAC5Be,IAAIf,UAAYA,UAAUuB,WAAW,IAAIF,IAAI,CAAC,UAAMG,EAAW,WAC/DT,IAAIf,UAAYyB,MAAMC,KAAKJ,KAAKP,IAAIf,WAC3C,MAAO2B,QAILhB,SAAWW,KAAKM,YAAYd,cAC9B,MAAOa,QAEAZ,IAAIc,eAAiBF,EAAEG,QAC5BnB,SAAWW,KAAKM,YAAY,MAEhCjB,SAASoB,eAAiB,CAACxB,MAAO,QAC7ByB,gBAAgBrB,UAWzBsB,SAASC,IAAK3B,aACJP,UAAY,GACZmC,WAAa,UACZD,SACE,qBAEA,gBAEA,eACI,MAAME,QAAQ7B,MACfP,UAAUqC,KAAKD,KAAK7B,cAEjBP,cACN,cACA,kBACMO,MAAMA,UACZ,eACIA,MAAMA,YACA,gBAMAA,MAAMA,MACf,MAAMoB,cAGP,iBACI,MAAMW,QAAQ/B,MAAO,CAClB+B,KAAKC,SAASJ,aAAe,IAC7BA,WAAWG,KAAKC,OAAS,IAEzBD,KAAKE,YAAYL,WAAWG,KAAKC,SAAW,GAASD,KAAKG,YAC1DN,WAAWG,KAAKC,OAAOD,KAAKE,UAAY,QAExCE,aAAe,KAEfA,aADmB,KAAnBJ,KAAKG,UACUN,WAAWG,KAAKC,OAAOD,KAAKE,UAE5BL,WAAWG,KAAKC,OAAOD,KAAKE,UAAUF,KAAKG,eAE1DlC,MAAQ,KAORA,MALCmC,aAEOjB,MAAMkB,QAAQD,cAGdA,aAAaE,OAAO,CAACN,KAAK/B,QAF1B,CAACmC,aAAcJ,KAAK/B,OAFpB+B,KAAK/B,MAMM,KAAnB+B,KAAKG,UACLN,WAAWG,KAAKC,OAAOD,KAAKE,UAAYjC,MAExC4B,WAAWG,KAAKC,OAAOD,KAAKE,UAAUF,KAAKG,WAAalC,aAGzDS,KAAK6B,UAAUV,2BAEf5B,OAenBuC,cAAcC,MAAOC,aACbC,OAASjC,KAAK6B,UAAUE,MAAOzB,KAAKW,iBACxCgB,OAASjC,KAAKC,MAAMgC,QACpBA,OAAOC,SAAWlC,KAAKC,MAAMgC,OAAOC,UACpCD,OAAOd,WAAanB,KAAKC,MAAMgC,OAAOd,YACtCc,OAASjC,KAAK6B,UAAUI,OAAQ,KAAMD,SAC/BC,OAWXE,QAAQjB,IAAK3B,aACH6C,OAAS,OACXC,GAAK,SACFnB,SACE,kBACI,MAAMoB,WAAW/C,MAClB+C,QAAQD,GAAKA,GACbD,OAAOf,KAAKiB,SACZD,YAEGD,WACN,eACI,MAAMhB,QAAQ7B,MACf6C,OAAOf,KAAK,CAACgB,GAAIA,GAAI9C,MAAO6B,OAC5BiB,YAEGD,WACN,cACA,iBACM,CAAC7C,MAAOA,WACd,iBACM,CAACA,MAAOS,KAAK6B,UAAUtC,uBAEvBA,OAWnBqB,YAAY2B,MACRA,KAAOvC,KAAKC,MAAMsC,UACb,IAAIf,YAAYe,KAGjBA,KAAKf,UAAYlB,KAAK6B,QAAQX,SAAUe,KAAKf,iBAK3CgB,cAAgB,CAAC,KAAM,YAAa,WAAY,cAAe,QAC/DC,eAAiB,CAAC,KAAM,UAH9BF,KAAOjC,KAAKoC,YAAYH,KADT,CAAC,UAAW,cAAe,WAAY,UAAW,WAAY,aAAc,cAMtFI,QAAUrC,KAAKsC,WAAWL,KAAKI,QAJd,CAAC,YAAa,WAAY,cAAe,SAK/DJ,KAAKM,YAAepC,MAAMkB,QAAQY,KAAKM,aAAgBN,KAAKM,YAAc,SACpEC,cAAgB,OACjB,IAAIC,WAAWR,KAAKM,YACrBE,QAAUzC,KAAKsC,WAAWG,QAASP,eACnCM,cAAczB,KAAK0B,SAEvBR,KAAKM,YAAcC,cACnBP,KAAKS,SAAYvC,MAAMkB,QAAQY,KAAKS,UAAaT,KAAKS,SAAW,SAC3DC,WAAa,OACd,IAAI7B,QAAQmB,KAAKS,SAClB5B,KAAOd,KAAKsC,WAAWxB,KAAMqB,gBAC7BQ,WAAW5B,KAAKD,MAEpBmB,KAAKS,SAAWC,WAChBV,KAAKW,SAAW5C,KAAKsC,WAAWL,KAAKW,SAAUT,gBAC/CF,KAAKY,QAAU7C,KAAKsC,WAAWL,KAAKY,QAASV,sBACvCW,UAAY,OACdC,MAAQ,MACP,MAAMC,YAAYf,KAAKpB,eACnB,MAAMoC,eAAehB,KAAKpB,WAAWmC,aAElCf,KAAKpB,WAAWmC,UAAUC,cACwB,iBAA3ChB,KAAKpB,WAAWmC,UAAUC,eAChC9C,MAAMkB,QAAQY,KAAKpB,WAAWmC,UAAUC,kBAEpC,MAAMC,gBAAgBjB,KAAKpB,WAAWmC,UAAUC,aAAc,KAC3DE,OAASlB,KAAKpB,WAAWmC,UAAUC,aAAaC,cACpDC,OAAUhD,MAAMkB,QAAQ8B,QAAWA,OAAS,CAACA,YACxC,MAAMlE,SAASkE,OAAQ,OAClBC,IAAM,CACRrB,GAAIgB,MACJ9B,MAAO+B,SACP9B,SAAU+B,YACV9B,UAAW+B,aACXjE,MAAOA,OAEX6D,UAAU/B,KAAKqC,KACfL,aAGL,KACCI,OAASlB,KAAKpB,WAAWmC,UAAUC,aACvCE,OAAUhD,MAAMkB,QAAQ8B,QAAWA,OAAS,CAACA,YACxC,MAAMlE,SAASkE,OAAQ,OAClBC,IAAM,CACRrB,GAAIgB,MACJ9B,MAAO+B,SACP9B,SAAU+B,YACV9B,UAAW,GACXlC,MAAOA,OAEX6D,UAAU/B,KAAKqC,KACfL,gBAKhBd,KAAKpB,WAAaiC,UAClBb,KAAKL,SAAW5B,KAAKsC,WAAWL,KAAKL,SAAUO,gBAExCF,KAUXG,YAAYiB,IAAKC,cACPC,OAAS,OACV,MAAMC,iBAAiBH,IACpBC,OAAOG,SAASD,iBAChBD,OAAOC,eAAiBH,IAAIG,uBAG7BD,OAUXG,UAAUL,IAAKC,YACN,MAAMK,SAASL,OACXM,OAAOC,OAAOR,IAAKM,OAGpBN,IAAIM,OAASG,OAAOT,IAAIM,QAFxBN,IAAIM,OAAS,UAKdN,IAUXf,WAAWe,IAAKC,eACZD,IAAOA,KAAsB,iBAARA,IAAoBA,IAAM,GAC/CA,IAAMrD,KAAKoC,YAAYiB,IAAKC,QAC5BD,IAAMrD,KAAK0D,UAAUL,IAAKC,eAQrBjE,SAAW,IAAIb,cAAc,CACtCuF,KAAM,uBACNC,UAAWC,mBAAWC,uBACtBC,cAAeC,qCACfC,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js b/amd/build/metadata/metadatacontent.min.js new file mode 100644 index 00000000000..e22cdaefce3 --- /dev/null +++ b/amd/build/metadata/metadatacontent.min.js @@ -0,0 +1,11 @@ +define("qtype_stack/metadata/metadatacontent",["exports","core/reactive","qtype_stack/metadata/metadata"],(function(_exports,_reactive,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0; +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class _default extends _reactive.BaseComponent{create(){this.name="stack-metadata-content",this.selectors={METADATACONTAINER:"[data-for='qtype-stack-metadata']",SUBMIT:"#stack-metadata-update"}}static init(target,selectors){return new this({element:document.querySelector(target),reactive:_metadata.metadata,selectors:selectors})}}return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=metadatacontent.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatacontent.min.js.map b/amd/build/metadata/metadatacontent.min.js.map new file mode 100644 index 00000000000..9097b1acb22 --- /dev/null +++ b/amd/build/metadata/metadatacontent.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadatacontent.min.js","sources":["../../src/metadata/metadatacontent.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * Main STACK metadata component\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport {BaseComponent} from 'core/reactive';\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nexport default class extends BaseComponent {\n create() {\n this.name = 'stack-metadata-content';\n this.selectors = {\n METADATACONTAINER: `[data-for='qtype-stack-metadata']`,\n SUBMIT: `#stack-metadata-update`,\n };\n }\n\n /**\n * Static method to create a component instance form the mustache template.\n *\n * @param {string} target the DOM main element or its ID\n * @param {object} selectors optional css selector overrides\n * @return {Component}\n */\n static init(target, selectors) {\n return new this({\n element: document.querySelector(target),\n reactive: metadata,\n selectors,\n });\n }\n}"],"names":["BaseComponent","create","name","selectors","METADATACONTAINER","SUBMIT","target","this","element","document","querySelector","reactive","metadata"],"mappings":";;;;;;;;uBA0B6BA,wBACzBC,cACSC,KAAO,8BACPC,UAAY,CACbC,sDACAC,6CAWIC,OAAQH,kBACT,IAAII,KAAK,CACZC,QAASC,SAASC,cAAcJ,QAChCK,SAAUC,mBACVT,UAAAA"} \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js b/amd/build/metadata/metadatamodal.min.js new file mode 100644 index 00000000000..f77a208120c --- /dev/null +++ b/amd/build/metadata/metadatamodal.min.js @@ -0,0 +1,3 @@ +define("qtype_stack/metadata/metadatamodal",["exports","core/modal","qtype_stack/metadata/metadata","qtype_stack/metadata/container"],(function(_exports,_modal,_metadata,_container){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setup=_exports.MetadataModal=void 0,_modal=_interopRequireDefault(_modal),_container=_interopRequireDefault(_container);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}class MetadataModal extends _modal.default{async hide(){if(await _metadata.metadata.container.update(!0)){const current=document.querySelector('input[name="metadata"]'),newValue=_metadata.metadata.jsonStringify(_metadata.metadata.state,0);if(current.value!==newValue){document.querySelector('input[name="metadata"]').value=newValue;try{document.querySelector('[data-name="metadata_text"]').textContent=document.querySelector("#id_stack_metadata").getAttribute("data-change")}catch(e){}}super.hide()}}cancel(){super.hide()}}_exports.MetadataModal=MetadataModal,_defineProperty(MetadataModal,"TYPE","qtype_stack/metadatamodal"),_defineProperty(MetadataModal,"TEMPLATE","qtype_stack/metadata/metadatamodal");let registered=!1;registered||(!async function(){if("function"!=typeof MetadataModal.create){(await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require(["core/modal_registry"],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require("core/modal_registry")):Promise.resolve(_systemImportTransformerGlobalIdentifier["core/modal_registry"]))).register(MetadataModal.TYPE,MetadataModal,MetadataModal.TEMPLATE)}}(),registered=!0);let modal=null;function closeModal(){modal.cancel.call(modal)}async function openModal(){let addListener=!1;if(modal)modal.show();else{if("function"==typeof MetadataModal.create)modal=await MetadataModal.create(),modal.show();else{const ModalFactory=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require(["core/modal_factory"],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require("core/modal_factory")):Promise.resolve(_systemImportTransformerGlobalIdentifier["core/modal_factory"]));modal=await ModalFactory.create({type:MetadataModal.TYPE}),modal.show(),_container.default.init("#qtype-stack-metadata-main")}addListener=!0}addListener&&document.querySelector("#stackmetadata_cancel").addEventListener("click",closeModal)}_exports.setup=()=>{var _document$querySelect;null===(_document$querySelect=document.querySelector("#id_metadatamodal"))||void 0===_document$querySelect||_document$querySelect.addEventListener("click",openModal),_metadata.metadata.loadState()}})); + +//# sourceMappingURL=metadatamodal.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/metadatamodal.min.js.map b/amd/build/metadata/metadatamodal.min.js.map new file mode 100644 index 00000000000..846bccf90c0 --- /dev/null +++ b/amd/build/metadata/metadatamodal.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metadatamodal.min.js","sources":["../../src/metadata/metadatamodal.js"],"sourcesContent":["// This file is part of Stack - http://stack.maths.ed.ac.uk/\n//\n// Stack is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Stack is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Stack. If not, see .\n\n/**\n * STACK metadata modal setup\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\n\nimport Modal from 'core/modal';\nimport {metadata} from 'qtype_stack/metadata/metadata';\nimport container from 'qtype_stack/metadata/container';\n\nexport class MetadataModal extends Modal {\n static TYPE = \"qtype_stack/metadatamodal\";\n static TEMPLATE = \"qtype_stack/metadata/metadatamodal\";\n\n /**\n * Override the default hide function to validate and update metadata JSON.\n * On success, stores new JSON to hidden edit form field and closed modal.\n */\n async hide() {\n const result = await metadata.container.update(true);\n if (result) {\n const current = document.querySelector('input[name=\"metadata\"]');\n const newValue = metadata.jsonStringify(metadata.state, 0);\n if (current.value !== newValue) {\n document.querySelector('input[name=\"metadata\"]').value = newValue;\n try {\n document.querySelector('[data-name=\"metadata_text\"]').textContent =\n document.querySelector('#id_stack_metadata').getAttribute('data-change');\n } catch (e) {\n // Don't update in Moodle 4.2.\n }\n }\n super.hide();\n }\n }\n\n /**\n * Cancel button needs to close the modal without updating form.\n */\n cancel() {\n super.hide();\n }\n}\n\nlet registered = false;\nif (!registered) {\n registerModal();\n registered = true;\n}\n\nlet modal = null;\n\n// Prepare for modal creation.\nexport const setup = () => {\n document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal);\n metadata.loadState();\n};\n\n/**\n * Need to pass appropriate 'this' to cancel function.\n */\nfunction closeModal() {\n modal.cancel.call(modal);\n}\n\n/**\n * Register the modal in old versions of Moodle.\n */\nasync function registerModal() {\n if (typeof MetadataModal.create !== \"function\") {\n const ModalRegistry = await import('core/modal_registry');\n ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE);\n }\n}\n\n/**\n * Open the metadata modal.\n * Only create modal and add listener once.\n */\nasync function openModal() {\n let addListener = false;\n if (!modal) {\n if (typeof MetadataModal.create === \"function\") {\n modal = await MetadataModal.create();\n modal.show();\n } else {\n // Pre Moodle 4.3 code.\n const ModalFactory = await import ('core/modal_factory');\n modal = await ModalFactory.create({\n type: MetadataModal.TYPE,\n });\n modal.show();\n // Why is this necessary? Mustache should do this anyway. Moodle bug?\n container.init('#qtype-stack-metadata-main');\n }\n addListener = true;\n } else {\n modal.show();\n }\n if (addListener) {\n document.querySelector('#stackmetadata_cancel').addEventListener('click', closeModal);\n }\n}\n\n"],"names":["MetadataModal","Modal","metadata","container","update","current","document","querySelector","newValue","jsonStringify","state","value","textContent","getAttribute","e","hide","cancel","registered","create","register","TYPE","TEMPLATE","registerModal","modal","closeModal","call","openModal","addListener","show","ModalFactory","type","init","addEventListener","loadState"],"mappings":"6vBA2BaA,sBAAsBC,qCASNC,mBAASC,UAAUC,QAAO,GACnC,OACFC,QAAUC,SAASC,cAAc,0BACjCC,SAAWN,mBAASO,cAAcP,mBAASQ,MAAO,MACpDL,QAAQM,QAAUH,SAAU,CAC5BF,SAASC,cAAc,0BAA0BI,MAAQH,aAErDF,SAASC,cAAc,+BAA+BK,YAClDN,SAASC,cAAc,sBAAsBM,aAAa,eAChE,MAAOC,WAIPC,QAOdC,eACUD,6DA9BDf,qBACK,6CADLA,yBAES,0CAgClBiB,YAAa,EACZA,kCAwBmC,mBAAzBjB,cAAckB,OAAuB,onBAE9BC,SAASnB,cAAcoB,KAAMpB,cAAeA,cAAcqB,WAzB5EC,GACAL,YAAa,OAGbM,MAAQ,cAWHC,aACLD,MAAMP,OAAOS,KAAKF,sBAiBPG,gBACPC,aAAc,KACbJ,MAgBDA,MAAMK,WAhBE,IAC4B,mBAAzB5B,cAAckB,OACrBK,YAAcvB,cAAckB,SAC5BK,MAAMK,WACH,OAEGC,2nBACNN,YAAcM,aAAaX,OAAO,CAC9BY,KAAM9B,cAAcoB,OAExBG,MAAMK,0BAEIG,KAAK,8BAEnBJ,aAAc,EAIdA,aACArB,SAASC,cAAc,yBAAyByB,iBAAiB,QAASR,2BA/C7D,6DAClBlB,SAASC,cAAc,6EAAsByB,iBAAiB,QAASN,8BAC9DO"} \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js b/amd/build/metadata/mutations.min.js new file mode 100644 index 00000000000..b5a9912f676 --- /dev/null +++ b/amd/build/metadata/mutations.min.js @@ -0,0 +1,11 @@ +define("qtype_stack/metadata/mutations",["exports","qtype_stack/metadata/metadata"],(function(_exports,_metadata){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.mutations=void 0;const mutations=new +/** + * Default mutation manager + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +class{async updateAll(stateManager,inputArray){let state=stateManager.state;const additionalCopy={};state.additional&&state.additional.forEach((addInfo=>{const rowCopy={scope:addInfo.scope,property:addInfo.property,qualifier:addInfo.qualifier,value:addInfo.value};additionalCopy[addInfo.id]=rowCopy}));for(const field of inputArray){const parts=field[0].split("_"),id=parts[1],property=parts[2],subproperty=parts[3];if("additional"===property)if("scope"===subproperty){const existingScope=additionalCopy[id].scope;if(existingScope!==field[1])for(const key in additionalCopy){const addInfo=additionalCopy[key];addInfo.scope===existingScope&&(addInfo.scope=field[1])}}else{const existing=additionalCopy[id];existing&&(existing[subproperty]=field[1])}}let problems=new Set;for(const key1 in additionalCopy){const addInfo=additionalCopy[key1];if(""===addInfo.qualifier)for(const key2 in additionalCopy){const addInfo2=additionalCopy[key2];addInfo.scope===addInfo2.scope&&addInfo.property===addInfo2.property&&""!==addInfo2.qualifier&&problems.add(key1)}}if(problems=Array.from(problems),problems.length){const output=problems.join(",");return Promise.reject(output)}stateManager.setReadOnly(!1);for(const field of inputArray){const parts=field[0].split("_"),id=parts[1],property=parts[2],subproperty=parts[3];if("scope"===subproperty){const existingScope=state.additional.get(id).scope;existingScope!==field[1]&&state.additional.forEach((addInfo=>{addInfo.scope===existingScope&&(addInfo.scope=field[1])}))}else if(0!=id){const existing=state[property].get(id);existing&&(existing[subproperty]=field[1])}else state[property][subproperty]=field[1]}return state.metadataTicker.value+=1,stateManager.setReadOnly(!0),Promise.resolve("Success")}deleteRow(stateManager,property,id){const state=stateManager.state;if(stateManager.setReadOnly(!1),"scope"===property){const matchingAddInfo=[],scope=state.additional.get(id).scope;state.additional.forEach((addInfo=>{addInfo.scope===scope&&matchingAddInfo.push(addInfo.id)}));for(const current of matchingAddInfo)state.additional.delete(current)}else state[property].delete(id);stateManager.setReadOnly(!0)}addItem(stateManager,category,id){const state=stateManager.state;let addCategory=category,newItem=null,existingProperty=null;switch(category){case"language":newItem={value:""};break;case"contributor":newItem={firstName:"user"===id?_metadata.metadata.lib.user.firstname:"",lastName:"user"===id?_metadata.metadata.lib.user.lastname:"",institution:"user"===id?_metadata.metadata.lib.user.institution:"",year:String((new Date).getFullYear())};break;case"scope":newItem={scope:"",property:"",qualifier:"",value:""},addCategory="additional";break;case"property":existingProperty=state.additional.get(id),newItem={scope:existingProperty.scope,property:"",qualifier:"",value:""},addCategory="additional"}const keys=Array.from(state[addCategory]);0===keys.length?newItem.id=1:(keys.sort(((a,b)=>b[0]-a[0])),newItem.id=1+parseInt(keys[0][0])),stateManager.setReadOnly(!1),state[addCategory].add(newItem),stateManager.setReadOnly(!0)}updateFromJson(stateManager,data){const state=stateManager.state;stateManager.setReadOnly(!1);for(const prop in data)state[prop]=data[prop];state.metadataTicker.value+=1,stateManager.setReadOnly(!0)}};_exports.mutations=mutations})); + +//# sourceMappingURL=mutations.min.js.map \ No newline at end of file diff --git a/amd/build/metadata/mutations.min.js.map b/amd/build/metadata/mutations.min.js.map new file mode 100644 index 00000000000..911be80f6b7 --- /dev/null +++ b/amd/build/metadata/mutations.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mutations.min.js","sources":["../../src/metadata/mutations.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Default mutation manager\n *\n * @module qtype_stack/metadata\n * @copyright 2025 University of Edinburgh\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.\n */\nimport {metadata} from 'qtype_stack/metadata/metadata';\n\nclass Mutations {\n /**\n * Update state from array of input field information.\n *\n * Inputs have ids in form smdi-id-category-field e.g. smdi-1-contributor-year.\n * id is row entry id in state. 0 is used for single elements e.g. license.\n * Multi-elements begin counting from 1.\n * For scope, row id is for one of the matching additional info rows.\n * @param {*} stateManager\n * @param {*} inputArray [['smdi-1-contributor-year', 2025], ...]\n * @returns\n */\n async updateAll(stateManager, inputArray) {\n let state = stateManager.state;\n const additionalCopy = {};\n if (state.additional) {\n state.additional.forEach((addInfo) => {\n const rowCopy = {\n scope: addInfo.scope,\n property: addInfo.property,\n qualifier: addInfo.qualifier,\n value: addInfo.value\n };\n additionalCopy[addInfo.id] = rowCopy;\n });\n }\n for (const field of inputArray) {\n const parts = field[0].split('_');\n const id = parts[1];\n const property = parts[2];\n const subproperty = parts[3];\n if (property !== 'additional') {\n continue;\n }\n if (subproperty === 'scope') {\n // Scope input updates multiple rows.\n // We find all entries for that scope and update.\n const existingScope = additionalCopy[id].scope;\n if (existingScope !== field[1]) {\n for (const key in additionalCopy) {\n const addInfo = additionalCopy[key];\n if (addInfo.scope === existingScope) {\n addInfo.scope = field[1];\n }\n }\n }\n } else {\n const existing = additionalCopy[id];\n if (existing) {\n existing[subproperty] = field[1];\n }\n }\n }\n\n let problems = new Set();\n for (const key1 in additionalCopy) {\n const addInfo = additionalCopy[key1];\n if (addInfo.qualifier === '') {\n for (const key2 in additionalCopy) {\n const addInfo2 = additionalCopy[key2];\n if (\n addInfo.scope === addInfo2.scope && addInfo.property === addInfo2.property && addInfo2.qualifier !== ''\n ) {\n problems.add(key1);\n }\n }\n }\n }\n problems = Array.from(problems);\n if (problems.length) {\n const output = problems.join(',');\n return Promise.reject(output);\n }\n stateManager.setReadOnly(false);\n for (const field of inputArray) {\n const parts = field[0].split('_');\n const id = parts[1];\n const property = parts[2];\n const subproperty = parts[3];\n if (subproperty === 'scope') {\n // Scope input updates multiple rows.\n // We find all entries for that scope and update.\n const existingScope = state.additional.get(id).scope;\n if (existingScope !== field[1]) {\n state.additional.forEach((addInfo) => {\n if (addInfo.scope === existingScope) {\n addInfo.scope = field[1];\n }\n });\n }\n } else if (id != 0) {\n const existing = state[property].get(id);\n if (existing) {\n existing[subproperty] = field[1];\n }\n } else {\n state[property][subproperty] = field[1];\n }\n }\n\n // Force display refresh in odd circumstances where state has not changed\n // but JSON needs to be updated.\n state.metadataTicker.value += 1;\n stateManager.setReadOnly(true);\n return Promise.resolve('Success');\n }\n\n /**\n * Delete a row from the metadata form.\n *\n * @param {*} stateManager\n * @param {*} property type to be deleted\n * @param {*} id of instance to be deleted. Form will be refreshed and ids reset.\n */\n deleteRow(stateManager, property, id) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n if (property === 'scope') {\n const matchingAddInfo = [];\n const scope = state.additional.get(id).scope;\n // Need to delete ALL entries with the same scope\n // as the supplied additional info.\n state.additional.forEach((addInfo) => {\n if (addInfo.scope === scope) {\n matchingAddInfo.push(addInfo.id);\n }\n });\n for (const current of matchingAddInfo) {\n state.additional.delete(current);\n }\n } else {\n state[property].delete(id);\n }\n stateManager.setReadOnly(true);\n }\n\n /**\n * Add a row\n *\n * @param {*} stateManager\n * @param {*} category\n * @param {*} id Only required for additional info. Allows us to get relevant scope.\n */\n addItem(stateManager, category, id) {\n const state = stateManager.state;\n let addCategory = category;\n let newItem = null;\n let existingProperty = null;\n switch (category) {\n case 'language':\n newItem = {\n value: \"\"\n };\n break;\n case 'contributor':\n newItem = {\n firstName: (id === 'user') ? metadata.lib.user.firstname : \"\",\n lastName: (id === 'user') ? metadata.lib.user.lastname : \"\",\n institution: (id === 'user') ? metadata.lib.user.institution : \"\",\n year: String(new Date().getFullYear())\n };\n break;\n case 'scope':\n newItem = {\n scope: '',\n property: '',\n qualifier: '',\n value: ''\n };\n addCategory = 'additional';\n break;\n case 'property':\n existingProperty = state.additional.get(id);\n newItem = {\n scope: existingProperty.scope,\n property: '',\n qualifier: '',\n value: ''\n };\n addCategory = 'additional';\n break;\n default:\n }\n\n // Ids are required for all objects. We add one to highest existing id.\n const keys = Array.from(state[addCategory]);\n if (keys.length === 0) {\n newItem.id = 1;\n } else {\n keys.sort((a, b) => b[0] - a[0]);\n newItem.id = 1 + parseInt(keys[0][0]);\n }\n stateManager.setReadOnly(false);\n state[addCategory].add(newItem);\n stateManager.setReadOnly(true);\n }\n\n /**\n * Straight update of state.\n *\n * @param {*} stateManager\n * @param {object} data Output of metadata.jsonToState\n */\n updateFromJson(stateManager, data) {\n const state = stateManager.state;\n stateManager.setReadOnly(false);\n for (const prop in data) {\n state[prop] = data[prop];\n }\n // Force display refresh in case inputs have been altered but JSON not changed.\n // Inputs will be matched to JSON.\n state.metadataTicker.value += 1;\n stateManager.setReadOnly(true);\n }\n}\n\nexport const mutations = new Mutations();"],"names":["mutations","stateManager","inputArray","state","additionalCopy","additional","forEach","addInfo","rowCopy","scope","property","qualifier","value","id","field","parts","split","subproperty","existingScope","key","existing","problems","Set","key1","key2","addInfo2","add","Array","from","length","output","join","Promise","reject","setReadOnly","get","metadataTicker","resolve","deleteRow","matchingAddInfo","push","current","delete","addItem","category","addCategory","newItem","existingProperty","firstName","metadata","lib","user","firstname","lastName","lastname","institution","year","String","Date","getFullYear","keys","sort","a","b","parseInt","updateFromJson","data","prop"],"mappings":"0MAgPaA,UAAY;;;;;;;;sBA5MLC,aAAcC,gBACtBC,MAAQF,aAAaE,YACnBC,eAAiB,GACnBD,MAAME,YACNF,MAAME,WAAWC,SAASC,gBAChBC,QAAU,CACZC,MAAOF,QAAQE,MACfC,SAAUH,QAAQG,SAClBC,UAAWJ,QAAQI,UACnBC,MAAOL,QAAQK,OAEnBR,eAAeG,QAAQM,IAAML,eAGhC,MAAMM,SAASZ,WAAY,OACtBa,MAAQD,MAAM,GAAGE,MAAM,KACvBH,GAAKE,MAAM,GACXL,SAAWK,MAAM,GACjBE,YAAcF,MAAM,MACT,eAAbL,YAGgB,UAAhBO,YAAyB,OAGnBC,cAAgBd,eAAeS,IAAIJ,SACrCS,gBAAkBJ,MAAM,OACpB,MAAMK,OAAOf,eAAgB,OACvBG,QAAUH,eAAee,KAC3BZ,QAAQE,QAAUS,gBAClBX,QAAQE,MAAQK,MAAM,SAI/B,OACGM,SAAWhB,eAAeS,IAC5BO,WACAA,SAASH,aAAeH,MAAM,SAKtCO,SAAW,IAAIC,QACd,MAAMC,QAAQnB,eAAgB,OACzBG,QAAUH,eAAemB,SACL,KAAtBhB,QAAQI,cACH,MAAMa,QAAQpB,eAAgB,OACzBqB,SAAWrB,eAAeoB,MAE5BjB,QAAQE,QAAUgB,SAAShB,OAASF,QAAQG,WAAae,SAASf,UAAmC,KAAvBe,SAASd,WAEvFU,SAASK,IAAIH,UAK7BF,SAAWM,MAAMC,KAAKP,UAClBA,SAASQ,OAAQ,OACXC,OAAST,SAASU,KAAK,YACtBC,QAAQC,OAAOH,QAE1B7B,aAAaiC,aAAY,OACpB,MAAMpB,SAASZ,WAAY,OACtBa,MAAQD,MAAM,GAAGE,MAAM,KACvBH,GAAKE,MAAM,GACXL,SAAWK,MAAM,GACjBE,YAAcF,MAAM,MACN,UAAhBE,YAAyB,OAGnBC,cAAgBf,MAAME,WAAW8B,IAAItB,IAAIJ,MAC3CS,gBAAkBJ,MAAM,IACxBX,MAAME,WAAWC,SAASC,UAClBA,QAAQE,QAAUS,gBAClBX,QAAQE,MAAQK,MAAM,YAI/B,GAAU,GAAND,GAAS,OACVO,SAAWjB,MAAMO,UAAUyB,IAAItB,IACjCO,WACAA,SAASH,aAAeH,MAAM,SAGlCX,MAAMO,UAAUO,aAAeH,MAAM,UAM7CX,MAAMiC,eAAexB,OAAS,EAC9BX,aAAaiC,aAAY,GAClBF,QAAQK,QAAQ,WAU3BC,UAAUrC,aAAcS,SAAUG,UACxBV,MAAQF,aAAaE,SAC3BF,aAAaiC,aAAY,GACR,UAAbxB,SAAsB,OAChB6B,gBAAkB,GAClB9B,MAAQN,MAAME,WAAW8B,IAAItB,IAAIJ,MAGvCN,MAAME,WAAWC,SAASC,UAClBA,QAAQE,QAAUA,OAClB8B,gBAAgBC,KAAKjC,QAAQM,WAGhC,MAAM4B,WAAWF,gBAClBpC,MAAME,WAAWqC,OAAOD,cAG5BtC,MAAMO,UAAUgC,OAAO7B,IAE3BZ,aAAaiC,aAAY,GAU7BS,QAAQ1C,aAAc2C,SAAU/B,UACtBV,MAAQF,aAAaE,UACvB0C,YAAcD,SACdE,QAAU,KACVC,iBAAmB,YACfH,cACC,WACDE,QAAU,CACNlC,MAAO,cAGV,cACDkC,QAAU,CACNE,UAAmB,SAAPnC,GAAiBoC,mBAASC,IAAIC,KAAKC,UAAY,GAC3DC,SAAkB,SAAPxC,GAAiBoC,mBAASC,IAAIC,KAAKG,SAAW,GACzDC,YAAqB,SAAP1C,GAAiBoC,mBAASC,IAAIC,KAAKI,YAAc,GAC/DC,KAAMC,QAAO,IAAIC,MAAOC,0BAG3B,QACDb,QAAU,CACNrC,MAAO,GACPC,SAAU,GACVC,UAAW,GACXC,MAAO,IAEXiC,YAAc,uBAEb,WACDE,iBAAmB5C,MAAME,WAAW8B,IAAItB,IACxCiC,QAAU,CACNrC,MAAOsC,iBAAiBtC,MACxBC,SAAU,GACVC,UAAW,GACXC,MAAO,IAEXiC,YAAc,mBAMhBe,KAAOjC,MAAMC,KAAKzB,MAAM0C,cACV,IAAhBe,KAAK/B,OACLiB,QAAQjC,GAAK,GAEb+C,KAAKC,MAAK,CAACC,EAAGC,IAAMA,EAAE,GAAKD,EAAE,KAC7BhB,QAAQjC,GAAK,EAAImD,SAASJ,KAAK,GAAG,KAEtC3D,aAAaiC,aAAY,GACzB/B,MAAM0C,aAAanB,IAAIoB,SACvB7C,aAAaiC,aAAY,GAS7B+B,eAAehE,aAAciE,YACnB/D,MAAQF,aAAaE,MAC3BF,aAAaiC,aAAY,OACpB,MAAMiC,QAAQD,KACf/D,MAAMgE,MAAQD,KAAKC,MAIvBhE,MAAMiC,eAAexB,OAAS,EAC9BX,aAAaiC,aAAY"} \ No newline at end of file diff --git a/amd/src/metadata/container.js b/amd/src/metadata/container.js new file mode 100644 index 00000000000..59af6eb27a0 --- /dev/null +++ b/amd/src/metadata/container.js @@ -0,0 +1,400 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +import {BaseComponent} from 'core/reactive'; +import {metadata} from 'qtype_stack/metadata/metadata'; +import {notifyFieldValidationFailure} from 'core_form/events'; + +export default class extends BaseComponent { + create() { + this.name = 'stack-metadata-container'; + this.selectors = { + METADATACONTAINER: `[data-for='qtype-stack-metadata']`, + UPDATEJSON: `#stack-metadata-update`, + UPDATEINPUTS: `#stack-metadata-update-inputs`, + ADDITEM: `[name="smd_add"]`, + DELETEITEM: `[name="smd_delete"]`, + MAKECONTRIBUTOR: `#stack-metadata-make-contributor`, + MAKECREATOR: `#stack-metadata-make-creator`, + REVERT: `#stack-metadata-revert`, + FORMJSON: 'input[name="metadata"]', + JSONINPUT: '#id_metadata_json', + REQUIREDINPUTS: '#qtype-stack-metadata-content input[aria-required="true"]', + ALLINPUTS: '#qtype-stack-metadata-content [id^="smdi"]', + }; + metadata.container = this; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: metadata, + selectors, + }); + } + + /** + * Initial state ready method. + * + * @param {object} state the initial state + */ + async stateReady(state) { + await this.reloadContainerComponent({state}); + } + + /** + * Set to refresh display on any state change. + * + * @returns {object} watchers + */ + getWatchers() { + return [ + {watch: `state:updated`, handler: this.reloadContainerComponent}, + ]; + } + + /** + * Converts field information into element suitable for feeding into Mustache templates. + * + * @param {bool} required + * @param {mixed} id to link to state + * @param {string} tag type of field + * @param {mixed} value of element + * @returns {object} + */ + createDataElement(required, id, tag, value) { + return { + required: required, + element: { + value: value, + wrapperid: 'fitem_smdi_' + id + '_' + tag, + id: 'smdi_' + id + '_' + tag, + name: 'smdi_' + id + '_' + tag, + iderror: 'smde_' + id + '_' + tag + '_error' + } + }; + } + + async reloadContainerComponent({state}) { + // Mustache data is not fully compatible with state object so we need to convert it + // into a plain object. + const data = { + creator: {}, + contributor: [], + language: [], + license: this.createDataElement(true, 0, 'license_value', state.license.value), + isPartOf: this.createDataElement(false, 0, 'isPartOf_value', state.isPartOf.value), + scope: [], + freeform: this.createDataElement(false, 0, 'freeform_value', state.freeform.value || '{}'), + }; + + // Need to copy licenses list as we modify to mark as selected. + data.license.element.options = JSON.parse(JSON.stringify(metadata.lib.licenses)); + const selectedLicense = state.license.value; + let selectedOption = data.license.element.options.find((op) => op.value === selectedLicense); + if (selectedOption) { + selectedOption.selected = true; + } else { + data.license.element.options.push({value: state.license.value, text: state.license.value, selected: true}); + } + data.license.element.tags = '[]'; + data.license.element.ajax = ''; + data.license.element.placeholder = metadata.lib.placeholder; + data.license.element.noselectionstring = ''; + data.license.element.showsuggestions = 'true'; + data.license.element.casesensitive = 'false'; + + state.language.forEach(language => { + const element = { id: language.id, lang: this.createDataElement(true, language.id, 'language_value', language.value) }; + data.language.push({...element}); + }); + + state.contributor.forEach(contributor => { + const element = { + firstname: this.createDataElement(false, contributor.id, 'contributor_firstName', contributor.firstName), + lastname: this.createDataElement(false, contributor.id, 'contributor_lastName', contributor.lastName), + institution: this.createDataElement(false, contributor.id, 'contributor_institution', contributor.institution), + year: this.createDataElement(false, contributor.id, 'contributor_year', contributor.year), + id: contributor.id, + }; + data.contributor.push({...element}); + }); + + const scopeHolder = {}; + // Rearrange additional metadata by scope. + state.additional.forEach(additional => { + const element = { + property: this.createDataElement(true, additional.id, 'additional_property', additional.property), + qualifier: this.createDataElement(false, additional.id, 'additional_qualifier', additional.qualifier), + value: this.createDataElement(false, additional.id, 'additional_value', additional.value), + id: additional.id, + }; + if (!scopeHolder[additional.scope]) { + scopeHolder[additional.scope] = []; + } + scopeHolder[additional.scope].push(element); + }); + for (const scope in scopeHolder) { + const current = { + name: scope, + firstProp: scopeHolder[scope][0].id, + properties: scopeHolder[scope], + input: this.createDataElement(true, scopeHolder[scope][0].id, 'additional_scope', scope) + }; + data.scope.push(current); + } + + data.creator = { + firstname: this.createDataElement(false, 0, 'creator_firstName', state.creator.firstName), + lastname: this.createDataElement(false, 0, 'creator_lastName', state.creator.lastName), + institution: this.createDataElement(false, 0, 'creator_institution', state.creator.institution), + year: this.createDataElement(false, 0, 'creator_year', state.creator.year), + }; + + data.json = { + required: true, + element: { + value: metadata.jsonStringify(state, 4), + attributes: 'rows="10"', + wrapperid: 'fitem_metadata_json', + id: 'id_metadata_json', + name: 'metadata_json', + } + }; + + // To render a child component we need a container. + const metadataContainer = this.getElement(this.selectors.METADATACONTAINER); + if (!metadataContainer) { + throw new Error('Missing metadata container.'); + } + + await this.renderComponent(metadataContainer, 'qtype_stack/metadata/metadatacontent', data); + + // Add all the event listeners as all elements have been destroyed and rebuilt. + this.addEventListener( + this.getElement(this.selectors.UPDATEJSON), + 'click', + this.update + ); + const addButtons = this.getElements(this.selectors.ADDITEM); + for (const addButton of addButtons) { + this.addEventListener( + addButton, + 'click', + this.addItem + ); + } + const deleteButtons = this.getElements(this.selectors.DELETEITEM); + for (const deleteButton of deleteButtons) { + this.addEventListener( + deleteButton, + 'click', + this.deleteItem + ); + } + this.addEventListener( + this.getElement(this.selectors.UPDATEINPUTS), + 'click', + this.updateInputs + ); + this.addEventListener( + this.getElement(this.selectors.MAKECREATOR), + 'click', + this.makeCreator + ); + this.addEventListener( + this.getElement(this.selectors.MAKECONTRIBUTOR), + 'click', + this.makeContributor + ); + this.addEventListener( + this.getElement(this.selectors.REVERT), + 'click', + this.revert + ); + + // Deal with case of brkon JSON in saved question. The errormessage is saved on initial setup. + // We load in the original un-prettified JSON and display error message, giving user chance to edit. + // After this, though, they'll need to sort it out - if we're back here again then we'll use + // JSON created from current content of state. + if (metadata.lib.brokenMetadata) { + const jsonElement = this.getElement(this.selectors.JSONINPUT); + jsonElement.value = document.querySelector(this.selectors.FORMJSON).value ?? ''; + notifyFieldValidationFailure(jsonElement, metadata.lib.brokenMetadata); + delete metadata.lib.brokenMetadata; + } + } + + /** + * Updates state based on contents of inputs. + * + * @param {bool} mustValidate Do we want validation to occur? + * We check when explicitly asked for and when attempting to close the modal other than by cancel. + * We don't check when e.g. adding a contributor. This means state can be invalid but we only + * update the edit form entry after successful validation on modal close. + * @returns {bool} Returns false on validation error. + */ + async update(mustValidate = true) { + if (mustValidate) { + // TO-DO Do we need other validation and/or different required fields. + const requiredElements = this.getElements(this.selectors.REQUIREDINPUTS); + let isError = false; + for (const element of requiredElements) { + if (element.value === '') { + isError = true; + notifyFieldValidationFailure(element, 'Required'); + } else if (element.classList.contains('is-invalid')) { + // Reset warning as field no longer empty. + notifyFieldValidationFailure(element, ''); + } + } + // Validate freeform JSON if non-empty. + const freeformElement = this.getElement('#smdi_0_freeform_value'); + if (freeformElement && freeformElement.value.trim() !== '') { + try { + JSON.parse(freeformElement.value); + notifyFieldValidationFailure(freeformElement, ''); + } catch(e) { + notifyFieldValidationFailure(freeformElement, e.message); + isError = true; + } + } + if (isError) { + return false; + } + } + // Elements have ids in form smdi_id_category_field e.g. smdi_1_contributor_year. + // id is category entry id in state. 0 is used for single elements e.g. license. + // Multi-elements begin counting from 1. + let inputElements = this.getElements(this.selectors.ALLINPUTS); + inputElements = Array.from(inputElements).map((el) => [el.id, el.value]); + try { + await this.reactive.dispatch('updateAll', inputElements); + } catch (e) { + const addIds = e.split(','); + for (const id of addIds) { + const element = this.getElement('#qtype-stack-metadata-content [id="smdi_' + id + '_additional_qualifier"]'); + notifyFieldValidationFailure(element, 'Required'); + } + return false; + } + return true; + } + + /** + * Add a new row to modal form. + * We have to update state from the input fields first or any changes will + * be wiped when we refresh the display to show the new row. + * + * @param {*} event + */ + async addItem(event) { + const result = await this.update(true); + if (result) { + const parts = event.target.id.split('_'); + this.reactive.dispatch('addItem', parts[1], parts[2]); + } + } + /** + * Delete a row from modal form. + * We have to update state from the input fields first or any changes will + * be wiped when we refresh the display toremove the row + * + * @param {*} event + */ + async deleteItem(event) { + const result = await this.update(false); + if (result) { + const parts = event.target.id.split('_'); + this.reactive.dispatch('deleteRow', parts[1], parts[2]); + } + } + + /** + * Update state from the currently entered JSON if JSON is valid. + */ + updateInputs() { + const jsonElement = this.getElement(this.selectors.JSONINPUT); + let data = null; + try { + data = metadata.jsonToState(jsonElement.value); + notifyFieldValidationFailure(jsonElement, ''); + } catch (e) { + notifyFieldValidationFailure(jsonElement, e.message); + return; + } + jsonElement.value = metadata.jsonStringify(data, 4); + this.reactive.dispatch('updateFromJson', data); + } + + /** + * Add the current user as a contributor. + */ + async makeContributor() { + const result = await this.update(false); + if (result) { + this.reactive.dispatch('addItem', 'contributor', 'user'); + } + } + + /** + * Make current user the creator. + */ + makeCreator() { + this.getElement('#smdi_0_creator_firstName').value = metadata.lib.user.firstname; + this.getElement('#smdi_0_creator_lastName').value = metadata.lib.user.lastname; + this.getElement('#smdi_0_creator_institution').value = metadata.lib.user.institution; + this.getElement('#smdi_0_creator_year').value = new Date().getFullYear(); + } + + /** + * Return JSON to the current version on the edit form. This will be either the saved + * version from the question or the update from a previous close and validate of the metadata modal. + * If the JSON is valid, update the state so the inputs match. If invalid, setup as on initial failure + * in metadata.js. + */ + revert() { + const jsonElement = this.getElement(this.selectors.JSONINPUT); + let previousdataJSON = document.querySelector(this.selectors.FORMJSON).value ?? null; + let previousdata = null; + try { + previousdata = metadata.jsonToState(previousdataJSON); + notifyFieldValidationFailure(jsonElement, ''); + } catch (e) { + notifyFieldValidationFailure(jsonElement, e.message); + jsonElement.value = previousdataJSON; + metadata.lib.brokenMetadata = e.message; + this.reactive.dispatch('updateFromJson', metadata.jsonToState('{}')); + return; + } + jsonElement.value = metadata.jsonStringify(previousdata, 4); + this.reactive.dispatch('updateFromJson', previousdata); + } +} \ No newline at end of file diff --git a/amd/src/metadata/events.js b/amd/src/metadata/events.js new file mode 100644 index 00000000000..3c238381861 --- /dev/null +++ b/amd/src/metadata/events.js @@ -0,0 +1,54 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +import {dispatchEvent} from 'core/event_dispatcher'; + +/** + * Javascript events for STACK metadata. + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +/** + * Events for STACK metadata + * + * @constant + * @property {String} qtypeStackStateUpdated See {@link event:qtypeStackStateUpdated} + */ +export const eventTypes = { + /** + * Event triggered when the activity reactive state is updated. + * + * @event qtypeStackStateUpdated + * @type {CustomEvent} + * @property {Array} nodes The list of parent nodes which were updated + */ + qtypeStackStateUpdated: 'qtype_stack/stateUpdated', +}; + +/** + * Trigger an event to indicate that the activity state is updated. + * + * @method qtypeStackStateUpdated + * @param {object} detail the full state + * @param {HTMLElement} container the custom event target (document if none provided) + * @returns {CustomEvent} + * @fires qtypeStackStateUpdated + */ +export const notifyQtypeStackStateUpdated = (detail, container) => { + return dispatchEvent(eventTypes.qtypeStackStateUpdated, detail, container); +}; \ No newline at end of file diff --git a/amd/src/metadata/metadata.js b/amd/src/metadata/metadata.js new file mode 100644 index 00000000000..f611e3da6d9 --- /dev/null +++ b/amd/src/metadata/metadata.js @@ -0,0 +1,339 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +/** + * Metadata entry reactive component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +import {Reactive} from 'core/reactive'; +import {mutations} from 'qtype_stack/metadata/mutations'; +import {eventTypes, notifyQtypeStackStateUpdated} from 'qtype_stack/metadata/events'; + +class StackMetadata extends Reactive { + // Default and config data passed through from Moodle. + lib = { + languages: ['en'], + user: { + firstname: '', + lastname: '', + institution: '', + year: '' + }, + licenses: [{value: 'unknown', text: 'unknown'}], + placeholder: '' + }; + + /** + * Load initial value of state from value on form. + */ + loadState() { + let metadata = document.querySelector('input[name="metadata"]'); + const metadataJSON = metadata.value ?? null; + try { + this.lib = JSON.parse(metadata.dataset.lib); + this.lib.user.year = new Date().getFullYear(); + // Weed out duplicates and falsy values. + let languages = new Set(this.lib.languages); + this.lib.languages = languages.difference(new Set([null, undefined, ""])); + this.lib.languages = Array.from(this.lib.languages); + } catch (e) { + // Lib will be set to defaults. + } + try { + metadata = this.jsonToState(metadataJSON); + } catch (e) { + // If the saved data is broken, show empty inputs and save error message for display in modal. + this.lib.brokenMetadata = e.message; + metadata = this.jsonToState('{}'); + } + metadata.metadataTicker = {value: 1}; + this.setInitialState(metadata); + } + + /** + * Replacer function for JSON stringify of state. + * Removed unwanted properties and converts some objects to plain values. + * + * @param {*} key + * @param {*} value + * @returns + */ + replacer(key, value) { + const languages = []; + const additional = {}; + switch(key) { + case 'metadataTicker': + return undefined; + case 'id': + return undefined; + case 'language': + for (const lang of value) { + languages.push(lang.value); + } + return languages; + case 'license': + case 'isPartOf': + return value.value; + case 'freeform': + if (!value.value) { + return '{}'; + } + try { + // If we parse the value here then the replacer + // works on it recursively which gets messed up + // if someone uses property values to match our own. + return value.value; + } catch(e) { + return undefined; + } + case 'additional': + for (const item of value) { + if (item.scope in additional === false) { + additional[item.scope] = {}; + } + if (item.property in additional[item.scope] === false && item.qualifier) { + additional[item.scope][item.property] = {}; + } + let currentValue = null; + if (item.qualifier === '') { + currentValue = additional[item.scope][item.property]; + } else { + currentValue = additional[item.scope][item.property][item.qualifier]; + } + let value = null; + // If we have multiple values, we need to convert to an array. + if (!currentValue) { + value = item.value; + } else if (!Array.isArray(currentValue)) { + value = [currentValue, item.value]; + } else { + value = currentValue.concat([item.value]); + } + if (item.qualifier === '') { + additional[item.scope][item.property] = value; + } else { + additional[item.scope][item.property][item.qualifier] = value; + } + } + return JSON.stringify(additional); + default: + return value; + } + + } + + /** + * Convert state into a JSON string. + * We can't simply stringify because of the freeform and additional properties. + * We have to deal with them as strings before this to prevent the replacer + * acting recursively. + * + * @param {*} state + * @param {*} spacing + * @returns + */ + jsonStringify(state, spacing) { + let output = JSON.stringify(state, this.replacer); + output = JSON.parse(output); + output.freeform = JSON.parse(output.freeform); + output.additional = JSON.parse(output.additional); + output = JSON.stringify(output, null, spacing); + return output; + } + + /** + * Reviver function for JSON parsing to feed into state. + * Adds id values and converts strings to obj.value. + * + * @param {*} key + * @param {*} value + * @returns + */ + reviver(key, value) { + const holder = []; + let id = 1; + switch(key) { + case 'contributor': + for (const current of value) { + current.id = id; + holder.push(current); + id++; + } + return holder; + case 'language': + for (const lang of value) { + holder.push({id: id, value: lang}); + id++; + } + return holder; + case 'license': + case 'isPartOf': + return {value: value}; + case 'freeform': + return {value: JSON.stringify(value)}; + default: + return value; + } + } + + /** + * Convert JSON to state format ready for updateFromJson mutation. + * Strips out extraneous fields; adds in missing fields with blank values. + * + * @param {*} data + * @returns + */ + jsonToState(data) { + data = JSON.parse(data); + for (let property in data) { + // We use the reviver once rather than recursively as it works from the bottom + // up and that causes issues if someone has re-used one of our property names. + data[property] = this.reviver(property, data[property]); + } + const fields = ['creator', 'contributor', 'language', 'license', 'isPartOf', 'additional', 'freeform']; + data = this.stripFields(data, fields); + const creatorFields = ['firstName', 'lastName', 'institution', 'year']; + const contribFields = ['id', 'firstName', 'lastName', 'institution', 'year']; + const standardFields = ['id', 'value']; + + data.creator = this.tidyObject(data.creator, creatorFields); + data.contributor = (Array.isArray(data.contributor)) ? data.contributor : []; + const contribHolder = []; + for (let contrib of data.contributor) { + contrib = this.tidyObject(contrib, contribFields); + contribHolder.push(contrib); + } + data.contributor = contribHolder; + data.language = (Array.isArray(data.language)) ? data.language : []; + const langHolder = []; + for (let lang of data.language) { + lang = this.tidyObject(lang, standardFields); + langHolder.push(lang); + } + data.language = langHolder; + data.isPartOf = this.tidyObject(data.isPartOf, standardFields); + data.license = this.tidyObject(data.license, standardFields); + const addHolder = []; + let addId = 1; + for (const addScope in data.additional) { + for (const addProperty in data.additional[addScope]) { + if ( + data.additional[addScope][addProperty] && + typeof data.additional[addScope][addProperty] === 'object' && + !Array.isArray(data.additional[addScope][addProperty]) + ) { + for (const addQualifier in data.additional[addScope][addProperty]) { + let values = data.additional[addScope][addProperty][addQualifier]; + values = (Array.isArray(values)) ? values : [values]; + for (const value of values) { + const add = { + id: addId, + scope: addScope, + property: addProperty, + qualifier: addQualifier, + value: value + }; + addHolder.push(add); + addId++; + } + } + } else { + let values = data.additional[addScope][addProperty]; + values = (Array.isArray(values)) ? values : [values]; + for (const value of values) { + const add = { + id: addId, + scope: addScope, + property: addProperty, + qualifier: '', + value: value + }; + addHolder.push(add); + addId++; + } + } + } + } + data.additional = addHolder; + data.freeform = this.tidyObject(data.freeform, standardFields); + + return data; + } + + /** + * Remove any properties from an object that are not in a supplied array of property names. + * + * @param {object} obj + * @param {array} fields + * @returns {object} + */ + stripFields(obj, fields) { + const result = {}; + for (const suppliedField in obj) { + if (fields.includes(suppliedField)) { + result[suppliedField] = obj[suppliedField]; + } + } + return result; + } + + /** + * Add any missing properties to an object from a supplied array of field names and set to ''. + * + * @param {object} obj + * @param {array} fields + * @returns + */ + addFields(obj, fields) { + for (const field of fields) { + if (!Object.hasOwn(obj, field)) { + obj[field] = ''; + } else { + obj[field] = String(obj[field]); + } + } + return obj; + } + + /** + * Set properties of an object to those from a supplied array of field names. + * + * @param {object} obj + * @param {array} fields + * @returns + */ + tidyObject(obj, fields) { + obj = (obj && typeof obj === 'object') ? obj : {}; + obj = this.stripFields(obj, fields); + obj = this.addFields(obj, fields); + return obj; + } +} + +/** + * The metadata state instance. + */ +export const metadata = new StackMetadata({ + name: 'qtype_stack_metadata', + eventName: eventTypes.qtypeStackStateUpdated, + eventDispatch: notifyQtypeStackStateUpdated, + mutations, +}); + + diff --git a/amd/src/metadata/metadatacontent.js b/amd/src/metadata/metadatacontent.js new file mode 100644 index 00000000000..1f457a48a3c --- /dev/null +++ b/amd/src/metadata/metadatacontent.js @@ -0,0 +1,50 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +/** + * Main STACK metadata component + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +import {BaseComponent} from 'core/reactive'; +import {metadata} from 'qtype_stack/metadata/metadata'; + +export default class extends BaseComponent { + create() { + this.name = 'stack-metadata-content'; + this.selectors = { + METADATACONTAINER: `[data-for='qtype-stack-metadata']`, + SUBMIT: `#stack-metadata-update`, + }; + } + + /** + * Static method to create a component instance form the mustache template. + * + * @param {string} target the DOM main element or its ID + * @param {object} selectors optional css selector overrides + * @return {Component} + */ + static init(target, selectors) { + return new this({ + element: document.querySelector(target), + reactive: metadata, + selectors, + }); + } +} \ No newline at end of file diff --git a/amd/src/metadata/metadatamodal.js b/amd/src/metadata/metadatamodal.js new file mode 100644 index 00000000000..6ef463a6eec --- /dev/null +++ b/amd/src/metadata/metadatamodal.js @@ -0,0 +1,121 @@ +// This file is part of Stack - http://stack.maths.ed.ac.uk/ +// +// Stack is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Stack is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Stack. If not, see . + +/** + * STACK metadata modal setup + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ + +import Modal from 'core/modal'; +import {metadata} from 'qtype_stack/metadata/metadata'; +import container from 'qtype_stack/metadata/container'; + +export class MetadataModal extends Modal { + static TYPE = "qtype_stack/metadatamodal"; + static TEMPLATE = "qtype_stack/metadata/metadatamodal"; + + /** + * Override the default hide function to validate and update metadata JSON. + * On success, stores new JSON to hidden edit form field and closed modal. + */ + async hide() { + const result = await metadata.container.update(true); + if (result) { + const current = document.querySelector('input[name="metadata"]'); + const newValue = metadata.jsonStringify(metadata.state, 0); + if (current.value !== newValue) { + document.querySelector('input[name="metadata"]').value = newValue; + try { + document.querySelector('[data-name="metadata_text"]').textContent = + document.querySelector('#id_stack_metadata').getAttribute('data-change'); + } catch (e) { + // Don't update in Moodle 4.2. + } + } + super.hide(); + } + } + + /** + * Cancel button needs to close the modal without updating form. + */ + cancel() { + super.hide(); + } +} + +let registered = false; +if (!registered) { + registerModal(); + registered = true; +} + +let modal = null; + +// Prepare for modal creation. +export const setup = () => { + document.querySelector('#id_metadatamodal')?.addEventListener('click', openModal); + metadata.loadState(); +}; + +/** + * Need to pass appropriate 'this' to cancel function. + */ +function closeModal() { + modal.cancel.call(modal); +} + +/** + * Register the modal in old versions of Moodle. + */ +async function registerModal() { + if (typeof MetadataModal.create !== "function") { + const ModalRegistry = await import('core/modal_registry'); + ModalRegistry.register(MetadataModal.TYPE, MetadataModal, MetadataModal.TEMPLATE); + } +} + +/** + * Open the metadata modal. + * Only create modal and add listener once. + */ +async function openModal() { + let addListener = false; + if (!modal) { + if (typeof MetadataModal.create === "function") { + modal = await MetadataModal.create(); + modal.show(); + } else { + // Pre Moodle 4.3 code. + const ModalFactory = await import ('core/modal_factory'); + modal = await ModalFactory.create({ + type: MetadataModal.TYPE, + }); + modal.show(); + // Why is this necessary? Mustache should do this anyway. Moodle bug? + container.init('#qtype-stack-metadata-main'); + } + addListener = true; + } else { + modal.show(); + } + if (addListener) { + document.querySelector('#stackmetadata_cancel').addEventListener('click', closeModal); + } +} + diff --git a/amd/src/metadata/mutations.js b/amd/src/metadata/mutations.js new file mode 100644 index 00000000000..02573a44cc6 --- /dev/null +++ b/amd/src/metadata/mutations.js @@ -0,0 +1,241 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Default mutation manager + * + * @module qtype_stack/metadata + * @copyright 2025 University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ +import {metadata} from 'qtype_stack/metadata/metadata'; + +class Mutations { + /** + * Update state from array of input field information. + * + * Inputs have ids in form smdi-id-category-field e.g. smdi-1-contributor-year. + * id is row entry id in state. 0 is used for single elements e.g. license. + * Multi-elements begin counting from 1. + * For scope, row id is for one of the matching additional info rows. + * @param {*} stateManager + * @param {*} inputArray [['smdi-1-contributor-year', 2025], ...] + * @returns + */ + async updateAll(stateManager, inputArray) { + let state = stateManager.state; + const additionalCopy = {}; + if (state.additional) { + state.additional.forEach((addInfo) => { + const rowCopy = { + scope: addInfo.scope, + property: addInfo.property, + qualifier: addInfo.qualifier, + value: addInfo.value + }; + additionalCopy[addInfo.id] = rowCopy; + }); + } + for (const field of inputArray) { + const parts = field[0].split('_'); + const id = parts[1]; + const property = parts[2]; + const subproperty = parts[3]; + if (property !== 'additional') { + continue; + } + if (subproperty === 'scope') { + // Scope input updates multiple rows. + // We find all entries for that scope and update. + const existingScope = additionalCopy[id].scope; + if (existingScope !== field[1]) { + for (const key in additionalCopy) { + const addInfo = additionalCopy[key]; + if (addInfo.scope === existingScope) { + addInfo.scope = field[1]; + } + } + } + } else { + const existing = additionalCopy[id]; + if (existing) { + existing[subproperty] = field[1]; + } + } + } + + let problems = new Set(); + for (const key1 in additionalCopy) { + const addInfo = additionalCopy[key1]; + if (addInfo.qualifier === '') { + for (const key2 in additionalCopy) { + const addInfo2 = additionalCopy[key2]; + if ( + addInfo.scope === addInfo2.scope && addInfo.property === addInfo2.property && addInfo2.qualifier !== '' + ) { + problems.add(key1); + } + } + } + } + problems = Array.from(problems); + if (problems.length) { + const output = problems.join(','); + return Promise.reject(output); + } + stateManager.setReadOnly(false); + for (const field of inputArray) { + const parts = field[0].split('_'); + const id = parts[1]; + const property = parts[2]; + const subproperty = parts[3]; + if (subproperty === 'scope') { + // Scope input updates multiple rows. + // We find all entries for that scope and update. + const existingScope = state.additional.get(id).scope; + if (existingScope !== field[1]) { + state.additional.forEach((addInfo) => { + if (addInfo.scope === existingScope) { + addInfo.scope = field[1]; + } + }); + } + } else if (id != 0) { + const existing = state[property].get(id); + if (existing) { + existing[subproperty] = field[1]; + } + } else { + state[property][subproperty] = field[1]; + } + } + + // Force display refresh in odd circumstances where state has not changed + // but JSON needs to be updated. + state.metadataTicker.value += 1; + stateManager.setReadOnly(true); + return Promise.resolve('Success'); + } + + /** + * Delete a row from the metadata form. + * + * @param {*} stateManager + * @param {*} property type to be deleted + * @param {*} id of instance to be deleted. Form will be refreshed and ids reset. + */ + deleteRow(stateManager, property, id) { + const state = stateManager.state; + stateManager.setReadOnly(false); + if (property === 'scope') { + const matchingAddInfo = []; + const scope = state.additional.get(id).scope; + // Need to delete ALL entries with the same scope + // as the supplied additional info. + state.additional.forEach((addInfo) => { + if (addInfo.scope === scope) { + matchingAddInfo.push(addInfo.id); + } + }); + for (const current of matchingAddInfo) { + state.additional.delete(current); + } + } else { + state[property].delete(id); + } + stateManager.setReadOnly(true); + } + + /** + * Add a row + * + * @param {*} stateManager + * @param {*} category + * @param {*} id Only required for additional info. Allows us to get relevant scope. + */ + addItem(stateManager, category, id) { + const state = stateManager.state; + let addCategory = category; + let newItem = null; + let existingProperty = null; + switch (category) { + case 'language': + newItem = { + value: "" + }; + break; + case 'contributor': + newItem = { + firstName: (id === 'user') ? metadata.lib.user.firstname : "", + lastName: (id === 'user') ? metadata.lib.user.lastname : "", + institution: (id === 'user') ? metadata.lib.user.institution : "", + year: String(new Date().getFullYear()) + }; + break; + case 'scope': + newItem = { + scope: '', + property: '', + qualifier: '', + value: '' + }; + addCategory = 'additional'; + break; + case 'property': + existingProperty = state.additional.get(id); + newItem = { + scope: existingProperty.scope, + property: '', + qualifier: '', + value: '' + }; + addCategory = 'additional'; + break; + default: + } + + // Ids are required for all objects. We add one to highest existing id. + const keys = Array.from(state[addCategory]); + if (keys.length === 0) { + newItem.id = 1; + } else { + keys.sort((a, b) => b[0] - a[0]); + newItem.id = 1 + parseInt(keys[0][0]); + } + stateManager.setReadOnly(false); + state[addCategory].add(newItem); + stateManager.setReadOnly(true); + } + + /** + * Straight update of state. + * + * @param {*} stateManager + * @param {object} data Output of metadata.jsonToState + */ + updateFromJson(stateManager, data) { + const state = stateManager.state; + stateManager.setReadOnly(false); + for (const prop in data) { + state[prop] = data[prop]; + } + // Force display refresh in case inputs have been altered but JSON not changed. + // Inputs will be matched to JSON. + state.metadataTicker.value += 1; + stateManager.setReadOnly(true); + } +} + +export const mutations = new Mutations(); \ No newline at end of file diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php index 34fb042bc4d..8acdccf10d1 100644 --- a/api/util/StackQuestionLoader.php +++ b/api/util/StackQuestionLoader.php @@ -211,6 +211,10 @@ public static function loadxml($xml, $includetests = false) { $question->compiledcache = []; $question->isbroken = (array) $xmldata->question->isbroken ? self::parseboolean($xmldata->question->isbroken) : self::get_default('question', 'isbroken', 0); + $question->metadata = + (string) $xmldata->question->metadata ? (string) $xmldata->question->metadata : ''; + $question->prescribedmetadata = + (string) $xmldata->question->prescribedmetadata ? (string) $xmldata->question->prescribedmetadata : ''; $question->options = new \stack_options(); $question->options->set_option( 'multiplicationsign', diff --git a/backup/moodle2/backup_qtype_stack_plugin.class.php b/backup/moodle2/backup_qtype_stack_plugin.class.php index 6079164650a..591d15bf8f8 100644 --- a/backup/moodle2/backup_qtype_stack_plugin.class.php +++ b/backup/moodle2/backup_qtype_stack_plugin.class.php @@ -58,6 +58,7 @@ protected function define_question_plugin_structure() { 'scientificnotation', 'multiplicationsign', 'sqrtsign', 'complexno', 'inversetrig', 'logicsymbol', 'matrixparens', 'variantsselectionseed', 'isbroken', + 'metadata', 'prescribedmetadata', ] ); diff --git a/backup/moodle2/restore_qtype_stack_plugin.class.php b/backup/moodle2/restore_qtype_stack_plugin.class.php index 3bdc4fc6aa4..9de7c267fd2 100644 --- a/backup/moodle2/restore_qtype_stack_plugin.class.php +++ b/backup/moodle2/restore_qtype_stack_plugin.class.php @@ -88,6 +88,12 @@ public static function convert_backup_to_questiondata(array $backupdata): stdCla if (!property_exists($questiondata->options, 'isbroken')) { $questiondata->options->isbroken = 0; } + if (!property_exists($questiondata->options, 'metadata')) { + $questiondata->options->metadata = ''; + } + if (!property_exists($questiondata->options, 'prescribedmetadata')) { + $questiondata->options->prescribedmetadata = ''; + } if (!property_exists($questiondata->options, 'inversetrig')) { $questiondata->options->inversetrig = 'cos-1'; @@ -208,6 +214,14 @@ public function process_qtype_stack_options($data) { $data->isbroken = 0; } + if (!property_exists($data, 'metadata')) { + $data->metadata = ''; + } + + if (!property_exists($data, 'prescribedmetadata')) { + $data->prescribedmetadata = ''; + } + if (!property_exists($data, 'inversetrig')) { $data->inversetrig = 'cos-1'; } diff --git a/classes/output/metadatamodal.php b/classes/output/metadatamodal.php new file mode 100644 index 00000000000..3d584cd34d3 --- /dev/null +++ b/classes/output/metadatamodal.php @@ -0,0 +1,38 @@ +. + +namespace qtype_stack\output; + +use renderable; +use templatable; + +/** + * Render metadata modal + * + * @package qtype_stack + * @copyright 2026 The University of Edinburgh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class metadatamodal implements renderable, templatable { + /** + * Constructor. + * + */ + public function __construct() { + global $PAGE; + $PAGE->requires->js_call_amd('qtype_stack/metadata/metadatamodal', 'init'); + } +} diff --git a/db/install.xml b/db/install.xml index 0c9bdbdee2a..82eb5197237 100644 --- a/db/install.xml +++ b/db/install.xml @@ -1,7 +1,7 @@ - @@ -10,6 +10,8 @@ + + diff --git a/db/upgrade.php b/db/upgrade.php index 28c225e3e8a..62bce52d5ef 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -1045,6 +1045,35 @@ function xmldb_qtype_stack_upgrade($oldversion) { // STACK savepoint reached. upgrade_plugin_savepoint(true, 2026042402, 'qtype', 'stack'); } + + + if ($oldversion < 2026042600) { + // Define field metadata to be added to qtype_stack_options. + $table = new xmldb_table('qtype_stack_options'); + $field = new xmldb_field('metadata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'isbroken'); + + // Conditionally launch add field metadata. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Stack savepoint reached. + upgrade_plugin_savepoint(true, 2026042600, 'qtype', 'stack'); + } + + if ($oldversion < 2026042700) { + // Define field prescribedmetadata to be added to qtype_stack_options. + $table = new xmldb_table('qtype_stack_options'); + $field = new xmldb_field('prescribedmetadata', XMLDB_TYPE_TEXT, null, null, null, null, null, 'isbroken'); + + // Conditionally launch add field prescribedmetadata. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Stack savepoint reached. + upgrade_plugin_savepoint(true, 2026042700, 'qtype', 'stack'); + } // Add new upgrade blocks just above here. // Check the version of the Maxima library code that comes with this version diff --git a/doc/content/metadata.png b/doc/content/metadata.png new file mode 100644 index 00000000000..e8020029aca Binary files /dev/null and b/doc/content/metadata.png differ diff --git a/doc/en/Authoring/Metadata.md b/doc/en/Authoring/Metadata.md new file mode 100644 index 00000000000..66afce4db54 --- /dev/null +++ b/doc/en/Authoring/Metadata.md @@ -0,0 +1,153 @@ +# Question metadata + +STACK questions support flexible metadata. Metadata is recorded in JSON format in two database fields - `metadata` and `prescribedmetadata`. The `metadata` field holds user-editable metadata. `prescribedmetadata` is currently empty and is reserved for storage of automatically generated and maintained metadata in the future, including a universal question id and some form of question ancestory. + +New questions will be created with editable `metadata` similar to the following: +``` +{ + "creator": { + "firstName": "Current", //$USER->firstname + "lastName": "User", // $USER->lastname + "institution": "", // $USER->institution + "year": "2026" // Current year + }, + "contributor": [], + "language": [ + "en" // Current language + ], + "license": "unknown", // $CFG->sitedefaultlicense + "isPartOf": "", + "additional": {}, + "freeform" {} +} +``` +A question has one creator but can have multiple contributors. `Last name` is required. + +Additional metadata is stored in Scope->Property->Qualifier->Value or Scope->Property->Value format. Scope identifies the metadata scheme being used. For instance, two institutions might have the property `Level` that has different meanings. Scope allows differentiation between the two: +``` +"additional": { + "Edinburgh": { + "Level": "Undergraduate" + }, + "Glasgow": { + "Level": "Year 2" + } +} +``` +A property within a scope can have qualifiers: +``` +"additional": { + "Edinburgh": { + "Course": { + "Name": "Introductory Maths", + "Week": "3" + } + } +} +``` +or simply a value: +``` +"additional": { + "Edinburgh": { + "Course": "Introductory Maths" + } +} +``` +If any entry for a property has a qualifier, then all entries must do so in order for valid JSON to be created: +``` +"additional": { + "Scope1": { + "Property1": { + "Qualifier1": "Value1", + "Qualifier2": "Value2", + "Qualifier3": [ + "Value3", + "Value4" + ] + }, + "Property2": [ + "Value5", + "Value6", + "Value7" + ], + "Property3": "Value8" + }, + "Scope2": { + "Property1": "Value2", + "Property4": "Value4" + } +} +``` +The freeform metadata input allows entry of further metadata in whatever format the user requires. It simply has to be valid JSON. This can also be added in the main JSON metadata input field but (unlike `additional` entries) does not create additional input fields and buttons. This field is for power users who require greater nesting depth in their metadata. + +All information in the question `metadata` field can be updated via the button 'View and edit full metadata' in the question edit form. This launches a pop-up where the metadata information can be updated via input boxes or the JSON can be manually amended and validated. + +![Metadata popup in action](../../content/metadata.png) + +Update the input boxes and click 'Validate inputs and update JSON' to display the JSON output. Update the JSON and click 'Update inputs from JSON' to fill in the input boxes from the JSON. 'Validate and close' will take the contents of the input boxes, validate them and then create and store JSON ready to be saved as part of the STACK question. The question edit form must still be saved normally once the pop-up has closed in order to save this JSON to the question. + +Please note that although you can update either the JSON or the input boxes, the form saves the contents of the input boxes. If you update the JSON you must click 'Update inputs from JSON' without a JSON error being displayed before clicking 'Validate and close'. + +Example metadata: +``` +{ + "creator": { + "firstName": "Dave", + "lastName": "Summers", + "institution": "Edinburgh", + "year": "2025" + }, + "contributor": [ + { + "firstName": "Bob", + "lastName": "Smith", + "institution": "Open University", + "year": "2026" + }, + { + "firstName": "Tim", + "lastName": "Jones", + "institution": "", + "year": "2026" + } + ], + "language": [ + "en", + "de" + ], + "isPartOf": "HELM", + "license": "cc-4.0", + "additional": { + "Edinburgh": { + "Course": { + "Name": "Introductory Maths", + "Week": "3", + "Code": [ + "AA", + "BB" + ] + }, + "Topic": [ + "Calculus", + "Geometry", + "Statistics" + ], + "Level": "Undergraduate" + }, + "HELM": { + "Workbook": "10", + "Level": "Basic" + } + }, + "freeform": {} +} +``` +Both metadata fields are exported and imported normally as part of Moodle XML, allowing automated addition of metadata to large question banks with the aid of Gitsync. +``` + + {"id": XXXX-XXX-XXXX_XXXX} + + + {"creator":{"firstName":"Dave","lastName":"Summers","institution":"Edinburgh","year":"2025"},"contributor":[{"firstName":"Bob","lastName":"Smith","institution":"Open University","year":"2026"}],"language":["en"],"isPartOf":"HELM","license":"cc-4.0","additional":{"UoE":{"Course":{"Name":"Introductory Maths","Week":"3"},"Topic":"Calculus"}},"freeform":{}} + +``` \ No newline at end of file diff --git a/doc/en/Authoring/index.md b/doc/en/Authoring/index.md index 37185c4221b..e6cb334bbe1 100644 --- a/doc/en/Authoring/index.md +++ b/doc/en/Authoring/index.md @@ -17,6 +17,7 @@ A "question" is the basic object in the system. The following table shows the f | [Inputs](Inputs/index.md) | The inputs are the things, such as form boxes, with which the student actually interacts. | [Potential response trees](Potential_response_trees.md) | These are the algorithms which establish the mathematical properties of the students' answers and generate feedback. | [Options](Question_options.md) | Many behaviours can be changed with the options. +| [Metadata](Metadata.md) | STACK questions support flexible metadata. The authoring documentation also covers topics on: diff --git a/doc/en/Developer/Development_track.md b/doc/en/Developer/Development_track.md index 7a61b9267a7..c16ebf33403 100644 --- a/doc/en/Developer/Development_track.md +++ b/doc/en/Developer/Development_track.md @@ -12,6 +12,7 @@ Issues with [github milestone 4.13.0](https://github.com/maths/moodle-qtype_stac 1. Remove all "cte" code from Maxima - mostly install. 2. Support for Maxima 5.47.0, 5.48.0, and 5.49.0. This includes a fix for issue #1281 from 5.48.0. 3. Question tests can now test the whole route through a PRT, rather than just the final node. This is a significant improvement on the ability to test questions. This is back-compatible with older questions. +4. Add in flexible [question metadata](../Authoring/Metadata.md). This fixes issue #1171. -------------------------------------- diff --git a/doc/en/Developer/Future_plans.md b/doc/en/Developer/Future_plans.md index 6d862f88b26..c80e832f331 100644 --- a/doc/en/Developer/Future_plans.md +++ b/doc/en/Developer/Future_plans.md @@ -137,7 +137,6 @@ Basic reports now work. * Text based potential response trees (would allow for easier copying of complicated trees, etc). * Changes to preset feedback to certain answer tests which might be more appropriate for different audiences. Could a 'simplified' English language pack allow for this (future changes might allow this to be done on a question-by-question basis). * Check for potential issues with default correct/incorrect feedback for different languages (defaults can already be set on the server level by a Moodle administrator). -* Metadata on language for questions. * Tools for language integrity (e.g. making it easier to identify what languages are in each question). * Making sure Maxima knows the intended language (will allow for Maxima code to choose from the available languages). * May want to have further discussions on how scores and penalties are handled (there is already a new feature in the latest version of STACK so that you can include functions in the "score" field. diff --git a/doc/en/Developer/Unit_tests.md b/doc/en/Developer/Unit_tests.md index 8335cf1fb00..c3476810a83 100644 --- a/doc/en/Developer/Unit_tests.md +++ b/doc/en/Developer/Unit_tests.md @@ -10,6 +10,8 @@ These three mechanisms aim to provide comprehensive testing of STACK. The last STACK uses the moodle-ci continuous integration mechanism via github actions so that all unit tests are triggered when a commit is pushed to github. +JavaScript unit tests added May 2026. + # PHP Unit tests Moodle uses PHPUnit for its unit tests. Setting this up and getting it working is a bit of a pain, but you only have to follow the instructions in [the Moodle PHPUnit documentation](http://docs.moodle.org/dev/PHPUnit) once to get it working. @@ -187,3 +189,11 @@ You need to output values to the file system, as the display can't manage this. In the STACK directory php cli/casstringtester.php --string="0..1" + +# Testing JavaScript + +In the tests/jest directory as a user with write permissions: +``` +npm install +npm test +``` diff --git a/doc/en/STACK_question_admin/Authoring_workflow.md b/doc/en/STACK_question_admin/Authoring_workflow.md index 69f510c023d..9edcec6dd46 100644 --- a/doc/en/STACK_question_admin/Authoring_workflow.md +++ b/doc/en/STACK_question_admin/Authoring_workflow.md @@ -69,7 +69,11 @@ In theory one test case should be created for each anticipated response which ge When updating a PRT at this stage we would _expect_ test cases added in step 2 to fail. This is reassuring as it indicates something significant has changed! You can easily confirm the new behaviour of the testcase is now what is intended. -### 6. Use data obtained from one cycle of attempts by students. +### 6. Add accurate metadata. + +Check and add accurate [question metadata](../Authoring/Metadata.md). + +### 7. Use data obtained from one cycle of attempts by students. Rather than second-guess what students _might_ get wrong it is more effective to look at what they _do_. See the section on [reporting](Reporting.md) for documentation on how to review students' answers. When feedback/marks are delayed (e.g. online exam) this can be done between students taking the assessment and results being released. If feedback/marks are immediate then better quality feedback can still be usefully added later. diff --git a/doc/meta_en.json b/doc/meta_en.json index 6c0005dd32e..eeda8ebfbd2 100644 --- a/doc/meta_en.json +++ b/doc/meta_en.json @@ -463,6 +463,11 @@ "description":"Information on writing STACK questions for several languages." }, { + "file":"Metadata.md", + "title":"Question Metadata - STACK Documentation", + "description":"Information on STACK question metadata." + }, + { "file":"Question_options.md", "title":"Options - STACK Documentation", "description":"Documentation for the Option section in a STACK question." diff --git a/edit_stack_form.php b/edit_stack_form.php index ca5af061159..d282322549c 100644 --- a/edit_stack_form.php +++ b/edit_stack_form.php @@ -221,7 +221,7 @@ protected function definition() { // phpcs:ignore moodle.Commenting.MissingDocblock.Function protected function definition_inner(/* MoodleQuickForm */ $mform) { - global $OUTPUT; + global $OUTPUT, $PAGE, $CFG, $SESSION, $USER; // Load the configuration. $this->stackconfig = stack_utils::get_config(); @@ -271,6 +271,60 @@ protected function definition_inner(/* MoodleQuickForm */ $mform) { $warnings = ($warnings) ? $warnings . '
' : $warnings; $warnings .= '' . stack_string('usetextarea'); } + $PAGE->requires->js_call_amd('qtype_stack/metadata/metadatamodal', 'setup'); + $mform->addElement('button', 'metadatamodal', stack_string('editmetadata')); + $datalib = new \stdClass(); + $datalib->licenses = explode(',', $CFG->licenses ?? ''); + $datalib->licenses = array_map(function ($license) { + return ['value' => $license, 'text' => get_string($license, 'license')]; + }, $datalib->licenses); + $datalib->languages = [ + $PAGE->cm->lang ?? '', + $PAGE->course->lang ?? '', + $SESSION->lang ?? '', + $USER->lang ?? '', + $CFG->lang ?? '', + 'en', + ]; + $datalib->user = new stdClass(); + $datalib->user->firstname = $USER->firstname ?? ''; + $datalib->user->lastname = $USER->lastname ?? ''; + $datalib->user->institution = $USER->institution ?? ''; + $datalib->placeholder = stack_string('licenseselect'); + $datalib = json_encode($datalib); + if (!isset($this->question->id)) { + $data = '{"creator":{"firstName":"' . ($USER->firstname ?? '') . '","lastName":"' . ($USER->lastname ?? '') . '",' . + '"institution":"' . ($USER->institution ?? '') . '","year":"' . date('Y') . '"},' . + '"contributor":[],"language":["' . current_language() . '"],"license":"' . $CFG->sitedefaultlicense . '"}'; + } else { + $data = ($this->question->options->metadata) ? $this->question->options->metadata : '{}'; + } + $md = $mform->createElement( + 'hidden', + 'metadata', + $data, + ['data-lib' => $datalib, 'id' => 'id_stack_metadata', 'data-change' => stack_string('metadatachange')] + ); + $mform->insertElementBefore($md, 'metadatamodal'); + $mform->setType('metadata', PARAM_RAW); + + $metadataobj = json_decode($data ?? ''); + if ($metadataobj && $data !== '{}') { + $metadatasummary = stack_string('creator') . ': ' . + ($metadataobj->creator->firstName ?? '') . ' ' . ($metadataobj->creator->lastName ?? ''); + if (isset($metadataobj->contributor) && count($metadataobj->contributor)) { + $contribsummary = ''; + foreach ($metadataobj->contributor as $contrib) { + $contribsummary .= ($contribsummary) ? ', ' : ''; + $contribsummary .= ($contrib->firstName ?? '') . ' ' . ($contrib->lastName ?? ''); + } + $metadatasummary .= '; ' . stack_string('contributor') . ': ' . $contribsummary; + } + } else { + $metadatasummary = stack_string('novalidmetadata'); + } + $metadatatext = $mform->createElement('static', 'metadata_text', stack_string('metadatahighlights'), $metadatasummary); + $mform->insertElementBefore($metadatatext, 'metadatamodal'); // Note that for the editor elements, we are using $mform->getElement('prtincorrect')->setValue(...); instead // of setDefault, because setDefault does not work for editors. @@ -329,8 +383,8 @@ protected function definition_inner(/* MoodleQuickForm */ $mform) { if ($courseid = optional_param('courseid', 0, PARAM_INT)) { $liburlparams['courseid'] = $courseid; } - if ($cmid = optional_param('returnurl', null, PARAM_LOCALURL)) { - $liburlparams['returnurl'] = $cmid; + if ($returnurl = optional_param('returnurl', null, PARAM_LOCALURL)) { + $liburlparams['returnurl'] = $returnurl; } $qlibrarylink = html_writer::link( new moodle_url('/question/type/stack/questionlibrary.php', $liburlparams), @@ -1117,6 +1171,7 @@ protected function data_preprocessing_options($question) { $question->assumepositive = $opt->assumepositive; $question->assumereal = $opt->assumereal; $question->isbroken = $opt->isbroken; + $question->metadata = $opt->metadata; return $question; } diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php index bbdfbdce6a2..15a28389401 100644 --- a/lang/en/qtype_stack.php +++ b/lang/en/qtype_stack.php @@ -418,6 +418,39 @@ $string['moodleerrors'] = 'You have errors related to Moodle\'s basic question setup.'; $string['stackerrors'] = 'You have errors in your question.'; $string['markedasbroken'] = 'The question has been marked as broken and will not be accessible to students.'; +$string['editmetadata'] = 'View and edit full metadata'; +$string['additionalmetadata'] = 'Additional metadata'; +$string['novalidmetadata'] = 'No valid metadata'; +$string['metadata'] = 'Metadata'; +$string['metadatahighlights'] = 'Metadata'; +$string['metadatachange'] = 'Please save your changes.'; +$string['firstname'] = 'First name'; +$string['lastname'] = 'Last name'; +$string['institution'] = 'Institution'; +$string['year'] = 'Year'; +$string['contributor'] = 'Contributor'; +$string['creator'] = 'Creator'; +$string['addcontributor'] = 'Add contributor'; +$string['addscope'] = 'Add scope'; +$string['addproperty'] = 'Add property'; +$string['addlanguage'] = 'Add language'; +$string['updateJSON'] = 'Validate inputs and update JSON'; +$string['updateinputs'] = 'Update inputs from JSON'; +$string['makemecontributor'] = 'Add me as a contributor'; +$string['makemecreator'] = 'Make me the creator'; +$string['validateandclose'] = 'Validate and close'; +$string['JSONmetadata'] = 'JSON metadata'; +$string['JSONtoolong'] = 'There is too much metadata. Metadata should be under 32000 bytes.'; +$string['JSONbroken'] = 'Metadata is invalid JSON.'; +$string['metadataexplanation'] = 'STACK metadata is stored as a JSON object. You can edit metadata in the inputs above or directly edit the JSON below and then click \'Update inputs from JSON\'. On closing this metadata window, the inputs will be validated and, if this is successful you will be returned to the main question edit form. You will still need to save the question as normal to save the metadata.'; +$string['scope'] = 'Scope'; +$string['property'] = 'Property'; +$string['qualifier'] = 'Qualifier'; +$string['value'] = 'Value'; +$string['ispartof'] = 'isPartOf'; +$string['freeformmetadata'] = 'Freeform metadata'; +$string['reverttosaved'] = 'Revert current changes'; +$string['licenseselect'] = 'Type or select license'; // Strings used by input elements. $string['studentinputtoolong'] = 'Your input is longer than permitted by STACK.'; diff --git a/question.php b/question.php index 6f2a2993ba2..0259829641e 100644 --- a/question.php +++ b/question.php @@ -256,6 +256,14 @@ class qtype_stack_question extends question_graded_automatically_with_countback * @var bool is this question marked as broken. */ public $isbroken = false; + /** + * @var string The question metadata in JSON form. Can be set by users. + */ + public $metadata = ''; + /** + * @var string The metadata created by STACK itself in JSON form. + */ + public $prescribedmetadata = ''; /** * Make sure the cache is valid for the current response. If not, clear it. diff --git a/questiontype.php b/questiontype.php index 6d9e8449df0..0dfbade1244 100644 --- a/questiontype.php +++ b/questiontype.php @@ -266,6 +266,8 @@ public function save_question_options($fromform) { $options->matrixparens = $fromform->matrixparens; $options->variantsselectionseed = $fromform->variantsselectionseed; $options->isbroken = !empty($fromform->isbroken) ? 1 : 0; + $options->metadata = $fromform->metadata; + $options->prescribedmetadata = ""; // We will not have the values for this. $options->compiledcache = '{}'; @@ -638,6 +640,8 @@ protected function initialise_question_instance(question_definition $question, $ $question->variantsselectionseed = $questiondata->options->variantsselectionseed; $question->compiledcache = $questiondata->options->compiledcache; $question->isbroken = $questiondata->options->isbroken; + $question->metadata = $questiondata->options->metadata; + $question->prescribedmetadata = $questiondata->options->prescribedmetadata; // Parse the cache in advance. if (is_string($question->compiledcache)) { @@ -1657,6 +1661,8 @@ public function export_to_xml($questiondata, qformat_xml $format, $notused = nul $output .= " {$options->logicsymbol}\n"; $output .= " {$options->matrixparens}\n"; $output .= " {$options->isbroken}\n"; + $output .= " {$options->prescribedmetadata}\n"; + $output .= " {$options->metadata}\n"; $output .= " {$format->xml_escape($options->variantsselectionseed)}\n"; foreach ($questiondata->inputs as $input) { @@ -1825,6 +1831,8 @@ public function import_from_xml($xml, $fromform, qformat_xml $format, $notused = $fromform->assumereal = $format->getpath($xml, ['#', 'assumereal', 0, '#'], get_config('qtype_stack', 'assumereal')); $fromform->isbroken = $format->getpath($xml, ['#', 'isbroken', 0, '#'], 0); + $fromform->metadata = $format->getpath($xml, ['#', 'metadata', 0, '#'], ''); + $fromform->prescribedmetadata = $format->getpath($xml, ['#', 'prescribedmetadata', 0, '#'], ''); $fformat = FORMAT_HTML; if (isset($fromform->prtcorrectformat)) { $fformat = $fromform->prtcorrectformat; @@ -2404,6 +2412,14 @@ public function validate_fromform($fromform, $errors, $question = null) { $fromform['questiondescription']['text'] ); + $errors['metadata_text'] = []; + if (mb_strlen($fromform['metadata']) > 32000) { + $errors['metadata_text'][] = stack_string('JSONtoolong'); + } + if ($fromform['metadata'] && !json_decode($fromform['metadata'])) { + $errors['metadata_text'][] = stack_string('JSONbroken'); + } + // 2) Validate all inputs. $stackinputfactory = new stack_input_factory(); foreach ($inputs as $inputname => $counts) { diff --git a/stack/maxima/stackmaxima.mac b/stack/maxima/stackmaxima.mac index c829d59aafe..4299b345a2c 100644 --- a/stack/maxima/stackmaxima.mac +++ b/stack/maxima/stackmaxima.mac @@ -1044,7 +1044,7 @@ plot(ex, [ra]) := /*stack_web_plot*/ /* Check for box. */ plotbox:sublist(ra, lambda([ex], if listp(ex) then is(first(ex) = box) else false)), if emptyp(plotbox) then plotbox:true else block( - ra:delete(first(plotbox), ra), + ra:delete(first(plotbox), ra), if not(second(first(plotbox))) then plotbox:false ), /* For new versions. */ @@ -1139,7 +1139,7 @@ set output ", afn), if plotgrid2d and MAXIMA_VERSION_NUM>34 then ral: append(ral, [grid2d]), /* Add in the command for the nobox. */ - if not(plotbox) then if MAXIMA_VERSION_NUM>47 then + if not(plotbox) then if MAXIMA_VERSION_NUM>47 then ral:append(ral, [nobox]) else ral:append(ral, [[box,false]]), if plotdebug then print(preamble), if PLOT_TERMINAL="svg" then set_plot_option([svg_file, afn]), @@ -3563,4 +3563,4 @@ is_lang(code):=ev(is(%_STACK_LANG=code),simp=true)$ /* Stack expects some output with the version number the output happens at */ /* maximalocal.mac after additional library loading */ -stackmaximaversion:2026042402$ +stackmaximaversion:2026042700$ diff --git a/styles.css b/styles.css index b8a5e47fffe..23e9d5f8e38 100644 --- a/styles.css +++ b/styles.css @@ -1293,3 +1293,18 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. .stacktestsuite .results-symbol { font-size: 1.5em; } +.stack-metadata input { + width: 98%; +} +.stack-metadata .inputcol { + padding-left: 0; + padding-right: 0; +} +.stack-metadata .scope { + padding: 20px 10px 0 10px; +} +.stack-metadata .btn-danger { + background-color: pink; + border-color: pink; + color: black; +} diff --git a/templates/metadata/metadataadditional.mustache b/templates/metadata/metadataadditional.mustache new file mode 100644 index 00000000000..c858ef19099 --- /dev/null +++ b/templates/metadata/metadataadditional.mustache @@ -0,0 +1,81 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata/metadataadditional + Example context (json): + { + "property": { + "required": true, + "element": { + "value": "h", + "wrapperid": "fitem_smdi_2_additional_property", + "id": "smdi_2_additional_property", + "name": "smdi_2_additional_property", + "iderror": "smde_2_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "i", + "wrapperid": "fitem_smdi_2_additional_qualifier", + "id": "smdi_2_additional_qualifier", + "name": "smdi_2_additional_qualifier", + "iderror": "smde_2_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "j", + "wrapperid": "fitem_smdi_2_additional_value", + "id": "smdi_2_additional_value", + "name": "smdi_2_additional_value", + "iderror": "smde_2_additional_value_error" + } + }, + "id": "2" + } +}} +
+
+ {{#property}} + + {{> core_form/element-text-inline }} + {{/property}} +
+
+ {{#qualifier}} + + {{> core_form/element-text-inline }} + {{/qualifier}} +
+
+ {{#value}} + + {{> core_form/element-text-inline }} + {{/value}} +
+
+ +
+
\ No newline at end of file diff --git a/templates/metadata/metadatacontent.mustache b/templates/metadata/metadatacontent.mustache new file mode 100644 index 00000000000..22899f62117 --- /dev/null +++ b/templates/metadata/metadatacontent.mustache @@ -0,0 +1,557 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata/metadatacontent + + + Example context (json): + { + "creator": { + "firstname": { + "required": false, + "element": { + "value": "Edmund", + "wrapperid": "fitem_smdi_0_creator_firstName", + "id": "smdi_0_creator_firstName", + "name": "smdi_0_creator_firstName", + "iderror": "smde_0_creator_firstName_error" + } + }, + "lastname": { + "required": true, + "element": { + "value": "Farrow", + "wrapperid": "fitem_smdi_0_creator_lastName", + "id": "smdi_0_creator_lastName", + "name": "smdi_0_creator_lastName", + "iderror": "smde_0_creator_lastName_error" + } + }, + "institution": { + "required": false, + "element": { + "value": "UoE", + "wrapperid": "fitem_smdi_0_creator_institution", + "id": "smdi_0_creator_institution", + "name": "smdi_0_creator_institution", + "iderror": "smde_0_creator_institution_error" + } + }, + "year": { + "required": false, + "element": { + "value": "2025", + "wrapperid": "fitem_smdi_0_creator_year", + "id": "smdi_0_creator_year", + "name": "smdi_0_creator_year", + "iderror": "smde_0_creator_year_error" + } + } + }, + "contributor": [ + { + "firstname": { + "required": false, + "element": { + "value": "a", + "wrapperid": "fitem_smdi_1_contributor_firstName", + "id": "smdi_1_contributor_firstName", + "name": "smdi_1_contributor_firstName", + "iderror": "smde_1_contributor_firstName_error" + } + }, + "lastname": { + "required": true, + "element": { + "value": "b", + "wrapperid": "fitem_smdi_1_contributor_lastName", + "id": "smdi_1_contributor_lastName", + "name": "smdi_1_contributor_lastName", + "iderror": "smde_1_contributor_lastName_error" + } + }, + "institution": { + "required": false, + "element": { + "value": "c", + "wrapperid": "fitem_smdi_1_contributor_institution", + "id": "smdi_1_contributor_institution", + "name": "smdi_1_contributor_institution", + "iderror": "smde_1_contributor_institution_error" + } + }, + "year": { + "required": false, + "element": { + "value": "2025", + "wrapperid": "fitem_smdi_1_contributor_year", + "id": "smdi_1_contributor_year", + "name": "smdi_1_contributor_year", + "iderror": "smde_1_contributor_year_error" + } + }, + "id": "1" + } + ], + "language": [ + { + "id": "1", + "lang": { + "required": true, + "element": { + "value": "en", + "wrapperid": "fitem_smdi_1_language_value", + "id": "smdi_1_language_value", + "name": "smdi_1_language_value", + "iderror": "smde_1_language_value_error" + } + } + }, + { + "id": "2", + "lang": { + "required": true, + "element": { + "value": "fr", + "wrapperid": "fitem_smdi_2_language_value", + "id": "smdi_2_language_value", + "name": "smdi_2_language_value", + "iderror": "smde_2_language_value_error" + } + } + } + ], + "license": { + "required": true, + "element": { + "value": "public", + "wrapperid": "fitem_smdi_0_license_value", + "id": "smdi_0_license_value", + "name": "smdi_0_license_value", + "options": [ + { + "value": "unknown", + "text": "Licence not specified" + }, + { + "value": "allrightsreserved", + "text": "All rights reserved" + }, + { + "value": "public", + "text": "Public domain", + "selected": true + }, + { + "value": "cc-4.0", + "text": "Creative Commons - 4.0 International" + }, + { + "value": "cc-nc-4.0", + "text": "Creative Commons - NonCommercial 4.0 International" + }, + { + "value": "cc-nd-4.0", + "text": "Creative Commons - NoDerivatives 4.0 International" + }, + { + "value": "cc-nc-nd-4.0", + "text": "Creative Commons - NonCommercial-NoDerivatives 4.0 International" + }, + { + "value": "cc-nc-sa-4.0", + "text": "Creative Commons - NonCommercial-ShareAlike 4.0 International" + }, + { + "value": "cc-sa-4.0", + "text": "Creative Commons - ShareAlike 4.0 International" + } + ], + "tags": "[]", + "ajax": "", + "placeholder": "Type or select license", + "noselectionstring": "", + "showsuggestions": "true", + "casesensitive": "false", + "iderror": "smde_0_license_value_error" + } + }, + "isPartOf": { + "required": false, + "element": { + "value": "HELM", + "wrapperid": "fitem_smdi_0_isPartOf_value", + "id": "smdi_0_isPartOf_value", + "name": "smdi_0_isPartOf_value", + "iderror": "smde_0_isPartOf_value_error" + } + }, + "scope": [ + { + "name": "d", + "firstProp": "1", + "properties": [ + { + "property": { + "required": true, + "element": { + "value": "e", + "wrapperid": "fitem_smdi_1_additional_property", + "id": "smdi_1_additional_property", + "name": "smdi_1_additional_property", + "iderror": "smde_1_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "f", + "wrapperid": "fitem_smdi_1_additional_qualifier", + "id": "smdi_1_additional_qualifier", + "name": "smdi_1_additional_qualifier", + "iderror": "smde_1_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "g", + "wrapperid": "fitem_smdi_1_additional_value", + "id": "smdi_1_additional_value", + "name": "smdi_1_additional_value", + "iderror": "smde_1_additional_value_error" + } + }, + "id": "1" + }, + { + "property": { + "required": true, + "element": { + "value": "h", + "wrapperid": "fitem_smdi_2_additional_property", + "id": "smdi_2_additional_property", + "name": "smdi_2_additional_property", + "iderror": "smde_2_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "i", + "wrapperid": "fitem_smdi_2_additional_qualifier", + "id": "smdi_2_additional_qualifier", + "name": "smdi_2_additional_qualifier", + "iderror": "smde_2_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "j", + "wrapperid": "fitem_smdi_2_additional_value", + "id": "smdi_2_additional_value", + "name": "smdi_2_additional_value", + "iderror": "smde_2_additional_value_error" + } + }, + "id": "2" + }, + { + "property": { + "required": true, + "element": { + "value": "k", + "wrapperid": "fitem_smdi_3_additional_property", + "id": "smdi_3_additional_property", + "name": "smdi_3_additional_property", + "iderror": "smde_3_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "l", + "wrapperid": "fitem_smdi_3_additional_qualifier", + "id": "smdi_3_additional_qualifier", + "name": "smdi_3_additional_qualifier", + "iderror": "smde_3_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "m", + "wrapperid": "fitem_smdi_3_additional_value", + "id": "smdi_3_additional_value", + "name": "smdi_3_additional_value", + "iderror": "smde_3_additional_value_error" + } + }, + "id": "3" + } + ], + "input": { + "required": true, + "element": { + "value": "d", + "wrapperid": "fitem_smdi_1_additional_scope", + "id": "smdi_1_additional_scope", + "name": "smdi_1_additional_scope", + "iderror": "smde_1_additional_scope_error" + } + } + }, + { + "name": "n", + "firstProp": "4", + "properties": [ + { + "property": { + "required": true, + "element": { + "value": "o", + "wrapperid": "fitem_smdi_4_additional_property", + "id": "smdi_4_additional_property", + "name": "smdi_4_additional_property", + "iderror": "smde_4_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "p", + "wrapperid": "fitem_smdi_4_additional_qualifier", + "id": "smdi_4_additional_qualifier", + "name": "smdi_4_additional_qualifier", + "iderror": "smde_4_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "q", + "wrapperid": "fitem_smdi_4_additional_value", + "id": "smdi_4_additional_value", + "name": "smdi_4_additional_value", + "iderror": "smde_4_additional_value_error" + } + }, + "id": "4" + } + ], + "input": { + "required": true, + "element": { + "value": "n", + "wrapperid": "fitem_smdi_4_additional_scope", + "id": "smdi_4_additional_scope", + "name": "smdi_4_additional_scope", + "iderror": "smde_4_additional_scope_error" + } + } + } + ], + "json": { + "required": true, + "element": { + "value": "{\n \"creator\": {\n \"firstName\": \"Edmund\",\n ... ... ... }", + "attributes": "rows=\"10\"", + "wrapperid": "fitem_metadata_json", + "id": "id_metadata_json", + "name": "metadata_json", + "iderror": "metadata_json_error" + } + } +} + +}} +
+
+
+
{{#str}} firstname, qtype_stack {{/str}}
+
{{#str}} lastname, qtype_stack {{/str}}
+
{{#str}} institution, qtype_stack {{/str}}
+
{{#str}} year, qtype_stack {{/str}}
+
+ {{#creator}} +
+
{{#str}} creator, qtype_stack {{/str}}
+
+ {{#firstname}} + + {{> core_form/element-text-inline }} + {{/firstname}} +
+
+ {{#lastname}} + + {{> core_form/element-text-inline }} + {{/lastname}} +
+
+ {{#institution}} + + {{> core_form/element-text-inline }} + {{/institution}} +
+
+ {{#year}} + + {{>qtype_stack/metadata/metadatayear}} + {{/year}} +
+
+ {{/creator}} + {{#contributor}} + {{>qtype_stack/metadata/metadatacontributor}} + {{/contributor}} +
+
+
+ + + +
+
+
+ {{#language}} + {{>qtype_stack/metadata/metadatalanguage}} + {{/language}} +
+
+
+
+
+
+
{{#str}} ispartof, qtype_stack {{/str}}
+
+ {{#isPartOf}} + + {{> core_form/element-text-inline}} + {{/isPartOf}} +
+
+
+
{{#str}} license {{/str}}
+
+ {{#license}} + + {{> core_form/element-autocomplete-inline}} + {{/license}} +
+
+
{{#str}} additionalmetadata, qtype_stack {{/str}}
+ {{#scope}} + {{>qtype_stack/metadata/metadatascope}} + {{/scope}} +
+
+
+
+
+
+
{{#str}} freeformmetadata, qtype_stack {{/str}}
+
+ {{#freeform}} + + {{< core_form/element-template-inline }} + {{$element}} + + {{/element}} + {{/ core_form/element-template-inline }} + {{/freeform}} +
+
+
+
+
+ + + + +
+
+
+

+ {{#str}} metadataexplanation, qtype_stack {{/str}} +

+
+
{{#str}} JSONmetadata, qtype_stack {{/str}}
+
+ {{#json}} + + {{< core_form/element-template-inline }} + {{$element}} + + {{/element}} + {{/ core_form/element-template-inline }} + {{/json}} +
+
+ +{{#js}} +require(['qtype_stack/metadata/metadatacontent'], function(component) { + component.init('#qtype-stack-metadata-content'); +}); +{{/js}} \ No newline at end of file diff --git a/templates/metadata/metadatacontributor.mustache b/templates/metadata/metadatacontributor.mustache new file mode 100644 index 00000000000..db3ff61b814 --- /dev/null +++ b/templates/metadata/metadatacontributor.mustache @@ -0,0 +1,100 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata/metadatacontributor + Example context (json): + { + "firstname": { + "required": false, + "element": { + "value": "a", + "wrapperid": "fitem_smdi_1_contributor_firstName", + "id": "smdi_1_contributor_firstName", + "name": "smdi_1_contributor_firstName", + "iderror": "smde_1_contributor_firstName_error" + } + }, + "lastname": { + "required": true, + "element": { + "value": "b", + "wrapperid": "fitem_smdi_1_contributor_lastName", + "id": "smdi_1_contributor_lastName", + "name": "smdi_1_contributor_lastName", + "iderror": "smde_1_contributor_lastName_error" + } + }, + "institution": { + "required": false, + "element": { + "value": "c", + "wrapperid": "fitem_smdi_1_contributor_institution", + "id": "smdi_1_contributor_institution", + "name": "smdi_1_contributor_institution", + "iderror": "smde_1_contributor_institution_error" + } + }, + "year": { + "required": false, + "element": { + "value": "2025", + "wrapperid": "fitem_smdi_1_contributor_year", + "id": "smdi_1_contributor_year", + "name": "smdi_1_contributor_year", + "iderror": "smde_1_contributor_year_error" + } + }, + "id": "1" + } +}} +
+
{{#str}} contributor, qtype_stack {{/str}}
+
+ {{#firstname}} + + {{> core_form/element-text-inline }} + {{/firstname}} +
+
+ {{#lastname}} + + {{> core_form/element-text-inline }} + {{/lastname}} +
+
+ {{#institution}} + + {{> core_form/element-text-inline }} + {{/institution}} +
+
+ {{#year}} + + {{>qtype_stack/metadata/metadatayear}} + {{/year}} +
+
+ +
+
\ No newline at end of file diff --git a/templates/metadata/metadatalanguage.mustache b/templates/metadata/metadatalanguage.mustache new file mode 100644 index 00000000000..6ee3dfea213 --- /dev/null +++ b/templates/metadata/metadatalanguage.mustache @@ -0,0 +1,47 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata/metadatalanguage + + Example context (json): + { + "id": "1", + "lang": { + "required": true, + "element": { + "value": "en", + "wrapperid": "fitem_smdi_1_language_value", + "id": "smdi_1_language_value", + "name": "smdi_1_language_value", + "iderror": "smde_1_language_error" + } + } + } +}} +
+
{{#str}} language, core {{/str}}
+
+ {{#lang}} + + {{> core_form/element-text-inline }} + {{/lang}} +
+
+ +
+
\ No newline at end of file diff --git a/templates/metadata/metadatamodal.mustache b/templates/metadata/metadatamodal.mustache new file mode 100644 index 00000000000..f5276190fe6 --- /dev/null +++ b/templates/metadata/metadatamodal.mustache @@ -0,0 +1,39 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata/metadatamodal + + Example context (json): {} +}} +{{< core/modal }} + {{$title}}{{#str}} metadata, qtype_stack {{/str}}{{/title}} + {{$classes}}modal-xl stack-metadata{{/classes}} + {{$body}} +
+
+
+
+ + {{/body}} + {{$footer}} + {{/footer}} +{{/ core/modal }} +{{#js}} +require(['qtype_stack/metadata/container'], function(component) { + component.init('#qtype-stack-metadata-main'); +}); +{{/js}} \ No newline at end of file diff --git a/templates/metadata/metadatascope.mustache b/templates/metadata/metadatascope.mustache new file mode 100644 index 00000000000..fd8fd406031 --- /dev/null +++ b/templates/metadata/metadatascope.mustache @@ -0,0 +1,162 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata/metadatascope + Example context (json): + { + "name": "d", + "firstProp": "1", + "properties": [ + { + "property": { + "required": true, + "element": { + "value": "e", + "wrapperid": "fitem_smdi_1_additional_property", + "id": "smdi_1_additional_property", + "name": "smdi_1_additional_property", + "iderror": "smde_1_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "f", + "wrapperid": "fitem_smdi_1_additional_qualifier", + "id": "smdi_1_additional_qualifier", + "name": "smdi_1_additional_qualifier", + "iderror": "smde_1_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "g", + "wrapperid": "fitem_smdi_1_additional_value", + "id": "smdi_1_additional_value", + "name": "smdi_1_additional_value", + "iderror": "smde_1_additional_value_error" + } + }, + "id": "1" + }, + { + "property": { + "required": true, + "element": { + "value": "h", + "wrapperid": "fitem_smdi_2_additional_property", + "id": "smdi_2_additional_property", + "name": "smdi_2_additional_property", + "iderror": "smde_2_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "i", + "wrapperid": "fitem_smdi_2_additional_qualifier", + "id": "smdi_2_additional_qualifier", + "name": "smdi_2_additional_qualifier", + "iderror": "smde_2_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "j", + "wrapperid": "fitem_smdi_2_additional_value", + "id": "smdi_2_additional_value", + "name": "smdi_2_additional_value", + "iderror": "smde_2_additional_value_error" + } + }, + "id": "2" + }, + { + "property": { + "required": true, + "element": { + "value": "k", + "wrapperid": "fitem_smdi_3_additional_property", + "id": "smdi_3_additional_property", + "name": "smdi_3_additional_property", + "iderror": "smde_3_additional_property_error" + } + }, + "qualifier": { + "required": false, + "element": { + "value": "l", + "wrapperid": "fitem_smdi_3_additional_qualifier", + "id": "smdi_3_additional_qualifier", + "name": "smdi_3_additional_qualifier", + "iderror": "smde_3_additional_qualifier_error" + } + }, + "value": { + "required": false, + "element": { + "value": "m", + "wrapperid": "fitem_smdi_3_additional_value", + "id": "smdi_3_additional_value", + "name": "smdi_3_additional_value", + "iderror": "smde_3_additional_value_error" + } + }, + "id": "3" + } + ], + "input": { + "required": true, + "element": { + "value": "d", + "wrapperid": "fitem_smdi_1_additional_scope", + "id": "smdi_1_additional_scope", + "name": "smdi_1_additional_scope", + "iderror": "smde_1_additional_scope_error" + } + } + } +}} +
+
+
{{#str}} scope, qtype_stack {{/str}}
+
+ {{#input}} + + {{> core_form/element-text-inline }} + {{/input}} +
+
+ +
+
+
+
{{#str}} property, qtype_stack {{/str}}
+
{{#str}} qualifier, qtype_stack {{/str}}
+
{{#str}} value, qtype_stack {{/str}}
+
+ {{#properties}} + {{>qtype_stack/metadata/metadataadditional}} + {{/properties}} +
+
+
+
+
\ No newline at end of file diff --git a/templates/metadata/metadatayear.mustache b/templates/metadata/metadatayear.mustache new file mode 100644 index 00000000000..5952d366468 --- /dev/null +++ b/templates/metadata/metadatayear.mustache @@ -0,0 +1,48 @@ +{{! + This file is part of Stack - http://stack.maths.ed.ac.uk/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template qtype_stack/metadata/metadatayear + Example context (json): + { + "required": false, + "element": { + "value": "2025", + "wrapperid": "fitem_smdi_1_contributor_year", + "id": "smdi_1_contributor_year", + "name": "smdi_1_contributor_year", + "iderror": "smde_1_contributor_year_error" + } + } +}} +{{< core_form/element-template-inline }} + {{$element}} + + {{/element}} +{{/ core_form/element-template-inline }} \ No newline at end of file diff --git a/tests/behat/behat_qtype_stack.php b/tests/behat/behat_qtype_stack.php index 5577d4417f9..1e7f706c580 100644 --- a/tests/behat/behat_qtype_stack.php +++ b/tests/behat/behat_qtype_stack.php @@ -125,6 +125,27 @@ public function i_set_the_part_to_in_the_combined_question($name, $value) { $formscontext->i_set_the_field_with_xpath_to($this->input_xpath($name), $value); } + /** + * Check a hidden value + * + * @param string $name name of hidden field. + * @param string $value the expected value with current year replaced with XXXX. + * + * @Given /^I check the hidden input "(?P[^"]*)" is '(?P[^']*)'$/ + */ + public function i_check_hidden_value($name, $value) { + $year = date('Y'); + $value = str_replace('XXXX', $year, $value); + $js = <<evaluate_script($js); + Assert::assertEquals($value, $formvalue); + } + /** * Set the response for a given input in the Moodle app. * diff --git a/tests/behat/metadata.feature b/tests/behat/metadata.feature new file mode 100644 index 00000000000..d774a10ff8e --- /dev/null +++ b/tests/behat/metadata.feature @@ -0,0 +1,359 @@ +@qtype @qtype_stack +Feature: Create and edit STACK metadata + In order catalogue questions effectively + As an teacher + I need to create and edit metadata for STACK questions + + Background: + Given I set up STACK using the PHPUnit configuration + And the following "courses" exist: + | fullname | shortname | format | + | Course 1 | C1 | topics | + And the following "users" exist: + | username | firstname | + | teacher | Teacher | + And the following "course enrolments" exist: + | user | course | role | + | teacher | C1 | editingteacher | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | template | + | Test questions | stack | Algebraic input | algebraic_input | + + @javascript + Scenario: Create and edit STACK metadata + When I am on the "Algebraic input" "core_question > edit" page logged in as teacher + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I set the field "smdi_0_creator_firstName" in the "#qtype-stack-metadata-content" "css_element" to "Edmund" + And I set the field "smdi_0_creator_lastName" in the "#qtype-stack-metadata-content" "css_element" to "Farrow" + And I set the field "smdi_0_creator_institution" in the "#qtype-stack-metadata-content" "css_element" to "UoE" + And I set the field "smdi_0_creator_year" in the "#qtype-stack-metadata-content" "css_element" to "2025" + And I click on "Add contributor" "button" + And I wait until "smdi_1_contributor_firstName" "field" exists + And I set the field "smdi_1_contributor_firstName" in the "#qtype-stack-metadata-content" "css_element" to "Bob" + And I set the field "smdi_1_contributor_lastName" in the "#qtype-stack-metadata-content" "css_element" to "Smith" + And I set the field "smdi_1_contributor_institution" in the "#qtype-stack-metadata-content" "css_element" to "MIT" + And I set the field "smdi_1_contributor_year" in the "#qtype-stack-metadata-content" "css_element" to "2026" + And I click on "Add language" "button" + And I wait until "smdi_1_language_value" "field" exists + And I set the field "smdi_1_language_value" in the "#qtype-stack-metadata-content" "css_element" to "en" + And I set the field "smdi_0_isPartOf_value" in the "#qtype-stack-metadata-content" "css_element" to "HELM" + And I open the autocomplete suggestions list in the "#qtype-stack-metadata-content" "css_element" + Then "[data-value='cc-nc-4.0']" "css_element" should be visible + And I click on "[data-value='cc-nc-4.0']" "css_element" + And I click on "Add scope" "button" + And I wait until "smdi_1_additional_scope" "field" exists + And I set the field "smdi_1_additional_scope" in the "#qtype-stack-metadata-content" "css_element" to "Added data" + And I set the field "smdi_1_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Dog info" + And I set the field "smdi_1_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Breed" + And I set the field "smdi_1_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "Al$%&^" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"}],"language":["en"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I click on "Add me as a contributor" "button" + And I click on "Add language" "button" + And I wait until "smdi_2_language_value" "field" exists + And I set the field "smdi_2_language_value" in the "#qtype-stack-metadata-content" "css_element" to "fr" + And I click on "Add property" "button" + And I wait until "smdi_2_additional_property" "field" exists + And I set the field "smdi_2_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Cat info" + And I set the field "smdi_2_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Breed" + And I set the field "smdi_2_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "Tabby" + And I click on "Add scope" "button" + And I wait until "smdi_3_additional_scope" "field" exists + And I set the field "smdi_3_additional_scope" in the "#qtype-stack-metadata-content" "css_element" to "More data" + And I set the field "smdi_3_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Question" + And I set the field "smdi_3_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Type" + And I set the field "smdi_3_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "MC" + And I click on "smd_property_3_add" "button" + And I wait until "smdi_4_additional_property" "field" exists + And I set the field "smdi_4_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "More" + And I set the field "smdi_4_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Things" + And I set the field "smdi_4_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "AAA" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"More data":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I set the field "smdi_3_additional_scope" in the "#qtype-stack-metadata-content" "css_element" to "Changed" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2026"},{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["en","fr"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"},"Cat info":{"Breed":"Tabby"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I click on "smd_contributor_1_delete" "button" + And I should not see "Smith" + And I set the field "smdi_1_language_value" in the "#qtype-stack-metadata-content" "css_element" to "en-del" + And I click on "smd_language_1_delete" "button" + And I should not see "en-del" + And I click on "smd_additional_2_delete" "button" + And I should not see "Cat info" + And I click on "Add language" "button" + And I wait until "smdi_3_language_value" "field" exists + And I set the field "smdi_3_language_value" in the "#qtype-stack-metadata-content" "css_element" to "it" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Added data":{"Dog info":{"Breed":"Al$%&^"}},"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I click on "smd_scope_1_delete" "button" + And I should not see "Added data" + And I click on "smd_property_3_add" "button" + And I wait until "smdi_5_additional_property" "field" exists + And I click on "Validate and close" "button" + And I should see "Required" in the "#smde_5_additional_property_error" "css_element" + And I click on "smd_additional_5_delete" "button" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I click on "Add scope" "button" + And I wait until "smdi_5_additional_scope" "field" exists + And I set the field "smdi_5_additional_scope" in the "#qtype-stack-metadata-content" "css_element" to "Another Scope" + And I set the field "smdi_5_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Question2" + And I set the field "smdi_5_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Type2" + And I set the field "smdi_5_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "MC2" + And I click on "smd_property_5_add" "button" + And I wait until "smdi_6_additional_property" "field" exists + And I set the field "smdi_6_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "More data" + And I set the field "smdi_6_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Things2" + And I set the field "smdi_6_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "AAA2" + And I click on "smd_property_5_add" "button" + And I wait until "smdi_7_additional_property" "field" exists + And I set the field "smdi_7_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "More data" + And I set the field "smdi_7_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Type2" + And I set the field "smdi_7_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "BBB2" + And I click on "smd_property_5_add" "button" + And I wait until "smdi_8_additional_property" "field" exists + And I set the field "smdi_8_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "No obj" + And I set the field "smdi_8_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "" + And I set the field "smdi_8_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "BBB3" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Edmund","lastName":"Farrow","institution":"UoE","year":"2025"},"contributor":[{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"}],"language":["fr","it"],"isPartOf":"HELM","license":"cc-nc-4.0","additional":{"Changed":{"Question":{"Type":"MC"},"More":{"Things":"AAA"}},"Another Scope":{"Question2":{"Type2":"MC2"},"More data":{"Things2":"AAA2","Type2":"BBB2"},"No obj":"BBB3"}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I set the field "id_metadata_json" to multiline: + """ + { + "creator": { + "firstName": "Bob", + "lastName": "Smith", + "institution": "MIT", + "year": "2024" + }, + "contributor": [ + { + "firstName": "Mike", + "lastName": "Jones", + "institution": "Bath", + "year": "2023" + } + ], + "language": [ + "en" + ], + "isPartOf": "Everything", + "license": "cc-nc-4.1", + "additional": + { + "Added": { + "Cat": { + "Breed": "Al$%&^" + }, + "Horse": "Dobbin", + "Dog": { + "Teeth": "50", + "Tails": "1" + } + }, + "Added too": { + "Fish": { + "Gills": "2" + } + } + }, + "freeform": {} + } + """ + And I click on "Update inputs from JSON" "button" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I click on "smd_property_5_add" "button" + And I wait until "smdi_6_additional_property" "field" exists + And I set the field "smdi_6_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Fish" + And I set the field "smdi_6_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Gills" + And I set the field "smdi_6_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "3" + And I click on "smd_property_1_add" "button" + And I wait until "smdi_7_additional_property" "field" exists + And I set the field "smdi_7_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Horse" + And I set the field "smdi_7_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "" + And I set the field "smdi_7_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "Champion" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I click on "smd_property_1_add" "button" + And I wait until "smdi_8_additional_property" "field" exists + And I set the field "smdi_8_additional_property" in the "#qtype-stack-metadata-content" "css_element" to "Horse" + And I set the field "smdi_8_additional_qualifier" in the "#qtype-stack-metadata-content" "css_element" to "Name" + And I set the field "smdi_8_additional_value" in the "#qtype-stack-metadata-content" "css_element" to "Nessie" + And I click on "Validate and close" "button" + And I should see "Required" in the "#smde_7_additional_qualifier_error" "css_element" + And I set the field "id_metadata_json" to multiline: + """ + { + "creator": { + "firstName": "Bo1b", + "lastName": "Smi1th", + "institution": "MI1T", + "year": "2024" + }, + "contributor": [ + { + "firstName": "Mi1ke", + "lastName": "Jon1es", + "institution": "1ath", + "year": "2023" + }, + { + "firstName": "Helen", + "lastName": "Lowell", + "institution": "Bath", + "year": "2023" + } + ], + "language": [ + "edfsedn" + ], + "isPartOf": "Eve1rything", + "license": "public", + "additional": { + "Adfded": { + "Cfat": { + "Bfreed": "Al$%f&^" + }, + "Dfog": { + "Tefeth": "5f0" + } + }, + "Addfed too": { + "Fifsh": { + "Giflls": "2f" + } + } + }, + "freeform": {} + } + """ + And I click on "Update inputs from JSON" "button" + And I wait until "smdi_2_contributor_firstName" "field" exists + And I click on "Revert current changes" "button" + And I should not see "Lowell" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + And I press "id_updatebutton" + Given the site is running Moodle version 4.6 or lower + Then I should see "Version 2" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + Given the site is running Moodle version 5.0 or higher + Then I should see "v2 (latest)" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"Added":{"Cat":{"Breed":"Al$%&^"},"Horse":["Dobbin","Champion"],"Dog":{"Teeth":"50","Tails":"1"}},"Added too":{"Fish":{"Gills":["2","3"]}}},"freeform":{}}' + + @javascript + Scenario: Create and edit STACK freeform metadata + When I am on the "Algebraic input" "core_question > edit" page logged in as teacher + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I set the field "id_metadata_json" to multiline: + """ + { + "creator": { + "firstName": "Bob", + "lastName": "Smith", + "institution": "MIT", + "year": "2024" + }, + "contributor": [ + { + "firstName": "Mike", + "lastName": "Jones", + "institution": "Bath", + "year": "2023" + } + ], + "language": [ + "en" + ], + "isPartOf": "Everything", + "license": "cc-nc-4.1", + "additional": + { + "additional": { + "Cat": { + "Breed": "Al$%&^" + }, + "Horse": "Dobbin", + "Dog": { + "Teeth": "50", + "Tails": "1" + }, + "Multi": [ + 1,2,3 + ], + "Multi1": { + "Multi2": [ + 4,5,6 + ] + } + }, + "Added too": { + "Fish": { + "Gills": "2" + } + } + }, + "freeform": + { + "license": { + "Cat": { + "Breed": "Al$%&^" + }, + "Horse": "Dobbin", + "Dog": { + "Teeth": "50", + "Tails": "1" + } + }, + "Freeform too": { + "Fish": { + "Gills": "2" + } + } + } + } + """ + And I click on "Update inputs from JSON" "button" + And I should see "{\"license\":{\"Cat\":{\"Breed\":\"Al$%&^\"},\"Horse\":\"Dobbin\",\"Dog\":{\"Teeth\":\"50\",\"Tails\":\"1\"}},\"Freeform too\":{\"Fish\":{\"Gills\":\"2\"}}}" + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"license":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"}},"Freeform too":{"Fish":{"Gills":"2"}}}}' + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I set the field "smdi_0_freeform_value" to multiline: + """ + {"x":[{"additional":"b"},{"license":"d"}]} + """ + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Bob","lastName":"Smith","institution":"MIT","year":"2024"},"contributor":[{"firstName":"Mike","lastName":"Jones","institution":"Bath","year":"2023"}],"language":["en"],"isPartOf":"Everything","license":"cc-nc-4.1","additional":{"additional":{"Cat":{"Breed":"Al$%&^"},"Horse":"Dobbin","Dog":{"Teeth":"50","Tails":"1"},"Multi":["1","2","3"],"Multi1":{"Multi2":["4","5","6"]}},"Added too":{"Fish":{"Gills":"2"}}},"freeform":{"x":[{"additional":"b"},{"license":"d"}]}}' + + @javascript + Scenario: New question metadata + When I am on the "Course 1" "core_question > course question bank" page logged in as "teacher" + And I click on "Create a new question" "button" + And I set the field "item_qtype_stack" to "1" + And I press "submitbutton" + And I click on "View and edit full metadata" "button" + And I should see "STACK metadata is stored as a JSON object." + And I click on "Validate and close" "button" + And I check the hidden input "metadata" is '{"creator":{"firstName":"Teacher","lastName":"Lastname1","institution":"","year":"XXXX"},"contributor":[],"language":["en"],"license":"unknown","isPartOf":"","additional":{},"freeform":{}}' diff --git a/tests/helper.php b/tests/helper.php index 0e655cc81f4..33c5db9c881 100644 --- a/tests/helper.php +++ b/tests/helper.php @@ -157,6 +157,8 @@ protected static function make_a_stack_question() { $q->questionnote = ''; $q->questionnoteformat = FORMAT_HTML; $q->isbroken = 0; + $q->metadata = ''; + $q->prescribedmetadata = ''; return $q; } @@ -224,6 +226,9 @@ public static function make_stack_question_test0() { $prt->nodes[] = $newnode; $q->prts[$prt->name] = new stack_potentialresponse_tree_lite($prt, $prt->value, $q); + $q->isbroken = 0; + $q->metadata = ''; + $q->prescribedmetadata = ''; return $q; } @@ -387,6 +392,9 @@ public function get_stack_question_form_data_test1() { 1 => ['text' => '

Hint 2

', 'format' => '1', 'itemid' => '0'], ]; $formform->qtype = 'stack'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -674,6 +682,9 @@ public static function make_stack_question_test3() { $q->deployedseeds = []; + $q->metadata = ''; + $q->prescribedmetadata = ''; + return $q; } @@ -1934,6 +1945,8 @@ public static function get_stack_question_data_variable_grade() { $qdata->options->id = 0; $qdata->options->stackversion = get_config('qtype_stack', 'version'); $qdata->options->isbroken = 0; + $qdata->options->metadata = '{}'; + $qdata->options->prescribedmetadata = ''; $qdata->options->questionvariables = ''; $qdata->options->specificfeedback = '[[feedback:firsttree]]'; $qdata->options->specificfeedbackformat = FORMAT_HTML; @@ -2436,6 +2449,8 @@ public static function get_stack_question_data_test0() { $qdata->options->id = 0; $qdata->options->stackversion = get_config('qtype_stack', 'version'); $qdata->options->isbroken = 0; + $qdata->options->metadata = '{}'; + $qdata->options->prescribedmetadata = ''; $qdata->options->questionvariables = ''; $qdata->options->specificfeedback = '[[feedback:firsttree]]'; $qdata->options->specificfeedbackformat = FORMAT_HTML; @@ -2575,6 +2590,8 @@ public static function get_stack_question_data_test3() { $qdata->options->id = 0; $qdata->options->stackversion = get_config('qtype_stack', 'version'); $qdata->options->isbroken = 0; + $qdata->options->metadata = ''; + $qdata->options->prescribedmetadata = ''; $qdata->options->questionvariables = ''; $qdata->options->specificfeedback = ''; $qdata->options->specificfeedbackformat = FORMAT_HTML; @@ -3331,6 +3348,8 @@ public function get_stack_question_form_data_test3() { ], ]; $formform->qtype = 'stack'; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -5016,6 +5035,9 @@ public static function get_stack_question_form_data_algebraic_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -5142,6 +5164,9 @@ public static function get_stack_question_form_data_algebraic_input_right() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -5409,6 +5434,9 @@ public static function get_stack_question_form_data_algebraic_input_size() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -5535,6 +5563,9 @@ public static function get_stack_question_form_data_algebraic_input_compact() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -5696,6 +5727,9 @@ public static function get_stack_question_form_data_algebraic_input_empty() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -5822,6 +5856,9 @@ public static function get_stack_question_form_data_algebraic_input_simpl() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -5951,6 +5988,9 @@ public static function get_stack_question_form_data_checkbox_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -6079,6 +6119,9 @@ public static function get_stack_question_form_data_checkbox_input_no_latex() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -6279,6 +6322,9 @@ public static function get_stack_question_form_data_checkbox_input_plots() { ]; $formform->prt1trueanswernote[2] = 'prt1-3-T'; $formform->prt1truenextnode[2] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -6409,6 +6455,9 @@ public static function get_stack_question_form_data_checkbox_show_tans() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -6538,6 +6587,9 @@ public static function get_stack_question_form_data_dropdown_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -6663,6 +6715,9 @@ public static function get_stack_question_form_data_equiv_input_compact() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -6816,6 +6871,9 @@ public static function get_stack_question_form_data_equiv_input() { ]; $formform->prt1trueanswernote[1] = 'prt1-2-T'; $formform->prt1truenextnode[1] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -6941,6 +6999,9 @@ public static function get_stack_question_form_data_matrix_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -7164,6 +7225,9 @@ public static function get_stack_question_form_data_varmatrix_input() { ]; $formform->prt1trueanswernote[3] = 'prt1-4-T'; $formform->prt1truenextnode[3] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -7308,6 +7372,9 @@ public static function get_stack_question_form_data_matrix_multi_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -7433,6 +7500,9 @@ public static function get_stack_question_form_data_notes_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -7559,6 +7629,9 @@ public static function get_stack_question_form_data_numerical_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -7687,6 +7760,9 @@ public static function get_stack_question_form_data_radio_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -7815,6 +7891,9 @@ public static function get_stack_question_form_data_radio_input_compact() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -7940,6 +8019,9 @@ public static function get_stack_question_form_data_single_char_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -8084,6 +8166,9 @@ public static function get_stack_question_form_data_string_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -8209,6 +8294,9 @@ public static function get_stack_question_form_data_textarea_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -8334,6 +8422,9 @@ public static function get_stack_question_form_data_textarea_input_compact() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -8459,6 +8550,9 @@ public static function get_stack_question_form_data_true_false_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -8584,6 +8678,9 @@ public static function get_stack_question_form_data_units_input() { ]; $formform->prt1trueanswernote[0] = 'prt1-1-T'; $formform->prt1truenextnode[0] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -8797,6 +8894,9 @@ public static function get_stack_question_form_data_jsx_graph_input() { ]; $formform->prt1trueanswernote[1] = 'prt1-2-T'; $formform->prt1truenextnode[1] = '-1'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } @@ -8903,6 +9003,9 @@ public function get_stack_question_form_data_response_test() { 1 => ['text' => '

Hint 2

', 'format' => '1', 'itemid' => '0'], ]; $formform->qtype = 'stack'; + $formform->isbroken = 0; + $formform->metadata = ''; + $formform->prescribedmetadata = ''; return $formform; } diff --git a/tests/jest/__mocks__/core/modal.js b/tests/jest/__mocks__/core/modal.js new file mode 100644 index 00000000000..092e8eed136 --- /dev/null +++ b/tests/jest/__mocks__/core/modal.js @@ -0,0 +1,8 @@ +export default class Modal { + constructor(..._args) {} + static create = jest.fn().mockResolvedValue(null); +} + +// Prototype methods so all instances share the same spy target. +Modal.prototype.hide = jest.fn(); +Modal.prototype.show = jest.fn(); diff --git a/tests/jest/__mocks__/core/reactive.js b/tests/jest/__mocks__/core/reactive.js new file mode 100644 index 00000000000..0b0dbbcc8be --- /dev/null +++ b/tests/jest/__mocks__/core/reactive.js @@ -0,0 +1,21 @@ +export class Reactive {} + +export class BaseComponent { + constructor({element, reactive, selectors} = {}) { + this.element = element ?? null; + this.reactive = reactive ?? null; + if (selectors) { + this.selectors = selectors; + } + if (typeof this.create === 'function') { + this.create(); + } + } +} + +// Prototype stubs — shared across instances; reset with jest.resetAllMocks() in beforeEach. +BaseComponent.prototype.getElement = jest.fn(); +BaseComponent.prototype.getElements = jest.fn(() => []); +BaseComponent.prototype.addEventListener = jest.fn(); +BaseComponent.prototype.renderComponent = jest.fn().mockResolvedValue(undefined); + diff --git a/tests/jest/__mocks__/core_form/events.js b/tests/jest/__mocks__/core_form/events.js new file mode 100644 index 00000000000..3fcc050c9c3 --- /dev/null +++ b/tests/jest/__mocks__/core_form/events.js @@ -0,0 +1 @@ +export const notifyFieldValidationFailure = jest.fn(); diff --git a/tests/jest/__mocks__/qtype_stack/metadata/container.js b/tests/jest/__mocks__/qtype_stack/metadata/container.js new file mode 100644 index 00000000000..85bbff807ab --- /dev/null +++ b/tests/jest/__mocks__/qtype_stack/metadata/container.js @@ -0,0 +1,3 @@ +export default { + init: jest.fn(), +}; diff --git a/tests/jest/__mocks__/qtype_stack/metadata/events.js b/tests/jest/__mocks__/qtype_stack/metadata/events.js new file mode 100644 index 00000000000..f93b9fa6be4 --- /dev/null +++ b/tests/jest/__mocks__/qtype_stack/metadata/events.js @@ -0,0 +1,2 @@ +export const eventTypes = {}; +export const notifyQtypeStackStateUpdated = () => {}; diff --git a/tests/jest/__mocks__/qtype_stack/metadata/metadata.js b/tests/jest/__mocks__/qtype_stack/metadata/metadata.js new file mode 100644 index 00000000000..161bdbc8aa4 --- /dev/null +++ b/tests/jest/__mocks__/qtype_stack/metadata/metadata.js @@ -0,0 +1,19 @@ +export const metadata = { + lib: { + user: { + firstname: 'Jane', + lastname: 'Doe', + institution: 'Test University', + }, + licenses: [ + {value: 'cc-by', text: 'CC BY'}, + {value: 'cc-by-sa', text: 'CC BY-SA'}, + ], + placeholder: 'Select a license', + }, + container: {update: jest.fn()}, + jsonStringify: jest.fn(), + jsonToState: jest.fn(), + state: {}, + loadState: jest.fn(), +}; diff --git a/tests/jest/__mocks__/qtype_stack/metadata/mutations.js b/tests/jest/__mocks__/qtype_stack/metadata/mutations.js new file mode 100644 index 00000000000..4df8d75ce4c --- /dev/null +++ b/tests/jest/__mocks__/qtype_stack/metadata/mutations.js @@ -0,0 +1 @@ +export const mutations = {}; diff --git a/tests/jest/babel.config.js b/tests/jest/babel.config.js new file mode 100644 index 00000000000..8283743ee3a --- /dev/null +++ b/tests/jest/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: [['@babel/preset-env', {targets: {node: 'current'}}]], +}; diff --git a/tests/jest/metadata.container.test.js b/tests/jest/metadata.container.test.js new file mode 100644 index 00000000000..363b3c3990d --- /dev/null +++ b/tests/jest/metadata.container.test.js @@ -0,0 +1,779 @@ +const ContainerClass = require('../../amd/src/metadata/container.js').default; +const {metadata} = require('qtype_stack/metadata/metadata'); +const {notifyFieldValidationFailure} = require('core_form/events'); +const {BaseComponent} = require('core/reactive'); + +// ── helpers ──────────────────────────────────────────────────────────────────── + +/** + * Build a testable container instance without going through the BaseComponent + * constructor (which needs real DOM elements and a reactive instance). + * We set required properties manually then call create() so the container's + * own initialisation runs. + */ +function makeInstance(reactiveOverrides = {}) { + const reactive = {dispatch: jest.fn(), ...reactiveOverrides}; + const instance = Object.create(ContainerClass.prototype); + // Provide the BaseComponent API that container methods rely on. + instance.element = null; + instance.reactive = reactive; + instance.getElement = jest.fn(); + instance.getElements = jest.fn(() => []); + instance.addEventListener = jest.fn(); + instance.renderComponent = jest.fn().mockResolvedValue(undefined); + // Runs the subclass create() hook which sets this.name, this.selectors, etc. + instance.create(); + return {instance, reactive}; +} + +// DOM mock - document.querySelector is used directly in revert() and reloadContainerComponent(). +const mockQuerySelector = jest.fn(); + +beforeAll(() => { + global.document = {querySelector: mockQuerySelector}; +}); + +beforeEach(() => { + jest.resetAllMocks(); + mockQuerySelector.mockReturnValue(null); + // After resetAllMocks the prototype stubs need their default implementations restored. + BaseComponent.prototype.getElements.mockReturnValue([]); + BaseComponent.prototype.renderComponent.mockResolvedValue(undefined); + // Prevent brokenMetadata state leaking between tests. + delete metadata.lib.brokenMetadata; +}); + +// ── createDataElement ────────────────────────────────────────────────────────── + +describe('createDataElement', () => { + test('returns the required flag as-is', () => { + const {instance} = makeInstance(); + expect(instance.createDataElement(true, 1, 'language_value', 'en').required).toBe(true); + expect(instance.createDataElement(false, 1, 'language_value', 'en').required).toBe(false); + }); + + test('element.value matches the supplied value', () => { + const {instance} = makeInstance(); + const el = instance.createDataElement(true, 0, 'license_value', 'MIT'); + expect(el.element.value).toBe('MIT'); + }); + + test('element IDs are composed from id and tag', () => { + const {instance} = makeInstance(); + const el = instance.createDataElement(false, 3, 'contributor_lastName', 'Smith'); + expect(el.element.id).toBe('smdi_3_contributor_lastName'); + expect(el.element.name).toBe('smdi_3_contributor_lastName'); + expect(el.element.wrapperid).toBe('fitem_smdi_3_contributor_lastName'); + expect(el.element.iderror).toBe('smde_3_contributor_lastName_error'); + }); + + test('works with id=0 for single-instance elements', () => { + const {instance} = makeInstance(); + const el = instance.createDataElement(true, 0, 'creator_firstName', 'Alice'); + expect(el.element.id).toBe('smdi_0_creator_firstName'); + }); +}); + +// ── makeCreator ──────────────────────────────────────────────────────────────── + +describe('makeCreator', () => { + test('populates all four creator fields from metadata.lib.user and current year', () => { + const {instance} = makeInstance(); + const fields = {}; + instance.getElement.mockImplementation(sel => { + fields[sel] = {value: ''}; + return fields[sel]; + }); + instance.makeCreator(); + expect(fields['#smdi_0_creator_firstName'].value).toBe('Jane'); + expect(fields['#smdi_0_creator_lastName'].value).toBe('Doe'); + expect(fields['#smdi_0_creator_institution'].value).toBe('Test University'); + expect(fields['#smdi_0_creator_year'].value).toBe(new Date().getFullYear()); + }); +}); + +// ── update ───────────────────────────────────────────────────────────────────── + +describe('update', () => { + function makeInputEl(id, value, classes = []) { + return { + id, + value, + classList: {contains: (c) => classes.includes(c)}, + }; + } + + test('skips validation and dispatches updateAll when mustValidate=false', async () => { + const {instance, reactive} = makeInstance(); + const allInputs = [makeInputEl('smdi_0_license_value', 'MIT')]; + instance.getElements.mockImplementation(sel => { + if (sel === instance.selectors.ALLINPUTS) return allInputs; + return []; + }); + reactive.dispatch.mockResolvedValue(undefined); + const result = await instance.update(false); + expect(reactive.dispatch).toHaveBeenCalledWith('updateAll', [['smdi_0_license_value', 'MIT']]); + expect(result).toBe(true); + }); + + test('returns false and notifies when a required field is empty', async () => { + const {instance, reactive} = makeInstance(); + const emptyEl = makeInputEl('smdi_0_creator_lastName', ''); + instance.getElements.mockImplementation(sel => { + if (sel === instance.selectors.REQUIREDINPUTS) return [emptyEl]; + return []; + }); + const result = await instance.update(true); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(emptyEl, 'Required'); + expect(reactive.dispatch).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + + test('clears error notification for an is-invalid field that now has a value', async () => { + const {instance, reactive} = makeInstance(); + const fixedEl = makeInputEl('smdi_0_creator_lastName', 'Smith', ['is-invalid']); + instance.getElements.mockImplementation(sel => { + if (sel === instance.selectors.REQUIREDINPUTS) return [fixedEl]; + if (sel === instance.selectors.ALLINPUTS) return [fixedEl]; + return []; + }); + reactive.dispatch.mockResolvedValue(undefined); + await instance.update(true); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(fixedEl, ''); + }); + + test('returns false and notifies on invalid freeform JSON', async () => { + const {instance, reactive} = makeInstance(); + const freeformEl = {value: '{bad json', classList: {contains: () => false}}; + instance.getElements.mockReturnValue([]); + instance.getElement.mockImplementation(sel => { + if (sel === '#smdi_0_freeform_value') return freeformEl; + return null; + }); + const result = await instance.update(true); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(freeformEl, expect.any(String)); + expect(reactive.dispatch).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + + test('clears freeform error and dispatches when freeform JSON is valid', async () => { + const {instance, reactive} = makeInstance(); + const freeformEl = {value: '{"ok":true}', classList: {contains: () => false}}; + instance.getElements.mockImplementation(sel => { + if (sel === instance.selectors.REQUIREDINPUTS) return []; + if (sel === instance.selectors.ALLINPUTS) return []; + return []; + }); + instance.getElement.mockImplementation(sel => { + if (sel === '#smdi_0_freeform_value') return freeformEl; + return null; + }); + reactive.dispatch.mockResolvedValue(undefined); + const result = await instance.update(true); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(freeformEl, ''); + expect(result).toBe(true); + }); + + test('returns false and notifies per-element when dispatch throws comma-separated IDs', async () => { + const {instance, reactive} = makeInstance(); + instance.getElements.mockReturnValue([]); + instance.getElement.mockReturnValue(null); + reactive.dispatch.mockRejectedValue('2,5'); + const errorEl = {value: ''}; + instance.getElement.mockImplementation(sel => { + if (sel.includes('smdi_2_additional_qualifier') || sel.includes('smdi_5_additional_qualifier')) { + return errorEl; + } + return null; + }); + const result = await instance.update(false); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(errorEl, 'Required'); + expect(result).toBe(false); + }); +}); + +// ── addItem ──────────────────────────────────────────────────────────────────── + +describe('addItem', () => { + test('does not dispatch when update() returns false', async () => { + const {instance, reactive} = makeInstance(); + jest.spyOn(instance, 'update').mockResolvedValue(false); + await instance.addItem({target: {id: 'smd_add_1_language'}}); + expect(reactive.dispatch).not.toHaveBeenCalled(); + }); + + test('dispatches addItem with type derived from event.target.id when update succeeds', async () => { + const {instance, reactive} = makeInstance(); + jest.spyOn(instance, 'update').mockResolvedValue(true); + reactive.dispatch.mockResolvedValue(undefined); + await instance.addItem({target: {id: 'smd_add_contributor_user'}}); + // id.split('_') → ['smd', 'add', 'contributor', 'user']; parts[1]='add', parts[2]='contributor' + expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'add', 'contributor'); + }); +}); + +// ── deleteItem ───────────────────────────────────────────────────────────────── + +describe('deleteItem', () => { + test('does not dispatch when update() returns false', async () => { + const {instance, reactive} = makeInstance(); + jest.spyOn(instance, 'update').mockResolvedValue(false); + await instance.deleteItem({target: {id: 'smd_delete_1_language'}}); + expect(reactive.dispatch).not.toHaveBeenCalled(); + }); + + test('dispatches deleteRow with id parts from event.target.id when update succeeds', async () => { + const {instance, reactive} = makeInstance(); + jest.spyOn(instance, 'update').mockResolvedValue(true); + reactive.dispatch.mockResolvedValue(undefined); + await instance.deleteItem({target: {id: 'smd_delete_1_language'}}); + expect(reactive.dispatch).toHaveBeenCalledWith('deleteRow', 'delete', '1'); + }); +}); + +// ── makeContributor ──────────────────────────────────────────────────────────── + +describe('makeContributor', () => { + test('does not dispatch when update() returns false', async () => { + const {instance, reactive} = makeInstance(); + jest.spyOn(instance, 'update').mockResolvedValue(false); + await instance.makeContributor(); + expect(reactive.dispatch).not.toHaveBeenCalled(); + }); + + test('dispatches addItem for contributor/user when update succeeds', async () => { + const {instance, reactive} = makeInstance(); + jest.spyOn(instance, 'update').mockResolvedValue(true); + reactive.dispatch.mockResolvedValue(undefined); + await instance.makeContributor(); + expect(reactive.dispatch).toHaveBeenCalledWith('addItem', 'contributor', 'user'); + }); +}); + +// ── updateInputs ─────────────────────────────────────────────────────────────── + +describe('updateInputs', () => { + test('notifies validation failure and returns early when JSON is invalid', () => { + const {instance, reactive} = makeInstance(); + const jsonEl = {value: '{invalid json'}; + instance.getElement.mockReturnValue(jsonEl); + metadata.jsonToState.mockImplementation(() => { throw new SyntaxError('Unexpected token'); }); + instance.updateInputs(); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(jsonEl, expect.any(String)); + expect(reactive.dispatch).not.toHaveBeenCalled(); + }); + + test('clears error, updates element value and dispatches updateFromJson on valid JSON', () => { + const {instance, reactive} = makeInstance(); + const jsonEl = {value: '{"language":[{"id":1,"value":"en"}]}'}; + instance.getElement.mockReturnValue(jsonEl); + const parsed = {language: [{id: 1, value: 'en'}]}; + metadata.jsonToState.mockReturnValue(parsed); + metadata.jsonStringify.mockReturnValue('prettified'); + reactive.dispatch.mockResolvedValue(undefined); + instance.updateInputs(); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(jsonEl, ''); + expect(jsonEl.value).toBe('prettified'); + expect(reactive.dispatch).toHaveBeenCalledWith('updateFromJson', parsed); + }); +}); + +// ── revert ───────────────────────────────────────────────────────────────────── + +describe('revert', () => { + test('restores JSON and dispatches updateFromJson when saved value is valid', () => { + const {instance, reactive} = makeInstance(); + const jsonEl = {value: ''}; + instance.getElement.mockReturnValue(jsonEl); + const formInput = {value: '{"language":[]}'}; + mockQuerySelector.mockReturnValue(formInput); + const parsed = {language: []}; + metadata.jsonToState.mockReturnValue(parsed); + metadata.jsonStringify.mockReturnValue('pretty'); + reactive.dispatch.mockResolvedValue(undefined); + instance.revert(); + expect(jsonEl.value).toBe('pretty'); + expect(reactive.dispatch).toHaveBeenCalledWith('updateFromJson', parsed); + }); + + test('notifies validation failure and sets brokenMetadata when saved value is invalid JSON', () => { + const {instance, reactive} = makeInstance(); + const jsonEl = {value: ''}; + instance.getElement.mockReturnValue(jsonEl); + mockQuerySelector.mockReturnValue({value: '{broken'}); + const parseError = new SyntaxError('Unexpected token'); + let callCount = 0; + metadata.jsonToState.mockImplementation(() => { + // First call: parsing the saved (invalid) JSON → throws. + // Second call: inside the catch block for the dispatch argument (valid '{}') → succeeds. + if (callCount++ === 0) throw parseError; + return {}; + }); + reactive.dispatch.mockResolvedValue(undefined); + instance.revert(); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(jsonEl, parseError.message); + expect(metadata.lib.brokenMetadata).toBe(parseError.message); + expect(reactive.dispatch).toHaveBeenCalledWith('updateFromJson', {}); + }); +}); + +// ── reloadContainerComponent ─────────────────────────────────────────────────── + +describe('reloadContainerComponent', () => { + // Build a state object; any top-level key can be overridden. + function makeState(overrides = {}) { + const items = (arr) => ({forEach: (cb) => arr.forEach(cb)}); + return { + creator: {firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}, + language: items([{id: 1, value: 'en'}]), + contributor: items([{id: 1, firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}]), + additional: items([]), + license: {value: 'cc-by'}, + isPartOf: {value: 'course1'}, + freeform: {value: '{}'}, + ...overrides, + }; + } + + // Convenience: a forEach-able collection from a plain array. + const rows = (arr) => ({forEach: (cb) => arr.forEach(cb)}); + + // Wire up the instance so reloadContainerComponent can run end-to-end. + function setupForRender(instance) { + const fakeContainer = {}; + instance.getElement.mockReturnValue(fakeContainer); + instance.getElements.mockReturnValue([]); + mockQuerySelector.mockReturnValue({value: '{}'}); + metadata.jsonStringify.mockReturnValue('prettified-json'); + return fakeContainer; + } + + // Run reloadContainerComponent and capture the data argument passed to renderComponent. + async function captureData(instance, state) { + let captured = null; + instance.renderComponent.mockImplementation(async (_el, _tmpl, data) => { captured = data; }); + await instance.reloadContainerComponent({state}); + return captured; + } + + // ── existing structural tests ──────────────────────────────────────────────── + + test('calls renderComponent with the metadatacontent template', async () => { + const {instance} = makeInstance(); + const state = makeState(); + metadata.jsonStringify.mockReturnValue('{}'); + const fakeContainer = {}; + mockQuerySelector.mockReturnValue({value: '{}'}); + instance.getElement.mockReturnValue(fakeContainer); + instance.getElements.mockReturnValue([]); + await instance.reloadContainerComponent({state}); + expect(instance.renderComponent).toHaveBeenCalledWith( + fakeContainer, + 'qtype_stack/metadata/metadatacontent', + expect.any(Object) + ); + }); + + test('throws when the metadata container DOM element is not found', async () => { + const {instance} = makeInstance(); + const state = makeState(); + metadata.jsonStringify.mockReturnValue('{}'); + instance.getElement.mockReturnValue(null); + await expect(instance.reloadContainerComponent({state})).rejects.toThrow('Missing metadata container'); + }); + + test('registers click listeners for add and delete buttons after render', async () => { + const {instance} = makeInstance(); + const state = makeState(); + metadata.jsonStringify.mockReturnValue('{}'); + const fakeContainer = {}; + const fakeButton = {}; + mockQuerySelector.mockReturnValue({value: '{}'}); + instance.getElement.mockReturnValue(fakeContainer); + instance.getElements.mockImplementation(sel => { + if (sel === instance.selectors.ADDITEM || sel === instance.selectors.DELETEITEM) { + return [fakeButton]; + } + return []; + }); + await instance.reloadContainerComponent({state}); + expect(instance.addEventListener).toHaveBeenCalledWith(fakeButton, 'click', instance.addItem); + expect(instance.addEventListener).toHaveBeenCalledWith(fakeButton, 'click', instance.deleteItem); + }); + + // ── data.creator ───────────────────────────────────────────────────────────── + + describe('data.creator', () => { + test('maps all four creator fields with correct values', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.creator.firstname.element.value).toBe('Alice'); + expect(data.creator.lastname.element.value).toBe('Smith'); + expect(data.creator.institution.element.value).toBe('Uni'); + expect(data.creator.year.element.value).toBe('2025'); + }); + + test('lastname is not required; firstname, institution and year are not', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.creator.lastname.required).toBe(false); + expect(data.creator.firstname.required).toBe(false); + expect(data.creator.institution.required).toBe(false); + expect(data.creator.year.required).toBe(false); + }); + + test('element IDs follow the creator_* naming convention', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.creator.firstname.element.id).toBe('smdi_0_creator_firstName'); + expect(data.creator.lastname.element.id).toBe('smdi_0_creator_lastName'); + expect(data.creator.institution.element.id).toBe('smdi_0_creator_institution'); + expect(data.creator.year.element.id).toBe('smdi_0_creator_year'); + }); + }); + + // ── data.language ───────────────────────────────────────────────────────────── + + describe('data.language', () => { + test('empty language list produces an empty array', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({language: rows([])})); + expect(data.language).toEqual([]); + }); + + test('two languages produce two entries with correct ids and values', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({language: rows([{id: 1, value: 'en'}, {id: 2, value: 'fr'}])}); + const data = await captureData(instance, state); + expect(data.language).toHaveLength(2); + expect(data.language[0].id).toBe(1); + expect(data.language[0].lang.element.value).toBe('en'); + expect(data.language[1].id).toBe(2); + expect(data.language[1].lang.element.value).toBe('fr'); + }); + + test('language element IDs embed the language id', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({language: rows([{id: 3, value: 'de'}])})); + expect(data.language[0].lang.element.id).toBe('smdi_3_language_value'); + }); + }); + + // ── data.contributor ────────────────────────────────────────────────────────── + + describe('data.contributor', () => { + test('single contributor has all four fields mapped', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.contributor).toHaveLength(1); + expect(data.contributor[0].id).toBe(1); + expect(data.contributor[0].firstname.element.value).toBe('Bob'); + expect(data.contributor[0].lastname.element.value).toBe('Jones'); + expect(data.contributor[0].institution.element.value).toBe('Uni2'); + expect(data.contributor[0].year.element.value).toBe('2026'); + }); + + test('contributor lastname is not required; firstname, institution and year are not', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.contributor[0].lastname.required).toBe(false); + expect(data.contributor[0].firstname.required).toBe(false); + expect(data.contributor[0].institution.required).toBe(false); + expect(data.contributor[0].year.required).toBe(false); + }); + + test('empty contributor list produces an empty array', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({contributor: rows([])})); + expect(data.contributor).toEqual([]); + }); + }); + + // ── data.license ────────────────────────────────────────────────────────────── + + describe('data.license', () => { + test('matching license option is marked as selected', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({license: {value: 'cc-by'}})); + const selected = data.license.element.options.find(o => o.selected); + expect(selected).toBeDefined(); + expect(selected.value).toBe('cc-by'); + }); + + test('original metadata.lib.licenses array is not mutated', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + await captureData(instance, makeState({license: {value: 'cc-by'}})); + expect(metadata.lib.licenses.some(o => o.selected)).toBe(false); + }); + + test('unknown license value is appended as a new selected option', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({license: {value: 'custom-license'}})); + const added = data.license.element.options.find(o => o.value === 'custom-license'); + expect(added).toBeDefined(); + expect(added.selected).toBe(true); + }); + + test('autocomplete properties are set on the license element', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.license.element.tags).toBe('[]'); + expect(data.license.element.ajax).toBe(''); + expect(data.license.element.placeholder).toBe(metadata.lib.placeholder); + expect(data.license.element.noselectionstring).toBe(''); + expect(data.license.element.showsuggestions).toBe('true'); + expect(data.license.element.casesensitive).toBe('false'); + }); + }); + + // ── data.isPartOf ───────────────────────────────────────────────────────────── + + describe('data.isPartOf', () => { + test('value is passed through and the field is not required', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({isPartOf: {value: 'quiz42'}})); + expect(data.isPartOf.element.value).toBe('quiz42'); + expect(data.isPartOf.required).toBe(false); + }); + }); + + // ── data.freeform ───────────────────────────────────────────────────────────── + + describe('data.freeform', () => { + test('non-empty freeform value is passed through unchanged', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({freeform: {value: '{"times":[1,2,3]}'}}); + const data = await captureData(instance, state); + expect(data.freeform.element.value).toBe('{"times":[1,2,3]}'); + }); + + test('empty string freeform value falls back to empty object literal', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({freeform: {value: ''}})); + expect(data.freeform.element.value).toBe('{}'); + }); + + test('falsy freeform value (null) falls back to empty object literal', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({freeform: {value: null}})); + expect(data.freeform.element.value).toBe('{}'); + }); + + test('freeform field is not required', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.freeform.required).toBe(false); + }); + }); + + // ── data.scope (additional metadata) ────────────────────────────────────────── + + describe('data.scope (additional metadata)', () => { + test('no additional items produces an empty scope array', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState({additional: rows([])})); + expect(data.scope).toEqual([]); + }); + + test('single item creates one scope entry with one property', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'q1', value: 'math'}, + ]), + }); + const data = await captureData(instance, state); + expect(data.scope).toHaveLength(1); + expect(data.scope[0].name).toBe('dc'); + expect(data.scope[0].properties).toHaveLength(1); + }); + + test('property, qualifier and value elements are mapped from the additional item', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'narrow', value: 'math'}, + ]), + }); + const data = await captureData(instance, state); + const prop = data.scope[0].properties[0]; + expect(prop.property.element.value).toBe('subject'); + expect(prop.qualifier.element.value).toBe('narrow'); + expect(prop.value.element.value).toBe('math'); + }); + + test('two items with the same scope are grouped into one scope entry', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'q1', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'q2', value: 'physics'}, + ]), + }); + const data = await captureData(instance, state); + expect(data.scope).toHaveLength(1); + expect(data.scope[0].properties).toHaveLength(2); + expect(data.scope[0].properties[1].qualifier.element.value).toBe('q2'); + }); + + test('items with different scopes produce separate scope entries', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'lom', property: 'keyword', qualifier: '', value: 'algebra'}, + ]), + }); + const data = await captureData(instance, state); + expect(data.scope).toHaveLength(2); + expect(data.scope.map(s => s.name)).toEqual(expect.arrayContaining(['dc', 'lom'])); + }); + + test('three items across two scopes: each scope groups its own correctly', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'q1', value: 'math'}, + {id: 2, scope: 'lom', property: 'keyword', qualifier: 'q2', value: 'algebra'}, + {id: 3, scope: 'dc', property: 'title', qualifier: 'q3', value: 'calculus'}, + ]), + }); + const data = await captureData(instance, state); + const dc = data.scope.find(s => s.name === 'dc'); + const lom = data.scope.find(s => s.name === 'lom'); + expect(dc.properties).toHaveLength(2); + expect(lom.properties).toHaveLength(1); + expect(lom.properties[0].value.element.value).toBe('algebra'); + }); + + test('firstProp is the id of the first item in the scope', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 5, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 6, scope: 'dc', property: 'subject', qualifier: '', value: 'physics'}, + ]), + }); + const data = await captureData(instance, state); + expect(data.scope[0].firstProp).toBe(5); + }); + + test('scope input element has the scope name as its value and is required', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 3, scope: 'lom', property: 'keyword', qualifier: '', value: 'algebra'}, + ]), + }); + const data = await captureData(instance, state); + expect(data.scope[0].input.element.value).toBe('lom'); + expect(data.scope[0].input.required).toBe(true); + }); + + test('additional element IDs embed the item id', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState({ + additional: rows([ + {id: 7, scope: 'dc', property: 'subject', qualifier: 'q', value: 'v'}, + ]), + }); + const data = await captureData(instance, state); + const prop = data.scope[0].properties[0]; + expect(prop.property.element.id).toBe('smdi_7_additional_property'); + expect(prop.qualifier.element.id).toBe('smdi_7_additional_qualifier'); + expect(prop.value.element.id).toBe('smdi_7_additional_value'); + }); + }); + + // ── data.json ───────────────────────────────────────────────────────────────── + + describe('data.json', () => { + test('calls jsonStringify with the state and spacing=4', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const state = makeState(); + await captureData(instance, state); + expect(metadata.jsonStringify).toHaveBeenCalledWith(state, 4); + }); + + test('jsonStringify return value appears in data.json.element.value', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + metadata.jsonStringify.mockReturnValue('{"pretty":true}'); + const data = await captureData(instance, makeState()); + expect(data.json.element.value).toBe('{"pretty":true}'); + }); + + test('fixed structure fields are always set correctly', async () => { + const {instance} = makeInstance(); + setupForRender(instance); + const data = await captureData(instance, makeState()); + expect(data.json.required).toBe(true); + expect(data.json.element.attributes).toBe('rows="10"'); + expect(data.json.element.id).toBe('id_metadata_json'); + expect(data.json.element.name).toBe('metadata_json'); + expect(data.json.element.wrapperid).toBe('fitem_metadata_json'); + }); + }); + + // ── brokenMetadata branch ───────────────────────────────────────────────────── + + describe('brokenMetadata branch', () => { + test('loads the FORMJSON value into the JSONINPUT element and shows the error', async () => { + const {instance} = makeInstance(); + metadata.lib.brokenMetadata = 'JSON parse error'; + metadata.jsonStringify.mockReturnValue('{}'); + const fakeContainer = {}; + const jsonInputEl = {value: ''}; + instance.renderComponent.mockResolvedValue(undefined); + instance.getElement.mockImplementation(sel => { + if (sel === instance.selectors.JSONINPUT) return jsonInputEl; + return fakeContainer; + }); + instance.getElements.mockReturnValue([]); + mockQuerySelector.mockReturnValue({value: '{"saved":true}'}); + await instance.reloadContainerComponent({state: makeState()}); + expect(jsonInputEl.value).toBe('{"saved":true}'); + expect(notifyFieldValidationFailure).toHaveBeenCalledWith(jsonInputEl, 'JSON parse error'); + }); + + test('deletes brokenMetadata from metadata.lib after displaying the error', async () => { + const {instance} = makeInstance(); + metadata.lib.brokenMetadata = 'JSON parse error'; + metadata.jsonStringify.mockReturnValue('{}'); + const fakeContainer = {}; + instance.getElement.mockReturnValue(fakeContainer); + instance.getElements.mockReturnValue([]); + mockQuerySelector.mockReturnValue({value: '{}'}); + await instance.reloadContainerComponent({state: makeState()}); + expect(metadata.lib.brokenMetadata).toBeUndefined(); + }); + }); +}); diff --git a/tests/jest/metadata.modal.test.js b/tests/jest/metadata.modal.test.js new file mode 100644 index 00000000000..89ee56a2629 --- /dev/null +++ b/tests/jest/metadata.modal.test.js @@ -0,0 +1,121 @@ +const {MetadataModal, setup} = require('../../amd/src/metadata/metadatamodal.js'); +const Modal = require('core/modal').default; +const {metadata} = require('qtype_stack/metadata/metadata'); + +// ── DOM mock ─────────────────────────────────────────────────────────────────── +// metadatamodal.js accesses the DOM only inside function calls (not at module +// load time), so a global mock set up before the test run is sufficient. + +let mockElements = {}; +const mockQuerySelector = jest.fn(); + +beforeAll(() => { + global.document = {querySelector: mockQuerySelector}; +}); + +beforeEach(() => { + // resetAllMocks clears call history AND return-value implementations. + jest.resetAllMocks(); + mockElements = {}; + // Re-apply implementation after reset. + mockQuerySelector.mockImplementation(sel => mockElements[sel] ?? null); +}); + +// ── cancel ───────────────────────────────────────────────────────────────────── + +describe('cancel', () => { + test('delegates directly to super.hide()', () => { + const modal = new MetadataModal(); + modal.cancel(); + expect(Modal.prototype.hide).toHaveBeenCalledTimes(1); + }); +}); + +// ── hide ─────────────────────────────────────────────────────────────────────── + +describe('hide', () => { + test('does not close the modal when update() resolves falsy', async () => { + metadata.container.update.mockResolvedValue(false); + const modal = new MetadataModal(); + await modal.hide(); + expect(Modal.prototype.hide).not.toHaveBeenCalled(); + }); + + test('closes the modal when update() resolves truthy', async () => { + metadata.container.update.mockResolvedValue(true); + metadata.jsonStringify.mockReturnValue('{"same":true}'); + mockElements['input[name="metadata"]'] = {value: '{"same":true}'}; + const modal = new MetadataModal(); + await modal.hide(); + expect(Modal.prototype.hide).toHaveBeenCalledTimes(1); + }); + + test('does not overwrite the hidden field when the JSON value is unchanged', async () => { + metadata.container.update.mockResolvedValue(true); + metadata.jsonStringify.mockReturnValue('{"same":true}'); + mockElements['input[name="metadata"]'] = {value: '{"same":true}'}; + const modal = new MetadataModal(); + await modal.hide(); + // The write path calls querySelector a second time; only one call means no write occurred. + const inputCalls = mockQuerySelector.mock.calls.filter( + ([sel]) => sel === 'input[name="metadata"]' + ); + expect(inputCalls).toHaveLength(1); + }); + + test('updates the hidden metadata field when the JSON value changes', async () => { + metadata.container.update.mockResolvedValue(true); + metadata.jsonStringify.mockReturnValue('{"new":true}'); + const metaInput = {value: '{"old":true}'}; + mockElements['input[name="metadata"]'] = metaInput; + const modal = new MetadataModal(); + await modal.hide(); + expect(metaInput.value).toBe('{"new":true}'); + expect(Modal.prototype.hide).toHaveBeenCalledTimes(1); + }); + + test('updates the change-indicator textContent when the element exists', async () => { + metadata.container.update.mockResolvedValue(true); + metadata.jsonStringify.mockReturnValue('{"new":true}'); + mockElements['input[name="metadata"]'] = {value: '{"old":true}'}; + const textEl = {textContent: ''}; + mockElements['[data-name="metadata_text"]'] = textEl; + mockElements['#id_stack_metadata'] = { + getAttribute: jest.fn().mockReturnValue('Form has been changed'), + }; + const modal = new MetadataModal(); + await modal.hide(); + expect(textEl.textContent).toBe('Form has been changed'); + }); + + test('still closes and does not throw when change-indicator element is absent (Moodle 4.2)', async () => { + metadata.container.update.mockResolvedValue(true); + metadata.jsonStringify.mockReturnValue('{"new":true}'); + mockElements['input[name="metadata"]'] = {value: '{"old":true}'}; + // No metadata_text element → querySelector returns null → caught by try/catch in hide(). + const modal = new MetadataModal(); + await expect(modal.hide()).resolves.toBeUndefined(); + expect(Modal.prototype.hide).toHaveBeenCalledTimes(1); + }); +}); + +// ── setup ────────────────────────────────────────────────────────────────────── + +describe('setup', () => { + test('calls metadata.loadState()', () => { + setup(); + expect(metadata.loadState).toHaveBeenCalledTimes(1); + }); + + test('adds a click listener to the modal button when it exists', () => { + const addListener = jest.fn(); + mockElements['#id_metadatamodal'] = {addEventListener: addListener}; + setup(); + expect(addListener).toHaveBeenCalledWith('click', expect.any(Function)); + }); + + test('does not throw when the modal button element is absent', () => { + // querySelector returns null → optional chaining skips addEventListener. + expect(() => setup()).not.toThrow(); + }); +}); diff --git a/tests/jest/metadata.mutations.test.js b/tests/jest/metadata.mutations.test.js new file mode 100644 index 00000000000..543159db544 --- /dev/null +++ b/tests/jest/metadata.mutations.test.js @@ -0,0 +1,354 @@ +const {mutations} = require('../../amd/src/metadata/mutations.js'); + +/** + * Minimal mock of a Moodle reactive state collection. + * Supports the same API used by the mutations: get, forEach, delete, add, + * and Array.from() (which delegates to the underlying Map iterator). + */ +class MockStateCollection { + constructor(items = []) { + this._map = new Map(items.map(item => [String(item.id), item])); + } + get(id) { return this._map.get(String(id)); } + forEach(callback) { for (const item of this._map.values()) callback(item); } + delete(id) { this._map.delete(String(id)); } + add(item) { this._map.set(String(item.id), item); } + [Symbol.iterator]() { return this._map[Symbol.iterator](); } +} + +function makeState(overrides = {}) { + return { + creator: {firstName: '', lastName: '', institution: '', year: ''}, + contributor: new MockStateCollection(), + language: new MockStateCollection(), + license: {id: '', value: ''}, + isPartOf: {id: '', value: ''}, + additional: new MockStateCollection(), + freeform: {id: '', value: ''}, + metadataTicker: {value: 1}, + ...overrides, + }; +} + +function makeStateManager(state) { + return {state, setReadOnly: jest.fn()}; +} + +// ── updateFromJson ───────────────────────────────────────────────────────────── + +describe('updateFromJson', () => { + test('copies each supplied property onto state', () => { + const state = makeState(); + const stateManager = makeStateManager(state); + mutations.updateFromJson(stateManager, { + creator: {firstName: 'Alice'}, + license: {id: '', value: 'MIT'}, + }); + expect(state.creator).toEqual({firstName: 'Alice'}); + expect(state.license).toEqual({id: '', value: 'MIT'}); + }); + + test('increments metadataTicker', () => { + const state = makeState(); + mutations.updateFromJson(makeStateManager(state), {}); + expect(state.metadataTicker.value).toBe(2); + }); + + test('opens and closes read-only around the update', () => { + const stateManager = makeStateManager(makeState()); + mutations.updateFromJson(stateManager, {}); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(false); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(true); + }); +}); + +// ── deleteRow ────────────────────────────────────────────────────────────────── + +describe('deleteRow', () => { + test('removes the specified contributor entry and leaves others intact', () => { + const contributor = new MockStateCollection([ + {id: 1, firstName: 'Alice'}, + {id: 2, firstName: 'Bob'}, + ]); + const stateManager = makeStateManager(makeState({contributor})); + mutations.deleteRow(stateManager, 'contributor', '1'); + expect(stateManager.state.contributor.get('1')).toBeUndefined(); + expect(stateManager.state.contributor.get('2')).toBeDefined(); + }); + + test('removes the specified language entry and leaves others intact', () => { + const language = new MockStateCollection([ + {id: 1, value: 'en'}, + {id: 2, value: 'fr'}, + ]); + const stateManager = makeStateManager(makeState({language})); + mutations.deleteRow(stateManager, 'language', '2'); + expect(stateManager.state.language.get('2')).toBeUndefined(); + expect(stateManager.state.language.get('1')).toBeDefined(); + }); + + test('scope: removes all additional entries sharing that scope', () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'title', qualifier: '', value: 'My Title'}, + {id: 3, scope: 'lrmi', property: 'level', qualifier: '', value: 'HE'}, + ]); + const stateManager = makeStateManager(makeState({additional})); + mutations.deleteRow(stateManager, 'scope', '1'); + expect(stateManager.state.additional.get('1')).toBeUndefined(); + expect(stateManager.state.additional.get('2')).toBeUndefined(); + }); + + test('scope: entries with a different scope are not removed', () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'lrmi', property: 'level', qualifier: '', value: 'HE'}, + ]); + const stateManager = makeStateManager(makeState({additional})); + mutations.deleteRow(stateManager, 'scope', '1'); + expect(stateManager.state.additional.get('2')).toBeDefined(); + }); + + test('opens and closes read-only around the delete', () => { + const stateManager = makeStateManager(makeState({ + language: new MockStateCollection([{id: 1, value: 'en'}]), + })); + mutations.deleteRow(stateManager, 'language', '1'); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(false); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(true); + }); +}); + +// ── addItem ──────────────────────────────────────────────────────────────────── + +describe('addItem', () => { + test('language: adds a blank entry with id 1 to an empty collection', () => { + const language = new MockStateCollection(); + const stateManager = makeStateManager(makeState({language})); + mutations.addItem(stateManager, 'language'); + expect(stateManager.state.language.get('1')).toEqual({value: '', id: 1}); + }); + + test('language: assigns next sequential id when collection is non-empty', () => { + const language = new MockStateCollection([{id: 1, value: 'en'}]); + const stateManager = makeStateManager(makeState({language})); + mutations.addItem(stateManager, 'language'); + expect(stateManager.state.language.get('2')).toMatchObject({value: '', id: 2}); + }); + + test('contributor: adds a blank entry when id is not "user"', () => { + const contributor = new MockStateCollection(); + const stateManager = makeStateManager(makeState({contributor})); + mutations.addItem(stateManager, 'contributor', 'blank'); + const item = stateManager.state.contributor.get('1'); + expect(item.firstName).toBe(''); + expect(item.lastName).toBe(''); + expect(item.institution).toBe(''); + expect(item.year).toBe(String(new Date().getFullYear())); + }); + + test('contributor: prefills user data when id is "user"', () => { + const contributor = new MockStateCollection(); + const stateManager = makeStateManager(makeState({contributor})); + mutations.addItem(stateManager, 'contributor', 'user'); + const item = stateManager.state.contributor.get('1'); + expect(item.firstName).toBe('Jane'); + expect(item.lastName).toBe('Doe'); + expect(item.institution).toBe('Test University'); + }); + + test('scope: adds a blank additional entry with all empty fields', () => { + const additional = new MockStateCollection(); + const stateManager = makeStateManager(makeState({additional})); + mutations.addItem(stateManager, 'scope'); + expect(stateManager.state.additional.get('1')).toEqual( + {scope: '', property: '', qualifier: '', value: '', id: 1} + ); + }); + + test('property: inherits scope from the referenced additional entry', () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + ]); + const stateManager = makeStateManager(makeState({additional})); + mutations.addItem(stateManager, 'property', '1'); + const newItem = stateManager.state.additional.get('2'); + expect(newItem.scope).toBe('dc'); + expect(newItem.property).toBe(''); + expect(newItem.qualifier).toBe(''); + expect(newItem.value).toBe(''); + }); + + test('opens and closes read-only around the add', () => { + const stateManager = makeStateManager(makeState({ + language: new MockStateCollection(), + })); + mutations.addItem(stateManager, 'language'); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(false); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(true); + }); +}); + +// ── updateAll ────────────────────────────────────────────────────────────────── + +describe('updateAll', () => { + test('updates a scalar field (id=0) directly on the state property', async () => { + const state = makeState(); + await mutations.updateAll(makeStateManager(state), [['smdi_0_creator_firstName', 'Alice']]); + expect(state.creator.firstName).toBe('Alice'); + }); + + test('updates a collection item by id', async () => { + const language = new MockStateCollection([{id: 1, value: 'en'}]); + const state = makeState({language}); + await mutations.updateAll(makeStateManager(state), [['smdi_1_language_value', 'fr']]); + expect(state.language.get('1').value).toBe('fr'); + }); + + test('updates an additional item field', async () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + ]); + const state = makeState({additional}); + await mutations.updateAll(makeStateManager(state), [['smdi_1_additional_property', 'title']]); + expect(state.additional.get('1').property).toBe('title'); + }); + + test('scope update propagates to all additional entries sharing that scope', async () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'title', qualifier: '', value: 'My Title'}, + ]); + const state = makeState({additional}); + await mutations.updateAll(makeStateManager(state), [['smdi_1_additional_scope', 'lrmi']]); + expect(state.additional.get('1').scope).toBe('lrmi'); + expect(state.additional.get('2').scope).toBe('lrmi'); + }); + + test('scope update does not affect entries with a different scope', async () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'lrmi', property: 'level', qualifier: '', value: 'HE'}, + ]); + const state = makeState({additional}); + await mutations.updateAll(makeStateManager(state), [['smdi_1_additional_scope', 'schema']]); + expect(state.additional.get('2').scope).toBe('lrmi'); + }); + + test('rejects when a qualifier-less row has the same scope+property as a qualified row', async () => { + // This shouldn't really happen anyway but hey. + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'en', value: 'physics'}, + ]); + const state = makeState({additional}); + await expect(mutations.updateAll(makeStateManager(state), [])).rejects.toBe('1'); + }); + + test('rejects when inputArray introduces a qualifier that conflicts with an existing qualifier-less row', async () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: '', value: 'physics'}, + ]); + const state = makeState({additional}); + // Setting a qualifier on row 2 leaves row 1 qualifier-less with the same scope+property. + await expect( + mutations.updateAll(makeStateManager(state), [['smdi_2_additional_qualifier', 'en']]) + ).rejects.toBe('1'); + }); + + test('rejects when inputArray introduces a qualifier-less row that conflicts with an existing qualified row', async () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'bob', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'bob', value: 'physics'}, + ]); + const state = makeState({additional}); + // Setting a qualifier on row 2 leaves row 1 qualifier-less with the same scope+property. + await expect( + mutations.updateAll(makeStateManager(state), [['smdi_1_additional_qualifier', '']]) + ).rejects.toBe('1'); + }); + + test('applies concurrent scope, property and qualifier updates across multiple rows', async () => { + // State: three additional rows across two scopes. + // row 1 – dc / subject / en = 'Algebra' + // row 2 – dc / level / en = 'HE' + // row 3 – lrmi / topic / '' = 'STEM' + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'en', value: 'Algebra'}, + {id: 2, scope: 'dc', property: 'level', qualifier: 'en', value: 'HE'}, + {id: 3, scope: 'lrmi', property: 'topic', qualifier: '', value: 'STEM'}, + ]); + const state = makeState({additional}); + + await mutations.updateAll(makeStateManager(state), [ + // Rename the entire 'dc' scope to 'schema' (rows 1 and 2 are affected). + ['smdi_1_additional_scope', 'schema'], + // Change the property of row 1 from 'subject' to 'name'. + ['smdi_1_additional_property', 'name'], + // Change the qualifier of row 2 from 'en' to 'fr'. + ['smdi_2_additional_qualifier', 'fr'], + // Change the value of row 3. + ['smdi_3_additional_value', 'Science'], + ]); + + // Both dc rows should now have scope 'schema'. + expect(state.additional.get('1').scope).toBe('schema'); + expect(state.additional.get('2').scope).toBe('schema'); + // Row 3 scope is untouched. + expect(state.additional.get('3').scope).toBe('lrmi'); + + // Row 1 property updated. + expect(state.additional.get('1').property).toBe('name'); + // Row 2 property untouched. + expect(state.additional.get('2').property).toBe('level'); + + // Row 2 qualifier updated. + expect(state.additional.get('2').qualifier).toBe('fr'); + // Row 1 qualifier untouched. + expect(state.additional.get('1').qualifier).toBe('en'); + + // Row 3 value updated. + expect(state.additional.get('3').value).toBe('Science'); + }); + + test('resolves with "Success" when there are no conflicts', async () => { + const state = makeState(); + await expect(mutations.updateAll(makeStateManager(state), [])).resolves.toBe('Success'); + }); + + test('increments metadataTicker on success', async () => { + const state = makeState(); + await mutations.updateAll(makeStateManager(state), []); + expect(state.metadataTicker.value).toBe(2); + }); + + test('processes multiple fields in a single call', async () => { + const state = makeState(); + await mutations.updateAll(makeStateManager(state), [ + ['smdi_0_creator_firstName', 'Alice'], + ['smdi_0_creator_lastName', 'Smith'], + ]); + expect(state.creator.firstName).toBe('Alice'); + expect(state.creator.lastName).toBe('Smith'); + }); + + test('opens and closes read-only around the update', async () => { + const stateManager = makeStateManager(makeState()); + await mutations.updateAll(stateManager, []); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(false); + expect(stateManager.setReadOnly).toHaveBeenCalledWith(true); + }); + + test('does not mutate state when promise is rejected', async () => { + const additional = new MockStateCollection([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'en', value: 'physics'}, + ]); + const state = makeState({additional}); + const stateManager = makeStateManager(state); + await mutations.updateAll(stateManager, []).catch(() => {}); + // State mutation only happens after the conflict check passes. + expect(stateManager.setReadOnly).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/jest/metadata.test.js b/tests/jest/metadata.test.js new file mode 100644 index 00000000000..e8cfe0a33ea --- /dev/null +++ b/tests/jest/metadata.test.js @@ -0,0 +1,365 @@ +const {metadata} = require('../../amd/src/metadata/metadata.js'); + +// ── reviver ──────────────────────────────────────────────────────────────────── + +describe('reviver', () => { + test('contributor: assigns sequential ids to each item', () => { + const input = [{firstName: 'Alice'}, {firstName: 'Bob'}]; + expect(metadata.reviver('contributor', input)).toEqual([ + {firstName: 'Alice', id: 1}, + {firstName: 'Bob', id: 2}, + ]); + }); + + test('language: converts array of strings to [{id, value}] objects', () => { + expect(metadata.reviver('language', ['en', 'fr'])).toEqual([ + {id: 1, value: 'en'}, + {id: 2, value: 'fr'}, + ]); + }); + + test('language: returns empty array for empty input', () => { + expect(metadata.reviver('language', [])).toEqual([]); + }); + + test('license: wraps value in {value: ...}', () => { + expect(metadata.reviver('license', 'MIT')).toEqual({value: 'MIT'}); + }); + + test('isPartOf: wraps value in {value: ...}', () => { + expect(metadata.reviver('isPartOf', 27)).toEqual({value: 27}); + }); + + test('freeform: JSON-stringifies the value into {value: ...}', () => { + expect(metadata.reviver('freeform', {a: 1})).toEqual({value: '{"a":1}'}); + }); + + test('freeform: handles empty object', () => { + expect(metadata.reviver('freeform', {})).toEqual({value: '{}'}); + }); + + test('default: returns value unchanged', () => { + expect(metadata.reviver('title', 'Hello')).toBe('Hello'); + expect(metadata.reviver('creator', {firstName: 'Alice'})).toEqual({firstName: 'Alice'}); + }); +}); + +// ── replacer ─────────────────────────────────────────────────────────────────── + +describe('replacer', () => { + test('metadataTicker: returns undefined', () => { + expect(metadata.replacer('metadataTicker', 'any')).toBeUndefined(); + }); + + test('id: returns undefined', () => { + expect(metadata.replacer('id', 5)).toBeUndefined(); + }); + + test('language: extracts .value from each item', () => { + expect(metadata.replacer('language', [{value: 'en'}, {value: 'fr'}])).toEqual(['en', 'fr']); + }); + + test('language: returns empty array for empty input', () => { + expect(metadata.replacer('language', [])).toEqual([]); + }); + + test('license: returns the value property', () => { + expect(metadata.replacer('license', {value: 'MIT'})).toBe('MIT'); + }); + + test('isPartOf: returns the value property', () => { + expect(metadata.replacer('isPartOf', {value: 'course1'})).toBe('course1'); + }); + + test('freeform: returns the value string', () => { + expect(metadata.replacer('freeform', {value: '{"a":1}'})).toBe('{"a":1}'); + }); + + test('freeform: returns "{}" when value is empty string', () => { + expect(metadata.replacer('freeform', {value: ''})).toBe('{}'); + }); + + test('freeform: returns "{}" when value is null', () => { + expect(metadata.replacer('freeform', {value: null})).toBe('{}'); + }); + + test('additional: empty array returns JSON-stringified empty object', () => { + expect(metadata.replacer('additional', [])).toBe('{}'); + }); + + test('additional: unqualified item produces flat nested object', () => { + const input = [{id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}]; + expect(metadata.replacer('additional', input)).toBe('{"dc":{"subject":"math"}}'); + }); + + test('additional: qualified item produces doubly-nested object', () => { + const input = [{id: 1, scope: 'dc', property: 'subject', qualifier: 'en', value: 'math'}]; + expect(metadata.replacer('additional', input)).toBe('{"dc":{"subject":{"en":"math"}}}'); + }); + + test('additional: two items with same scope/property are combined into an array', () => { + const input = [ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: '', value: 'physics'}, + ]; + expect(metadata.replacer('additional', input)).toBe('{"dc":{"subject":["math","physics"]}}'); + }); + + test('additional: two items with same scope/property/qualifier are combined into an array', () => { + const input = [ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'bob', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'bob', value: 'physics'}, + ]; + expect(metadata.replacer('additional', input)).toBe('{"dc":{"subject":{"bob":["math","physics"]}}}'); + }); + + test('additional: items from different scopes stay separate', () => { + const input = [ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'lrmi', property: 'subject', qualifier: '', value: 'HE'}, + ]; + const result = JSON.parse(metadata.replacer('additional', input)); + expect(result).toEqual({dc: {subject: 'math'}, lrmi: {subject: 'HE'}}); + }); + + test('default: returns value unchanged', () => { + expect(metadata.replacer('title', 'Hello World')).toBe('Hello World'); + expect(metadata.replacer('firstName', 'Alice')).toBe('Alice'); + }); +}); + +// ── stripFields ──────────────────────────────────────────────────────────────── + +describe('stripFields', () => { + test('keeps only the specified fields', () => { + expect(metadata.stripFields({a: 1, b: 2, c: 3}, ['a', 'c'])).toEqual({a: 1, c: 3}); + }); + + test('returns empty object when no fields match', () => { + expect(metadata.stripFields({a: 1}, ['b'])).toEqual({}); + }); + + test('returns empty object for empty input object', () => { + expect(metadata.stripFields({}, ['a', 'b'])).toEqual({}); + }); + + test('returns empty object when fields list is empty', () => { + expect(metadata.stripFields({a: 1, b: 2}, [])).toEqual({}); + }); +}); + +// ── addFields ────────────────────────────────────────────────────────────────── + +describe('addFields', () => { + test('adds missing fields as empty string', () => { + expect(metadata.addFields({a: 'hello'}, ['a', 'b'])).toEqual({a: 'hello', b: ''}); + }); + + test('converts existing values to strings', () => { + expect(metadata.addFields({a: 42, b: true}, ['a', 'b'])).toEqual({a: '42', b: 'true'}); + }); + + test('populates all fields as empty string for empty object', () => { + expect(metadata.addFields({}, ['x', 'y'])).toEqual({x: '', y: ''}); + }); +}); + +// ── tidyObject ───────────────────────────────────────────────────────────────── + +describe('tidyObject', () => { + test('strips unlisted fields and adds missing ones as empty string', () => { + expect(metadata.tidyObject({a: 1, b: 2, c: 'hello'}, ['a', 'c', 'd'])) + .toEqual({a: '1', c: 'hello', d: ''}); + }); + + test('handles null input by returning all fields as empty string', () => { + expect(metadata.tidyObject(null, ['a', 'b'])).toEqual({a: '', b: ''}); + }); + + test('handles non-object input by returning all fields as empty string', () => { + expect(metadata.tidyObject('not an object', ['a'])).toEqual({a: ''}); + }); + + test('handles undefined input', () => { + expect(metadata.tidyObject(undefined, ['a'])).toEqual({a: ''}); + }); +}); + +// ── jsonToState ──────────────────────────────────────────────────────────────── + +describe('jsonToState', () => { + test('empty JSON produces a fully-defaulted state', () => { + expect(metadata.jsonToState('{}')).toEqual({ + creator: {firstName: '', lastName: '', institution: '', year: ''}, + contributor: [], + language: [], + license: {id: '', value: ''}, + isPartOf: {id: '', value: ''}, + additional: [], + freeform: {id: '', value: ''}, + }); + }); + + test('parses creator fields and coerces values to strings', () => { + const input = JSON.stringify({creator: {firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: 2025}}); + expect(metadata.jsonToState(input).creator) + .toEqual({firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}); + }); + + test('strips unrecognised creator fields', () => { + const input = JSON.stringify({creator: {firstName: 'Alice', unknownField: 'ignored'}}); + expect(metadata.jsonToState(input).creator).not.toHaveProperty('unknownField'); + }); + + test('parses language strings into [{id, value}] objects', () => { + const input = JSON.stringify({language: ['en', 'fr']}); + expect(metadata.jsonToState(input).language) + .toEqual([{id: '1', value: 'en'}, {id: '2', value: 'fr'}]); + }); + + test('parses contributors and assigns sequential string ids', () => { + const input = JSON.stringify({contributor: [{firstName: 'Bob', lastName: 'Jones'},{firstName: 'Dave', lastName: 'Smith'}]}); + expect(metadata.jsonToState(input).contributor) + .toEqual([ + {id: '1', firstName: 'Bob', lastName: 'Jones', institution: '', year: ''}, + {id: '2', firstName: 'Dave', lastName: 'Smith', institution: '', year: ''} + ]); + }); + + test('parses license string into {id, value}', () => { + const input = JSON.stringify({license: 'MIT'}); + expect(metadata.jsonToState(input).license).toEqual({id: '', value: 'MIT'}); + }); + + test('parses isPartOf string into {id, value}', () => { + const input = JSON.stringify({isPartOf: 'course1'}); + expect(metadata.jsonToState(input).isPartOf).toEqual({id: '', value: 'course1'}); + }); + + test('strips top-level unrecognised fields', () => { + const input = JSON.stringify({unknownField: 'should be removed'}); + expect(metadata.jsonToState(input)).not.toHaveProperty('unknownField'); + }); + + test('parses additional unqualified entries in array form', () => { + const input = JSON.stringify({additional: {dc: {subject: ['math','physics']}}}); + expect(metadata.jsonToState(input).additional) + .toEqual([ + {id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: '', value: 'physics'}, + ]); + }); + + test('parses additional qualified entries in array form', () => { + const input = JSON.stringify({additional: {dc: {subject: {qual: ['math','physics']}}}}); + expect(metadata.jsonToState(input).additional) + .toEqual([ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'qual', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'qual', value: 'physics'} + ]); + }); + + test('parses additional unqualified entry', () => { + const input = JSON.stringify({additional: {dc: {subject: 'math'}}}); + expect(metadata.jsonToState(input).additional) + .toEqual([{id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}]); + }); + + test('parses additional qualified entry', () => { + const input = JSON.stringify({additional: {dc: {subject: {en: 'math'}}}}); + expect(metadata.jsonToState(input).additional) + .toEqual([{id: 1, scope: 'dc', property: 'subject', qualifier: 'en', value: 'math'}]); + }); + + test('parses additional qualified entry containing key words', () => { + const input = JSON.stringify({additional: {dc: {additional: {license: 'math'}}}}); + expect(metadata.jsonToState(input).additional) + .toEqual([{id: 1, scope: 'dc', property: 'additional', qualifier: 'license', value: 'math'}]); + }); + + test('parses freeform object into {id, value} where value is JSON string', () => { + const input = JSON.stringify({freeform: {custom: 'data'}}); + expect(metadata.jsonToState(input).freeform) + .toEqual({id: '', value: '{"custom":"data"}'}); + }); + + test('parses freeform object containing key waords into {id, value} where value is JSON string', () => { + const input = JSON.stringify({freeform: {additional: 'data'}}); + expect(metadata.jsonToState(input).freeform) + .toEqual({id: '', value: '{"additional":"data"}'}); + }); +}); + +// ── jsonStringify ────────────────────────────────────────────────────────────── + +describe('jsonStringify', () => { + const baseState = { + creator: {firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}, + contributor: [{firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}], + language: [{id: 1, value: 'en'},{id: 2, value: 'fr'}], + license: {id: 0, value: 'MIT'}, + isPartOf: {id: 0, value: 'course1'}, + additional: [ + {id: 1, scope: 'dc', property: 'subject', qualifier: 'bob', value: 'math'}, + {id: 2, scope: 'dc', property: 'subject', qualifier: 'bob', value: 'physics'}, + ], + freeform: {id: '', value: '{"times":[1,2,3]}'}, + }; + + test('round-trips creator fields unchanged', () => { + const result = JSON.parse(metadata.jsonStringify(baseState)); + expect(result.creator).toEqual({firstName: 'Alice', lastName: 'Smith', institution: 'Uni', year: '2025'}); + expect(result.contributor).toEqual([{firstName: 'Bob', lastName: 'Jones', institution: 'Uni2', year: '2026'}]); + }); + + test('converts language objects back to string array', () => { + const result = JSON.parse(metadata.jsonStringify(baseState)); + expect(result.language).toEqual(['en', 'fr']); + }); + + test('converts license object back to plain value', () => { + const result = JSON.parse(metadata.jsonStringify(baseState)); + expect(result.license).toBe('MIT'); + }); + + test('converts isPartOf object back to plain value', () => { + const result = JSON.parse(metadata.jsonStringify(baseState)); + expect(result.isPartOf).toBe('course1'); + }); + + test('removes metadataTicker field', () => { + const state = {...baseState, metadataTicker: {value: 5}}; + const result = JSON.parse(metadata.jsonStringify(state)); + expect(result).not.toHaveProperty('metadataTicker'); + }); + + test('converts additional qualified items into nested object', () => { + const result = JSON.parse(metadata.jsonStringify(baseState)); + expect(result.additional).toEqual({dc: {subject: {bob: ['math', 'physics']}}}); + }); + + test('converts additional array items back to nested object', () => { + const state = { + ...baseState, + additional: [{id: 1, scope: 'dc', property: 'subject', qualifier: '', value: 'math'}], + }; + const result = JSON.parse(metadata.jsonStringify(state)); + expect(result.additional).toEqual({dc: {subject: 'math'}}); + }); + + test('converts freeform value string back to parsed object', () => { + const result = JSON.parse(metadata.jsonStringify(baseState)); + expect(result.freeform).toEqual({times: [1, 2, 3]}); + }); + + test('respects spacing parameter for pretty-printing', () => { + const result = metadata.jsonStringify(baseState, 2); + expect(result).toContain('\n'); + expect(result).toContain('\n "creator'); + }); + + test('produces compact output with no spacing', () => { + const result = metadata.jsonStringify(baseState); + expect(result).not.toContain('\n'); + }); +}); diff --git a/tests/jest/package-lock.json b/tests/jest/package-lock.json new file mode 100644 index 00000000000..cc9d365e19d --- /dev/null +++ b/tests/jest/package-lock.json @@ -0,0 +1,5891 @@ +{ + "name": "jest", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.5", + "babel-jest": "^30.4.1", + "jest": "^30.4.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz", + "integrity": "sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.5.tgz", + "integrity": "sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.4", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.4.1", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.4.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.4.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.4.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.356", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz", + "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", + "import-local": "^3.2.0", + "jest-cli": "30.4.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0", + "pretty-format": "30.4.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "parse-json": "^5.2.0", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "jest-util": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.4.1", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/jest/package.json b/tests/jest/package.json new file mode 100644 index 00000000000..e4b2eb4ce41 --- /dev/null +++ b/tests/jest/package.json @@ -0,0 +1,18 @@ +{ + "scripts": { + "test": "jest" + }, + "jest": { + "moduleNameMapper": { + "^core/(.*)$": "/__mocks__/core/$1.js", + "^core_form/(.*)$": "/__mocks__/core_form/$1.js", + "^qtype_stack/(.*)$": "/__mocks__/qtype_stack/$1.js" + } + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.5", + "babel-jest": "^30.4.1", + "jest": "^30.4.2" + } +} diff --git a/tests/questiontype_test.php b/tests/questiontype_test.php index 1da69125dab..94c1e0ca7ec 100644 --- a/tests/questiontype_test.php +++ b/tests/questiontype_test.php @@ -270,6 +270,8 @@ public function test_xml_export(): void { lang [ 0 + + {} ans1 diff --git a/version.php b/version.php index 29afe2bbfea..d82e335a7a7 100644 --- a/version.php +++ b/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2026042402; +$plugin->version = 2026042700; $plugin->requires = 2022041900; $plugin->cron = 0; $plugin->component = 'qtype_stack';