feat: 开始使用typescript重构runtime

This commit is contained in:
wxzhang 2023-04-01 17:23:35 +08:00
parent 9521f48ebe
commit d5dcae4c8a
29 changed files with 1587 additions and 1602 deletions

View File

@ -1,25 +0,0 @@
module.exports = {
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"debug": false,
"modules": false,
"corejs":{
"version":"3.27.1",
"proposals": true
}
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs":3,
"proposals": true
}
]
]
}

View File

@ -1,129 +0,0 @@
/**
*
* 处理中文数字和货币相关
*
*/
const { isNumber } = require('../utils')
const { FlexFormatter,Formatter } = require('../formatter')
const CN_DATETIME_UNITS = ["年","季度","月","周","日","小时","分钟","秒","毫秒","微秒"]
const CN_WEEK_DAYS = ["星期日","星期一","星期二","星期三","星期四","星期五","星期六"]
const CN_SHORT_WEEK_DAYS =["周日","周一","周二","周三","周四","周五","周六"]
const CN_MONTH_NAMES= ["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"]
const CN_SHORT_MONTH_NAMES = ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"]
const CN_NUMBER_DIGITS = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"]
const CN_NUMBER_UNITS = ['', '十', '百', '千', '万', '十', '百', '千', '亿', '十', '百', '千', '兆', '十', '百', '千', '京', '十', '百', '千', '垓']
const CN_NUMBER_BIG_DIGITS = ["零", '壹', '貳', '參', '肆', '伍', '陸', '柒', '捌', '玖']
const CN_NUMBER_BIG_UNITS = ['', '拾', '佰', '仟', '萬', '拾', '佰', '仟', '億', '拾', '佰', '仟', '兆', '拾', '佰', '仟', '京', '拾', '佰', '仟', '垓']
/**
*
* 将数字转换为中文数字
*
* 注意会忽略掉小数点后面的数字
*
* @param {*} value 数字
* @param {*} isBig 是否大写数字
* @returns
*/
function toChineseNumber(value,isBig) {
if(!isNumber(value)) return value;
let [wholeValue,decimalValue] = String(value).split(".") // 处理小数点
const DIGITS = isBig ? CN_NUMBER_BIG_DIGITS : CN_NUMBER_DIGITS
const UNITS = isBig ? CN_NUMBER_BIG_UNITS : CN_NUMBER_UNITS
let result = ''
if(wholeValue.length==1) return DIGITS[parseInt(wholeValue)]
for(let i=wholeValue.length-1; i>=0; i--){
let bit = parseInt(wholeValue[i])
let digit = DIGITS[bit]
let unit = UNITS[wholeValue.length-i-1]
if(bit==0){
let preBit =i< wholeValue.length ? parseInt(wholeValue[i+1]) : null// 上一位
let isKeyBits = ((wholeValue.length-i-1) % 4)==0
if(preBit && preBit!=0 && !isKeyBits) result = "零" + result
if(isKeyBits) result = UNITS[wholeValue.length-i-1] + result
}else{
result=`${digit}${unit}` + result
}
}
if(isBig){
result = result.replace("垓京","垓")
.replace("京兆","京")
.replace("兆億","兆")
.replace("億萬","億")
.replace("萬仟","萬")
}else{
result = result.replace("垓京","垓")
.replace("京兆","京")
.replace("兆亿","兆")
.replace("亿万","亿")
.replace("万千","万")
if(result.startsWith("一十")) result=result.substring(1)
}
return result // 中文数字忽略小数部分
}
function toChineseBigNumber(value) {
return toChineseNumber(value,true)
}
const chineseNumberFormatter = Formatter((value,isBig,$config)=>{
return toChineseNumber(value,isBig,$config)
},{
params:["isBig"]
})
/**
* 转换为中文大写货币
* @param {*} value
* @param {*} division 分割符号位数,3代表每3个数字添加一个,
* @param {*} prefix 前缀
* @param {*} suffix 后缀
* @param {*} showWhole 显示
*/
function toChineseCurrency(value,{big,prefix,unit,suffix}={},$config){
let [wholeValue,decimalValue] = String(value).split(".")
let result
if(big){
result = toChineseBigNumber(wholeValue)+unit
}else{
result = toChineseNumber(wholeValue)+unit
}
if(decimalValue){
if(decimalValue[0]) {
let bit0 = parseInt(decimalValue[0])
result =result+ (big ? CN_NUMBER_BIG_DIGITS[bit0] : CN_NUMBER_DIGITS[bit0])+"角"
}
if(decimalValue[1]){
let bit1 = parseInt(decimalValue[1])
result =result+ (big ? CN_NUMBER_BIG_DIGITS[bit1] : CN_NUMBER_DIGITS[bit1])+"分"
}
}
return prefix+result+suffix
}
const rmbFormater = FlexFormatter((value,params,$config)=>{
return toChineseCurrency(value,params,$config)
},{
params:["big","prefix","unit","suffix"],
configKey:"rmb"
})
module.exports ={
toChineseCurrency,
toChineseNumber,
toChineseBigNumber,
rmbFormater,
chineseNumberFormatter,
CN_DATETIME_UNITS,
CN_WEEK_DAYS,
CN_SHORT_WEEK_DAYS,
CN_MONTH_NAMES,
CN_SHORT_MONTH_NAMES,
CN_NUMBER_DIGITS,
CN_NUMBER_UNITS,
CN_NUMBER_BIG_DIGITS,
CN_NUMBER_BIG_UNITS
}

View File

@ -1,256 +0,0 @@
/**
处理日期时间相关
*/
const { isFunction,replaceAll,toDate } = require('../utils')
const { Formatter } = require('../formatter');
/**
* 获取一天中的时间段
* @param {*} hour 小时取值0-23
* @param {*} options
* @returns
*/
function getTimeSlot(hour,caseStyle = 0){
if(hour<0 && hour>23) hour = 0
const timeSlots = this.timeSlots
const slots = [0,...timeSlots.slots,24]
let slotIndex = slots.findIndex(v=>v>hour) - 1
return caseStyle == 0 ? timeSlots.lowerCases[slotIndex] : timeSlots.upperCases[slotIndex]
}
/**
* 根据模板格式化日期时间
*
YY 18 两位数
YYYY 2018 四位数
M 1-12 从1开始
MM 01-12 两位数字
MMM Jan-Dec 英文缩写
D 1-31
DD 01-31 两位数
H 0-23 24小时
HH 00-23 24小时两位数
h 1-12 12小时
hh 01-12 12小时两位数
m 0-59 分钟
mm 00-59 分钟两位数
s 0-59
ss 00-59 两位数
S 0-9 毫秒一位数
SS 00-99 毫秒两位数
SSS 000-999 毫秒三位数
Z -05:00 UTC偏移
ZZ -0500 UTC偏移两位数
A AM / PM /下午大写
a am / pm /下午小写
Do 1st... 31st 月份的日期与序号
t 小写时间段如am,pm
T 大写时间段段如上午中午下午
* 重点 this-->当前格化器的配置参数
*
* 因此可以通过this.month.long来读取长格式模板
*
*
* @param {*} value
* @param {*} template
* @param {Boolean} onlyTime 仅格式化时间
* @param {*} options = {month:[<短月份名称>,<短月份名称>],timeSlots:{}}
* @returns
*/
function formatDatetime(value,template="YYYY/MM/DD HH:mm:ss",onlyTime=false){
const $config = this.datetime// this->指向的是当前语言的格式化化器配置
const v = toDate(value)
const hour = v.getHours(),Hour = String(hour).padStart(2, "0")
const hour12 = hour > 12 ? hour - 12 : hour ,Hour12 = String(hour12).padStart(2, "0")
const minute = String(v.getMinutes()),second = String(v.getSeconds()),millisecond=String(v.getMilliseconds())
let vars = [
["HH", Hour], // 00-23 24小时两位数
["H", hour], // 0-23 24小时
["hh", Hour12], // 01-12 12小时两位数
["h", hour12], // 1-12 12小时
["mm", minute.padStart(2, "0")], // 00-59 分钟,两位数
["m", minute], // 0-59 分钟
["ss", second.padStart(2, "0")], // 00-59 秒,两位数
["s", second], // 0-59 秒
["SSS", millisecond], // 000-999 毫秒,三位数
["A", hour > 12 ? "PM" : "AM"], // AM / PM 上/下午,大写
["a", hour > 12 ? "pm" : "am"], // am / pm 上/下午,小写
["t", getTimeSlot.call($config,hour,0)], // 小写时间段,如上午、中午、下午
["T", getTimeSlot.call($config,hour,1)], // 大写时间段,如上午、中午、下午
]
if(!onlyTime){
const year =String(v.getFullYear()),month = v.getMonth(),weekday=v.getDay(),day=String(v.getDate())
vars.push(...[
["YYYY", year], // 2018 年,四位数
["YY", year.substring(2)], // 18年两位数
["MMM", $config.month.short[month]], // Jan-Dec月缩写
["MM", String(month+1).padStart(2, "0")], // 01-12月两位数字
["M", month+1], // 1-12 月从1开始
["DD", day.padStart(2, "0")], // 01-31 日,两位数
["D", day], // 1-31 日
["d",weekday], // 0-6 一周中的一天,星期天是 0
["dd",$config.weekday.short[weekday]], // Su-Sa 最简写的星期几
["ddd",$config.weekday.short[weekday]], // Sun-Sat 简写的星期几
["dddd",$config.weekday.long[weekday]], // Sunday-Saturday 星期几,英文全称
])
}
let result = template
vars.forEach(([k,v])=>result = replaceAll(result,k,v))
return result
}
/**
* 只格式化时间
* @param {*} value
* @param {*} template
* @returns
*/
function formatTime(value,template="HH:mm:ss"){
return formatDatetime.call(this,value,template,true)
}
/**
* 该类型的格式化器具有以下特点
*
* 1. 接受一个format参数
* 2. format参数取值可以是若干预设的值如long,short等也可能是一个模板字符串
* 3. 当format值时如果定义在$config[configKey]里面代表了$config[configKey][format]是一个模板字符串
* 4. 如果!(format in $config[configKey])则代表format值是一个模板字符串
* 5. 如果format in presets, 则要求presets[format ]是一个(value)=>{....}直接返回
*
**/
function createDateTimeFormatter(options={},transformer){
let opts = Object.assign({presets:{}},options)
return Formatter(function(value,format,$config){
if((format in opts.presets) && isFunction(opts.presets[format])){
return opts.presets[format](value)
}else if((format in $config)){
format = $config[format]
}else if(format == "number"){
return value
}
try{// this指向的是activeFormatter.$config
return format==null ? value : transformer.call(this,value,format)
}catch(e){
return value
}
},opts)
}
/**
* 日期格式化器
* - format取值local,long,short,iso,gmt,utc,<模板字符串>
* - 默认值由$config.datetime.date.format指定
*/
const dateFormatter = createDateTimeFormatter({
normalize: toDate,
params : ["format"],
configKey: "datetime.date",
presets : {
local: value=>value.toLocaleString(),
iso : value=>value.toISOString(),
utc : value=>value.toUTCString(),
gmt : value=>value.toGMTString()
}
},formatDatetime)
/**
* 季度格式化器
* - format: long,short,number
* - 默认值是 short
*/
const quarterFormatter = createDateTimeFormatter({
normalize : value=>{
const month = value.getMonth() + 1
return Math.floor( ( month % 3 == 0 ? ( month / 3 ) : (month / 3 + 1 ) ))
},
params : ["format"],
configKey: "datetime.quarter"
},(quarter,format)=>format[quarter-1])
/**
* 月份格式化器
* - format: long,short,number
* - 默认值是 short
*/
const monthFormatter = createDateTimeFormatter({
normalize: value=> value.getMonth() + 1,
params : ["format"],
configKey: "datetime.month"
},(month,format)=>format[month-1])
/**
* 周格式化器
* - format: long,short,number
* - 默认值是 long
*/
const weekdayFormatter = createDateTimeFormatter({
normalize: value=> value.getDay(),
params : ["format"],
configKey: "datetime.weekday"
},(day,format)=>format[day])
/**
* 时间格式化器
* - format取值local,long,short,timestamp,<模板字符串>
* - 默认值由$config.datetime.time.format指定
*/
const timeFormatter = createDateTimeFormatter({
normalize : toDate,
params : ["format"],
configKey : "datetime.time",
presets : {
local : value=>value.toLocaleTimeString(),
timestamp: value=>value.getTime()
}
},formatTime)
/**
* 返回相对于rel的时间
* @param {*} value
* @param {*} baseTime 基准时间默认是相对现在
*/
// 对应:秒,分钟,小时,天,周,月,年的毫秒数,月取30天年取365天概数
const TIME_SECTIONS = [1000,60000,3600000,86400000,604800000,2592000000,31536000000,Number.MAX_SAFE_INTEGER]
const relativeTimeFormatter = Formatter((value,baseTime,$config)=>{
const { units,now,before,base = Date.now() , after } = $config
let ms = value.getTime()
let msBase = (baseTime instanceof Date) ? baseTime.getTime() : toDate(base).getTime()
let msDiff = ms - msBase
let msIndex = TIME_SECTIONS.findIndex(x=>Math.abs(msDiff) < x) - 1
if(msIndex < 0) msIndex = 0
if(msIndex > TIME_SECTIONS.length-1 ) msIndex = TIME_SECTIONS.length-1
if(msDiff==0){
return now
}else if(msDiff<0){// 之前
let result = parseInt(Math.abs(msDiff) / TIME_SECTIONS[msIndex])
return before.replace("{value}",result).replace("{unit}",units[msIndex])
}else{// 之后
let result = parseInt(Math.abs(msDiff) / TIME_SECTIONS[msIndex])
return after.replace("{value}",result).replace("{unit}",units[msIndex])
}
},{
normalize:toDate,
params:["base"],
configKey:"datetime.relativeTime"
})
module.exports = {
toDate,
formatTime,
formatDatetime,
createDateTimeFormatter,
relativeTimeFormatter,
dateFormatter,
quarterFormatter,
monthFormatter,
weekdayFormatter,
timeFormatter
}

