499 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			499 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 基础小工具,可通用于服务端和用户端
 | ||
|  */
 | ||
| 
 | ||
| const fs = require('fs')
 | ||
| const path = require('path')
 | ||
| 
 | ||
| module.exports = {
 | ||
|   BASEPORT_API_SERVER: 7000,
 | ||
|   BASEPORT_WEB_SERVER: 8000,
 | ||
|   BASEPORT_CHAIN_SERVER: 6000,
 | ||
|   BASEPORT_NET_NODE: 60000,
 | ||
|   PORT_WEB_SERVER_DEV: 8338, // 338 stands for dev
 | ||
| 
 | ||
|   sleep: (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms)),
 | ||
| 
 | ||
|   deepFreeze (obj) {
 | ||
|     if (typeof obj !== 'object' || obj === null) {
 | ||
|       return obj
 | ||
|     }
 | ||
|     Object.freeze(obj)
 | ||
|     const propNames = Object.getOwnPropertyNames(obj)
 | ||
|     propNames.forEach((prop) => {
 | ||
|       const propValue = obj[prop]
 | ||
|       // for nested object
 | ||
|       if (propValue && typeof propValue === 'object' && !Object.isFrozen(propValue)) {
 | ||
|         deepFreeze(propValue)
 | ||
|       }
 | ||
|       // for nested array
 | ||
|       if (Array.isArray(propValue)) {
 | ||
|         propValue.forEach((item) => {
 | ||
|           if (item && typeof item === 'object' && !Object.isFrozen(item)) {
 | ||
|             deepFreeze(item)
 | ||
|           }
 | ||
|         })
 | ||
|         Object.freeze(propValue) // Freeze the array itself
 | ||
|       }
 | ||
|     })
 | ||
|     return obj
 | ||
|   },
 | ||
| 
 | ||
|   parse_json_anyway (value) {
 | ||
|     try {
 | ||
|       return JSON.parse(value)
 | ||
|     } catch (e) {
 | ||
|       return undefined
 | ||
|     }
 | ||
|   },
 | ||
| 
 | ||
|   parse_json_or_keep (value) {
 | ||
|     try {
 | ||
|       return JSON.parse(value)
 | ||
|     } catch (e) {
 | ||
|       return value
 | ||
|     }
 | ||
|   },
 | ||
| 
 | ||
|   // 按顺序展开,哪怕嵌套。
 | ||
