obsidian-douban/src/org/wanxp/utils/VariableUtil.ts
callmer00t d35dcfe513 feat: 支持自定义数组字段长度限制
为豆瓣拉取内容中数组类型的字段(actor、director、author、translator、aliases、menu 等)
新增按内容类型可配置的最大数量限制, 解决电影演员等列表过长导致笔记冗余的问题。

- 新增设置项 arrayLengthLimits, 支持按 type(电影/电视剧/书籍/音乐/游戏/笔记/戏剧/全部)
  + field + limit 三元组进行配置
- 在 VariableUtil.replaceSubject 中央渲染入口集中截断, 自动覆盖所有类型的所有数组字段
- 在设置面板新增 "数组限制" 页签, 提供添加/删除/编辑 UI
- 同字段同时存在 type=all 和具体类型时取较小限制值
- 新增对应中英文文案
2026-05-19 20:21:20 +08:00

357 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SettingsManager from "../douban/setting/SettingsManager";
import {ArraySetting, DEFAULT_SETTINGS_ARRAY_NAME} from "../douban/setting/model/ArraySetting";
import StringUtil from "./StringUtil";
import YamlUtil from "./YamlUtil";
import {log} from "./Logutil";
import {i18nHelper} from "../lang/helper";
import {DataValueType, SupportType} from "../constant/Constsant";
import {DataField} from "./model/DataField";
import {FieldVariable} from "./model/FieldVariable";
import {CustomProperty} from "../douban/setting/model/CustomProperty";
import {FileUtil} from "./FileUtil";
import {ArrayLengthLimit} from "../douban/setting/model/ArrayLengthLimit";
type TargetType = 'text' | 'path' | 'yml_text';
export class VariableUtil {
/**
*
* @param obj
* @param content
* @param subjectType
* @param settingManager
* @param targetType
*/
static replaceSubject(obj: any, content: string, subjectType: SupportType, settingManager:SettingsManager, targetType: TargetType): string {
if (!content || !obj) {
return content;
}
const allVariables = this.getAllVariables(content, settingManager);
if (!allVariables || allVariables.length == 0) {
return content;
}
if (obj instanceof Map) {
this.applyArrayLengthLimits(subjectType, obj, settingManager);
this.handleCustomVariable(subjectType, obj, settingManager, 'text')
content = this.replaceMap(obj, allVariables, content, settingManager, targetType);
}else {
const map = this.objToMap(obj);
this.applyArrayLengthLimits(subjectType, map, settingManager);
this.handleCustomVariable(subjectType, map, settingManager, 'text')
content = this.replaceMap(map, allVariables, content, settingManager, targetType);
}
return content;
}
/**
*
* @param obj
* @param content
* @param settingManager
* @param targetType
*/
static replace(obj: any, content: string, settingManager:SettingsManager, targetType : TargetType): string {
if (!content || !obj) {
return content;
}
const allVariables = this.getAllVariables(content, settingManager);
if (!allVariables || allVariables.length == 0) {
return content;
}
if (obj instanceof Map) {
content = this.replaceMap(obj, allVariables, content, settingManager, targetType);
}else {
const map = this.objToMap(obj);
content = this.replaceMap(map, allVariables, content, settingManager, targetType); }
return content;
}
/**
* <h3>type</h3>
* <li>Number</li>
* <li>String</li>
* <li>Array</li>
*
* <h3>name</h3>
* <li> aliases</li>
* @param variable
* @param value
* @param content
* @param settingManager
* @param targetType
*/
static replaceVariable(variable: FieldVariable, value: any, content: string, settingManager:SettingsManager, targetType: TargetType): string {
if (!content) {
return content;
}
//根据value的类型替换对应的变量
if (value instanceof Array) {
content = this.replaceArray(variable, value, content, settingManager, targetType);
} else if(value instanceof DataField) {
content = this.replaceDataField(variable, value, content, settingManager, targetType);
} else {
content = this.replaceString(variable, value, value, content, settingManager, targetType);
}
return content;
}
static replaceArray(variable: FieldVariable, value: any[], content: string, settingManager:SettingsManager, targetType: TargetType): string {
if (!content) {
return content;
}
const variableStr = variable.variable;
const outTypeName = variable.outTypeName;
if (!value) {
return content.replaceAll(variableStr, "");
}
const arraySettings = this.getArraySetting(outTypeName, settingManager);
if (!arraySettings) {
log.warn(i18nHelper.getMessage(`130107`, variable.variable, outTypeName));
return content;
}
const strValues:string[] = value.map((v) => {
if (typeof v === 'string') {
return v;
} else {
return v?v.toString() : null;
}
})
.filter(v => v)
.map(v => this.handleText(v, targetType))
;
const arrayValue = StringUtil.handleArray(strValues, arraySettings);
content = content.replaceAll(variableStr, arrayValue);
return content;
}
static keyToVariable(key: string): string {
return `{{${key}}}`;
}
static replaceString(variable: FieldVariable, valueField: DataField, value: any, content: string, settingManager:SettingsManager, targetType: TargetType): string {
if (!content) {
return content;
}
let strValue = value? value.toString() : "";
return content.replaceAll(variable.variable, this.handleText(strValue, targetType, valueField));
}
/**
* 从key中提取 arrayName, 然后从settings中获取对应的ArraySetting
* @private
* @param content
* @param settingManager
*/
private static getAllVariables(content: string, settingManager:SettingsManager): FieldVariable[] {
const reg =/\{\{[a-zA-Z-0-9_.]+([(a-zA-Z-0-9)]+)?}}/g
const result = content.match(reg);
if (!result) {
return [];
}
return result.map((v) => {
const reg2 = new RegExp(`[a-zA-Z-0-9_.]+`, 'g');
const result2 = v.match(reg2);
if (!result2) {
return null;
}
if (result2.length == 1) {
return new FieldVariable(result2[0], v, null);
}
return new FieldVariable(result2[0], v, result2[1]);
}).filter(v => v);
}
/**
* 从key中提取 arrayName, 然后从settings中获取对应的ArraySetting
* @param outTypeName
* @param settingManager
* @private
*/
private static getArraySetting(outTypeName: string, settingManager:SettingsManager): ArraySetting {
if (!outTypeName) {
return settingManager.getArraySetting(DEFAULT_SETTINGS_ARRAY_NAME);
} else {
return settingManager.getArraySetting(outTypeName);
}
}
private static replaceMap(obj: Map<string, any>, allVariables:FieldVariable[], content: string, settingManager: SettingsManager, targetType: TargetType) {
allVariables.forEach(variable => {
const value = obj.get(variable.key);
content = this.replaceVariable(variable, value, content, settingManager, targetType);
});
return content;
}
static getType(value: any):DataValueType {
if (typeof value === 'number') {
return DataValueType.number;
} else if (typeof value === 'string') {
if (value.startsWith('http://') || value.startsWith('https://')) {
return DataValueType.url;
}else if (/^(\/|\.\/|\.\.\/|~\/|.*\.[a-zA-Z0-9]+$)/.test(value)) {
return DataValueType.path;
}
return DataValueType.string;
} else if (value instanceof Date) {
return DataValueType.date;
} else if (value instanceof Array) {
return DataValueType.array;
} else {
return DataValueType.string;
}
}
private static replaceDataField(variable: FieldVariable, value: DataField, content: string, settingManager: SettingsManager, targetType: TargetType) {
if (!content) {
return content;
}
const variableStr = variable.variable;
if (!value) {
return content.replaceAll(variableStr, "");
}
switch (value.type) {
case DataValueType.string:
content = this.replaceString(variable, value, value.value, content, settingManager, targetType);
break;
case DataValueType.number:
content = content.replaceAll(variableStr, this.handleText(value.value.toString(), targetType, value));
break;
case DataValueType.date:
content = content.replaceAll(variableStr, this.handleText(value.value, targetType, value));
break;
case DataValueType.array:
content = this.replaceArray(variable, value.value, content, settingManager, targetType);
break;
default:
content = content.replaceAll(variableStr, this.handleText(value.value, targetType, value));
break;
}
return content;
}
/**
* 处理自定义参数
* @private
* @param subjectType
* @param variableMap
* @param settingMananger
* @param targetType
*/
static handleCustomVariable(subjectType: SupportType, variableMap:Map<string, DataField>, settingMananger: SettingsManager, targetType:TargetType): void {
// @ts-ignore
const customProperties: CustomProperty[] = settingMananger.getSetting('customProperties');
if (!customProperties) {
return ;
}
const customPropertiesMap= new Map();
customProperties.filter(customProperty => customProperty.name &&
customProperty.field
&& (customProperty.field.toLowerCase() == SupportType.all ||
customProperty.field.toLowerCase() == subjectType)).forEach(customProperty => {
customPropertiesMap.set(customProperty.name, customProperty.value);
});
customPropertiesMap.forEach((value, key) => {
variableMap.set(key,
new DataField(
key, DataValueType.string, value,
VariableUtil.replace(variableMap, value, settingMananger, targetType)));
})
}
private static objToMap(obj: any):Map<string, any> {
const map = new Map<string, any>();
Object.keys(obj).forEach(key => {
map.set(key, obj[key]);
});
return map;
}
/**
* 根据用户配置的 ArrayLengthLimit 截断 variableMap 中匹配的数组字段。
*
* 仅对 DataField.value 为数组的字段生效。当 limit > 0 且数组长度大于 limit 时,
* 会取前 limit 个元素生成新的 DataField 替换原值; origin 保持不变以便后续逻辑。
*
* 当 type=all 时, 该限制作用于所有内容类型, 否则仅作用于 type 与当前 subjectType 相等的条目。
*
* @param subjectType 当前内容类型
* @param variableMap 变量映射
* @param settingManager
*/
static applyArrayLengthLimits(subjectType: SupportType,
variableMap: Map<string, DataField>,
settingManager: SettingsManager): void {
if (!variableMap || variableMap.size == 0) {
return;
}
// @ts-ignore
const limits: ArrayLengthLimit[] = settingManager.getSetting('arrayLengthLimits');
if (!limits || limits.length == 0) {
return;
}
const effectiveLimits: Map<string, number> = new Map();
limits
.filter(l => l && l.field && l.limit && l.limit > 0)
.filter(l => {
if (!l.type) {
return false;
}
const t = String(l.type).toLowerCase();
return t === SupportType.all || t === subjectType;
})
.forEach(l => {
const existing = effectiveLimits.get(l.field);
// 当同一字段同时存在 type=all 和 type=具体 的限制时,
// 优先使用更具体的(更小的)限制值, 行为更符合用户预期。
if (existing == null || l.limit < existing) {
effectiveLimits.set(l.field, l.limit);
}
});
if (effectiveLimits.size == 0) {
return;
}
effectiveLimits.forEach((limit, fieldName) => {
const dataField = variableMap.get(fieldName);
if (!dataField) {
return;
}
const value = dataField.value;
if (Array.isArray(value) && value.length > limit) {
const truncated = value.slice(0, limit);
variableMap.set(fieldName, new DataField(
dataField.name,
dataField.type,
dataField.origin,
truncated
));
}
});
}
private static handleText(v: string, targetType: TargetType, dataField: DataField = null): string {
if (targetType === 'yml_text') {
return YamlUtil.handleText(v, dataField);
}
if (targetType === 'text') {
return v;
}
if (targetType === 'path') {
return FileUtil.replaceSpecialCharactersForFileName(v);
}
}
}