View File

@ -1,31 +0,0 @@
/**
*
* 简单的事件触发器
*
*/
module.exports = class EventEmitter{
constructor(){
this._callbacks = []
}
on(callback){
if(this._callbacks.includes(callback)) return
this._callbacks.push(callback)
}
off(callback){
for(let i=0;i<this._callbacks.length;i++){
if(this._callbacks[i]===callback ){
this._callbacks.splice(i,1)
}
}
}
offAll(){
this._callbacks = []
}
async emit(...args){
if(Promise.allSettled){
await Promise.allSettled(this._callbacks.map(cb=>cb(...args)))
}else{
await Promise.all(this._callbacks.map(cb=>cb(...args)))
}
}
}

View File

@ -1,359 +0,0 @@
/**
*
* 解析格式化器
*
* 解析{ varname | formater(...params) }中的params部分
*
*
*
*/
const { getByPath,isNumber,isFunction,isPlainObject,escapeRegexpStr,safeParseJson } = require("./utils")
/**
使用正则表达式对原始文本内容进行解析匹配后得到的便以处理的数组
formatters="| aaa(1,1) | bbb "
统一解析为
[
[aaa,[1,1]], // [<格式化器名称>,[args,...]]
[<格式化器名称>,[<参数>,<参数>,...]]
]
formatters="| aaa(1,1,"dddd") | bbb "
特别注意
- 目前对参数采用简单的split(",")来解析因此如果参数中包括了逗号等会影响解析的字符时可能导致错误
例如aaa(1,1,"dd,,dd")形式的参数
在此场景下基本够用了如果需要支持更复杂的参数解析可以后续考虑使用正则表达式来解析
- 如果参数是{},[]则尝试解决为对象和数组但是里面的内容无法支持复杂和嵌套数据类型
@param {String} formatters
@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
*/
function parseFormatters(formatters) {
if (!formatters) return [];
// 1. 先解析为 ["aaa()","bbb"]形式
let result = formatters.trim().substring(1).trim().split("|").map((r) => r.trim());
// 2. 解析格式化器参数
return result.map((formatter) => {
if (formatter == "") return null;
let firstIndex = formatter.indexOf("(");
let lastIndex = formatter.lastIndexOf(")");
if (firstIndex !== -1 && lastIndex !== -1) { //参数的格式化器
// 带参数的格式化器: 取括号中的参数字符串部分
const strParams = formatter.substring(firstIndex + 1, lastIndex).trim();
// 解析出格式化的参数数组
let params = parseFormaterParams(strParams);
// 返回[<格式化器名称>,[<参数>,<参数>,...]
return [formatter.substring(0, firstIndex), params];
} else { // 不带参数的格式化器
return [formatter, []];
}
}).filter((formatter) => Array.isArray(formatter));
}
/**
* 生成可以解析指定标签的正则表达式
*
* getNestingParamsRegex() -- 能解析{}[]
* getNestingParamsRegex(["<b>","</b>"]),
*
* @param {...any} tags
* @returns
*/
function getNestingParamsRegex(...tags){
if(tags.length==0){
tags.push(["{","}"])
tags.push(["[","]"])
}
const tagsRegexs = tags.map(([beginTag,endTag])=>{
return `(${escapeRegexpStr(beginTag)}1%.*?%1${escapeRegexpStr(endTag)})`
})
return formatterNestingParamsRegex.replace("__TAG_REGEXP__",tagsRegexs.length > 0 ? tagsRegexs.join("|")+"|" : "")
}
/**
*
* 遍历字符串中的 beginTag和endTag,添加辅助序号
*
* @param {*} str
* @param {*} beginTag
* @param {*} endTag
* @returns
*/
function addTagFlags(str,beginTag="{",endTag="}"){
let i = 0
let flagIndex = 0
while(i<str.length){
let beginChars = str.slice(i,i+beginTag.length)
let endChars = str.slice(i,i+endTag.length)
if(beginChars==beginTag){
flagIndex++
str = str.substring(0,i+beginTag.length) + `${flagIndex}%` + str.substring(i+beginTag.length)
i+=beginTag.length + String(flagIndex).length+1
continue
}
if(endChars==endTag){
if(flagIndex>0){
str = str.substring(0,i) + `%${flagIndex}` + str.substring(i)
}
i+= endTag.length + String(flagIndex).length +1
flagIndex--
continue
}
i++
}
return str
}
/**
* 增加标签组辅助标识
*
* addTagHelperFlags("sss",["<div>","</div>"]
*
* @param {*} str
* @param {...any} tags 默认已包括{},[]
*/
function addTagHelperFlags(str,...tags){
if(tags.length==0){
tags.push(["{","}"])
tags.push(["[","]"])
}
tags.forEach(tag=>{
if(str.includes(tag[0]) && str.includes(tag[1])){
str = addTagFlags(str,...tag)
}
})
return str
}
function removeTagFlags(str,beginTag,endTag){
const regexs = [
[beginTag,new RegExp(escapeRegexpStr(beginTag)+"\\d+%")],
[endTag,new RegExp("%\\d+"+escapeRegexpStr(endTag))]
]
regexs.forEach(([tag,regex])=>{
let matched
while ((matched = regex.exec(str)) !== null) {
if (matched.index === regex.lastIndex) regex.lastIndex++;
str = str.replace(regex,tag)
}
})
return str
}
function removeTagHelperFlags(str,...tags){
if(tags.length==0){
tags.push(["{","}"])
tags.push(["[","]"])
}
tags.forEach(([beginTag,endTag])=>{
if(str.includes(beginTag) && str.includes(endTag)){
str = removeTagFlags(str,beginTag,endTag)
}
})
return str
}
// 提取匹配("a",1,2,'b',{..},[...]),不足:当{}嵌套时无法有效匹配
// const formatterParamsRegex = /((([\'\"])(.*?)\3)|(\{.*?\})|(\[.*?\])|([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,)))(?<=\s*[,\)]?\s*)/g;
// 支持解析嵌套的{}和[]参数, 前提是字符串需要经addTagHelperFlags操作后会在{}[]等位置添加辅助字符
// 解析嵌套的{}和[]参数基本原理:在{}[]等位置添加辅助字符,然后使用正则表达式匹配,匹配到的字符串中包含辅助字符,然后再去除辅助字符
const formatterNestingParamsRegex = String.raw`((([\'\"])(.*?)\3))|__TAG_REGEXP__([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,))(?<=\s*[,\)]?\s*)`
/**
* 解析格式化器的参数,即解析使用,分割的函数参数
*
* 采用正则表达式解析
* 支持number,boolean,null,String,{},[]的参数可以识别嵌套的{}[]
*
* @param {*} strParams 格式化器参数字符串即formater(<...参数....>)括号里面的参数使用,分割
* @returns {Array} 返回参数值数组 []
*/
function parseFormaterParams(strParams) {
let params = [];
let matched;
// 1. 预处理: 处理{}和[]嵌套问题,增加嵌套标识
strParams = addTagHelperFlags(strParams)
try{
let regex =new RegExp(getNestingParamsRegex(),"g")
while ((matched = regex.exec(strParams)) !== null) {
// 这对于避免零宽度匹配的无限循环是必要的
if (matched.index === regex.lastIndex) {
regex.lastIndex++;
}
let value = matched[0]
if(value.trim()==''){
value = null
}else if((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))){
value = value.substring(1,value.length-1)
value = removeTagHelperFlags(value)
}else if((value.startsWith("{") && value.endsWith("}")) || (value.startsWith('[') && value.endsWith(']'))){
try{
value = removeTagHelperFlags(value)
value = safeParseJson(value)
}catch{}
}else if(["true","false","null"].includes(value)){
value = JSON.parse(value)
}else if(isNumber(value)){
value = parseFloat(value)
}else{
value = removeTagHelperFlags(String(value))
}
params.push(value)
}
}catch{
}
return params
}
/**
* 创建格式化器
*
* 格式化器是一个普通的函数具有以下特点
*
* - 函数第一个参数是上一上格式化器的输出
* - 支持0-N个简单类型的入参
* - 可以是定参也可以变参
* - 格式化器可以在格式化器的$config参数指定一个键值来配置不同语言时的参数
*
* "currency":createFormatter((value,prefix,suffix, division ,precision,$config)=>{
* // 无论在格式化入参数是多少个经过处理后在此得到prefix,suffix, division ,precision参数已经是经过处理后的参数
* 依次读取格式化器的参数合并
* - 创建格式化时的defaultParams参数
* - 从当前激活格式化器的$config中读取配置参数
* - 在t函数后传入参数
* 比如currency格式化器支持4参数其入参顺序是prefix,suffix, division ,precision
* 那么在t函数中可以使用以下五种入参数方式
* {value | currency } //prefix=undefined,suffix=undefined, division=undefined ,precision=undefined
* {value | currency(prefix) }
* {value | currency(prefix,suffix) }
* {value | currency(prefix,suffix,division) }
* {value | currency(prefix,suffix,division,precision)}
*
* 经过createFormatter处理后会从当前激活格式化器的$config中读取prefix,suffix, division ,precision参数作为默认参数
* 然后t函数中的参数会覆盖默认参数优先级更高
* },
* {
* unit:"$",
* prefix,
* suffix,
* division,
* precision
* },
* {
* normalize:value=>{...},
* params:["prefix","suffix", "division" ,"precision"] // 声明参数顺序
* configKey:"currency" // 声明特定语言下的配置在$config.currency
* }
* )
*
* @param {*} fn
* @param {*} options 配置参数
* @param {*} defaultParams 可选默认值
* @returns
*/
function createFormatter(fn,options={},defaultParams={}){
let opts = Object.assign({
normalize : null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等需要对输入值进行处理如强制类型转换等
params : null, // 可选的声明参数顺序如果是变参的则需要传入null
configKey : null // 声明该格式化器在$config中的路径支持简单的使用.的路径语法
},options)
// 最后一个参数是传入activeFormatterConfig参数
// 并且格式化器的this指向的是activeFormatterConfig
const $formatter = function(value,...args){
let finalValue = value
// 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性
if(isFunction(opts.normalize)){
try{
finalValue = opts.normalize(finalValue)
}catch{}
}
// 2. 读取activeFormatterConfig
let activeFormatterConfigs = args.length>0 ? args[args.length-1] : {}
if(!isPlainObject( activeFormatterConfigs)) activeFormatterConfigs ={}
// 3. 从当前语言的激活语言中读取配置参数
const formatterConfig =Object.assign({},defaultParams,getByPath(activeFormatterConfigs,opts.configKey,{}))
let finalArgs
if(opts.params==null){// 如果格式化器支持变参则需要指定params=null
finalArgs = args.slice(0,args.length-1)
}else{ // 具有固定的参数个数
finalArgs = opts.params.map(param=>{
let paramValue = getByPath(formatterConfig,param,undefined)
return isFunction(paramValue) ? paramValue(finalValue) : paramValue
})
// 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数
for(let i =0; i<finalArgs.length;i++){
if(i==args.length-1) break // 最后一参数是配置
if(args[i]!==undefined) finalArgs[i] = args[i]
}
}
return fn.call(this,finalValue,...finalArgs,formatterConfig)
}
$formatter.configurable = true
return $formatter
}
const Formatter = createFormatter
/**
* 创建支持弹性参数的格式化器
* 弹性参数指的是该格式式化器支持两种传参数形式:
* - 位置参数 { value | format(a,b,c,d,e) }
* - 对象参数 { value | format({a,b,c,e}) }
*
* 弹性参数的目的是只需要位置参数中的第N参数时简化传参
*
* 上例中我们只需要传入e参数则不得不使用{ value | format(,,,,e) }
* 弹性参数允许使用 { value | format({e:<value>}) }
*
* 请参阅currencyFormatter用法
*
* @param {*} fn
* @param {*} options
* @param {*} defaultParams
*/
const createFlexFormatter = function(fn,options={},defaultParams={}){
const $flexFormatter = Formatter(function(value,...args){
// 1. 最后一个参数是格式化器的参数,不同语言不一样
let $config = args[args.length-1]
// 2. 从语言配置中读取默认参数
let finalParams = options.params.reduce((r,name) =>{
r[name] = $config[name]==undefined ? defaultParams[name] : $config[name]
return r
} ,{})
// 3. 从格式化器中传入的参数具有最高优先级,覆盖默认参数
if(args.length==2 && isPlainObject(args[0])){ // 一个参数且是{}
Object.assign(finalParams,{format:"custom"},args[0])
}else{ // 位置参数,如果是空则代表
for(let i=0; i<args.length-1; i++){
if(args[i]!==undefined) finalParams[options.params[i]] = args[i]
}
}
return fn.call(this,value,finalParams,$config)
},{...options,params:null}) // 变参工式化器需要指定params=null
return $flexFormatter
}
const FlexFormatter = createFlexFormatter
module.exports = {
createFormatter,
createFlexFormatter,
Formatter,
FlexFormatter,
parseFormatters
}