|   stringify_by_keyorder (obj, { cmp, cycles = false, space = '', replacer, schemaColumns, excludeKeys = [] } = {}) {
 | ||
|     /* 这个解决方法不考虑缺省值,不能把嵌套对象也按顺序展开。*/
 | ||
|     //    return JSON.stringify(obj, Object.keys(schemaColumns || entity).sort().filter(key => ! excludeKeys.includes(key))) // JSON.stringify 可根据第二个数组参数的顺序排序,但这导致了嵌套对象不能按顺序展开。
 | ||
|     let newObj = {}
 | ||
|     if (schemaColumns) {
 | ||
|       for (let key in schemaColumns) {
 | ||
|         // JSON.stringify (包括本函数)会把 NaN 或 Infinity 输出为 null,会把 undefined 忽略掉。
 | ||
|         // 而在 typeorm sqlite 数据库中,undefined 会自动存为 schemaColumns[key].default 或 null。从数据库读出时,就会和事先JSON.stringify的结果不一致。
 | ||
|         // 为了和 torm 数据库保持一致习惯,对schemaColumns里的键值应当把 undefined 也设为 default 或 null。
 | ||
|         if (obj[key] === undefined || Number.isNaN(obj[key]) || obj[key] === Infinity) {
 | ||
|           newObj[key] = typeof schemaColumns[key].default === 'undefined' ? null : schemaColumns[key].default
 | ||
|           obj[key] = newObj[key] // 确保内存中的数据和数据库保持一致
 | ||
|         } else {
 | ||
|           newObj[key] = obj[key]
 | ||
|         }
 | ||
|       }
 | ||
|     } else {
 | ||
|       newObj = obj
 | ||
|     }
 | ||
|     for (let exkey of excludeKeys) {
 | ||
|       delete newObj[exkey]
 | ||
|     }
 | ||
| 
 | ||
|     /* 以下代码来自 https://github.com/substack/json-stable-stringify 可把嵌套的复杂值也按顺序输出,例如 { c: 8, b: [{z:6,y:5,x:4},7], a: 3 } */
 | ||
|     if (typeof space === 'number') space = Array(space + 1).join(' ')
 | ||
|     var cycles = typeof cycles === 'boolean' ? cycles : false
 | ||
|     var replacer =
 | ||
|       replacer ||
 | ||
|       function (key, value) {
 | ||
|         return value
 | ||
|       }
 | ||
| 
 | ||
|     var cmp =
 | ||
|       cmp &&
 | ||
|       (function (f) {
 | ||
|         return function (node) {
 | ||
|           return function (a, b) {
 | ||
|             var aobj = { key: a, value: node[a] }
 | ||
|             var bobj = { key: b, value: node[b] }
 | ||
|             return f(aobj, bobj)
 | ||
|           }
 | ||
|         }
 | ||
|       })(cmp)
 | ||
| 
 | ||
|     var seen = []
 | ||
|     return (function stringify (parent, key, node, level) {
 | ||
|       var indent = space ? '\n' + new Array(level + 1).join(space) : ''
 | ||
|       var colonSeparator = space ? ': ' : ':'
 | ||
| 
 | ||
|       if (node && node.toJSON && typeof node.toJSON === 'function') {
 | ||
|         node = node.toJSON()
 | ||
|       }
 | ||
| 
 | ||
|       node = replacer.call(parent, key, node)
 | ||
| 
 | ||
|       if (node === undefined) {
 | ||
|         return
 | ||
|       }
 | ||
|       if (typeof node !== 'object' || node === null) {
 | ||
|         return JSON.stringify(node)
 | ||
|       }
 | ||
|       if (Array.isArray(node)) {
 | ||
|         var out = []
 | ||
|         for (var i = 0; i < node.length; i++) {
 | ||
|           var item = stringify(node, i, node[i], level + 1) || JSON.stringify(null)
 | ||
|           out.push(indent + space + item)
 | ||
|         }
 | ||
|         return '[' + out.join(',') + indent + ']'
 | ||
|       } else {
 | ||
|         if (seen.indexOf(node) !== -1) {
 | ||
|           if (cycles) return JSON.stringify('__cycle__')
 | ||
|           throw new TypeError('Converting circular structure to JSON')
 | ||
|         } else seen.push(node)
 | ||
| 
 | ||
|         var keys = Object.keys(node).sort(cmp && cmp(node))
 | ||
|         var out = []
 | ||
|         for (var i = 0; i < keys.length; i++) {
 | ||
|           var key = keys[i]
 | ||
|           var value = stringify(node, key, node[key], level + 1)
 | ||
| 
 | ||
|           if (!value) continue
 | ||
| 
 | ||
|           var keyValue = JSON.stringify(key) + colonSeparator + value
 | ||
| 
 | ||
|           out.push(indent + space + keyValue)
 | ||
|         }
 | ||
|         seen.splice(seen.indexOf(node), 1)
 | ||
|         return '{' + out.join(',') + indent + '}'
 | ||
|       }
 | ||
|     })({ '': newObj }, '', newObj, 0)
 | ||
|   },
 | ||
| 
 | ||
|   alpha_to_digit (name = '') {
 | ||
|     let port = name
 | ||
|       .toLowerCase()
 | ||
|       .replace(/[abc]/g, 2)
 | ||
|       .replace(/[def]/g, 3)
 | ||
|       .replace(/[ghi]/g, 4)
 | ||
|       .replace(/[jkl]/g, 5)
 | ||
|       .replace(/[mno]/g, 6)
 | ||
|       .replace(/[pqrs]/g, 7)
 | ||
|       .replace(/[tuv]/g, 8)
 | ||
|       .replace(/[wxyz]/g, 9)
 | ||
|     return parseInt(port)
 | ||
|   },
 | ||
| 
 | ||
|   keyname_to_apiport (keyname) {
 | ||
|     return this.BASEPORT_API_SERVER + parseInt(this.alpha_to_digit(keyname))
 | ||
|   },
 | ||
