Files
AutoJs6/.utils/utils/anchors.mjs
2025-09-09 17:24:48 +08:00

264 lines
10 KiB
JavaScript

// utils/anchors.mjs
import * as fsp from 'node:fs/promises';
import * as path from 'node:path';
import { toUpdatedStamp } from './date.mjs';
/**
* @param {string} s
* @returns {string}
*/
const normalize = s => String(s).replace(/\s+/g, '');
/**
* 在指定 Anchor 块中, 用给定的替换函数生成新块内容.
*
* @param {string} src
* @param {string} anchorTag
* @param {(block: string) => { newBlock: string, changed: boolean }} replaceBlockFn
* @returns {{ src: string, changed: boolean }} - 返回 { src: 新源码, changed: 是否发生变更 }. 若找不到锚点, 原样返回.
*/
export function replaceInAnchoredBlock(src, anchorTag, replaceBlockFn) {
const beginTag = `// @AnchorBegin ${anchorTag}`;
const endTag = `// @AnchorEnd ${anchorTag}`;
const beginIdx = src.indexOf(beginTag);
if (beginIdx === -1) return { src, changed: false };
const endIdx = src.indexOf(endTag, beginIdx + beginTag.length);
if (endIdx === -1) return { src, changed: false };
const before = src.slice(0, beginIdx);
const block = src.slice(beginIdx, endIdx); // 不包含 endTag
const after = src.slice(endIdx);
const { newBlock, changed } = replaceBlockFn(block) || {};
if (!changed || !newBlock) return { src, changed: false };
return { src: before + newBlock + after, changed };
}
/**
* 替换锚点块中的某个 map 声明 (如 mapOf(...)), 并在变更时自动刷新 @Updated 日期.
*
* @param {string} src
* @param {Object} options
* @param {string} options.anchorTag - 块的锚点名
* @param {string} options.mapName - 变量名, 如 agpVersionMap
* @param {string[]} options.lines - map 体内的每行 (不含缩进, 由函数自动缩进)
* @param {number} [options.linesIndent=4]
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - 自定义时间戳函数 (可选)
* @returns {{ src: string, changed: boolean }}}
*/
export function replaceAnchoredMapBlock(src, {
anchorTag,
mapName,
lines,
linesIndent = 4,
toUpdatedStamp: toStamp = toUpdatedStamp,
}) {
return replaceInAnchoredBlock(src, anchorTag, (block) => {
let changed = false;
const re = new RegExp(`([\\t\\x20]*)(va[lr]\\s+)?${mapName}\\s*=\\s*mapOf\\([\\s\\S]*?\\)(,?)`, 'm');
let updatedBlock = block.replace(re, (/** @type {string} */ original, /** @type {string} */ indent, /** @type {string} */ keyword, /** @type {string} */ comma) => {
const kw = keyword ?? '';
const body = lines.map(l => `${' '.repeat(linesIndent)}${indent}${l}`).join('\n');
const next = `${indent}${kw}${mapName} = mapOf(\n${body}\n${indent})${comma}`;
if (normalize(original) !== normalize(next)) changed = true;
return next;
});
if (changed) {
updatedBlock = updatedBlock.replace(
/(@Updated[^\n]*?\son\s)([A-Z][a-z]{2}\s\d{1,2},\s\d{4})(\.?)/,
(_, p1, _old, p3) => `${p1}${(toStamp())}${p3}`,
);
}
return { newBlock: updatedBlock, changed };
});
}
/**
* 替换锚点块中的某个 list 声明 (如 listOf(...)), 并在变更时自动刷新 @Updated 日期.
*
* @param {string} src
* @param {Object} options
* @param {string} options.anchorTag - 块的锚点名
* @param {string} options.listName - 变量名, 如 modules 或 libs
* @param {string[]} options.lines - list 体内的每行 (不含缩进, 由函数自动缩进)
* @param {number} [options.linesIndent=4]
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - 自定义时间戳函数 (可选)
* @returns {{ src: string, changed: boolean }}}
*/
export function replaceAnchoredListBlock(src, {
anchorTag,
listName,
lines,
linesIndent = 4,
toUpdatedStamp: toStamp = toUpdatedStamp,
}) {
return replaceInAnchoredBlock(src, anchorTag, (block) => {
let changed = false;
const re = new RegExp(`([\\t\\x20]*)(va[lr]\\s+)?${listName}\\s*=\\s*listOf\\([\\s\\S]*?\\)(,?)`, 'm');
let updatedBlock = block.replace(re, (original, indent, keyword, comma) => {
const kw = keyword ?? '';
const body = lines.map(l => `${' '.repeat(linesIndent)}${indent}${l}`).join('\n');
const next = `${indent}${kw}${listName} = listOf(\n${body}\n${indent})${comma}`;
if (normalize(original) !== normalize(next)) changed = true;
return next;
});
if (changed) {
updatedBlock = updatedBlock.replace(
/(@Updated[^\n]*?\son\s)([A-Z][a-z]{2}\s\d{1,2},\s\d{4})(\.?)/,
(_, p1, _old, p3) => `${p1}${(toStamp())}${p3}`,
);
}
return { newBlock: updatedBlock, changed };
});
}
/**
* 高层封装: 读取文件 -> 替换锚点 map -> 若有变更则写回 -> 打印日志.
*
* @param {string} filePath
* @param {Object} options
* @param {string} options.anchorTag - 块的锚点名
* @param {string} options.mapName - 变量名, 如 agpVersionMap
* @param {string[]} options.lines - map 体内的每行 (不含缩进, 由函数自动缩进)
* @param {number} [options.linesIndent=4]
* @param {string} [options.updatedLabel='']
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - 自定义时间戳函数 (可选)
* @param {Console} [options.logger=console]
* @returns {Promise<{ changed: boolean, content: string }>}
*/
export async function updateAnchoredMapInFile(filePath, {
anchorTag,
mapName,
lines,
linesIndent = 4,
updatedLabel = '',
toUpdatedStamp: toStamp = toUpdatedStamp,
logger = console,
}) {
const filename = path.basename(filePath);
const raw = await fsp.readFile(filePath, 'utf8');
const { src: updated, changed } = replaceAnchoredMapBlock(raw, { anchorTag, mapName, lines, linesIndent, toUpdatedStamp: toStamp });
if (changed) {
await fsp.writeFile(filePath, updated, 'utf8');
logger.log(`[${filename}] 已更新` + (updatedLabel ? ` (${updatedLabel})` : ''));
} else {
// logger.log(`[${filename}] 无需更新` + (updatedLabel ? ` (${updatedLabel})` : ''));
}
return { changed, content: updated };
}
/**
* 高层封装: 读取文件 -> 替换锚点 list -> 若有变更则写回 -> 打印日志.
*
* @param {string} filePath
* @param {Object} options
* @param {string} options.anchorTag - 块的锚点名
* @param {string} options.listName - 变量名, 如 modules 或 libs
* @param {string[]} options.lines - list 体内的每行 (不含缩进, 由函数自动缩进)
* @param {number} [options.linesIndent=4]
* @param {string} [options.updatedLabel='']
* @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - 自定义时间戳函数 (可选)
* @param {Console} [options.logger=console]
* @returns {Promise<{ changed: boolean, content: string }>}
*/
export async function updateAnchoredListInFile(filePath, {
anchorTag,
listName,
lines,
linesIndent = 4,
updatedLabel = '',
toUpdatedStamp: toStamp = toUpdatedStamp,
logger = console,
}) {
const filename = path.basename(filePath);
const raw = await fsp.readFile(filePath, 'utf8');
const { src: updated, changed } = replaceAnchoredListBlock(raw, { anchorTag, listName, lines, linesIndent, toUpdatedStamp: toStamp });
if (changed) {
await fsp.writeFile(filePath, updated, 'utf8');
logger.log(`[${filename}] 已更新` + (updatedLabel ? ` (${updatedLabel})` : ''));
} else {
// logger.log(`[${filename}] 无需更新` + (updatedLabel ? ` (${updatedLabel})` : ''));
}
return { changed, content: updated };
}
/**
* @typedef {Object} AnchoredBlockUpdateOption
* @property {'map' | 'list' | 'custom'} type
* @property {string} anchorTag
* @property {string} [mapName]
* @property {string} [listName]
* @property {string[]} lines
* @property {number} [linesIndent=4]
* @property {string} [updatedLabel]
* @property {(srcInBlock: string, options: { toUpdatedStamp?: (date?: Date) => string }) => { newBlock: string, changed: boolean }} [replacer]
*/
/**
* 批量在同一文件内进行多锚点替换 (map 与 list 都支持, 读一次/写一次).
*
* @param {string} filePath
* @param {AnchoredBlockUpdateOption[]} optionList
* @param {Object} [extraOptions={}]
* @param {(date?: Date) => string} [extraOptions.toUpdatedStamp=toUpdatedStamp] - 自定义时间戳函数 (可选)
* @param {Console} [extraOptions.logger=console]
* @returns {Promise<{ changed: boolean, content: string }>}
*/
export async function batchUpdateAnchoredBlocks(filePath, optionList, {
toUpdatedStamp: toStamp = toUpdatedStamp,
logger = console,
} = {}) {
const filename = path.basename(filePath);
let raw = await fsp.readFile(filePath, 'utf8');
let changedAny = false;
for (const op of optionList) {
let res = { src: raw, changed: false };
if (op.type === 'map') {
res = replaceAnchoredMapBlock(raw, {
anchorTag: op.anchorTag,
mapName: op.mapName,
lines: op.lines,
linesIndent: op.linesIndent,
toUpdatedStamp: toStamp,
});
} else if (op.type === 'list') {
res = replaceAnchoredListBlock(raw, {
anchorTag: op.anchorTag,
listName: op.listName,
lines: op.lines,
linesIndent: op.linesIndent,
toUpdatedStamp: toStamp,
});
} else if (op.type === 'custom' && typeof op.replacer === 'function') {
res = replaceInAnchoredBlock(raw, op.anchorTag, (block) => op.replacer(block, { toUpdatedStamp: toStamp }));
} else {
logger.warn(`[${filename}] 未知操作类型或缺少参数:`, op);
continue;
}
if (res.changed) {
changedAny = true;
raw = res.src;
logger.log(`[${filename}] 已更新 (${op['updatedLabel'] ?? op.anchorTag})`);
} else {
// logger.log(`[${filename}] 无需更新 (${op['updatedLabel'] ?? op.anchorTag})`);
}
}
if (changedAny) {
await fsp.writeFile(filePath, raw, 'utf8');
}
return { changed: changedAny, content: raw };
}