View File

@ -1,162 +0,0 @@
declare global {
export type VoerkaI18nILanguageMessages = Record<string, string>
export interface VoerkaI18nLanguagePack {
[key: string]: VoerkaI18nILanguageMessages
}
export interface VoerkaI18nManagerSettings {
debug?: boolean
defaultLanguage: string
activeLanguage: string
formatters: VoerkI18nFormatters
languages: VoerkaI18nLanguage[]
}
export class VoerkaI18nManager {
constructor(settings: VoerkaI18nManagerSettings)
get settings(): Required<VoerkaI18nManagerSettings> // 配置参数
get scopes(): VoerkaI18nScope[] // 注册的报有i18nScope实例q
get activeLanguage(): string // 当前激活语言名称
get defaultLanguage(): string // 默认语言名称
get languages(): VoerkaI18nSupportedLanguages // 支持的语言列表
get formatters(): VoerkI18nFormatters // 内置格式化器{*:{$config,$types,...},zh:{$config,$types,...},en:{$config,$types,...}}
get defaultMessageLoader(): VoerkI18nLoader // 默认语言包加载器
// 通过默认加载器加载文件
loadMessagesFromDefaultLoader(newLanguage: string, scope: VoerkaI18nScope): Promise<VoerkaI18nILanguageMessages>
change(language: string): Promise<void>
register(scope: VoerkaI18nScope): Promise<void>
registerFormatter(name: string, formatter: VoerkI18nFormatter, options?: { language: string | string[] }): void
registerDefaultLoader(fn: VoerkI18nLoader): void
refresh(): Promise<void>
}
export type Voerkai18nIdMap = Record<string, number>
export interface VoerkaI18nLanguage {
name: string
title?: string
default?: boolean
fallback?: string
}
export type VoerkI18nFormatter = (value: string, ...args: any[]) => string
export type VoerkI18nFormatterConfigs = Record<string, any>
export type VoerkI18nFormatters = Record<string, ({
$types?: Record<string, VoerkI18nFormatter>
$config?: Record<string, string>
} & {
[key: string]: VoerkI18nFormatter
}) | (() => Promise<any>)>
export type VoerkI18nLoader = () => Awaited<Promise<any>>
export interface VoerkI18nLoaders {
[key: string]: VoerkI18nLoader
}
export interface VoerkaVoerkaI18nScopeOptions {
id?: string
debug?: boolean
languages: VoerkaI18nLanguage[]
defaultLanguage: string // 默认语言名称
activeLanguage: string // 当前语言名称
default: VoerkaI18nILanguageMessages // 默认语言包
messages: VoerkaI18nILanguageMessages // 当前语言包
idMap: Voerkai18nIdMap // 消息id映射列表
formatters: VoerkI18nFormatters // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
loaders: VoerkI18nLoaders; // 异步加载语言文件的函数列表
}
export var VoerkaI18n: VoerkaI18nManager
}
export type TranslateMessageVars = number | boolean | string | Function | Date
export interface VoerkaI18nTranslate {
(message: string, ...args: TranslateMessageVars[]): string
(message: string, vars?: Record<string, TranslateMessageVars>): string
}
export interface VoerkaI18nSupportedLanguages {
[key: string]: VoerkaI18nLanguage
}
export class VoerkaI18nScope {
constructor(options: VoerkaVoerkaI18nScopeOptions, callback?: Function)
get id(): string // 作用域唯一id
get debug(): boolean // 调试开关
get defaultLanguage(): string // 默认语言名称
get activeLanguage(): string // 默认语言名称
get default(): VoerkaI18nILanguageMessages // 默认语言包
get messages(): VoerkaI18nILanguageMessages // 当前语言包
get idMap(): Voerkai18nIdMap // 消息id映射列表
get languages(): VoerkaI18nSupportedLanguages // 当前作用域支持的语言列表[{name,title,fallback}]
get loaders(): VoerkI18nLoaders // 异步加载语言文件的函数列表
get global(): VoerkaI18nManager // 引用全局VoerkaI18n配置注册后自动引用
get formatters(): VoerkI18nFormatters // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
get activeFormatters(): VoerkI18nFormatters // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
get activeFormatterConfig(): VoerkI18nFormatterConfigs // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
get t(): VoerkaI18nTranslate
get translate(): VoerkaI18nTranslate
/**
*
* @param {*} callback
*/
register(callback: Function): void
/**
*
*/
registerFormatter(name: string, formatter: VoerkI18nFormatter, { language, global }: { language: string | string[], global: boolean }): void
/**
*
*/
registerFormatters(formatters: VoerkI18nFormatters, asGlobal?: boolean): void
/**
*
*/
registerDefaultLoader(fn: Function): void
/**
*
* @param {*} language
* @returns
*/
getLanguage(language: string): VoerkaI18nILanguageMessages
hasLanguage(language: string): boolean
refresh(newLanguage: string): Promise<void>
on(): void
off(): void
offAll(): void
change(language: string): Promise<void>
}
export type VoerkI18nFormatterConfigs = Record<string, any>
// 翻译函数
export var translate: {
(message: string, ...args: (string | Function)[]): string
(message: string, vars?: Record<string, any>): string
}
export interface CreateFormatterOptions {
normalize?: (value: any) => any // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等需要对输入值进行处理如强制类型转换等
params?: string[] | null // 可选的声明参数顺序如果是变参的则需要传入null
configKey?: string // 声明该格式化器在$config中的路径支持简单的使用.的路径语法
}
export type Primitive = string | number | boolean | null | undefined
export interface FormatterDefine {
(this: VoerkI18nFormatterConfigs, value: any, ...args: Primitive[]): string
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, $config: VoerkI18nFormatterConfigs): string
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, $config: VoerkI18nFormatterConfigs): string
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, arg3: Primitive, $config: VoerkI18nFormatterConfigs): string
configurable?: boolean
}
// 创建格式化器
export type CreateFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
export var createFormatter: CreateFormatterType
export var Formatter: CreateFormatterType
export type CreateFlexFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
export var createFlexFormatter: CreateFlexFormatterType
export var FlexFormatter: CreateFlexFormatterType
export var getDataTypeName: (value: any) => string
export var toDate: (value: any) => Date | number
export var toNumber: (value: any, defaultValue: number) => number
export var toBoolean: (value: any) => boolean

View File

@ -21,9 +21,11 @@
"author": "wxzhang",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.7",
"@babel/runtime-corejs3": "latest",
"core-js": "^3.27.1",
"@babel/runtime": "^7.20.7"
"flex-tools": "^1.0.72",
"string.prototype.replaceall": "^1.0.7"
},
"devDependencies": {
"@babel/cli": "^7.17.6",
@ -38,7 +40,8 @@
"jest": "^27.5.1",
"rollup": "^2.69.0",
"rollup-plugin-clear": "^2.0.7",
"rollup-plugin-terser": "^7.0.2"
"rollup-plugin-terser": "^7.0.2",
"vitest": "^0.29.8"
},
"lastPublish": "2023-03-30T08:53:24+08:00"
}

View File

@ -1,38 +0,0 @@
import clear from 'rollup-plugin-clear'
import commonjs from '@rollup/plugin-commonjs';
// import resolve from "@rollup/plugin-node-resolve";
import { terser } from "rollup-plugin-terser";
import { babel } from '@rollup/plugin-babel';
export default [
{
input: './index.js',
output: [
{
file: 'dist/index.esm.js',
format:"esm",
exports:"default",
esModule:"if-default-prop",
sourcemap:true
},
{
file: 'dist/index.cjs',
exports:"default",
format:"cjs",
sourcemap:true
}
],
plugins: [
//resolve(),
commonjs(),
babel({
babelHelpers:"runtime",
exclude: 'node_modules/**'
}),
clear({targets:["dist"]}),
// terser()
],
external:["@babel/runtime"]
}
]

View File

@ -0,0 +1,25 @@
/**
*
*
*
*/
import { FlexFormatter, Formatter } from '../formatter'
import { toChineseNumber } from "flex-tools/chinese/toChineseNumber"
import { toChineseCurrency } from "flex-tools/chinese/toChineseCurrency"
export const chineseNumberFormatter = Formatter((value: number, isBig: boolean, $config: any) => {
return toChineseNumber(value, isBig)
}, {
params: ["isBig"]
})
export const rmbFormater = FlexFormatter((value:number | string, params : any, $config:any) => {
return toChineseCurrency(value, params, $config)
}, {
params: ["big", "prefix", "unit", "suffix"],
configKey: "rmb"
})

View File