|   keyname_to_chainport (keyname) {
 | ||
|     return this.BASEPORT_CHAIN_SERVER + parseInt(this.alpha_to_digit(keyname))
 | ||
|   },
 | ||
|   keyname_to_netport (keyname) {
 | ||
|     return this.BASEPORT_NET_NODE + parseInt(this.alpha_to_digit(keyname))
 | ||
|   },
 | ||
|   keyname_to_webport (keyname) {
 | ||
|     return this.BASEPORT_WEB_SERVER + parseInt(this.alpha_to_digit(keyname))
 | ||
|   },
 | ||
| 
 | ||
|   randomize_number ({ length, min, max } = {}) {
 | ||
|     // 长度为 length 的随机数字,或者 (min||0) <= num < max
 | ||
|     var num = 0
 | ||
|     if (typeof length === 'number' && length > 0) {
 | ||
|       num = parseInt(Math.random() * Math.pow(10, length))
 | ||
|       num = num.toString().padStart(length, '0')
 | ||
|     } else if (typeof max === 'number' && max > 0) {
 | ||
|       min = typeof min === 'number' && min >= 0 ? min : 0
 | ||
|       num = parseInt(Math.random() * (max - min)) + min
 | ||
|     } else {
 | ||
|       // 如果 option 为空
 | ||
|       num = Math.random()
 | ||
|     }
 | ||
|     return num
 | ||
|   },
 | ||
| 
 | ||
|   hash_stable (data, { hasher = 'sha256', salt, input = 'utf8', output = 'hex' } = {}) {
 | ||
|     if (typeof data !== 'string' && !(data instanceof Buffer) && !(data instanceof DataView)) data = this.stringify_by_keyorder(data)
 | ||
|     if (salt && typeof salt === 'string') data = data + salt
 | ||
|     const inputEncoding = input // my.INPUT_LIST.indexOf(option.input)>=0?option.input:my.INPUT // 'utf8', 'ascii' or 'latin1' for string data, default to utf8 if not specified; ignored for Buffer, TypedArray, or DataView.
 | ||
|     const outputEncoding = output === 'buf' ? undefined : output // (my.OUTPUT_LIST.indexOf(output)>=0?output:my.OUTPUT)  // option.output: 留空=》默认输出hex格式;或者手动指定 'buf', hex', 'latin1' or 'base64'
 | ||
|     return require('crypto').createHash(hasher).update(data, inputEncoding).digest(outputEncoding)
 | ||
|   },
 | ||
| 
 | ||
|   /**
 | ||
|    * 用户编号转邀请码
 | ||
|    *
 | ||
|    * @static
 | ||
|    * @param {*} aiid
 | ||
|    * @return {*}
 | ||
|    * @memberof TICrypto
 | ||
|    */
 | ||
|   aiid_to_haid (aiid) {
 | ||
|     const alphabet = 'e5fcdg3hqa4b1n0pij2rstuv67mwx89klyz'
 | ||
|     const base = 16367
 | ||
|     let num = (aiid + base) * (base - alphabet.length)
 | ||
|     let code = ''
 | ||
|     let mod
 | ||
|     while (num > 0) {
 | ||
|       mod = num % alphabet.length
 | ||
|       num = (num - mod) / alphabet.length
 | ||
|       code = code + alphabet[mod] // 倒序存放
 | ||
|     }
 | ||
|     return code
 | ||
|   },
 | ||
| 
 | ||
|   /**
 | ||
|    * 邀请码转用户编号
 | ||
|    *
 | ||
|    * @static
 | ||
|    * @param {*} code
 | ||
|    * @return {*}
 | ||
|    * @memberof TICrypto
 | ||
|    */
 | ||
|   haid_to_aiid (haid) {
 | ||
|     if (typeof haid === 'string' && /^[a-zA-Z0-9]+$/.test(haid)) {
 | ||
|       const alphabet = 'e5fcdg3hqa4b1n0pij2rstuv67mwx89klyz'
 | ||
|       const base = 16367
 | ||
|       haid = haid.toLowerCase()
 | ||
|       let len = haid.length
 | ||
|       let num = 0
 | ||
|       for (let i = 0; i < len; i++) {
 | ||
|         num += alphabet.indexOf(haid[i]) * Math.pow(alphabet.length, i)
 | ||
|       }
 | ||
|       let aiid = num / (base - alphabet.length) - base
 | ||
|       if (aiid >= 0 && Number.isInteger(aiid)) {
 | ||
|         // 允许 aiid===0:当第一个用户(aiid==1)登录时,需要一个系统默认的邀请码。
 | ||
|         return aiid
 | ||
|       }
 | ||
|     }
 | ||
|     return null // null 代表一切非法的 haid
 | ||
|   },
 | ||
