UNPKG

cns-lib

Version:

Library functions for the Cream & Sugar language

939 lines (878 loc) 31 kB
var CNS_ = { /************************************************* * All private functions will go at the top *************************************************/ /** * @private * Contains runtime configuration options */ config: { use: { react: true } }, /** * @private * Wrapper/polyfill for Object.assign that always returns a new object. * * @param {Object} base The base object to begin with. * @param {Object} toAdd Properties to add to the base. * * @return {Object} Contains all of the properties. */ assign: function (base, toAdd) { if (typeof Object.assign !== 'function') { const out = {}; Object.keys(base).forEach(function (key) { out[key] = base[key] }); Object.keys(toAdd).forEach(function (key) { out[key] = toAdd[key] }); if (typeof Object.getOwnPropertySymbols === 'function') { Object.getOwnPropertySymbols(base).forEach(function (sym) { out[sym] = base[sym] }); Object.getOwnPropertySymbols(toAdd).forEach(function (sym) { out[sym] = toAdd[sym] }); } return out; } return Object.assign({}, base, toAdd); }, /** * @private * Converts `arguments` to an array. * * @param {Arguments} args Any `arguments` object. * @return {Array} Converted from the `arguments`. */ args: function (args) { const out = []; Array.prototype.push.apply(out, args); return out; }, /** * @private * Retrieves runtime configuration options * * @param {String} key The name of the value to retrieve * @return {Any} */ getConfig: function (key) { const pieces = key.split('.'); var obj = CNS_.config; pieces.every(function (item) { obj = obj[item]; return obj !== undefined; }); return obj; }, /** * @private * Determines whether a value is reserved and shouldn't be used. * * @param {Any} val Any value. * @return {Boolean} Whether or not the value is reserved. */ isReserved: function (val) { if ( typeof val === 'string' && val.length > 1 && val[val.length-1] === '_' && val[val.length-2] !== '_' // Let it through if it ends with "__" ) { throw new Error('Value ' + val + ' matches a reserved system pattern.'); } return false; }, /** * @private * Determines whether a value is a tuple. * * @param {Any} val Any value * @return {Boolean} Whether the value was a tuple. */ isTuple: function (val) { return val.CNS_isTuple_ === CNS_; }, /** * @private * Binds a function to the current context. If the value is not a function, * Generates a function that returns the value, bound to the current context. * * @param {Any} val Any value. * @param {Any} context Any value. * @return {Function} Retrieves `val` and is bound to `context`. */ lazify: function(val, context) { return typeof val === 'function' ? val.bind(context) : function () { return val }.bind(context); }, /** * @private * Matches a function's arguments against expected patterns. * For example: CNS_.match(args, [['Identifier', 'x']]); * * @param {Array} args Contains a function's arguments. * @param {Array} pattern Describes patterns to match arguments against. * @return {Boolean} Whether or not arguments match patterns. */ match: function (args, pattern) { const NUMTEST = /^(\-)?[0-9]+(\.[0-9]+)?(e\-?[0-9]+)?$/; const ATOMTEST = /^[\$_A-z][\$_A-z0-9]*$/; const SYMTEST = /^Symbol\.for\(/; const SYMREPLACE = /^Symbol\.for\((\'|\")|(\'|\")\)$/g; function convertSpecial(special) { switch (special) { case 'null': return null; case 'undefined': return undefined; case 'true': return true; case 'false': return false; default: return special; } } function arrMismatch(matchType, arg) { const isTuple = CNS_.isTuple(arg); return matchType === 'Tuple' && !isTuple || matchType === 'Arr' && isTuple; } function testArrParam(arg, arrParam, position) { if (arrParam === '_') return true; // Yes, if it's the catch all. if (arrParam === 'NaN') return isNaN(arg[position]); // Yes, if they're both NaN. if ((converted = convertSpecial(arrParam)) !== arrParam) return arg[position] === converted; // Yes, if it's a special and the specials match. if (SYMTEST.test(arrParam)) return arg[position] === Symbol.for(arrParam.replace(SYMREPLACE, '')); // Yes, if it's a symbol and symbols match. return ATOMTEST.test(arrParam) ? true : CNS_.eql(arg[position], JSON.parse(arrParam)); // Yes, for identifiers, recursive equality check for anything else. } return args.every(function (arg, index) { if (!pattern[index]) return false; // No match if the arity's wrong. var matchType = pattern[index][0]; // For example "Tuple" var matchVal = pattern[index][1]; // For example ["x","y"] var converted; switch(matchType) { case 'Identifier': return true; // An identifier is a variable assignment so we allow it. case 'Atom': return arg === Symbol.for(matchVal); // Match if it's the same atom. case 'String': return arg === matchVal; // Match if it's the same string. case 'Number': return NUMTEST.test(arg) && arg === parseFloat(matchVal); // Match if the arg is a number and the numbers are equal. case 'Special': return matchVal === 'NaN' ? isNaN(arg) : arg === convertSpecial(matchVal); // Match if the special values are equal. case 'HeadTail': case 'LeadLast': return Array.isArray(arg); // Match any array because we're just doing array destructuring. case 'Arr': case 'Tuple': // No match if it's not an array, we've mismatched arrays/tuples, or if we have the wrong amount of items. if (!Array.isArray(arg) || arrMismatch(matchType, arg) || arg.length !== matchVal.length) return false; // Match if all of the subobjects match. return matchVal.every(function (arrParam, position) { return testArrParam(arg, arrParam, position); }); case 'Keys': case 'Obj': // Similar to the arr/tuple condition except here we don't care if the amount of keys matches our pattern length. if (typeof arg !== 'object' || arg.constructor !== Object) return false; // No match if the arg isn't an object. if (matchType === 'Keys') return true; // Match if the arg is an object and we're just destructuring keys. return matchVal.every(function (pair) { const kv = pair.split(':'); if (SYMTEST.test(kv[0])) (kv[0] = Symbol.for(kv[0].replace(SYMREPLACE, ''))); return testArrParam(arg, kv[1].trim(), typeof kv[0] === 'string' ? kv[0].trim() : kv[0]); }); default: CNS_.die('Can not pattern match against type ' + matchType); // No match if we don't have a matchable type. } }); }, /** * @private * Conditionally executes one function or another. * * @param {Any} condition Assessed for its truthiness. * @param {Function} callback Executed if `condition` is truthy. * @param {Function} elseCase Executed if `condition` is falsy. * @return {Any} The result of the executed function or `undefined`. */ qualify: function (condition, callback, elseCase) { return condition ? callback() : elseCase ? elseCase() : undefined; }, /** * @private * Generates a tuple data type * * @param {Array} arr Any array. * @return {Array} Marked with a special property identifying it as a tuple. */ tuple: function (arr) { if (!arr.length) CNS_.die('Tuples can not be empty.'); Object.defineProperty ? Object.defineProperty(arr, 'CNS_isTuple_', {enumerable: false, configurable: false, writable: false, value: CNS_}) : (arr.CNS_isTuple_ = CNS_); return arr; }, /************************************************* * End private functions, begin exposed functions *************************************************/ /** * @public * Calls a function. * For example: CNS_.apply(function () {}, x, y, z); * * @param {Function} fn The function to call. * @return {Any} The result of calling the function. */ apply: function (fn) { var args = Array.prototype.slice.call(arguments, 1); return args.length ? fn.apply(null, args) : fn(); }, /** * @public * Makes a new function that calls an existing function but only * accepts a certain amount of arguments. * * @param {Function} fun A function to lock down. * @param {Number} arity The acceptable amount of arguments. * @return {Function} A resulting function. */ aritize: function (fun, arity) { return function () { if (arguments.length === arity) { return fun.apply(undefined, arguments); } CNS_.die('Function ' + (fun.name || '') + ' called with wrong arity. Expected ' + arity + ' got ' + arguments.length + '.'); }; }, /** * @public * Converts an array to a tuple. * * @param {Array} arr Any array. * @return {Tuple} Converted from the array. */ arrayToTuple: function (arr) { if (CNS_.isTuple(arr) || !Array.isArray(arr)) CNS_.die('Argument provided is not an array'); return CNS_.tuple(arr.slice()); }, /** * @public * Creates a new function that caches its result after being called and will * forever return that same result immediately when called. The function * can also be decached. * * @param {Function} fn The function to use as a base for a caching function. * @return {Function} The caching function. */ cache: function (fn) { var executed, storedOutput; var out = function (deCache) { if (deCache === CNS_) { executed = false; storedOutput = undefined; } else if (!executed) { executed = true; storedOutput = fn.apply(null, arguments); } return storedOutput; }; out.CNS_isCachedFn_ = true; return out; }, /** * @public * Creates and extends basic classes. * * @param {Class (Function)} ext Optional. A class to extend. * @param {Object} proto The new prototype methods. * @param {Object} stat Optional. Any static methods to include. * @return The new class. */ classof: function (ext, proto, stat) { // Allow the extension class to be optional if (typeof ext !== 'function') { stat = proto; proto = ext; ext = null; } var Class = (proto && typeof proto.constructor === 'function') ? proto.constructor : function () {}; var extProto; // Mix in the prototype from the extension if (ext) { extProto = Object.getOwnPropertyNames(ext.prototype); extProto.forEach(function (key) { key !== 'constructor' && (Class.prototype[key] = ext.prototype[key]); }); } // Mix in the new prototype if (proto) { Object.keys(proto).forEach(function (key) { key !== 'constructor' && (Class.prototype[key] = proto[key]); }); } // Mix in static methods if (stat) { Object.keys(stat).forEach(function (key) { Class[key] = stat[key]; }); } return Class; }, /** * @public * Creates a `new` object. * For example: CNS_.create(ClassName, arg1, arg2); * * @param {Constructor} cls Any constructor function. * @return {Instance} An instance of the constructor's object. */ create: function(cls) { return new (Function.prototype.bind.apply(cls, arguments)); }, /** * @public * Creats an html element. * * @param {String|Constructor} type The type of element to create. * @param {Object} attrs The element's attributes. * @param {Array} body Children of the element. * @return {HtmlElement} The resulting element. */ createElement: function (type, attrs, body) { var react; const a = attrs || {}; const b = body || []; // If we have React, reference it. typeof React !== 'undefined' && (react = React); // If we have require, try to require React. if (!react && typeof require !== 'undefined') { try { react = require('react'); } catch (_) { react = null; } } // If we have react, pass to React. End. if (react && CNS_.getConfig('use.react')) { return react.createElement.apply(react, [type, a].concat(b)); } // Die if we're not in a browser environment. if (typeof document === 'undefined') { return CNS_.die('No HTML document is available.'); } // Create an element and set attributes. const elem = document.createElement(type); Object.keys(a).forEach(function (key) { switch (key) { case 'className' : return elem.setAttribute('class', a[key]); case 'htmlFor' : return elem.setAttribute('for', a[key]); default : return elem.setAttribute(key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(), a[key]); } }); // Append children. b.forEach(function (node) { elem.appendChild(node); }); return elem; }, /** * @public * Breaks functionalism and mutates a value on an existing object. * Necessary for actions like setting location.href. For example: * * dangerouslyMutate 'href', '/example', location * * @param {String|Number} keyOrIndex Identifies the item to update. * @param {Any} val The new value. * @param {Collection} collection Any collection type. * @return {Collection} A copy of the original collection. */ dangerouslyMutate: function (keyOrIndex, val, collection) { collection[keyOrIndex] = val; return collection; }, /** * @public * Assesses data types. * * @param {Any} val Any value. * @return {String} The type of data assessed. */ dataType: function (val) { const type = typeof val; switch (type) { case 'symbol': return 'atom'; case 'number': return isNaN(val) ? 'nan' : type; case 'object': if (val === null) return 'null'; if (Array.isArray(val)) return CNS_.isTuple(val) ? 'tuple' : 'array'; if (val instanceof Date) return 'date'; if (val instanceof RegExp) return 'regexp'; if (typeof HTMLElement !== 'undefined' && val instanceof HTMLElement) return 'htmlelement'; if ( (typeof Worker !== 'undefined' && val instanceof Worker) || (val.constructor.name === 'ChildProcess' && typeof val.pid === 'number') ) return 'process'; return type; default: return type; } }, /** * @public * Decaches a previously cached function. If the provided function is not a * caching function, an error will be triggered. * * @param {Function} fn A function generated via a call to `cache`. * @return {undefined} */ decache: function (fn) { if (!fn.CNS_isCachedFn_) { throw new Error('Can not decache a non caching function.'); } return fn(CNS_); }, /** * @public * Shorcuts console.debug but only if it exists. * Tries to fall back to console.log. */ debug: function () { if (typeof console !== 'undefined' && typeof console.debug === 'function') { return console.debug.apply(console, arguments); } return CNS_.log.apply(null, arguments); }, /** * @public * Shortcuts throwing an error in a way that can be returned by a * JavaScript function. * * @param {String} msg The error message. * @return {undefined} */ die: function (msg) { throw new Error(msg); }, /** * @public * Selects a single DOM element by selector. * * @param {String} selector Describes the element to select. * @return {HTMLElement} The selected element. */ dom: function (selector) { return document.querySelector(selector); }, /** * @public * Selects an array of DOM elements by selector. * * @param {String} selector Describes the element to select. * @return {Array} The selected elements. */ domArray: function (selector) { return Array.prototype.slice.call(document.querySelectorAll(selector)); }, /** * @public * Performs a deep equal operation on two collections. * * @param {Any} a Any data. * @param {Any} b Any data. * @return {Boolean} Whether `a` and `b` are deep equal. */ eql: function (a, b) { if (a === CNS_ || b === CNS_) return true; // <- Hack to force a match if (a === b || (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b))) return true; if (typeof a !== typeof b) return false; if (typeof a === 'object') { if (Array.isArray(a)) return a.every(function(item, index) { return CNS_.eql(item, b[index]) }.bind(this)); const ks = Object.keys, ak = ks(a), bk = ks(b); if (!CNS_.eql(ak, bk)) return false; return ak.every(function (key) { return CNS_.eql(a[key], b[key]) }.bind(this)); } return false; }, /** * @public * Retrieves an item from a collection. * * @param {String|Number} item Identifies the item by key or position. * @param {Collection} collection Any collection type. * @return {Any} The retrieved item. */ get: function (item, collection) { if (!CNS_.isReserved(item)) return collection[item]; }, /** * @public * Returns the first item in a list. * * @param {Array|Tuple|String} list Any list type. * @return {Any} The item in position 0. */ head: function (list) { return list[0]; }, /** * @public * Functionizes the `instanceof` operator. * * @param {Any} val Any value. * @param {Constructor} type Any constructor function. * @return {Boolean} The result of calling `val instanceof type`. */ instanceof: function (val, type) { return val instanceof type; }, /** * @public * Sets a language configuration option. * * @param {String} key The name of the config option. * @param {Any} val The value for the option. * @return {undefined} */ lang: function (key, val) { const pieces = key.split('.'); var obj = CNS_.config; pieces.forEach(function (item, index) { const isLast = index === pieces.length - 1; if (isLast) { obj[item] = val; } else { obj[item] = obj[item] || {}; obj = obj[item]; } }); }, /** * @public * Returns the last item in a list. * * @param {Array|Tuple|String} list Any list type. * @return {Any} The item in the last position. */ last: function (list) { return list[list.length - 1]; }, /** * @public * Returns all but the last item in a list. * * @param {Array|Tuple|String} list Any list type. * @return {Array|Tuple|String} Minus the last item in `list`. */ lead: function (list) { const out = list.slice(0, list.length - 1); if (CNS_.isTuple(list)) return CNS_.tuple(out); return out; }, /** * @public * Shorcuts console.log but only if it exists. */ log: function () { if (typeof console !== 'undefined' && typeof console.log === 'function') { return console.log.apply(console, arguments); } }, /** * @public * A nice noop function included for you. */ noop: function () {}, /** * @public * Selects a random item from a list type colelction. * * @param {Array|Tuple|String} list Any list type. * @return {Any} A randomly selected item. */ random: function (list) { return list[Math.floor(Math.random()*list.length)]; }, /** * @public * Generates an array of a certain length specified by the bound parameters. * * @param {Number} from A lower bound. * @param {Number} through An upper bound. * @return {Array} The resulting array. */ range: function (from, through) { const out = []; for (var i = from; i <= through; i += 1) out.push(i); return out; }, /** * @public * Creates a new collection by removing an item in an existing collection. * * @param {String|Number} keyOrIndex Identifies the item to remove. * @param {Collection} collection Any collection type. * @return {Collection} A copy of the original collection. */ remove: function (keyOrIndex, collection) { CNS_.isReserved(keyOrIndex); if (Array.isArray(collection)) { if (CNS_.isTuple(collection)) CNS_.die('Can not remove items from tuples.'); const splicer = collection.slice(); splicer.splice(keyOrIndex, 1); return splicer; } else { const newObj = {}; const keys = Object.keys(collection).concat( Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(collection) : [] ); keys.forEach(function (key) { keyOrIndex !== key && (newObj[key] = collection[key]); }); return newObj; } }, /** * @public * Returns all but the first item in a list. * * @param {Array|Tuple|String} list Any list type. * @return {Array|Tuple|String} Minus the first item in `list`. */ tail: function (list) { const out = list.slice(1); if (CNS_.isTuple(list)) return CNS_.tuple(out); return out; }, /** * @public * Throws an instance of an error object. * For example: CNS_.throw(CNS_.create(Error)); * * @param {Error} err Any error object. * @return {undefined} */ throw: function (err) { throw err; }, /** * @public * Converts a tuple to an array. * * @param {Tuple} tuple An instance of a tuple data type. * @return {Array} Converted from the tuple. */ tupleToArray: function (tuple) { if (!CNS_.isTuple(tuple)) CNS_.die('Argument provided is not a tuple'); return tuple.slice(); }, /** * @public * Converts a tuple to an object. * * @param {Tuple} list A tuple data type. * @param {Function} fn Optional. Called once for each item and determines * how to name the object key. * @return {Object} */ tupleToObject: function (list, fn) { const obj = {}; if (!CNS_.isTuple(list)) CNS_.die('Argument provided is not a tuple'); list.forEach(function (item, index) { obj[fn ? fn(item, index) : index] = item }); return obj; }, /** * @public * Creates a new collection by updating an item in an existing collection. * EXCEPTION: If the collection is a function, modifies the existing function. * * @param {String|Number} keyOrIndex Identifies the item to update. * @param {Any} val The new value. * @param {Collection} collection Any collection type. * @return {Collection} A copy of the original collection. */ update: function (keyOrIndex, val, collection) { CNS_.isReserved(keyOrIndex); if (Array.isArray(collection)) { if (CNS_.isTuple(collection) && collection.indexOf(keyorIndex) === -1) { CNS_.die('Can not add extra items to tuples.'); } const newSlice = collection.slice(); newSlice[keyOrIndex] = val; return newSlice; } else if (typeof HTMLElement !== 'undefined' && collection instanceof HTMLElement) { const clone = collection.cloneNode(); clone[keyOrIndex] = val; return clone; } else if (typeof collection === 'function') { // Updating a function allows breaking the functionalism rule only // because JavaScript makes it impossible to clone a function and account // for all necessary cases. This should be avoided where possible. collection[keyOrIndex] = val; return collection; } else { const replacer = {}; replacer[keyOrIndex] = val; return CNS_.assign(collection, replacer); } }, /** * @public * Shorcuts console.warn but only if it exists. * Tries to fall back to console.log. */ warn: function () { if (typeof console !== 'undefined' && typeof console.warn === 'function') { return console.warn.apply(console, arguments); } return CNS_.log.apply(null, arguments); }, /************************************************* * End public functions, begin multithreading *************************************************/ /** * @private * A package of utilities for multithreading. */ msgs: { isBrowser: function () { return typeof navigator !== 'undefined' }, isChild: false, queue: [], handlers: [], isWaiting: false, // Should only be used on objects that you know for sure only contain // functions, objects, and arrays, deeply nested. // Top signifies we are stringifying at the top level (true/false) stringify: function (obj, top) { if (typeof obj === 'function') { return obj.toString(); } else if (typeof obj === 'string') { return obj; } else if (obj === true || obj === false) { return obj; } else if (Array.isArray(obj)) { return '[' + obj.map(function (item) { return CNS_.msgs.stringify(item, false) }).join(', ') + ']'; } else { return '{' + Object.keys(obj).map(function (key) { if (top && key === 'config') return key + ':' + JSON.stringify({use:{react:true}}); return key + ':' + CNS_.msgs.stringify(obj[key], false); }).join(',\n') + '}'; } }, symbolize: function (data, reSymbolize) { // If we need to stringify a symbol, give it a special syntax and return it. if (!reSymbolize && typeof data === 'symbol') return '__' + data.toString() + '__'; // If we need to turn a string into a symbol, generate a symbol and return it. if (reSymbolize && typeof data === 'string' && /^__Symbol\(.+\)__$/.test(data)) { return Symbol.for(data.replace(/^__Symbol\(|\)__$/g, '')); } // If this is an array, map over it and see if we need to symbolize any data in it. // Return the new array. if (Array.isArray(data)) { var out = []; data.forEach(function (item) { out.push(CNS_.msgs.symbolize(item, reSymbolize)) }); if (!reSymbolize && CNS_.isTuple(data)) (out = { CNS_tuple_: out }); return out; // If this is an object, check to see if it's supposed to be a tuple. } else if (typeof data === 'object' && data !== null) { // If it's supposed to be a tuple, take care of recursively building a new // array and then turn it into a tuple and return it. if (reSymbolize && data.CNS_tuple_) { var out = CNS_.msgs.symbolize(data.CNS_tuple_, true); return CNS_.tuple(out); // If it's actually an object, build a new object, symbolizing all the values. // We don't try to symbolize object keys because the purpose of a symbol as an object // key is to not have it be enumerable. } else { var out = {}; Object.keys(data).forEach(function (key) { out[key] = CNS_.msgs.symbolize(data[key], reSymbolize) }); return out; } } return data; }, onMsg: function (msg) { CNS_.msgs.isBrowser() && (msg = msg.data); const m = CNS_.msgs.symbolize(msg, true); CNS_.msgs.queue.push(m); if (!CNS_.msgs.isWaiting) { CNS_.msgs.isWaiting = true; setTimeout(function () { CNS_.msgs.runQueue(); }, 0); } }, runQueue: function () { this.queue.forEach(function (msgObj) { this.handlers.forEach(function (handler) { handler(msgObj); }); }.bind(this)); this.queue = []; this.isWaiting = false; }, Thread: function(fnBody) { const isBrowser = typeof navigator !== 'undefined'; const body = 'const CNS_ = ' + CNS_.msgs.stringify(CNS_, true) + ';\n' + 'CNS_.msgs.isChild = true;\n' + 'CNS_.msgs.handlers = [];\n' + (isBrowser ? 'this.onmessage = CNS_.msgs.onMsg;\n' : 'process.on("message", CNS_.msgs.onMsg);\n') + 'var arguments = [];\n' + fnBody; this.isBrowser = isBrowser; this.thread = isBrowser ? new Worker(window.URL.createObjectURL( new Blob([body], {type: 'application/javascript'}) )) : require('child_process').fork(null, [], { execPath: 'node', execArgv: ['-e', body] }) ; isBrowser ? (this.thread.onmessage = CNS_.msgs.onMsg) : this.thread.on('message', CNS_.msgs.onMsg); !isBrowser && this.thread.on('exit', function () { console.log('process exited') }) return this; } }, /** * @public * Spins up a new thread from a function. * * @param {Function} fn Contains the process body. * @return {Object} The multithreading tools package. */ spawn: function (fn) { return new CNS_.msgs.Thread('(' + fn.toString() + '())'); }, /** * @public * Determines what to do when messages come in from another thread. * * @param {Function} fn Handles the incoming message. * @return {undefined} */ receive: function (fn) { CNS_.msgs.handlers.push(fn); }, /** * @public * Kills a process. * * @param {Process} thread A thread to kill. * @return {undefined} */ kill: function (thread) { thread.isBrowser ? thread.thread.terminate() : thread.thread.kill('SIGINT'); }, /** * @public * Sends a message back to a parent thread from a child thread. * * @param {Serializable} msg Any serializable data. * @return {undefined} */ reply: function (msg) { const m = CNS_.msgs.symbolize(msg, false); CNS_.msgs.isBrowser() ? postMessage(m) : process.send(m) ; }, /** * @public * Sends a message to a child thread. * * @param {Process} thread The child process. * @param {Serializable} msg Any serializable data. * @return {undefined} */ send: function (thread, msg) { const m = CNS_.msgs.symbolize(msg, false); CNS_.msgs.isBrowser() ? thread.thread.postMessage(m) : thread.thread.send(m); } /******************************** * End message passing stuff ********************************/ }; // export default CNS_; module.exports = CNS_;