UNPKG

@truenewx/tnxet

Version:

互联网技术解决方案:Electron扩展支持

275 lines (257 loc) 8.7 kB
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; }