| 
 | ||
|   isEmpty (value) {
 | ||
|     switch (typeof value) {
 | ||
|       case 'number':
 | ||
|         if (value === 0 || value !== value) return true
 | ||
|         return false
 | ||
|       case 'object':
 | ||
|         for (var attr in value) {
 | ||
|           return false
 | ||
|         }
 | ||
|         /*	  	  if (JSON.stringify(value)==='{}'){
 | ||
|             return true;
 | ||
|           }
 | ||
|           if (Object.keys(value).length===0){ // Object.keys(null) 会出错。
 | ||
|             return true;
 | ||
|           } */
 | ||
|         return true
 | ||
|       case 'string':
 | ||
|         return value === '' ? true : false
 | ||
|       case 'undefined':
 | ||
|         return true
 | ||
|       case 'boolean':
 | ||
|         return value
 | ||
|     }
 | ||
|     return true
 | ||
|   },
 | ||
| 
 | ||
|   get_jstype (o) {
 | ||
|     // 返回:一个字符串,表示标量类型 undefined,boolean,number,string 以及对象类型 Null, Object, Array, String, Boolean, Number, Function
 | ||
|     var t = typeof o
 | ||
|     return t === 'object' || t === 'function' // function是特殊的,typeof 结果是function, 但 Object.prototype.toString.call 结果是 [object Function]。我选用大写形式。
 | ||
|       ? Object.prototype.toString.call(o).slice(8, -1) // 可以是 Null, Object, Function, Boolean, String, Number, Array (如果 o===undefined, 那就是Undefined), 还可能是 Date, Math, Uint8Array(如果是个Buffer)
 | ||
|       : t // 可以是 undefined, boolean, number, string
 | ||
|   },
 | ||
| 
 | ||
|   read_varchain (path, root, emptyValue) {
 | ||
|     let parent = root || globalThis || global || window || {}
 | ||
|     let keychain = path.split('.')
 | ||
|     for (let key of keychain) {
 | ||
|       if (!parent) {
 | ||
|         // 如果 parent 是 null 或 undefined,直接返回空值。
 | ||
|         return emptyValue
 | ||
|       } else if (typeof parent === 'object' && /^\w+\(.*\)$/.test(key)) {
 | ||
|         // 支持 myfunc(param) 作为一个路径节点。
 | ||
|         let [all, func, param] = key.match(/^(\w+)\((.*)\)$/)
 | ||
|         parent = parent[func](param)
 | ||
|       } else if (typeof parent === 'object' && /^\w+\[.*\]$/.test(key)) {
 | ||
|         // 支持 myarr[index] 作为一个路径节点
 | ||
|         let [all, arr, index] = key.match(/^(\w+)\[(.*)\]$/)
 | ||
|         parent = parent[arr][parseInt(index)]
 | ||
|       } else if (typeof parent === 'object' && /^\w+$/.test(key) && typeof parent[key] != 'undefined') {
 | ||
|         parent = parent[key]
 | ||
|       } else {
 | ||
|         return emptyValue
 | ||
|       }
 | ||
|     }
 | ||
|     return typeof parent !== 'undefined' ? parent : emptyValue
 | ||
|   },
 | ||
| 
 | ||
|   set_varchain (path, root, value) {
 | ||
|     var parent = root || global || window || {}
 | ||
|     var keychain = path.split('.')
 | ||
|     for (let i = 0; i < keychain.length - 1; i++) {
 | ||
|       if (typeof parent === 'object' && keychain[i].match(/^\w+$/)) {
 | ||
|         if (typeof parent[keychain[i]] !== 'object') parent[keychain[i]] = {}
 | ||
|         parent = parent[keychain[i]]
 | ||
|       } else {
 | ||
|         return null
 | ||
|       }
 | ||
|     }
 | ||
|     return (parent[keychain[keychain.length - 1]] = value)
 | ||
|   },
 | ||