@ -4,8 +4,11 @@
*
*/
const { isNumber, toNumber, getByPath } = require('../utils')
const { FlexFormatter } = require("../formatter")
import { toNumber } from '../utils'
import { get as getByPath } from "flex-tools/object/get"
import { isNumber } from "flex-tools/typecheck/isNumber"
import { FlexFormatter } from "../formatter"
/**
* bits位添加一个,
@ -13,7 +16,7 @@ const { FlexFormatter } = require("../formatter")
* @param {*} bits
* @returns
*/
function addSplitChars(str,bits=3){
function addSplitChars(str:string,bits:number=3){
let regexp = new RegExp(String.raw`(?!^)(?=(\d{${bits}})+$)`,"g")
let r = str.replace(regexp,",")
if(r.startsWith(",")) r = r.substring(1)
@ -33,7 +36,7 @@ function addSplitChars(str,bits=3){
* @param {*} format
* @returns
*/
function toCurrency(value,params={},$config={}){
export function toCurrency(value:string|number ,params:Record<string,any>={},$config={}){
let {symbol="",division=3,prefix="",precision=2,suffix="",unit=0,radix=3,format="{symbol}{value}{unit}"} = params
// 1. 分离出整数和小数部分
@ -47,7 +50,7 @@ function addSplitChars(str,bits=3){
// 不足位数时补零
if(wholeDigits.length<radix*unit) wholeDigits = new Array(radix*unit-wholeDigits.length+1).fill(0).join("")+ wholeDigits
// 将整数的最后radix*unit字符移到小数部分前面
decimalDigits=wholeDigits.substring(wholeDigits,wholeDigits.length-radix*unit)+decimalDigits
decimalDigits=wholeDigits.substring(Number(wholeDigits),wholeDigits.length-radix*unit)+decimalDigits
wholeDigits = wholeDigits.substring(0,wholeDigits.length-radix*unit)
if(wholeDigits=="") wholeDigits = "0"
}
@ -67,14 +70,18 @@ function addSplitChars(str,bits=3){
result.push(`.${decimalDigits}`)
}
// 5. 模板替换
const unitName = getByPath($config,`units`,[])[unit] || ""
const unitName = getByPath($config,`units`,{defaultValue:[]})[unit] || ""
return format.replace("{value}",result.join(""))
.replace("{symbol}",symbol)
.replace("{prefix}",prefix)
.replace("{suffix}",suffix)
.replace("{unit}",unitName)
}
const currencyFormatter = FlexFormatter((value,params={},$config)=>{
/**
*
*/
export const currencyFormatter = FlexFormatter((value:string | number,params:Record<string,any>={},$config:any)=>{
// format可以取预设值的名称如long,short等
if(params.format in $config){
params.format = $config[params.format]
@ -99,8 +106,4 @@ const currencyFormatter = FlexFormatter((value,params={},$config)=>{
unit:0 // 小数点精度控制,0代表不控制
})
module.exports = {
toCurrency,
currencyFormatter
}

View File

@ -0,0 +1,133 @@
/**
*/
const { isFunction,replaceAll,toDate } = require('../utils')
const { Formatter } = require('../formatter');
import { formatDateTime } from "flex-tools/misc/formatDateTime"
import { relativeTime } from "flex-tools/misc/relativeTime"
import { assignObject } from "flex-tools/object/assignObject"
function formatTime(value:number ,template="HH:mm:ss"){
return formatDateTime(value,template,{
})
}
/**
*
*
* 1. format参数
* 2. format参数取值可以是若干预设的值long,short等
* 3. format值时$config[configKey]$config[configKey][format]
* 4. !(format in $config[configKey])format值是一个模板字符串
* 5. format in presets, presets[format ](value)=>{....}
*
**/
export type FormatterTransformer = (value:any,format:string)=>string
export function createDateTimeFormatter(options={},transformer:FormatterTransformer){
let opts = assignObject({presets:{}},options)
return Formatter(function(value,format,$config){
if((format in opts.presets) && isFunction(opts.presets[format])){
return opts.presets[format](value)
}else if((format in $config)){
format = $config[format]
}else if(format == "number"){
return value
}
try{// this指向的是activeFormatter.$config
return format==null ? value : transformer.call(this,value,format)
}catch(e){
return value
}
},opts)
}
/**
*
* - format取值local,long,short,iso,gmt,utc,<模板字符串>
* - $config.datetime.date.format指定
*/
const dateFormatter = createDateTimeFormatter({
normalize: toDate,
params : ["format"],
configKey: "datetime.date",
presets : {
local: value=>value.toLocaleString(),
iso : value=>value.toISOString(),
utc : value=>value.toUTCString(),
gmt : value=>value.toGMTString()
}
},formatDatetime)
/**
*
* - format: long,short,number
* - short
*/
const quarterFormatter = createDateTimeFormatter({
normalize : value=>{
const month = value.getMonth() + 1
return Math.floor( ( month % 3 == 0 ? ( month / 3 ) : (month / 3 + 1 ) ))
},
params : ["format"],
configKey: "datetime.quarter"
},(quarter,format)=>format[quarter-1])
/**
*
* - format: long,short,number
* - short
*/
const monthFormatter = createDateTimeFormatter({
normalize: (value:Date)=> value.getMonth() + 1,
params : ["format"],
configKey: "datetime.month"
},(month,format)=>format[month-1])
/**
*
* - format: long,short,number
* - long
*/
const weekdayFormatter = createDateTimeFormatter({
normalize: (value:Date)=> value.getDay(),
params : ["format"],
configKey: "datetime.weekday"
},(day,format)=>format[day])
/**
*
* - format取值local,long,short,timestamp,<模板字符串>
* - $config.datetime.time.format指定
*/
const timeFormatter = createDateTimeFormatter({
normalize : toDate,
params : ["format"],
configKey : "datetime.time",
presets : {
local : (value:Date)=>value.toLocaleTimeString(),
timestamp: (value:Date)=>value.getTime()
}
},formatTime)
/**
* rel的时间
* @param {*} value
* @param {*} baseTime
*/
const relativeTimeFormatter = Formatter((value:Date,baseTime:Date,$config:any)=>{
//const { units,now,before,base = Date.now() , after } = $config
return relativeTime(value, $config)
},{
normalize:toDate,
params:["base"],
configKey:"datetime.relativeTime"
})

View File

@ -20,8 +20,4 @@ const numberFormartter = Formatter(function(value,precision,division,$config){
configKey: "number"
})
module.exports = {
numberFormartter
}

View File

@ -0,0 +1,32 @@
/**
*
*
*
*/
export class EventEmitter{
#callbacks:Function[]
constructor(){
this.#callbacks = []
}
on(callback:Function){
if(this.#callbacks.includes(callback)) return
this.#callbacks.push(callback)
}
off(callback:Function){
for(let i=0;i<this.#callbacks.length;i++){
if(this.#callbacks[i]===callback ){
this.#callbacks.splice(i,1)
}
}
}
offAll(){
this.#callbacks = []
}
async emit(...args:any[]){
if(Promise.allSettled){
await Promise.allSettled(this.#callbacks.map(cb=>cb(...args)))
}else{
await Promise.all(this.#callbacks.map(cb=>cb(...args)))
}
}
}

View File

@ -0,0 +1,366 @@
/**
*
*
*
* { varname | formater(...params) }params部分
*
*
*
*/
import { escapeRegexpStr } from "./utils"
import { get as getByPath } from "flex-tools/object/get"
import { isNumber } from "flex-tools/typecheck/isNumber"
import { isFunction } from "flex-tools/typecheck/isFunction"
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
import { safeParseJson } from "flex-tools/object/safeParseJson"
import { assignObject } from "flex-tools/object/assignObject"
/**
使便
formatters="| aaa(1,1) | bbb "
[
[aaa,[1,1]], // [<格式化器名称>,[args,...]]
[<格式化器名称>,[<参数>,<参数>,...]]
]
formatters="| aaa(1,1,"dddd") | bbb "
- split(",")
aaa(1,1,"dd,,dd")
使
- {},[]
@param {String} formatters
@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
*/
//@returns [ [<格式化器名称>,[<参数>,<参数>,...],[<格式化器名称>,[<参数>,<参数>,...]],...]
export type FormatterDefines = ([string, string[]])[]
export function parseFormatters(formatters: string): FormatterDefines {
if (!formatters) return [];
// 1. 先解析为 ["aaa()","bbb"]形式
let result = formatters.trim().substring(1).trim().split("|").map((r) => r.trim());
// 2. 解析格式化器参数
return result.map((formatter: string) => {
if (formatter == "") return null;
let firstIndex = formatter.indexOf("(");
let lastIndex = formatter.lastIndexOf(")");
if (firstIndex !== -1 && lastIndex !== -1) { //参数的格式化器
// 带参数的格式化器: 取括号中的参数字符串部分
const strParams = formatter.substring(firstIndex + 1, lastIndex).trim();
// 解析出格式化的参数数组
let params = parseFormaterParams(strParams);
// 返回[<格式化器名称>,[<参数>,<参数>,...]
return [formatter.substring(0, firstIndex), params];
} else { // 不带参数的格式化器
return [formatter, []];
}
}).filter((formatter) => Array.isArray(formatter)) as FormatterDefines
}
/**
*
*
* getNestingParamsRegex() -- {}[]
* getNestingParamsRegex(["<b>","</b>"]),
*
* @param {...any} tags
* @returns
*/
function getNestingParamsRegex(...tags: ([string, string])[]) {
if (tags.length == 0) {
tags.push(["{", "}"])
tags.push(["[", "]"])
}
const tagsRegexs = tags.map(([beginTag, endTag]) => {
return `(${escapeRegexpStr(beginTag)}1%.*?%1${escapeRegexpStr(endTag)})`
})
return formatterNestingParamsRegex.replace("__TAG_REGEXP__", tagsRegexs.length > 0 ? tagsRegexs.join("|") + "|" : "")
}
/**
*
* beginTag和endTag,
*
* @param {*} str
* @param {*} beginTag
* @param {*} endTag
* @returns
*/
function addTagFlags(str: string, beginTag = "{", endTag = "}") {
let i = 0
let flagIndex = 0
while (i < str.length) {
let beginChars = str.slice(i, i + beginTag.length)
let endChars = str.slice(i, i + endTag.length)
if (beginChars == beginTag) {
flagIndex++
str = str.substring(0, i + beginTag.length) + `${flagIndex}%` + str.substring(i + beginTag.length)
i += beginTag.length + String(flagIndex).length + 1
continue
}
if (endChars == endTag) {
if (flagIndex > 0) {
str = str.substring(0, i) + `%${flagIndex}` + str.substring(i)
}
i += endTag.length + String(flagIndex).length + 1
flagIndex--
continue
}
i++
}
return str
}
// 指<div></div>成对标签
export type TagPair = [string, string]
/**
*
*
* addTagHelperFlags("sss",["<div>","</div>"]
*
* @param {*} str
* @param {...any} tags {},[]
*/
function addTagHelperFlags(str: string, ...tags: TagPair[]) {
if (tags.length == 0) {
tags.push(["{", "}"])
tags.push(["[", "]"])
}
tags.forEach(tag => {
if (str.includes(tag[0]) && str.includes(tag[1])) {
str = addTagFlags(str, ...tag)
}
})
return str
}
function removeTagFlags(str: string, beginTag: string, endTag: string) {
const regexs:([string,RegExp])[] = [
[beginTag, new RegExp(escapeRegexpStr(beginTag) + "\\d+%")],
[endTag, new RegExp("%\\d+" + escapeRegexpStr(endTag))]
]
regexs.forEach(([tag, regex]) => {
let matched
while ((matched = regex.exec(str)) !== null) {
if (matched.index === regex.lastIndex) regex.lastIndex++;
str = str.replace(regex, tag)
}
})
return str
}
function removeTagHelperFlags(str: string, ...tags: TagPair[]) {
if (tags.length == 0) {
tags.push(["{", "}"])
tags.push(["[", "]"])
}
tags.forEach(([beginTag, endTag]) => {
if (str.includes(beginTag) && str.includes(endTag)) {
str = removeTagFlags(str, beginTag, endTag)
}
})
return str
}
// 提取匹配("a",1,2,'b',{..},[...]),不足:当{}嵌套时无法有效匹配
// const formatterParamsRegex = /((([\'\"])(.*?)\3)|(\{.*?\})|(\[.*?\])|([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,)))(?<=\s*[,\)]?\s*)/g;
// 支持解析嵌套的{}和[]参数, 前提是字符串需要经addTagHelperFlags操作后会在{}[]等位置添加辅助字符
// 解析嵌套的{}和[]参数基本原理:在{}[]等位置添加辅助字符,然后使用正则表达式匹配,匹配到的字符串中包含辅助字符,然后再去除辅助字符
const formatterNestingParamsRegex = String.raw`((([\'\"])(.*?)\3))|__TAG_REGEXP__([\d]+\.?[\d]?)|((true|false|null)(?=[,\b\s]))|([\w\.]+)|((?<=,)\s*(?=,))(?<=\s*[,\)]?\s*)`
/**
* ,使,
*
*
* number,boolean,null,String,{},[]{}[]
*
* @param {*} strParams formater(<...参数....>)使,
* @returns {Array} []
*/
function parseFormaterParams(strParams: string): any[] {
let params = [];
let matched;
// 1. 预处理: 处理{}和[]嵌套问题,增加嵌套标识
strParams = addTagHelperFlags(strParams)
try {
let regex = new RegExp(getNestingParamsRegex(), "g")
while ((matched = regex.exec(strParams)) !== null) {
// 这对于避免零宽度匹配的无限循环是必要的
if (matched.index === regex.lastIndex) {
regex.lastIndex++;
}
let value: any = matched[0]
if (value.trim() == '') {
value = null
} else if ((value.startsWith("'") && value.endsWith("'")) || (value.startsWith('"') && value.endsWith('"'))) {
value = value.substring(1, value.length - 1)
value = removeTagHelperFlags(value)
} else if ((value.startsWith("{") && value.endsWith("}")) || (value.startsWith('[') && value.endsWith(']'))) {
try {
value = removeTagHelperFlags(value)
value = safeParseJson(value)
} catch { }
} else if (["true", "false", "null"].includes(value)) {
value = JSON.parse(value)
} else if (isNumber(value)) {
value = parseFloat(value)
} else {
value = removeTagHelperFlags(String(value))
}
params.push(value)
}
} catch { }
return params
}
/**
*
*
*
*
* -
* - 0-N个简单类型的入参
* -
* - $config参数指定一个键值来配置不同语言时的参数
*
* "currency":createFormatter((value,prefix,suffix, division ,precision,$config)=>{
* // 无论在格式化入参数是多少个经过处理后在此得到prefix,suffix, division ,precision参数已经是经过处理后的参数
*
* - defaultParams参数
* - $config中读取配置参数
* - t函数后传入参数
* currency格式化器支持4参数prefix,suffix, division ,precision
* t函数中可以使用以下五种入参数方式
* {value | currency } //prefix=undefined,suffix=undefined, division=undefined ,precision=undefined
* {value | currency(prefix) }
* {value | currency(prefix,suffix) }
* {value | currency(prefix,suffix,division) }
* {value | currency(prefix,suffix,division,precision)}
*
* createFormatter处理后$config中读取prefix,suffix, division ,precision参数作为默认参数
* t函数中的参数会覆盖默认参数
* },
* {
* unit:"$",
* prefix,
* suffix,
* division,
* precision
* },
* {
* normalize:value=>{...},
* params:["prefix","suffix", "division" ,"precision"] // 声明参数顺序
* configKey:"currency" // 声明特定语言下的配置在$config.currency
* }
* )
*
* @param {*} fn
* @param {*} options
* @param {*} defaultParams
* @returns
*/
export type Formatter = (this: any, value: string, ...args: any[]) => string
export interface FormatterOptions {
normalize?: (value: string) => string // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等需要对输入值进行处理如强制类型转换等
params?: Record<string, any>, // 可选的声明参数顺序如果是变参的则需要传入null
configKey?: string // 声明该格式化器在$config中的路径支持简单的使用.的路径语法
}
export function createFormatter(fn: Formatter, options?: FormatterOptions, defaultParams?: Record<string, any>) {
let opts = assignObject({
normalize: null, // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等需要对输入值进行处理如强制类型转换等
params: null, // 可选的声明参数顺序如果是变参的则需要传入null
configKey: null // 声明该格式化器在$config中的路径支持简单的使用.的路径语法
}, options)
// 最后一个参数是传入activeFormatterConfig参数
// 并且格式化器的this指向的是activeFormatterConfig
const $formatter = function (this: any, value: string, ...args: any[]) {
let finalValue = value
// 1. 输入值规范处理,主要是进行类型转换,确保输入的数据类型及相关格式的正确性,提高数据容错性
if (isFunction(opts.normalize)) {
try {
finalValue = opts.normalize(finalValue)
} catch { }
}
// 2. 读取activeFormatterConfig
let activeFormatterConfigs = args.length > 0 ? args[args.length - 1] : {}
if (!isPlainObject(activeFormatterConfigs)) activeFormatterConfigs = {}
// 3. 从当前语言的激活语言中读取配置参数
const formatterConfig = assignObject({}, defaultParams, getByPath(activeFormatterConfigs, opts.configKey, { defaultValue: {} }))
let finalArgs
if (opts.params == null) {// 如果格式化器支持变参则需要指定params=null
finalArgs = args.slice(0, args.length - 1)
} else { // 具有固定的参数个数
finalArgs = opts.params.map((param: string) => {
let paramValue = getByPath(formatterConfig, param, undefined)
return isFunction(paramValue) ? paramValue(finalValue) : paramValue
})
// 4. 将翻译函数执行格式化器时传入的参数覆盖默认参数
for (let i = 0; i < finalArgs.length; i++) {
if (i == args.length - 1) break // 最后一参数是配置
if (args[i] !== undefined) finalArgs[i] = args[i]
}
}
return fn.call(this, finalValue, ...finalArgs, formatterConfig)
}
$formatter.configurable = true
return $formatter
}
/**
*
* :
* - { value | format(a,b,c,d,e) }
* - { value | format({a,b,c,e}) }
*
* N参数时
*
* e参数使{ value | format(,,,,e) }
* 使 { value | format({e:<value>}) }
*
* currencyFormatter用法
*
* @param {*} fn
* @param {*} options
* @param {*} defaultParams
*/
export const createFlexFormatter = function (fn: Formatter, options: FormatterOptions, defaultParams?: Record<string, any>) {
const opts = assignObject({
params: {}
}, options)
const $flexFormatter = Formatter(function (this: any, value: string, ...args: string[]) {
// 1. 最后一个参数总是代表格式化器的参数,不同语言不一样
let $config = args[args.length - 1] as unknown as Record<string, any>
// 2. 从语言配置中读取默认参数
let finalParams = (options.params || {}).reduce((r: Record<string, any>, name: string) => {
r[name] = $config[name] == undefined ? (defaultParams || {})[name] : $config[name]
return r
}, {})
// 3. 从格式化器中传入的参数具有最高优先级,覆盖默认参数
if (args.length == 2 && isPlainObject(args[0])) { // 一个参数且是{}
Object.assign(finalParams, { format: "custom" }, args[0])
} else { // 位置参数,如果是空则代表
for (let i = 0; i < args.length - 1; i++) {
if (args[i] !== undefined) finalParams[opts.params[i]] = args[i]
}
}
return fn.call(this, value, finalParams, $config)
}, { ...options, params: null }) // 变参工式化器需要指定params=null
return $flexFormatter
}
export const FlexFormatter = createFlexFormatter

View File

@ -2,12 +2,12 @@
*
*
*/
const { isFunction,isPlainObject} = require("../utils")
const { Formatter } = require("../formatter")
const { dateFormatter,quarterFormatter,monthFormatter,weekdayFormatter,timeFormatter,relativeTimeFormatter } = require("../datatypes/datetime")
const { numberFormartter } = require("../datatypes/numeric")
const { currencyFormatter } = require("../datatypes/currency")
import { Formatter } fom "../formatter"
import { dateFormatter,quarterFormatter,monthFormatter,weekdayFormatter,timeFormatter,relativeTimeFormatter } from "../datatypes/datetime"
import { numberFormartter } from "../datatypes/numeric"
import { currencyFormatter } from "../datatypes/currency"
@ -70,7 +70,7 @@ const { currencyFormatter } = require("../datatypes/currency")
// })
module.exports = {
export default {
// 配置参数
$config:{
datetime : {

View File

@ -3,14 +3,14 @@
*
*/
const enFormatters = require("./en")
const zhFormatters = require("./zh")
const defaultFormatters = require("./default")
module.exports = {
import enFormatters from "./en"
import zhFormatters from "./zh"
import defaultFormatters from "./default"
export default {
"*":{
...enFormatters,
...defaultFormatters
},
zh:zhFormatters
}
}

View File

@ -0,0 +1,8 @@
import replaceAll from "string.prototype.replaceall"
replaceAll.shim()
export * from "./types"
export * from "./manager"
export * from "./scope"
export * from "./formatter"
export * from "./utils"

View File

@ -28,8 +28,12 @@
*
*/
const { getDataTypeName,isPlainObject,isFunction,replaceAll } = require("./utils");
const { parseFormatters } = require("./formatter")
import { getDataTypeName } from "./utils"
import { isNumber } from "flex-tools/typecheck/isNumber"
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
import { isFunction } from "flex-tools/typecheck/isFunction"
import { parseFormatters } from "./formatter"
import { VoerkaI18nScope } from "./scope"
// 用来提取字符里面的插值变量参数 , 支持管道符 { var | formatter | formatter }
// 支持参数: { var | formatter(x,x,..) | formatter }
@ -100,7 +104,7 @@ function getInterpolatedVars(str) {
* @param {Boolean} replaceAll 使true使false
* @returns
*/
function forEachInterpolatedVars(str, replacer, options = {}) {
function forEachInterpolatedVars(str:string, replacer, options = {}) {
let result = str, matched;
let opts = Object.assign({replaceAll: true },options);
varWithPipeRegexp.lastIndex = 0;
@ -134,7 +138,7 @@ function forEachInterpolatedVars(str, replacer, options = {}) {
* @param {*} scope
* @param {*} activeLanguage
*/
function resetScopeCache(scope, activeLanguage = null) {
function resetScopeCache(scope:VoerkaI18nScope, activeLanguage:string | null) {
scope.$cache = { activeLanguage, typedFormatters: {}, formatters: {} };
}
@ -201,7 +205,7 @@ function getDataTypeDefaultFormatter(scope, activeLanguage, dataType) {
* @param {*} name
* @returns {Function}
*/
function getFormatter(scope, activeLanguage, name) {
function getFormatter(scope:VoerkaI18nScope, activeLanguage:string, name:string) {
// 1. 从缓存中直接读取: 缓存格式化器引用,避免重复检索
if (!scope.$cache) resetScopeCache(scope);
if (scope.$cache.activeLanguage === activeLanguage) {
@ -422,7 +426,7 @@ function getFormattedValue(scope, activeLanguage, formatters, value, template) {
* @param {*} template
* @returns
*/
function replaceInterpolatedVars(template, ...args) {
export function replaceInterpolatedVars(this:VoerkaI18nScope,template:string, ...args:any[]) {
const scope = this;
// 当前激活语言
const activeLanguage = scope.global.activeLanguage;

View File

@ -1,11 +1,10 @@
const {DataTypes,getDataTypeName,isFunction,deepMerge,toBoolean} = require("./utils")
const {createFormatter,Formatter,FlexFormatter,createFlexFormatter} = require("./formatter")
const { toDate } = require("./datatypes/datetime")
const { toNumber } = require("./datatypes/numeric")
const EventEmitter = require("./eventemitter")
const inlineFormatters = require("./formatters")
const VoerkaI18nScope = require("./scope")
const { translate } = require("./translate")
import { isFunction } from "flex-tools/typecheck/isFunction"
import { deepMerge } from "flex-tools/object/deepMerge"
import {DataTypes} from "./utils"
import { EventEmitter } from "./eventemitter"
import inlineFormatters from "./formatters"
import { VoerkaI18nLanguage, VoerkaI18nScope, VoerkaI18nFormatters, VoerkI18nMessageLoader, VoerkaI18nFormatter } from "./scope"
// 默认语言配置
const defaultLanguageSettings = {
@ -17,8 +16,15 @@ const defaultLanguageSettings = {
{name:"zh",title:"中文",default:true},
{name:"en",title:"英文"}
]
}
} as VoerkaI18nManagerOptions
export interface VoerkaI18nManagerOptions {
debug?: boolean
defaultLanguage: string
activeLanguage: string
formatters: VoerkaI18nFormatters
languages: VoerkaI18nLanguage[]
}
/**
*
*
@ -34,37 +40,43 @@ const defaultLanguageSettings = {
* VoerkaI18n.off("change",(language)=>{})
*
* */
class VoerkaI18nManager extends EventEmitter{
constructor(settings={}){
export class VoerkaI18nManager extends EventEmitter{
static instance?:VoerkaI18nManager
#options?:Required<VoerkaI18nManagerOptions>
#scopes:VoerkaI18nScope[] = []
#defaultMessageLoader?:VoerkI18nMessageLoader
constructor(options?:VoerkaI18nManagerOptions){
super()
if(VoerkaI18nManager.instance!=null){
if(VoerkaI18nManager.instance){
return VoerkaI18nManager.instance;
}
VoerkaI18nManager.instance = this;
this._settings = deepMerge(defaultLanguageSettings,settings)
this._scopes=[] // 保存VoerkaI18nScope实例
this._defaultMessageLoader = null // 默认语言包加载器
this.#options = deepMerge(defaultLanguageSettings,options) as Required<VoerkaI18nManagerOptions>
this.#scopes=[] // 保存VoerkaI18nScope实例
}
get settings(){ return this._settings } // 配置参数
get scopes(){ return this._scopes } // 注册的报有VoerkaI18nScope实例q
get activeLanguage(){ return this._settings.activeLanguage} // 当前激活语言 名称
get defaultLanguage(){ return this._settings.defaultLanguage} // 默认语言名称
get languages(){ return this._settings.languages} // 支持的语言列表
get formatters(){ return this._settings.formatters } // 内置格式化器{*:{$config,$types,...},zh:{$config,$types,...},en:{$config,$types,...}}
get defaultMessageLoader(){ return this._defaultMessageLoader} // 默认语言包加载器
get debug(){return this.#options!.debug}
get options(){ return this.#options! } // 配置参数
get scopes(){ return this.#scopes } // 注册的报有VoerkaI18nScope实例q
get activeLanguage(){ return this.#options!.activeLanguage} // 当前激活语言 名称
get defaultLanguage(){ return this.#options!.defaultLanguage} // 默认语言名称
get languages(){ return this.#options!.languages} // 支持的语言列表
get formatters(){ return this.#options!.formatters } // 内置格式化器{*:{$config,$types,...},zh:{$config,$types,...},en:{$config,$types,...}}
get defaultMessageLoader(){ return this.#defaultMessageLoader} // 默认语言包加载器
// 通过默认加载器加载文件
async loadMessagesFromDefaultLoader(newLanguage,scope){
if(!isFunction(this._defaultMessageLoader)) return //throw new Error("No default message loader specified")
return await this._defaultMessageLoader.call(scope,newLanguage,scope)
async loadMessagesFromDefaultLoader(newLanguage:string,scope:VoerkaI18nScope){
if(this.#defaultMessageLoader && isFunction(this.#defaultMessageLoader)){
return await this.#defaultMessageLoader.call(scope,newLanguage,scope)
}
}
/**
*
*/
async change(language){
if(this.languages.findIndex(lang=>lang.name === language)!==-1 || isFunction(this._defaultMessageLoader)){
async change(language:string){
if(this.languages.findIndex(lang=>lang.name === language)!==-1 || isFunction(this.#defaultMessageLoader)){
await this._refreshScopes(language) // 通知所有作用域刷新到对应的语言包
this._settings.activeLanguage = language
this.#options!.activeLanguage = language
await this.emit(language) // 触发语言切换事件
return language
}else{
@ -75,9 +87,9 @@ const defaultLanguageSettings = {
*
* @param {*} newLanguage
*/
async _refreshScopes(newLanguage){
async _refreshScopes(newLanguage:string){
try{
const scopeRefreshers = this._scopes.map(scope=>{
const scopeRefreshers = this.#scopes.map(scope=>{
return scope.refresh(newLanguage)
})
if(Promise.allSettled){
@ -85,7 +97,7 @@ const defaultLanguageSettings = {
}else{
await Promise.all(scopeRefreshers)
}
}catch(e){
}catch(e:any){
console.warn("Error while refreshing i18n scopes:",e.message)
}
}
@ -98,11 +110,11 @@ const defaultLanguageSettings = {
*
* @param {*} scope
*/
async register(scope){
async register(scope:VoerkaI18nScope){
if(!(scope instanceof VoerkaI18nScope)){
throw new TypeError("Scope must be an instance of VoerkaI18nScope")
}
this._scopes.push(scope)
this.#scopes.push(scope)
await scope.refresh(this.activeLanguage)
}
/**
@ -119,16 +131,15 @@ const defaultLanguageSettings = {
* @param {*} formatter
language : 声明该格式化器适用语言
isGlobal : 注册到全局
*/
registerFormatter(name,formatter,{language="*"}={}){
registerFormatter(name:string,formatter:VoerkaI18nFormatter,{language="*"}:{language:string | string[] | '*'}){
if(!isFunction(formatter) || typeof(name)!=="string"){
throw new TypeError("Formatter must be a function")
}
language = Array.isArray(language) ? language : (language ? language.split(",") : [])
language.forEach(lng=>{
if(DataTypes.includes(name)){
this.formatters[lng].$types[name] = formatter
this.formatters[lng].$types![name] = formatter
}else{
this.formatters[lng][name] = formatter
}
@ -137,37 +148,23 @@ const defaultLanguageSettings = {
/**
*
*/
registerDefaultLoader(fn){
registerDefaultLoader(fn:VoerkI18nMessageLoader){
if(!isFunction(fn)) throw new Error("The default loader must be a async function or promise returned")
this._defaultMessageLoader = fn
this.#defaultMessageLoader = fn
this.refresh()
}
async refresh(){
try{
let requests = this._scopes.map(scope=>scope.refresh())
let requests = this.#scopes.map(scope=>scope.refresh())
if(Promise.allSettled){
await Promise.allSettled(requests)
}else{
await Promise.all(requests)
}
}catch(e){
if(this._debug) console.error(`Error while refresh voerkai18n scopes:${e.message}`)
}catch(e:any){
if(this.debug) console.error(`Error while refresh voerkai18n scopes:${e.message}`)
}
}
}
module.exports ={
toDate,
toNumber,
toBoolean,
deepMerge,
VoerkaI18nManager,
translate,
VoerkaI18nScope,
createFormatter,
Formatter,
createFlexFormatter,
FlexFormatter,
getDataTypeName
}

View File

@ -1,23 +1,55 @@
const { DataTypes,isPlainObject, isFunction, getByPath, deepMixin,deepClone } = require("./utils");
const { translate } = require("./translate")
import { DataTypes } from "./utils"
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
import { isFunction } from "flex-tools/typecheck/isFunction"
import { deepClone } from "flex-tools/object/deepClone"
import { get as getByPath } from "flex-tools/object/get"
import { translate } from "./translate"
import { deepMerge } from "flex-tools/object/deepMerge"
import { assignObject } from "flex-tools/object/assignObject"
import type {VoerkaI18nManager } from "./manager"
import type { VoerkaI18nFormatterConfigs, VoerkaI18nFormatters, Voerkai18nIdMap, VoerkaI18nLanguage, VoerkaI18nLanguageMessages, VoerkaI18nLanguagePack, VoerkaI18nScopeCache, VoerkaI18nTranslate, VoerkI18nLoaders } from "./types"
module.exports = class VoerkaI18nScope {
constructor(options = {}, callback) {
this._id = options.id || Date.now().toString() + parseInt(Math.random() * 1000);
this._debug = options.debug == undefined ? process && process.env && process.env.NODE_ENV === "development" : options.debug; // 当出错时是否在控制台台输出错误信息
this._languages = options.languages; // 当前作用域支持的语言列表
this._defaultLanguage = options.defaultLanguage || "zh"; // 默认语言名称
this._activeLanguage = options.activeLanguage; // 当前语言名称
this._default = options.default; // 默认语言包
this._messages = options.messages; // 当前语言包
this._idMap = options.idMap; // 消息id映射列表
this._formatters = options.formatters; // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
this._loaders = options.loaders; // 异步加载语言文件的函数列表
this._global = null; // 引用全局VoerkaI18n配置注册后自动引用
this._patchMessages = {}; // 语言包补丁信息{<language>: {....},<language>:{....}}
this._refreshing = false; // 正在加载语言包标识
export interface VoerkaI18nScopeOptions {
id?: string
debug?: boolean
languages: VoerkaI18nLanguage[] // 当前作用域支持的语言列表
defaultLanguage: string // 默认语言名称
activeLanguage: string // 当前语言名称
default: VoerkaI18nLanguageMessages // 默认语言包
messages: VoerkaI18nLanguageMessages // 当前语言包
idMap: Voerkai18nIdMap // 消息id映射列表
formatters: VoerkaI18nFormatters // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
loaders: VoerkI18nLoaders; // 异步加载语言文件的函数列表
}
export class VoerkaI18nScope {
#options:Required<VoerkaI18nScopeOptions>
#global:VoerkaI18nManager // 引用全局VoerkaI18nManager配置注册后自动引用
#refreshing:boolean = false
#patchMessages:VoerkaI18nLanguagePack = {}
#t:VoerkaI18nTranslate
#activeFormatters:VoerkaI18nFormatters ={}
#activeFormatterConfig: VoerkaI18nFormatterConfigs={}
#cache:VoerkaI18nScopeCache ={}
#messages:VoerkaI18nLanguageMessages
constructor(options:VoerkaI18nScopeOptions, callback:Function) {
this.#options = assignObject({
id : Date.now().toString() + parseInt(String(Math.random() * 1000)),
debug : false,
languages : {}, // 当前作用域支持的语言列表
defaultLanguage: "zh", // 默认语言名称
activeLanguage : "zh", // 当前语言名称
default : {}, // 默认语言包
messages : {}, // 当前语言包
idMap : {}, // 消息id映射列表
formatters : {}, // 当前作用域的格式化函数列表{<lang>: {$types,$config,[格式化器名称]: () => {},[格式化器名称]: () => {}}}
loaders : {} // 异步加载语言文件的函数列表
},options) as Required<VoerkaI18nScopeOptions>
this.#patchMessages = {}; // 语言包补丁信息{<language>: {....},<language>:{....}}
this.#refreshing = false; // 正在加载语言包标识
// 用来缓存格式化器的引用,当使用格式化器时可以直接引用,减少检索遍历
this.$cache = {
this.#cache = {
activeLanguage : null,
typedFormatters: {},
formatters : {},
@ -27,53 +59,49 @@ module.exports = class VoerkaI18nScope {
if (!globalThis.VoerkaI18n) {
const { VoerkaI18nManager } = require("./");
globalThis.VoerkaI18n = new VoerkaI18nManager({
debug : this._debug,
defaultLanguage: this._defaultLanguage,
activeLanguage : this._activeLanguage,
debug : this.debug,
defaultLanguage: this.defaultLanguage,
activeLanguage : this.activeLanguage,
languages : options.languages,
});
}
this._t = translate.bind(this)
this._global = globalThis.VoerkaI18n;
this.#t = translate.bind(this)
this.#global = globalThis.VoerkaI18n as unknown as VoerkaI18nManager;
this._initFormatters(this.activeLanguage) // 初始化活动的格式化器
this._mergePatchedMessages(); // 从本地缓存中读取并合并补丁语言包
this._patch(this._messages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包
this._patch(this.messages, this.activeLanguage); // 延后执行补丁命令,该命令会向远程下载补丁包
this.register(callback); // 在全局注册作用域
}
get id() {return this._id;} // 作用域唯一id
get debug() {return this._debug;} // 调试开关
get defaultLanguage() {return this._defaultLanguage;} // 默认语言名称
get activeLanguage() {return this._global.activeLanguage;} // 默认语言名称
get default() {return this._default;} // 默认语言包
get messages() {return this._messages; } // 当前语言包
get idMap() {return this._idMap;} // 消息id映射列表
get languages() {return this._languages;} // 当前作用域支持的语言列表[{name,title,fallback}]
get loaders() { return this._loaders;} // 异步加载语言文件的函数列表
get global() { return this._global;} // 引用全局VoerkaI18n配置注册后自动引用
get formatters() { return this._formatters;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
get activeFormatters() {return this._activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
get activeFormatterConfig(){return this._activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
get translate(){return this._t}
get t(){return this._t}
get id() {return this.#options.id;} // 作用域唯一id
get debug() {return this.#options.debug;} // 调试开关
get defaultLanguage() {return this.#options.defaultLanguage;} // 默认语言名称
get activeLanguage() {return this.#global.activeLanguage;} // 默认语言名称
get default() {return this.#options.default;} // 默认语言包
get messages() {return this.#options.messages; } // 当前语言包
get idMap() {return this.#options.idMap;} // 消息id映射列表
get languages() {return this.#options.languages;} // 当前作用域支持的语言列表[{name,title,fallback}]
get loaders() { return this.#options.loaders;} // 异步加载语言文件的函数列表
get global() { return this.#global;} // 引用全局VoerkaI18n配置注册后自动引用
get formatters() { return this.#options.formatters;} // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
get activeFormatters() {return this.#activeFormatters} // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
get activeFormatterConfig(){return this.#activeFormatterConfig} // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
get cache(){return this.#cache }
get translate(){return this.#t}
get t(){return this.#t}
/**
*
* - en配置为默认回退语言
*/
_initiLanguages(){
if(!Array.isArray(this._languages)){
if(!Array.isArray(this.languages)){
console.warn("[VoerkaI18n] 无效的语言配置")
this._languages = [
{
name: "zh",
title: "中文"
},
{
name: "en",
title: "英文"
}
this.#options.languages = [
{name: "zh",title: "中文"},
{name: "en",title: "英文"}
]
}
Object.entries(this._languages).forEach(([name,language])=>{
Object.entries(this.languages).forEach(([name,language])=>{
if(!language.fallback) language.fallback = "en"
})
}
@ -82,7 +110,7 @@ module.exports = class VoerkaI18nScope {
*
* @param {*} callback
*/
register(callback) {
register(callback:Function) {
if (!isFunction(callback)) callback = () => {};
this.global.register(this).then(callback).catch(callback);
}
@ -103,7 +131,7 @@ module.exports = class VoerkaI18nScope {
使,
asGlobal : 注册到全局
*/
registerFormatter(name, formatter, { language = "*", global : asGlobal } = {}) {
registerFormatter(name:string, formatter:VoerkaI18nFormatter, { language = "*", global : asGlobal } = {}) {
if (!isFunction(formatter) || typeof name !== "string") {
throw new TypeError("Formatter must be a function");
}
@ -128,7 +156,7 @@ module.exports = class VoerkaI18nScope {
* @param {*} formatters ={"*",zh:{...},en:{...}}
* @returns
*/
registerFormatters(formatters,asGlobal=false) {
registerFormatters(formatters:VoerkaI18nFormatters,asGlobal=false) {
Object.entries(formatters).forEach(([language,fns])=>{
Object.entries(fns).forEach(([name,formatter])=>{
this.registerFormatter(name,formatter,{language,global:asGlobal})
@ -138,27 +166,27 @@ module.exports = class VoerkaI18nScope {
/**
*
*
* _activeFormatters={$config:{...},$types:{...},[]:()=>{...},[]:()=>{...},...}}
* #activeFormatters={$config:{...},$types:{...},[]:()=>{...},[]:()=>{...},...}}
*/
_initFormatters(newLanguage){
private _initFormatters(newLanguage:string){
// 全局格式化器,用来注册到全局
Object.entries(this._formatters).forEach(([langName,formatters])=>{
Object.entries(this.formatters).forEach(([langName,formatters])=>{
if(formatters.global===true){
this.registerFormatters({[langName]:formatters},true)
}else if(isPlainObject(formatters.global)){
this.registerFormatters({[langName]:formatters.global},true)
}
})
this._activeFormatters = {}
this.activeFormatters = {}
try {
if (newLanguage in this._formatters) {
this._activeFormatters = this._formatters[newLanguage];
this.#activeFormatters = this._formatters[newLanguage];
} else {
if (this._debug) console.warn(`Not initialize <${newLanguage}> formatters.`);
if (this.debug) console.warn(`Not initialize <${newLanguage}> formatters.`);
}
this._generateFormatterConfig(newLanguage)
} catch (e) {
if (this._debug) console.error(`Error while initialize ${newLanguage} formatters: ${e.message}`);
if (this.debug) console.error(`Error while initialize ${newLanguage} formatters: ${e.message}`);
}
}
@ -174,22 +202,22 @@ module.exports = class VoerkaI18nScope {
*
* @param {*} language
*/
async _changeFormatters(newLanguage) {
async _changeFormatters(newLanguage:string) {
try {
if (newLanguage in this._formatters) {
let loader = this._formatters[newLanguage];
if (newLanguage in this.formatters) {
let loader = this.formatters[newLanguage];
if (isPlainObject(loader)) {
this._activeFormatters = loader;
this.#activeFormatters = loader as unknown as VoerkaI18nFormatters;
} else if (isFunction(loader)) {
this._activeFormatters = (await loader()).default;
this.#activeFormatters = (await (loader as Function).call(this)).default;
}
// 合并生成格式化器的配置参数,当执行格式化器时该参数将被传递给格式化器
this._generateFormatterConfig(newLanguage)
} else {
if (this._debug) console.warn(`Not configured <${newLanguage}> formatters.`);
if (this.debug) console.warn(`Not configured <${newLanguage}> formatters.`);
}
} catch (e) {
if (this._debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`);
} catch (e:any) {
if (this.debug) console.error(`Error loading ${newLanguage} formatters: ${e.message}`);
}
}
/**
@ -198,25 +226,25 @@ module.exports = class VoerkaI18nScope {
* - global.formatters[language].$config
* - scope.activeFormatters.$config
*/
_generateFormatterConfig(language){
_generateFormatterConfig(language:string){
try{
const fallbackLanguage = this.getLanguage(language).fallback;
let configSources = [
getByPath(this._global.formatters,`${fallbackLanguage}.$config`,{}),
getByPath(this._global.formatters,"*.$config",{}),
getByPath(this._formatters,`${fallbackLanguage}.$config`,{}),
getByPath(this.#global.formatters,`${fallbackLanguage}.$config`,{}),
getByPath(this.#global.formatters,"*.$config",{}),
getByPath(this.formatters,`${fallbackLanguage}.$config`,{}),
getByPath(this._formatter,"*.$config",{}),
getByPath(this._global.formatters,`${language}.$config`,{}),
getByPath(this._activeFormatters,"$config",{})
getByPath(this.#global.formatters,`${language}.$config`,{}),
getByPath(this.#activeFormatters,"$config",{})
]
return this._activeFormatterConfig = configSources.reduce((finalConfig, config)=>{
if(isPlainObject(config)) deepMixin(finalConfig,config)
return this.#activeFormatterConfig = configSources.reduce((finalConfig, config)=>{
if(isPlainObject(config)) deepMerge(finalConfig,config,{newObject:false})
return finalConfig
},deepClone(getByPath(this._global.formatters,`*.$config`,{})))
},deepClone(getByPath(this.#global.formatters,`*.$config`,{})))
}catch(e){
if(this.debug) console.error(`Error while generate <${language}> formatter options: `,e)
return this._activeFormatters.$config || {}
return this.#activeFormatters.$config || {}
}
}
@ -224,7 +252,7 @@ module.exports = class VoerkaI18nScope {
*
* @param {Function} Promise
*/
registerDefaultLoader(fn) {
registerDefaultLoader(fn:Function) {
this.global.registerDefaultLoader(fn);
}
/**
@ -232,37 +260,37 @@ module.exports = class VoerkaI18nScope {
* @param {*} language
* @returns
*/
getLanguage(language) {
let index = this._languages.findIndex((lng) => lng.name == language);
if (index !== -1) return this._languages[index];
getLanguage(language:string) {
let index = this.languages.findIndex((lng) => lng.name == language);
if (index !== -1) return this.languages[index];
}
/**
*
* @param {*} language
* @returns
*/
hasLanguage(language) {
return this._languages.indexOf((lang) => lang.name == language) !== -1;
hasLanguage(language:string) {
return this.languages.indexOf((lang:VoerkaI18nLanguage) => lang.name == language) !== -1;
}
/**
* 退
*/
_fallback() {
this._messages = this._default;
this._activeLanguage = this.defaultLanguage;
this.#options.messages = this.default;
this.#options.activeLanguage = this.defaultLanguage;
}
/**
*
* @param {*} newLanguage
*/
async refresh(newLanguage) {
this._refreshing = true;
async refresh(newLanguage?:string) {
this.#refreshing = true;
if (!newLanguage) newLanguage = this.activeLanguage;
// 默认语言:由于默认语言采用静态加载方式而不是异步块,因此只需要简单的替换即可
if (newLanguage === this.defaultLanguage) {
this._messages = this._default;
await this._patch(this._messages, newLanguage); // 异步补丁
this.#options.messages = this.default;
await this._patch(this.#options.messages, newLanguage); // 异步补丁
await this._changeFormatters(newLanguage);
return;
}
@ -303,10 +331,10 @@ module.exports = class VoerkaI18nScope {
}
} catch (e) {
if (this._debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
if (this.debug) console.warn(`Error while loading language <${newLanguage}> on i18nScope(${this.id}): ${e.message}`);
this._fallback();
} finally {
this._refreshing = false;
this.#refreshing = false;
}
}
/**
@ -327,7 +355,7 @@ module.exports = class VoerkaI18nScope {
this._savePatchedMessages(pachedMessages, newLanguage);
}
} catch (e) {
if (this._debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e);
if (this.debug) console.error(`Error while loading <${newLanguage}> patch messages from remote:`,e);
}
}
/**
@ -363,7 +391,7 @@ module.exports = class VoerkaI18nScope {
globalThis.localStorage.setItem(`voerkai18n_${this.id}_${language}_patched_messages`,JSON.stringify(messages));
}
} catch (e) {
if (this.$cache._debug) console.error("Error while save voerkai18n patched messages:",e);
if (this.cache._debug) console.error("Error while save voerkai18n patched messages:",e);
}
}
/**
@ -379,10 +407,10 @@ module.exports = class VoerkaI18nScope {
}
}
// 以下方法引用全局VoerkaI18n实例的方法
on() {return this._global.on(...arguments); }
off() {return this._global.off(...arguments); }
offAll() {return this._global.offAll(...arguments);}
on() {return this.#global.on(...arguments); }
off() {return this.#global.off(...arguments); }
offAll() {return this.#global.offAll(...arguments);}
async change(language) {
await this._global.change(language);
await this.#global.change(language);
}
};

View File

@ -1,5 +1,10 @@
const {isNumber,isPlainObject,isFunction} = require("./utils")
const { replaceInterpolatedVars } = require("./interpolate")
import { isNumber } from "flex-tools/typecheck/isNumber"
import { isPlainObject } from "flex-tools/typecheck/isPlainObject"
import { isFunction } from "flex-tools/typecheck/isFunction"
import { replaceInterpolatedVars } from "./interpolate"
import type { VoerkaI18nScope } from "./scope"
/**
*
*
@ -10,7 +15,7 @@ const { replaceInterpolatedVars } = require("./interpolate")
* @param {*} value
* @returns
*/
function transformToString(value){
function transformToString(value:any){
let result = value
try{
if(isFunction(result)) result = value()
@ -34,7 +39,7 @@ const { replaceInterpolatedVars } = require("./interpolate")
* @param {*} content
* @returns
*/
function isMessageId(content){
function isMessageId(content:string){
return isNumber(content)
}
/**
@ -43,7 +48,7 @@ const { replaceInterpolatedVars } = require("./interpolate")
* @param {*} messages = [<=0><=1><=2>,...]
* @param {*} value
*/
function getPluraMessage(messages,value){
function getPluraMessage(messages:any,value:number){
try{
if(Array.isArray(messages)){
return messages.length > value ? messages[value] : messages[messages.length-1]
@ -67,27 +72,28 @@ function getPluraMessage(messages,value){
* this===scope scope
*
*/
function translate(message) {
export function translate(this:VoerkaI18nScope,message:string,...args:any[]) {
const scope = this
const activeLanguage = scope.global.activeLanguage
let content = message
let content:string = message
let vars=[] // 插值变量列表
let pluralVars= [] // 复数变量
let pluraValue = null // 复数值
if(!typeof(message)==="string") return message
if(!(typeof(message)==="string")) return message
try{
// 1. 预处理变量: 复数变量保存至pluralVars中 , 变量如果是Function则调用
if(arguments.length === 2 && isPlainObject(arguments[1])){
const dictVars:Record<string,any>={}
Object.entries(arguments[1]).forEach(([name,value])=>{
if(isFunction(value)){
if(typeof(value)=="function"){
try{
vars[name] = value()
dictVars[name] = value()
}catch(e){
vars[name] = value
dictVars[name] = value
}
}
// 以$开头的视为复数变量
if(name.startsWith("$") && typeof(vars[name])==="number") pluralVars.push(name)
if(name.startsWith("$") && typeof(dictVars[name])==="number") pluralVars.push(name)
})
vars = [arguments[1]]
}else if(arguments.length >= 2){
@ -141,9 +147,4 @@ function translate(message) {
return content // 出错则返回原始文本
}
}
module.exports = {
translate
}

View File

@ -0,0 +1,137 @@
import type { VoerkaI18nManager } from "./manager"
import type { VoerkaI18nScope } from "./scope"
declare global {
export var VoerkaI18n: VoerkaI18nManager
}
export type VoerkaI18nLanguageMessages = Record<string, string | string[]>
export interface VoerkaI18nLanguagePack {
[language: string]: VoerkaI18nLanguageMessages
}
export type Voerkai18nIdMap = Record<string, number>
export interface VoerkaI18nLanguage {
name: string
title?: string
default?: boolean
fallback?: string
}
export type VoerkaI18nFormatter = (value: string, ...args: any[]) => string
export type VoerkaI18nFormatterConfigs = Record<string, any>
export type VoerkaI18nFormatters = Record<string, ({
$types?: Record<string, VoerkaI18nFormatter>
$config?: Record<string, string>
} & {
[key: string]: VoerkaI18nFormatter
})> //| (() => Promise<any>)>
export type VoerkI18nLoader = () => Awaited<Promise<any>>
export interface VoerkI18nLoaders {
[key: string]: VoerkI18nLoader
}
export type VoerkI18nMessageLoader = (this:VoerkaI18nScope,newLanguage:string,scope:VoerkaI18nScope)=>Promise<VoerkaI18nLanguageMessages>
export interface VoerkaI18nScopeCache{
activeLanguage? : null,
typedFormatters?: {},
formatters? : {},
}
export type TranslateMessageVars = number | boolean | string | Function | Date
export interface VoerkaI18nTranslate {
(message: string, ...args: TranslateMessageVars[]): string
(message: string, vars?: Record<string, TranslateMessageVars>): string
}
export interface VoerkaI18nSupportedLanguages {
[key: string]: VoerkaI18nLanguage
}
// export class VoerkaI18nScope {
// constructor(options: VoerkaI18nScopeOptions, callback?: Function)
// get id(): string // 作用域唯一id
// get debug(): boolean // 调试开关
// get defaultLanguage(): string // 默认语言名称
// get activeLanguage(): string // 默认语言名称
// get default(): VoerkaI18nLanguageMessages // 默认语言包
// get messages(): VoerkaI18nLanguageMessages // 当前语言包
// get idMap(): Voerkai18nIdMap // 消息id映射列表
// get languages(): VoerkaI18nSupportedLanguages // 当前作用域支持的语言列表[{name,title,fallback}]
// get loaders(): VoerkI18nLoaders // 异步加载语言文件的函数列表
// get global(): VoerkaI18nManager // 引用全局VoerkaI18n配置注册后自动引用
// get formatters(): VoerkI18nFormatters // 当前作用域的所有格式化器定义 {<语言名称>: {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () => {}}}
// get activeFormatters(): VoerkI18nFormatters // 当前作用域激活的格式化器定义 {$types,$config,[格式化器名称]: () = >{},[格式化器名称]: () = >{}}
// get activeFormatterConfig(): VoerkI18nFormatterConfigs // 当前格式化器合并后的配置参数,参数已经合并了全局格式化器中的参数
// get t(): VoerkaI18nTranslate
// get translate(): VoerkaI18nTranslate
// /**
// * 在全局注册作用域当前作用域
// * @param {*} callback 注册成功后的回调
// */
// register(callback: Function): void
// /**
// * 注册格式化器
// */
// registerFormatter(name: string, formatter: VoerkI18nFormatter, { language, global }: { language: string | string[], global: boolean }): void
// /**
// * 注册多种格式化器
// */
// registerFormatters(formatters: VoerkI18nFormatters, asGlobal?: boolean): void
// /**
// * 注册默认文本信息加载器
// */
// registerDefaultLoader(fn: Function): void
// /**
// * 获取指定语言信息
// * @param {*} language
// * @returns
// */
// getLanguage(language: string): VoerkaI18nLanguageMessages
// hasLanguage(language: string): boolean
// refresh(newLanguage: string): Promise<void>
// on(): void
// off(): void
// offAll(): void
// change(language: string): Promise<void>
// }
export type VoerkI18nFormatterConfigs = Record<string, any>
// 翻译函数
export var translate: {
(message: string, ...args: (string | Function)[]): string
(message: string, vars?: Record<string, any>): string
}
export interface CreateFormatterOptions {
normalize?: (value: any) => any // 对输入值进行规范化处理,如进行时间格式化时,为了提高更好的兼容性,支持数字时间戳/字符串/Date等需要对输入值进行处理如强制类型转换等
params?: string[] | null // 可选的声明参数顺序如果是变参的则需要传入null
configKey?: string // 声明该格式化器在$config中的路径支持简单的使用.的路径语法
}
export type Primitive = string | number | boolean | null | undefined
export interface FormatterDefine {
(this: VoerkI18nFormatterConfigs, value: any, ...args: Primitive[]): string
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, $config: VoerkI18nFormatterConfigs): string
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, $config: VoerkI18nFormatterConfigs): string
(this: VoerkI18nFormatterConfigs, value: any, arg1: Primitive, arg2: Primitive, arg3: Primitive, $config: VoerkI18nFormatterConfigs): string
configurable?: boolean
}
// 创建格式化器
export type CreateFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
export var createFormatter: CreateFormatterType
export var Formatter: CreateFormatterType
export type CreateFlexFormatterType = (fn: Function, options: CreateFormatterOptions, defaultParams: Record<string, any>) => FormatterDefine
export var createFlexFormatter: CreateFlexFormatterType
export var FlexFormatter: CreateFlexFormatterType
export var getDataTypeName: (value: any) => string
export var toDate: (value: any) => Date | number
export var toNumber: (value: any, defaultValue: number) => number
export var toBoolean: (value: any) => boolean

View File

@ -0,0 +1,72 @@
import { isNumber } from "flex-tools/typecheck/isNumber"
/**
*
* getDataTypeName(1) == Number
* getDataTypeName("") == String
* getDataTypeName(null) == Null
* getDataTypeName(undefined) == Undefined
* getDataTypeName(new Date()) == Date
* getDataTypeName(new Error()) == Error
*
* @param {*} v
* @returns
*/
export function getDataTypeName(v:any):string{
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
};
/**
*
*
* str = "I am {username}"
* replace(new RegExp(str),"Tom") !=== I am Tom
*
* {} "\{username\}"
*
* replace(new RegExp(escapeRegexpStr(str)),"Tom")
*
*
* @param {*} str
* @returns
*/
export function escapeRegexpStr(str:string):string{
return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1")
}
export const DataTypes = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]
/**
*
*/
export function toNumber(value:any,defualt=0):number {
try {
if (isNumber(value)) {
return parseFloat(value)
} else {
return defualt
}
} catch {
return value
}
}
/**
* Date类型
* @param {*} value
*/
export function toDate(value:any) {
try {
return value instanceof Date ? value : new Date(value)
} catch {
return parseInt(value)
}
}
export function toBoolean(value:any){
return !!value
}

View File

@ -0,0 +1,106 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
//"suppressImplicitAnyIndexErrors":true,
/* Language and Environment */
"target": "ES2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
"experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
"emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
//"moduleResolution": "node16",
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "baseUrl": ".",
// "rootDir": "./src",
// "outDir": "./dist",
// "paths": {
// },
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
//"baseUrl": "./dist", /* Specify the base directory to resolve non-relative module names. */
//"paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
//"rootDirs": ["src"], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
},
"include":[
"src"
]
}

View File

@ -1,279 +0,0 @@
/**
* 判断是否是JSON对象
* @param {*} obj
* @returns
*/
function isPlainObject(obj){
if (typeof obj !== 'object' || obj === null) return false;
var proto = Object.getPrototypeOf(obj);
if (proto === null) return true;
var baseProto = proto;
while (Object.getPrototypeOf(baseProto) !== null) {
baseProto = Object.getPrototypeOf(baseProto);
}
return proto === baseProto;
}
/**
* 判断值是否是一个数字
* @param {*} value
* @returns
*/
function isNumber(value){
if(value==undefined) return false
if(typeof(value)=='number') return true
if(typeof(value)!='string') return false
try{
if(value.includes(".")){
let v = parseFloat(value)
if(value.endsWith(".")){
return !isNaN(v) && String(v).length===value.length-1
}else{
return !isNaN(v) && String(v).length===value.length
}
}else{
let v = parseInt(value)
return !isNaN(v) && String(v).length===value.length
}
}catch{
return false
}
}
function isFunction(fn){
return typeof fn === "function"
}
/**
* 当value= null || undefined || "" || [] || {} 时返回true
* @param {*} value
* @returns
*/
function isNothing(value){
if(["boolean","function"].includes(typeof(value))) return false
if(value=="") return true
if(value==undefined) return true
if(Array.isArray(value) && value.length==0) return true
if(typeof(value)=="object" && Object.keys(value).length==0) return true
return false
}
/**
* 深度合并对象
*
* 注意
* - 不会对数组成员进行再次遍历
* - 不能处理循环引入
*
* @param {*} toObj
* @param {*} formObj
* @param {*} options
* array : 数组合并策略0-替换1-合并2-去重合并
* mixin : 是否采用混入方式来=false, 则会创建新对象并返回
*/
function deepMerge(toObj,formObj,options={}){
let results = options.mixin ? toObj : Object.assign({},toObj)
Object.entries(formObj).forEach(([key,value])=>{
if(key in results){
if(typeof value === "object" && value !== null){
if(Array.isArray(value)){
if(options.array === 1){//合并
results[key] = [...results[key],...value]
}else if(options.array === 2){//去重合并
results[key] = [...new Set([...results[key],...value])]
}else{ //默认: 替换
results[key] = value
}
}else{
results[key] = deepMerge(results[key],value,options)
}
}else{
results[key] = value
}
}else{
results[key] = value
}
})
return results
}
function deepMixin(toObj,formObj,options={}){
return deepMerge(toObj,formObj,{...options,mixin:true})
}
/**
* 获取指定变量类型名称
* getDataTypeName(1) == Number
* getDataTypeName("") == String
* getDataTypeName(null) == Null
* getDataTypeName(undefined) == Undefined
* getDataTypeName(new Date()) == Date
* getDataTypeName(new Error()) == Error
*
* @param {*} v
* @returns
*/
function getDataTypeName(v){
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
if(typeof(v)==="function") return "Function"
return v.constructor && v.constructor.name;
};
/**
* 根据路径获取指定值
* 只支持简单的.分割路径
* getByPath({a:{b:1}},"a.b") == 1
* getByPath({a:{b:1}},"a.c",2) == 2
*
* @param {*} obj
* @param {*} path 使用.分割的路径
* @param {*} defaultValue 默认值
* @returns
*/
function getByPath(obj,path,defaultValue){
if(typeof(obj)!="object" || typeof(path)!="string") return defaultValue
let paths = path.split(".")
let cur = obj
for(let key of paths){
if(typeof(cur)=="object" && (key in cur) ){
cur = cur[key]
}else{
return defaultValue
}
}
return cur
}
function deepClone(obj){
if(obj==undefined) return obj
if (['string',"number","boolean","function","undefined"].includes(typeof(obj))){
return obj
}else if(Array.isArray(obj)){
return obj.map(item => deepClone(item))
}else if(typeof(obj)=="object"){
let results = {}
Object.entries(obj).forEach(([key,value])=>{
results[key] = deepClone(value)
})
return results
}else{
return obj
}
}
/**
* 替换所有字符串
* 低版本ES未提供replaceAll,此函数用来替代
* @param {*} str
* @param {*} findValue
* @param {*} replaceValue
*/
function replaceAll(str,findValue,replaceValue){
if(typeof(str)!=="string" || findValue=="" || findValue==replaceValue) return str
try{
return str.replace(new RegExp(escapeRegexpStr(findValue),"g"),replaceValue)
}catch{}
return str
}
/**
* 使用正则表达式解析非标JOSN
*
*/
const bastardJsonKeyRegex = /(?<value>(?<=:\s*)(\'.*?\')+)|(?<key>(([\w\u4e00-\u9fa5])|(\'.*?\'))+(?=\s*\:))/g
/**
* 当需要采用正则表达式进行字符串替换时需要对字符串进行转义
*
* 比如 str = "I am {username}"
* replace(new RegExp(str),"Tom") !=== I am Tom
*
* 因为{}是正则表达式元字符需要转义成 "\{username\}"
*
* replace(new RegExp(escapeRegexpStr(str)),"Tom")
*
*
* @param {*} str
* @returns
*/
function escapeRegexpStr(str){
return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1")
}
/**
* 解析非标的JSON字符串为{}
* 非标的JSON字符串指的是
* - key没有使用使用""包裹
* - 字符串value没有使用""包裹
*
* @param {*} str
* @returns
*/
function safeParseJson(str){
let matched;
while ((matched = bastardJsonKeyRegex.exec(str)) !== null) {
if (matched.index === bastardJsonKeyRegex.lastIndex) {
bastardJsonKeyRegex.lastIndex++;
}
let item = matched[0]
if(item.startsWith("'") && item.endsWith("'")){
item = item.substring(1,item.length-1)
}
const findValue = matched.groups.key ? new RegExp( escapeRegexpStr(matched[0]) + "\s*:") : new RegExp(":\s*" + escapeRegexpStr(matched[0]))
const replaceTo = matched.groups.key ? `"${item}":` : `: "${item}"`
str = str.replace(findValue,replaceTo)
}
return JSON.parse(str)
}
const DataTypes = ["String","Number","Boolean","Object","Array","Function","Error","Symbol","RegExp","Date","Null","Undefined","Set","Map","WeakSet","WeakMap"]
/**
* 转换为数字类型
*/
function toNumber(value,defualt=0) {
try {
if (isNumber(value)) {
return parseFloat(value)
} else {
return defualt
}
} catch {
return value
}
}
/**
* 将值转换为Date类型
* @param {*} value
*/
function toDate(value) {
try {
return value instanceof Date ? value : new Date(value)
} catch {
return parseInt(value)
}
}
function toBoolean(value){
return !!value
}
module.exports ={
DataTypes,
isPlainObject,
isFunction,
isNumber,
isNothing,
toNumber,
toDate,
toBoolean,
deepClone,
deepMerge,
deepMixin,
replaceAll,
getByPath,
getDataTypeName,
escapeRegexpStr,
safeParseJson
}

581
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff