mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-05 17:34:55 -04:00
577 lines
19 KiB
JavaScript
577 lines
19 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2012 Google Inc.
|
|
* https://developers.google.com/blockly/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Utility functions for handling procedures.
|
|
* @author fraser@google.com (Neil Fraser)
|
|
*/
|
|
'use strict';
|
|
|
|
/**
|
|
* @name Blockly.Procedures
|
|
* @namespace
|
|
**/
|
|
goog.provide('Blockly.Procedures');
|
|
|
|
goog.require('Blockly.Blocks');
|
|
goog.require('Blockly.constants');
|
|
goog.require('Blockly.Events.BlockChange');
|
|
goog.require('Blockly.Field');
|
|
goog.require('Blockly.Names');
|
|
goog.require('Blockly.Workspace');
|
|
|
|
|
|
/**
|
|
* Constant to separate procedure names from variables and generated functions
|
|
* when running generators.
|
|
* @deprecated Use Blockly.PROCEDURE_CATEGORY_NAME
|
|
*/
|
|
Blockly.Procedures.NAME_TYPE = Blockly.PROCEDURE_CATEGORY_NAME;
|
|
|
|
/**
|
|
* Find all user-created procedure definitions in a workspace.
|
|
* @param {!Blockly.Workspace} root Root workspace.
|
|
* @return {!Array.<!Array.<!Array>>} Pair of arrays, the
|
|
* first contains procedures without return variables, the second with.
|
|
* Each procedure is defined by a three-element list of name, parameter
|
|
* list, and return value boolean.
|
|
*/
|
|
Blockly.Procedures.allProcedures = function(root) {
|
|
var blocks = root.getAllBlocks();
|
|
var proceduresReturn = [];
|
|
var proceduresNoReturn = [];
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
if (blocks[i].getProcedureDef) {
|
|
var tuple = blocks[i].getProcedureDef();
|
|
if (tuple) {
|
|
if (tuple[2]) {
|
|
proceduresReturn.push(tuple);
|
|
} else {
|
|
proceduresNoReturn.push(tuple);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_);
|
|
proceduresReturn.sort(Blockly.Procedures.procTupleComparator_);
|
|
return [proceduresNoReturn, proceduresReturn];
|
|
};
|
|
|
|
/**
|
|
* Find all user-created procedure definition mutations in a workspace.
|
|
* @param {!Blockly.Workspace} root Root workspace.
|
|
* @return {!Array.<Element>} Array of mutation xml elements.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.allProcedureMutations = function(root) {
|
|
var blocks = root.getAllBlocks();
|
|
var mutations = [];
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
if (blocks[i].type == Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE) {
|
|
var mutation = blocks[i].mutationToDom(/* opt_generateShadows */ true);
|
|
if (mutation) {
|
|
mutations.push(mutation);
|
|
}
|
|
}
|
|
}
|
|
return mutations;
|
|
};
|
|
|
|
/**
|
|
* Sorts an array of procedure definition mutations alphabetically.
|
|
* (Does not mutate the given array.)
|
|
* @param {!Array.<Element>} mutations Array of mutation xml elements.
|
|
* @return {!Array.<Element>} Sorted array of mutation xml elements.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.sortProcedureMutations_ = function(mutations) {
|
|
var newMutations = mutations.slice();
|
|
|
|
newMutations.sort(function(a, b) {
|
|
var procCodeA = a.getAttribute('proccode');
|
|
var procCodeB = b.getAttribute('proccode');
|
|
|
|
return Blockly.scratchBlocksUtils.compareStrings(procCodeA, procCodeB);
|
|
});
|
|
|
|
return newMutations;
|
|
};
|
|
|
|
/**
|
|
* Comparison function for case-insensitive sorting of the first element of
|
|
* a tuple.
|
|
* @param {!Array} ta First tuple.
|
|
* @param {!Array} tb Second tuple.
|
|
* @return {number} -1, 0, or 1 to signify greater than, equality, or less than.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.procTupleComparator_ = function(ta, tb) {
|
|
return Blockly.scratchBlocksUtils.compareStrings(ta[0], tb[0]);
|
|
};
|
|
|
|
/**
|
|
* Ensure two identically-named procedures don't exist.
|
|
* @param {string} name Proposed procedure name.
|
|
* @param {!Blockly.Block} block Block to disambiguate.
|
|
* @return {string} Non-colliding name.
|
|
*/
|
|
Blockly.Procedures.findLegalName = function(name, block) {
|
|
if (block.isInFlyout) {
|
|
// Flyouts can have multiple procedures called 'do something'.
|
|
return name;
|
|
}
|
|
while (!Blockly.Procedures.isLegalName_(name, block.workspace, block)) {
|
|
// Collision with another procedure.
|
|
var r = name.match(/^(.*?)(\d+)$/);
|
|
if (!r) {
|
|
name += '2';
|
|
} else {
|
|
name = r[1] + (parseInt(r[2], 10) + 1);
|
|
}
|
|
}
|
|
return name;
|
|
};
|
|
|
|
/**
|
|
* Does this procedure have a legal name? Illegal names include names of
|
|
* procedures already defined.
|
|
* @param {string} name The questionable name.
|
|
* @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
|
|
* @param {Blockly.Block=} opt_exclude Optional block to exclude from
|
|
* comparisons (one doesn't want to collide with oneself).
|
|
* @return {boolean} True if the name is legal.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.isLegalName_ = function(name, workspace, opt_exclude) {
|
|
return !Blockly.Procedures.isNameUsed(name, workspace, opt_exclude);
|
|
};
|
|
|
|
/**
|
|
* Return if the given name is already a procedure name.
|
|
* @param {string} name The questionable name.
|
|
* @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
|
|
* @param {Blockly.Block=} opt_exclude Optional block to exclude from
|
|
* comparisons (one doesn't want to collide with oneself).
|
|
* @return {boolean} True if the name is used, otherwise return false.
|
|
*/
|
|
Blockly.Procedures.isNameUsed = function(name, workspace, opt_exclude) {
|
|
var blocks = workspace.getAllBlocks();
|
|
// Iterate through every block and check the name.
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
if (blocks[i] == opt_exclude) {
|
|
continue;
|
|
}
|
|
if (blocks[i].getProcedureDef) {
|
|
var procName = blocks[i].getProcedureDef();
|
|
if (Blockly.Names.equals(procName[0], name)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Rename a procedure. Called by the editable field.
|
|
* @param {string} name The proposed new name.
|
|
* @return {string} The accepted name.
|
|
* @this {Blockly.Field}
|
|
*/
|
|
Blockly.Procedures.rename = function(name) {
|
|
// Strip leading and trailing whitespace. Beyond this, all names are legal.
|
|
name = name.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
|
|
|
|
// Ensure two identically-named procedures don't exist.
|
|
var legalName = Blockly.Procedures.findLegalName(name, this.sourceBlock_);
|
|
var oldName = this.text_;
|
|
if (oldName != name && oldName != legalName) {
|
|
// Rename any callers.
|
|
var blocks = this.sourceBlock_.workspace.getAllBlocks();
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
if (blocks[i].renameProcedure) {
|
|
blocks[i].renameProcedure(oldName, legalName);
|
|
}
|
|
}
|
|
}
|
|
return legalName;
|
|
};
|
|
|
|
/**
|
|
* Construct the blocks required by the flyout for the procedure category.
|
|
* @param {!Blockly.Workspace} workspace The workspace contianing procedures.
|
|
* @return {!Array.<!Element>} Array of XML block elements.
|
|
*/
|
|
Blockly.Procedures.flyoutCategory = function(workspace) {
|
|
var xmlList = [];
|
|
|
|
Blockly.Procedures.addCreateButton_(workspace, xmlList);
|
|
|
|
// Create call blocks for each procedure defined in the workspace
|
|
var mutations = Blockly.Procedures.allProcedureMutations(workspace);
|
|
mutations = Blockly.Procedures.sortProcedureMutations_(mutations);
|
|
for (var i = 0; i < mutations.length; i++) {
|
|
var mutation = mutations[i];
|
|
// <block type="procedures_call">
|
|
// <mutation ...></mutation>
|
|
// </block>
|
|
var block = goog.dom.createDom('block');
|
|
block.setAttribute('type', 'procedures_call');
|
|
block.setAttribute('gap', 16);
|
|
block.appendChild(mutation);
|
|
xmlList.push(block);
|
|
}
|
|
return xmlList;
|
|
};
|
|
|
|
/**
|
|
* Create the "Make a Block..." button.
|
|
* @param {!Blockly.Workspace} workspace The workspace contianing procedures.
|
|
* @param {!Array.<!Element>} xmlList Array of XML block elements to add to.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.addCreateButton_ = function(workspace, xmlList) {
|
|
var button = goog.dom.createDom('button');
|
|
var msg = Blockly.Msg.NEW_PROCEDURE;
|
|
var callbackKey = 'CREATE_PROCEDURE';
|
|
var callback = function() {
|
|
Blockly.Procedures.createProcedureDefCallback_(workspace);
|
|
};
|
|
button.setAttribute('text', msg);
|
|
button.setAttribute('callbackKey', callbackKey);
|
|
workspace.registerButtonCallback(callbackKey, callback);
|
|
xmlList.push(button);
|
|
};
|
|
|
|
/**
|
|
* Find all callers of a named procedure.
|
|
* @param {string} name Name of procedure (procCode in scratch-blocks).
|
|
* @param {!Blockly.Workspace} ws The workspace to find callers in.
|
|
* @param {!Blockly.Block} definitionRoot The root of the stack where the
|
|
* procedure is defined.
|
|
* @param {boolean} allowRecursive True if the search should include recursive
|
|
* procedure calls. False if the search should ignore the stack starting
|
|
* with definitionRoot.
|
|
* @return {!Array.<!Blockly.Block>} Array of caller blocks.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.getCallers = function(name, ws, definitionRoot,
|
|
allowRecursive) {
|
|
var allBlocks = [];
|
|
var topBlocks = ws.getTopBlocks();
|
|
|
|
// Start by deciding which stacks to investigate.
|
|
for (var i = 0; i < topBlocks.length; i++) {
|
|
var block = topBlocks[i];
|
|
if (block.id == definitionRoot.id && !allowRecursive) {
|
|
continue;
|
|
}
|
|
allBlocks.push.apply(allBlocks, block.getDescendants(false));
|
|
}
|
|
|
|
var callers = [];
|
|
for (var i = 0; i < allBlocks.length; i++) {
|
|
var block = allBlocks[i];
|
|
if (block.type == Blockly.PROCEDURES_CALL_BLOCK_TYPE ) {
|
|
var procCode = block.getProcCode();
|
|
if (procCode && procCode == name) {
|
|
callers.push(block);
|
|
}
|
|
}
|
|
}
|
|
return callers;
|
|
};
|
|
|
|
/**
|
|
* Find and edit all callers with a procCode using a new mutation.
|
|
* @param {string} name Name of procedure (procCode in scratch-blocks).
|
|
* @param {!Blockly.Workspace} ws The workspace to find callers in.
|
|
* @param {!Element} mutation New mutation for the callers.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.mutateCallersAndPrototype = function(name, ws, mutation) {
|
|
var defineBlock = Blockly.Procedures.getDefineBlock(name, ws);
|
|
var prototypeBlock = Blockly.Procedures.getPrototypeBlock(name, ws);
|
|
if (defineBlock && prototypeBlock) {
|
|
var callers = Blockly.Procedures.getCallers(name,
|
|
defineBlock.workspace, defineBlock, true /* allowRecursive */);
|
|
callers.push(prototypeBlock);
|
|
Blockly.Events.setGroup(true);
|
|
for (var i = 0, caller; caller = callers[i]; i++) {
|
|
var oldMutationDom = caller.mutationToDom();
|
|
var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
|
|
caller.domToMutation(mutation);
|
|
var newMutationDom = caller.mutationToDom();
|
|
var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
|
|
if (oldMutation != newMutation) {
|
|
Blockly.Events.fire(new Blockly.Events.BlockChange(
|
|
caller, 'mutation', null, oldMutation, newMutation));
|
|
}
|
|
}
|
|
Blockly.Events.setGroup(false);
|
|
} else {
|
|
alert('No define block on workspace'); // TODO decide what to do about this.
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Find the definition block for the named procedure.
|
|
* @param {string} procCode The identifier of the procedure.
|
|
* @param {!Blockly.Workspace} workspace The workspace to search.
|
|
* @return {Blockly.Block} The procedure definition block, or null not found.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.getDefineBlock = function(procCode, workspace) {
|
|
// Assume that a procedure definition is a top block.
|
|
var blocks = workspace.getTopBlocks(false);
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
if (blocks[i].type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) {
|
|
var prototypeBlock = blocks[i].getInput('custom_block').connection.targetBlock();
|
|
if (prototypeBlock.getProcCode && prototypeBlock.getProcCode() == procCode) {
|
|
return blocks[i];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Find the prototype block for the named procedure.
|
|
* @param {string} procCode The identifier of the procedure.
|
|
* @param {!Blockly.Workspace} workspace The workspace to search.
|
|
* @return {Blockly.Block} The procedure prototype block, or null not found.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.getPrototypeBlock = function(procCode, workspace) {
|
|
var defineBlock = Blockly.Procedures.getDefineBlock(procCode, workspace);
|
|
if (defineBlock) {
|
|
return defineBlock.getInput('custom_block').connection.targetBlock();
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Create a mutation for a brand new custom procedure.
|
|
* @return {Element} The mutation for a new custom procedure
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.newProcedureMutation = function() {
|
|
var mutationText = '<xml>' +
|
|
'<mutation' +
|
|
' proccode="' + Blockly.Msg['PROCEDURE_DEFAULT_NAME'] + '"' +
|
|
' argumentids="[]"' +
|
|
' argumentnames="[]"' +
|
|
' argumentdefaults="[]"' +
|
|
' warp="false">' +
|
|
'</mutation>' +
|
|
'</xml>';
|
|
return Blockly.Xml.textToDom(mutationText).firstChild;
|
|
};
|
|
|
|
/**
|
|
* Callback to create a new procedure custom command block.
|
|
* @param {!Blockly.Workspace} workspace The workspace to create the new procedure on.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.createProcedureDefCallback_ = function(workspace) {
|
|
Blockly.Procedures.externalProcedureDefCallback(
|
|
Blockly.Procedures.newProcedureMutation(),
|
|
Blockly.Procedures.createProcedureCallbackFactory_(workspace)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Callback factory for adding a new custom procedure from a mutation.
|
|
* @param {!Blockly.Workspace} workspace The workspace to create the new procedure on.
|
|
* @return {function(?Element)} callback for creating the new custom procedure.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.createProcedureCallbackFactory_ = function(workspace) {
|
|
return function(mutation) {
|
|
if (mutation) {
|
|
var blockText = '<xml>' +
|
|
'<block type="procedures_definition">' +
|
|
'<statement name="custom_block">' +
|
|
'<shadow type="procedures_prototype">' +
|
|
Blockly.Xml.domToText(mutation) +
|
|
'</shadow>' +
|
|
'</statement>' +
|
|
'</block>' +
|
|
'</xml>';
|
|
var blockDom = Blockly.Xml.textToDom(blockText).firstChild;
|
|
Blockly.Events.setGroup(true);
|
|
var block = Blockly.Xml.domToBlock(blockDom, workspace);
|
|
var scale = workspace.scale; // To convert from pixel units to workspace units
|
|
// Position the block so that it is at the top left of the visible workspace,
|
|
// padded from the edge by 30 units. Position in the top right if RTL.
|
|
var posX = -workspace.scrollX;
|
|
if (workspace.RTL) {
|
|
posX += workspace.getMetrics().contentWidth - 30;
|
|
} else {
|
|
posX += 30;
|
|
}
|
|
block.moveBy(posX / scale, (-workspace.scrollY + 30) / scale);
|
|
block.scheduleSnapAndBump();
|
|
Blockly.Events.setGroup(false);
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Callback to open the modal for editing custom procedures.
|
|
* @param {!Blockly.Block} block The block that was right-clicked.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.editProcedureCallback_ = function(block) {
|
|
// Edit can come from one of three block types (call, define, prototype)
|
|
// Normalize by setting the block to the prototype block for the procedure.
|
|
if (block.type == Blockly.PROCEDURES_DEFINITION_BLOCK_TYPE) {
|
|
var input = block.getInput('custom_block');
|
|
if (!input) {
|
|
alert('Bad input'); // TODO: Decide what to do about this.
|
|
return;
|
|
}
|
|
var conn = input.connection;
|
|
if (!conn) {
|
|
alert('Bad connection'); // TODO: Decide what to do about this.
|
|
return;
|
|
}
|
|
var innerBlock = conn.targetBlock();
|
|
if (!innerBlock ||
|
|
!innerBlock.type == Blockly.PROCEDURES_PROTOTYPE_BLOCK_TYPE) {
|
|
alert('Bad inner block'); // TODO: Decide what to do about this.
|
|
return;
|
|
}
|
|
block = innerBlock;
|
|
} else if (block.type == Blockly.PROCEDURES_CALL_BLOCK_TYPE) {
|
|
// This is a call block, find the prototype corresponding to the procCode.
|
|
// Make sure to search the correct workspace, call block can be in flyout.
|
|
var workspaceToSearch = block.workspace.isFlyout ?
|
|
block.workspace.targetWorkspace : block.workspace;
|
|
block = Blockly.Procedures.getPrototypeBlock(
|
|
block.getProcCode(), workspaceToSearch);
|
|
}
|
|
// Block now refers to the procedure prototype block, it is safe to proceed.
|
|
Blockly.Procedures.externalProcedureDefCallback(
|
|
block.mutationToDom(),
|
|
Blockly.Procedures.editProcedureCallbackFactory_(block)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Callback factory for editing an existing custom procedure.
|
|
* @param {!Blockly.Block} block The procedure prototype block being edited.
|
|
* @return {function(?Element)} Callback for editing the custom procedure.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.editProcedureCallbackFactory_ = function(block) {
|
|
return function(mutation) {
|
|
if (mutation) {
|
|
Blockly.Procedures.mutateCallersAndPrototype(block.getProcCode(),
|
|
block.workspace, mutation);
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Callback to create a new procedure custom command block.
|
|
* @public
|
|
*/
|
|
Blockly.Procedures.externalProcedureDefCallback = function(/** mutator, callback */) {
|
|
alert('External procedure editor must be override Blockly.Procedures.externalProcedureDefCallback');
|
|
};
|
|
|
|
/**
|
|
* Make a context menu option for editing a custom procedure.
|
|
* This appears in the context menu for procedure definitions and procedure
|
|
* calls.
|
|
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
|
|
* @return {!Object} A menu option, containing text, enabled, and a callback.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.makeEditOption = function(block) {
|
|
var editOption = {
|
|
enabled: true,
|
|
text: Blockly.Msg.EDIT_PROCEDURE,
|
|
callback: function() {
|
|
Blockly.Procedures.editProcedureCallback_(block);
|
|
}
|
|
};
|
|
return editOption;
|
|
};
|
|
|
|
/**
|
|
* Callback to show the procedure definition corresponding to a custom command
|
|
* block.
|
|
* TODO(#1136): Implement.
|
|
* @param {!Blockly.Block} block The block that was right-clicked.
|
|
* @private
|
|
*/
|
|
Blockly.Procedures.showProcedureDefCallback_ = function(block) {
|
|
alert('TODO(#1136): implement showing procedure definition (procCode was "' +
|
|
block.procCode_ + '")');
|
|
};
|
|
|
|
/**
|
|
* Make a context menu option for showing the definition for a custom procedure,
|
|
* based on a right-click on a custom command block.
|
|
* @param {!Blockly.BlockSvg} block The block where the right-click originated.
|
|
* @return {!Object} A menu option, containing text, enabled, and a callback.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.makeShowDefinitionOption = function(block) {
|
|
var option = {
|
|
enabled: true,
|
|
text: Blockly.Msg.SHOW_PROCEDURE_DEFINITION,
|
|
callback: function() {
|
|
Blockly.Procedures.showProcedureDefCallback_(block);
|
|
}
|
|
};
|
|
return option;
|
|
};
|
|
|
|
/**
|
|
* Callback to try to delete a custom block definitions.
|
|
* @param {string} procCode The identifier of the procedure to delete.
|
|
* @param {!Blockly.Block} definitionRoot The root block of the stack that
|
|
* defines the custom procedure.
|
|
* @return {boolean} True if the custom procedure was deleted, false otherwise.
|
|
* @package
|
|
*/
|
|
Blockly.Procedures.deleteProcedureDefCallback = function(procCode,
|
|
definitionRoot) {
|
|
var callers = Blockly.Procedures.getCallers(procCode,
|
|
definitionRoot.workspace, definitionRoot, false /* allowRecursive */);
|
|
if (callers.length > 0) {
|
|
return false;
|
|
}
|
|
|
|
var workspace = definitionRoot.workspace;
|
|
|
|
// Delete the whole stack.
|
|
Blockly.Events.setGroup(true);
|
|
definitionRoot.dispose();
|
|
Blockly.Events.setGroup(false);
|
|
|
|
// TODO (#1354) Update this function when '_' is removed
|
|
// Refresh toolbox, so caller doesn't appear there anymore
|
|
workspace.refreshToolboxSelection_();
|
|
|
|
return true;
|
|
};
|