| 
 | ||
|   number_precision (number, precision = 4) {
 | ||
|     if (isFinite(number)) {
 | ||
|       // 包括 null, '', '0', [] 都 isFinite
 | ||
|       return Number(Number(number).toFixed(precision))
 | ||
|     } else {
 | ||
|       // undefined, NaN, Infinity, {}
 | ||
|       return 0
 | ||
|     }
 | ||
|   },
 | ||
| 
 | ||
|   // 返回新的数组
 | ||
|   filter_story (story) {
 | ||
|     if (Array.isArray(story) && story.length) {
 | ||
|       return story.filter((section) => {
 | ||
|         if (!section) return false
 | ||
|         return Object.values(section).some((val) => !this.is_empty(val))
 | ||
|         //return Object.entries(section).some(([key, val]) => !key.startsWith('_') && !this.is_empty(val)) // don't check properties '_xxx'
 | ||
|       })
 | ||
|     } else {
 | ||
|       return []
 | ||
|     }
 | ||
|   },
 | ||
| 
 | ||
|   // 20250621 todo 如果 text 是 {enUS,zhCN} 怎么处理
 | ||
|   extract_story_title ({ story, lang = 'enUS' } = {}) {
 | ||
|     if (Array.isArray(story) && story.length) {
 | ||
|       return (
 | ||
|         story
 | ||
|           .map(({ text = '' } = {}) => {
 | ||
|             if (typeof text === 'string') return text.trim?.()?.replace?.(/\n+/g, ' ')
 | ||
|             else if (typeof text === 'object') return (text[lang] || text['enUS'])?.trim?.()?.replace?.(/\n+/g, ' ')
 | ||
|           })
 | ||
|           ?.join(' ')
 | ||
|           ?.trim?.()
 | ||
|           ?.substring?.(0, 140) || ''
 | ||
|       )
 | ||
|     } else {
 | ||
|       return ''
 | ||
|     }
 | ||
|   },
 | ||
| 
 | ||
|   summarize_story (story = []) {
 | ||
|     // story is an array of objects, each object could either be {text:'some string'}, {image: url} or {video:url}. Please construct a summary object as result: { textLength, imageCount, VideoCount }
 | ||
|     return story.reduce(
 | ||
|       (summary, { text, image, video, audio, file, linkTarget, _autocontent } = {}) => {
 | ||
|         if (text) {
 | ||
|           summary.textLength += text.length
 | ||
|           summary.wordCount += text.split(/\s+/).length
 | ||
|         } else if (image) {
 | ||
|           summary.imageCount++
 | ||
|         } else if (video) {
 | ||
|           summary.videoCount++
 | ||
|         } else if (audio) {
 | ||
|           summary.audioCount++
 | ||
|         } else if (file) {
 | ||
|           summary.fileCount++
 | ||
|         } else if (linkTarget) {
 | ||
|           summary.linkCount++
 | ||
|         } else if (_autocontent) {
 | ||
|           summary.emptyContent++
 | ||
|         } else {
 | ||
|           summary.untypeCount++
 | ||
|         }
 | ||
|         return summary
 | ||
|       },
 | ||
|       { textLength: 0, wordCount: 0, imageCount: 0, videoCount: 0, audioCount: 0, fileCount: 0, linkCount: 0, emptyContent: 0, untypeCount: 0 }
 | ||
|     )
 | ||
|   },
 | ||
| 
 | ||
|   is_empty (data) {
 | ||
|     // empty: undefined, null, false, 0, NaN, '', '空格,tab, \n等', [], {}
 | ||
|     if (data) {
 | ||
|       if (typeof data === 'string' && data.trim() === '') {
 | ||
|         return true
 | ||
|       }
 | ||
|       if (Array.isArray(data) && data.length === 0) {
 | ||
|         return true
 | ||
|       }
 | ||
|       if (typeof data === 'object' && Object.keys(data).length === 0) {
 | ||
|         return true
 | ||
|       }
 | ||
|       return false
 | ||
|     }
 | ||
|     return true
 | ||
|   },
 | ||
| 
 | ||
|   /**
 | ||
|    * 对数组中的对象,按对象的key进行sortType排序
 | ||
|    */
 | ||
