@truenewx/tnxet
Version:
互联网技术解决方案:Electron扩展支持
275 lines (257 loc) • 8.7 kB
text/typescript
import Enum from '../../../tnxcore/src/foundation/enum.ts';
export function containsTag(xml: string, tagPath: string): boolean {
const range = findTagPathRange(xml, tagPath);
return range !== null;
}
/**
* 在 xml 中查找按路径(如 a.b.c,不含根标签)指定的第一个目标标签节点的范围
* @param xml xml 字符串
* @param path 路径型标签(以点分隔的层级,不含根)
* @returns 目标节点的 [start, endExclusive] 范围,找不到返回 null
*/
function findTagPathRange(xml: string, path: string): [start: number, endExclusive: number] | null {
if (!path || typeof path !== 'string') {
return null;
}
const tags = path.split('.').filter(Boolean);
if (tags.length === 0) {
return null;
}
// 获取根内容范围
const rootRange = findRootContentRange(xml);
if (!rootRange) {
return null;
}
let containerStart = rootRange[0];
let containerEnd = rootRange[1];
let range: [start: number, endExclusive: number] | null = null;
for (let i = 0; i < tags.length; i++) {
const tag = tags[i];
range = findDirectChildRange(xml, tag, containerStart, containerEnd);
if (!range) {
return null;
}
// 下一层在当前标签的内容区继续查找
const openTokenLen = ('<' + tag + '>').length;
containerStart = range[0] + openTokenLen;
const closeTokenLen = ('</' + tag + '>').length;
containerEnd = range[1] - closeTokenLen;
}
return range;
}
/**
* 查找根标签的内容范围(不含开闭标签)
*/
function findRootContentRange(xml: string): [start: number, endExclusive: number] | null {
let i = 0;
while (i < xml.length) {
let lt = xml.indexOf('<', i);
if (lt < 0) {
return null;
}
if (xml.startsWith('<?', lt) || xml.startsWith('<!', lt)) {
i = lt + 2;
continue;
}
if (xml.startsWith('</', lt)) {
i = lt + 2;
continue;
}
// 读取根标签名
let j = lt + 1;
let name = '';
while (j < xml.length) {
const ch = xml[j];
if (ch === '>' || /\s/.test(ch)) {
break;
}
name += ch;
j++;
}
if (!name) {
return null;
}
const openEnd = xml.indexOf('>', j);
if (openEnd < 0) {
return null;
}
const closeToken = '</' + name + '>';
const closeIdx = xml.indexOf(closeToken, openEnd + 1);
if (closeIdx < 0) {
return null;
}
return [openEnd + 1, closeIdx];
}
return null;
}
/**
* 在指定容器范围内查找给定标签的直接子节点范围
* @param xml
* @param tag
* @param containerStart 起始(含)
* @param containerEnd 结束(不含)
*/
function findDirectChildRange(xml: string, tag: string, containerStart: number, containerEnd: number): [start: number, endExclusive: number] | null {
let pos = containerStart;
let depth = 0;
while (pos < containerEnd) {
const lt = xml.indexOf('<', pos);
if (lt < 0 || lt >= containerEnd) {
break;
}
if (xml.startsWith('</', lt)) {
// 关闭标签,深度-1
const gt = xml.indexOf('>', lt + 2);
if (gt < 0 || gt >= containerEnd) {
break;
}
depth = Math.max(0, depth - 1);
pos = gt + 1;
continue;
}
// 开始标签,读取名称
let j = lt + 1;
let name = '';
while (j < containerEnd) {
const ch = xml[j];
if (ch === '>' || /\s/.test(ch) || ch === '/') {
break;
}
name += ch;
j++;
}
const gt = xml.indexOf('>', j);
if (gt < 0 || gt >= containerEnd) {
break;
}
const isSelfClose = xml[gt - 1] === '/';
if (depth === 0 && name === tag && !isSelfClose) {
const closeToken = '</' + tag + '>';
const closeIdx = xml.indexOf(closeToken, gt + 1);
if (closeIdx >= 0 && closeIdx < containerEnd) {
return [lt, closeIdx + closeToken.length];
}
return null;
}
if (!isSelfClose) {
depth++;
}
pos = gt + 1;
}
return null;
}
/**
* 将指定xml字符串中的指定标签的第一个节点替换为指定字符串
* @param xml xml字符串
* @param tagPath 标签名或路径(如 a.b.c),不含<>
* @param s 新的字符串
* @returns {string} 替换后的xml字符串
*/
export function replaceFirstTag(xml: string, tagPath: string, s: string): string {
let start = -1, endExclusive = -1;
const range = findTagPathRange(xml, tagPath);
if (range) {
start = range[0];
endExclusive = range[1];
}
if (start >= 0 && endExclusive >= 0) {
if (s) {
xml = xml.substring(0, start) + s + xml.substring(endExclusive);
} else {
xml = xml.substring(0, start).trim() + xml.substring(endExclusive);
}
}
return xml;
}
/**
* 在指定xml字符串中的指定标签的第一个节点后插入指定字符串。指定标签为数组时,依次判断标签是否存在,存在时在其后插入,否则检索下一个标签
* @param xml xml字符串
* @param tags 标签名清单
* @param s 要插入的字符串
*/
export function insertAfterTag(xml: string, tags: string | string[], s: string): string {
if (!Array.isArray(tags)) {
tags = [tags];
}
let index = -1;
for (let tag of tags) {
let tagEnd = '</' + tag + '>';
index = xml.indexOf(tagEnd);
if (index >= 0) {
index += tagEnd.length;
break;
}
}
if (index >= 0) {
if (s && !s.startsWith('\n')) {
s = '\n' + s;
}
xml = xml.substring(0, index) + s + xml.substring(index);
}
return xml;
}
export function setTag(xml: string, replaceTag: string, tagXml: string, afterTags: string | string[], afterIndent = 4): string {
tagXml = tagXml.trim();
if (containsTag(xml, replaceTag)) {
xml = replaceFirstTag(xml, replaceTag, tagXml);
} else if (tagXml) {
xml = insertAfterTag(xml, afterTags, ' '.repeat(afterIndent) + tagXml);
}
if (!xml.endsWith('\n')) {
xml += '\n';
}
return xml;
}
export function toFormattedXml(
object: Record<string, any>,
indent = 0,
ignoreBlank?: boolean,
keyMapper?: (key: string) => string,
valueMapper?: (value: any, key: string) => any
) {
let xml = '';
if (object && !Array.isArray(object) && typeof object === 'object') {
let indentPrefix = ' '.repeat(indent);
let keys = Object.keys(object);
for (let key of keys) {
let value = object[key];
if (value === undefined || value === null) {
continue;
}
// 以英文字母开头的才是有效字段,可转换为合法的xml标签
if (/^[A-Za-z]/.test(key)) {
let subKeyMapper = undefined;
if (typeof keyMapper === 'function') {
key = keyMapper(key);
if (typeof key !== 'string') {
continue;
}
subKeyMapper = function (subKey: string): string {
return keyMapper(key + '.' + subKey);
}
}
if (typeof valueMapper === 'function') {
value = valueMapper(value, key);
}
let items = Array.isArray(value) ? value : [value];
for (let item of items) {
if (ignoreBlank && typeof item === 'string' && !item.trim()) {
continue;
}
xml += indentPrefix + '<' + key + '>';
if (item instanceof Date) {
item = item.formatDateTime();
}
if (typeof item === 'object' && !(item instanceof Enum)) {
xml += '\n' + toFormattedXml(item, indent + 4, ignoreBlank,
subKeyMapper, valueMapper) + indentPrefix;
} else {
xml += item;
}
xml += '</' + key + '>\n';
}
}
}
}
return xml;
}