amis
Version:
一种MIS页面生成工具
507 lines (438 loc) • 16.4 kB
text/typescript
import {reigsterTplEnginer, filter} from './tpl';
import moment from 'moment';
import {PlainObject} from '../types';
import isPlainObject = require('lodash/isPlainObject');
import {createObject, isObject, setVariable, qsstringify} from './helper';
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
export const prettyBytes = (num: number) => {
if (!Number.isFinite(num)) {
throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
}
const neg = num < 0;
if (neg) {
num = -num;
}
if (num < 1) {
return (neg ? '-' : '') + num + ' B';
}
const exponent = Math.min(Math.floor(Math.log(num) / Math.log(1000)), UNITS.length - 1);
const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
const unit = UNITS[exponent];
return (neg ? '-' : '') + numStr + ' ' + unit;
};
const entityMap: {
[propName: string]: string;
} = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
export const escapeHtml = (str: string) =>
String(str).replace(/[&<>"'\/]/g, function(s) {
return entityMap[s];
});
export function formatDuration(value: number): string {
const unit = ['秒', '分', '时', '天', '月', '季', '年'];
const steps = [1, 60, 3600, 86400, 2592000, 7776000, 31104000];
let len = steps.length;
const parts = [];
while (len--) {
if (steps[len] && value >= steps[len]) {
parts.push(Math.round(value / steps[len]) + unit[len]);
value %= steps[len];
} else if (len === 0 && value) {
parts.push((value.toFixed ? value.toFixed(2) : '0') + unit[0]);
}
}
return parts.join('');
}
const timeUnitMap: {
[propName: string]: string;
} = {
year: 'Y',
years: 'Y',
month: 'M',
months: 'M',
week: 'w',
weeks: 'w',
weekday: 'W',
day: 'd',
days: 'd',
hour: 'h',
hours: 'h',
minute: 'm',
minutes: 'm',
min: 'm',
mins: 'm'
};
export const relativeValueRe = /^(.+)?(\+|-)(\d+)(minute|minutes|min|mins|hours|hour|day|days|week|weeks|month|months|year|years|weekday)$/i;
export const filterDate = (value: string, data: object = {}, format = 'X'): moment.Moment => {
let m;
if (typeof value === 'string') {
value = value.trim();
}
value = filter(value, data);
if (value && typeof value === 'string' && (m = relativeValueRe.exec(value))) {
const date = new Date();
const step = parseInt(m[3], 10);
const from = m[1]
? filterDate(m[1], data, format)
: moment(
/minute|minutes|min|mins|hours|hour/.test(m[4])
? [
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds()
]
: [date.getFullYear(), date.getMonth(), date.getDate()]
);
return m[2] === '-'
? from.subtract(step, timeUnitMap[m[4]] as moment.DurationInputArg2)
: from.add(step, timeUnitMap[m[4]] as moment.DurationInputArg2);
// return from[m[2] === '-' ? 'subtract' : 'add'](step, mapping[m[4]] || m[4]);
} else if (value === 'now') {
return moment();
} else if (value === 'today') {
const date = new Date();
return moment([date.getFullYear(), date.getMonth(), date.getDate()]);
} else {
return moment(value, format);
}
};
export const filters: {
[propName: string]: (input: any, ...args: any[]) => any;
} = {
html: (input: string) => escapeHtml(input),
json: (input, tabSize: number | string = 2) =>
tabSize ? JSON.stringify(input, null, parseInt(tabSize as string, 10)) : JSON.stringify(input),
toJson: input => {
let ret;
try {
ret = JSON.parse(input);
} catch (e) {
ret = null;
}
return ret;
},
raw: input => input,
date: (input, format = 'LLL', inputFormat = 'X') => moment(input, inputFormat).format(format),
number: input => {
let parts = String(input).split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return parts.join('.');
},
trim: input => input.trim(),
percent: (input, decimals = 0) => {
input = parseFloat(input) || 0;
decimals = parseInt(decimals, 10) || 0;
let whole = input * 100;
let multiplier = Math.pow(10, decimals);
return (Math.round(whole * multiplier) / multiplier).toFixed(decimals) + '%';
},
duration: input => (input ? formatDuration(input) : input),
bytes: input => (input ? prettyBytes(parseFloat(input)) : input),
round: (input, decimals = 0) => {
if (isNaN(input)) {
return 0;
}
decimals = parseInt(decimals, 10) || 2;
let multiplier = Math.pow(10, decimals);
return (Math.round(input * multiplier) / multiplier).toFixed(decimals);
},
truncate: (input, length, end) => {
end = end || '...';
if (length == null) {
return input;
}
length = parseInt(length, 10) || 200;
return input.substring(0, length) + (input.length > length ? end : '');
},
url_encode: input => encodeURIComponent(input),
url_decode: input => decodeURIComponent(input),
default: (input, defaultValue) =>
input ||
(() => {
try {
if (defaultValue === 'undefined') {
return undefined;
}
return JSON.parse(defaultValue);
} catch (e) {
return defaultValue;
}
})(),
join: (input, glue) => (input && input.join ? input.join(glue) : input),
split: (input, delimiter = ',') => (typeof input === 'string' ? input.split(delimiter) : input),
first: input => input && input[0],
nth: (input, nth = 0) => input && input[nth],
last: input => input && (input.length ? input[input.length - 1] : null),
minus: (input, step = 1) => (parseInt(input, 10) || 0) - parseInt(step, 10),
plus: (input, step = 1) => (parseInt(input, 10) || 0) + parseInt(step, 10),
pick: (input, path = '&') =>
Array.isArray(input) && !/^\d+$/.test(path)
? input.map(item => pickValues(path, item))
: pickValues(path, input),
pick_if_exist: (input, path = '&') =>
Array.isArray(input)
? input.map(item => resolveVariable(path, item) || item)
: resolveVariable(path, input) || input,
str2date: function(input, inputFormat = 'X', outputFormat = 'X') {
return input ? filterDate(input, this, inputFormat).format(outputFormat) : '';
},
asArray: input => (input ? [input] : input),
filter: function(input, keys, expOrDirective, arg1) {
if (!Array.isArray(input) || !keys || !expOrDirective) {
return input;
}
let directive = expOrDirective;
let fn: (value: any, key: string, item: any) => boolean = () => true;
if (directive === 'isTrue') {
fn = value => !!value;
} else if (directive === 'isFalse') {
fn = value => !value;
} else if (directive === 'isExists') {
fn = value => typeof value !== 'undefined';
} else {
if (directive !== 'match') {
directive = 'match';
arg1 = expOrDirective;
}
arg1 = arg1 ? (/^('|")(.*)\1$/.test(arg1) ? RegExp.$2 : resolveVariable(arg1, this as any)) : '';
// 比对的值是空时直接返回。
if (!arg1) {
return input;
}
fn = value =>
!!~String(value)
.toLowerCase()
.indexOf(arg1);
}
keys = keys.split(/\s*,\s*/);
return input.filter((item: any) => keys.some((key: string) => fn(resolveVariable(key, item), key, item)));
},
base64Encode(str) {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function toSolidBytes(match, p1) {
return String.fromCharCode(('0x' + p1) as any);
})
);
},
base64Decode(str) {
return decodeURIComponent(
atob(str)
.split('')
.map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
},
lowerCase: input => (input && typeof input === 'string' ? input.toLowerCase() : input),
upperCase: input => (input && typeof input === 'string' ? input.toUpperCase() : input)
};
export function registerFilter(name: string, fn: (input: any, ...args: any[]) => any): void {
filters[name] = fn;
}
export function pickValues(names: string, data: object) {
let arr: Array<string>;
if (!names || ((arr = names.split(',')) && arr.length < 2)) {
let idx = names.indexOf('~');
if (~idx) {
let key = names.substring(0, idx);
let target = names.substring(idx + 1);
return {
[key]: resolveVariable(target, data)
};
}
return resolveVariable(names, data);
}
let ret: any = {};
arr.forEach(name => {
let idx = name.indexOf('~');
let target = name;
if (~idx) {
target = name.substring(idx + 1);
name = name.substring(0, idx);
}
setVariable(ret, name, resolveVariable(target, data));
});
return ret;
}
export const resolveVariable = (path: string, data: any = {}): any => {
if (!path) {
return undefined;
}
if (path === '$$') {
return data;
} else if (path[0] === '$') {
path = path.substring(1);
} else if (path === '&') {
return data;
}
if (typeof data[path] !== 'undefined') {
return data[path];
}
let parts = path.replace(/^{|}$/g, '').split('.');
return parts.reduce((data, path) => {
if ((isObject(data) || Array.isArray(data)) && path in data) {
return (data as {[propName: string]: any})[path];
}
return undefined;
}, data);
};
export const resolveVariableAndFilter = (path: string, data: object = {}, defaultFilter: string = '| html'): any => {
if (!path) {
return undefined;
}
const m = /^(\\)?\$(?:([a-z0-9_.]+)|{([\s\S]+)})$/i.exec(path);
if (!m) {
return undefined;
}
const [_, escape, key, key2] = m;
// 如果是转义如: `\$abc` => `$abc`
if (escape) {
return _.substring(1);
}
let finalKey: string = key || key2;
// 先只支持一层吧
finalKey = finalKey.replace(/(\\)?\$(?:([a-z0-9_.]+)|{([^}{]+)})/g, (_, escape) => {
return escape ? _.substring(1) : resolveVariableAndFilter(_, data, defaultFilter);
});
// 默认 html 转义
if (!~finalKey.indexOf('|')) {
finalKey += defaultFilter;
}
let paths = finalKey.split(/\s*\|\s*/g);
let originalKey = finalKey;
finalKey = paths.shift() as string;
let ret = resolveVariable(finalKey, data);
return ret == null && !~originalKey.indexOf('default')
? ''
: paths.reduce((input, filter) => {
let params = filter
.replace(
/([^\\])\\([\:\\])/g,
(_, affix, content) => `${affix}__${content === ':' ? 'colon' : 'slash'}__`
)
.split(':')
.map(item => item.replace(/__(slash|colon)__/g, (_, type) => (type === 'colon' ? ':' : '\\')));
let key = params.shift() as string;
return (filters[key] || filters.raw).call(data, input, ...params);
}, ret);
};
export const tokenize = (str: string, data: object, defaultFilter: string = '| html') => {
if (!str || typeof str !== 'string') {
return str;
}
return str
.replace(/(\\)?\$(?:([a-z0-9_\.]+|&)|{([^}{]+?)})/gi, (_, escape) =>
escape ? _.substring(1) : resolveVariableAndFilter(_, data, defaultFilter)
)
.replace(/\$\$/g, (_, index: number, source: string) => {
const prefix = source[index - 1];
return prefix === '=' ? encodeURIComponent(JSON.stringify(data)) : qsstringify(data);
});
};
function resolveMapping(value: any, data: PlainObject) {
return typeof value === 'string' && /^\$(?:([a-z0-9_.]+)|{[^}{]+})$/.test(value)
? resolveVariableAndFilter(value, data, '| raw')
: typeof value === 'string' && ~value.indexOf('$')
? tokenize(value, data, '| raw')
: value;
}
export function dataMapping(to: any, from: PlainObject): any {
let ret = {};
if (Array.isArray(to)) {
return to.map(item => dataMapping(item, from));
} else if (!to) {
return ret;
}
Object.keys(to).forEach(key => {
const value = to[key];
let keys: Array<string>;
if (key === '&' && value === '$$') {
ret = {
...ret,
...from
};
} else if (key === '&') {
const v =
isPlainObject(value) &&
(keys = Object.keys(value)) &&
keys.length === 1 &&
from[keys[0].substring(1)] &&
Array.isArray(from[keys[0].substring(1)])
? from[keys[0].substring(1)].map((raw: object) =>
dataMapping(value[keys[0]], createObject(from, raw))
)
: resolveMapping(value, from);
if (Array.isArray(v) || typeof v === 'string') {
ret = v;
} else if (typeof v === 'function') {
ret = {
...ret,
...v(from)
};
} else {
ret = {
...ret,
...v
};
}
} else if (value === '$$') {
(ret as PlainObject)[key] = from;
} else if (value && value[0] === '$') {
const v = resolveMapping(value, from);
(ret as PlainObject)[key] = v;
if (v === '__undefined') {
delete (ret as PlainObject)[key];
}
} else if (
isPlainObject(value) &&
(keys = Object.keys(value)) &&
keys.length === 1 &&
from[keys[0].substring(1)] &&
Array.isArray(from[keys[0].substring(1)])
) {
// 支持只取数组中的部分值这个需求
// 如:
// data: {
// items: {
// '$rows': {
// id: '$id',
// forum_id: '$forum_id'
// }
// }
// }
const arr = from[keys[0].substring(1)];
const mapping = value[keys[0]];
(ret as PlainObject)[key] = arr.map((raw: object) => dataMapping(mapping, createObject(from, raw)));
} else if (isPlainObject(value)) {
(ret as PlainObject)[key] = dataMapping(value, from);
} else if (Array.isArray(value)) {
(ret as PlainObject)[key] = value.map((value: any) =>
isPlainObject(value) ? dataMapping(value, from) : resolveMapping(value, from)
);
} else if (typeof value == 'string' && ~value.indexOf('$')) {
(ret as PlainObject)[key] = resolveMapping(value, from);
} else if (typeof value === 'function') {
(ret as PlainObject)[key] = value(from);
} else {
(ret as PlainObject)[key] = value;
if (value === '__undefined') {
delete (ret as PlainObject)[key];
}
}
});
return ret;
}
reigsterTplEnginer('builtin', {
test: str => !!~str.indexOf('$'),
compile: (str: string, data: object, defaultFilter = '| html') => tokenize(str, data, defaultFilter)
});