|   keysort_array (arr, key, order = 'ASC') {
 | ||
|     if (Array.isArray(arr) && typeof key === 'string' && key) {
 | ||
|       return arr.sort((a, b) => {
 | ||
|         // 负数: a, b. 正数: b, a
 | ||
|         if (typeof a[key] === 'number' && typeof b[key] === 'number') {
 | ||
|           return order === 'DESC' ? b[key] - a[key] : a[key] - b[key]
 | ||
|         } else if (typeof a[key] === 'string' && typeof b[key] === 'string') {
 | ||
|           return order === 'DESC' ? b[key].localeCompare(a[key]) : a[key].localeCompare(b[key])
 | ||
|         } else {
 | ||
|           return 0
 | ||
|         }
 | ||
|       })
 | ||
|     } else {
 | ||
|       return arr
 | ||
|     }
 | ||
|   },
 | ||
| 
 | ||
|   objectize_array (arr, key) {
 | ||
|     // 是 Object.keys/values/entries 的反向操作,相当于 Array.objectize,把数据转成对象
 | ||
|     return arr.reduce((obj, item) => ({ ...obj, [item[key]]: item }), {})
 | ||
|   },
 | ||
| 
 | ||
|   simplify_file (file = {}) {
 | ||
|     delete file.baseUrl
 | ||
|     delete file.fileUrl
 | ||
|     delete file.ipfsUrl
 | ||
|     delete file.fieldname
 | ||
|     delete file.filename
 | ||
|     delete file.destination
 | ||
|     return file
 | ||
|   },
 | ||
| 
 | ||
|   has_module (module) {
 | ||
|     if (typeof module === 'string' && module) {
 | ||
|       return module.paths.some((modulesPath) => fs.existsSync(path.join(modulesPath, module)))
 | ||
|     } else {
 | ||
|       return false
 | ||
|     }
 | ||
|   },
 | ||
| 
 | ||
|   segment_number (sizeBytes = '') {
 | ||
|     // segment a number with a space between each 3 digits
 | ||
|     let segmented = sizeBytes.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
 | ||
|     const parts = segmented.split(' ')
 | ||
|     parts[parts.length - 3] += 'M'
 | ||
|     parts[parts.length - 2] += 'K'
 | ||
|     return parts.join(' ') + 'B'
 | ||
|   },
 | ||
| 
 | ||
|   get_file_ext (filename = '') {
 | ||
|     const parts = filename.split('.')
 | ||
|     // 注意,'abc'.split('.') === ['abc']
 | ||
|     if (parts.length >= 2) {
 | ||
|       return parts.pop().toLowerCase()
 | ||
|     }
 | ||
|     return ''
 | ||
| 
 | ||
|     // 如果用 path.extname:
 | ||
|     // if (/^\./.test(filename)) filename = `added$filename` // path.extname('.abc') 结果为 '',所以要添加前缀
 | ||
|     // return path.extname(filename).toLowerCase() // openAi*Ext 是包含 . 的,所以用 path.extname
 | ||
|   },
 | ||
| 
 | ||
|   delete_undefined (obj, { depth } = {}) {
 | ||
|     // delete all undefined properties recursively inside an obect
 | ||
|     Object.keys(obj).forEach((key) => {
 | ||
|       if (typeof obj[key] === 'undefined') {
 | ||
|         delete obj[key]
 | ||
|       } else if (typeof obj[key] === 'object') {
 | ||
|         this.delete_undefined(obj[key])
 | ||
|       }
 | ||
|     })
 | ||
|   },
 | ||
| 
 | ||
|   // 洗牌算法,随机打乱数组顺序
 | ||
|   shuffle_array (array = []) {
 | ||
|     if (Array.isArray(array)) {
 | ||
|       for (let i = array.length - 1; i > 0; i--) {
 | ||
|         const j = Math.floor(Math.random() * (i + 1))
 | ||
|         ;[array[i], array[j]] = [array[j], array[i]]
 | ||
|       }
 | ||
|     }
 | ||
|     return array
 | ||
|   },
 | ||
| 
 | ||
|   is_uuid (uuid) {
 | ||
|     return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)
 | ||
|   },
 | ||
| }
 |