mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 22:25:37 +08:00
feat(config): 添加配置验证和YAML解析功能
- 实现了配置模式解析器,支持递归对象/数组/标量树结构 - 添加了可编辑字段收集功能,支持标量和数组类型的批量编辑 - 集成了YAML解析器,支持嵌套对象、数组和注释提取 - 实现了配置验证诊断,支持中英文本地化错误消息 - 添加了表单更新应用功能,支持安全的嵌套对象编辑 - 实现了示例配置生成功能,包含模式描述作为YAML注释 - 提供了批量数组值解析和枚举值标准化工具函数 - 集成了多语言支持,包含中英文验证消息本地化
This commit is contained in:
parent
55602b16f2
commit
3c52c8c1ea
64
tools/gframework-config-tool/src/configPath.js
Normal file
64
tools/gframework-config-tool/src/configPath.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Join one object property onto a logical config path.
|
||||||
|
*
|
||||||
|
* @param {string} parentPath Parent logical path.
|
||||||
|
* @param {string} propertyName Property name.
|
||||||
|
* @returns {string} Combined logical path.
|
||||||
|
*/
|
||||||
|
function joinPropertyPath(parentPath, propertyName) {
|
||||||
|
return parentPath ? `${parentPath}.${propertyName}` : propertyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join one indexed array item onto a logical config path.
|
||||||
|
*
|
||||||
|
* @param {string} arrayPath Array logical path.
|
||||||
|
* @param {number} itemIndex Zero-based item index.
|
||||||
|
* @returns {string} Indexed logical path.
|
||||||
|
*/
|
||||||
|
function joinArrayIndexPath(arrayPath, itemIndex) {
|
||||||
|
return `${arrayPath}[${itemIndex}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join one array-item template marker onto a logical config path.
|
||||||
|
*
|
||||||
|
* @param {string} arrayPath Array logical path.
|
||||||
|
* @returns {string} Template logical path.
|
||||||
|
*/
|
||||||
|
function joinArrayTemplatePath(arrayPath) {
|
||||||
|
return `${arrayPath}[]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a logical path still contains one template array marker.
|
||||||
|
*
|
||||||
|
* @param {string} path Logical path.
|
||||||
|
* @returns {boolean} True when the path contains a template array segment.
|
||||||
|
*/
|
||||||
|
function isTemplatePath(path) {
|
||||||
|
return String(path).includes("[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split one logical object path into individual property segments.
|
||||||
|
* The current form model only supports dotted object paths here and keeps
|
||||||
|
* array indexing as part of other dedicated helpers.
|
||||||
|
*
|
||||||
|
* @param {string} path Logical path.
|
||||||
|
* @returns {string[]} Property segments.
|
||||||
|
*/
|
||||||
|
function splitObjectPath(path) {
|
||||||
|
return String(path)
|
||||||
|
.split(".")
|
||||||
|
.map((segment) => segment.trim())
|
||||||
|
.filter((segment) => segment.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isTemplatePath,
|
||||||
|
joinArrayIndexPath,
|
||||||
|
joinArrayTemplatePath,
|
||||||
|
joinPropertyPath,
|
||||||
|
splitObjectPath
|
||||||
|
};
|
||||||
@ -1,3 +1,11 @@
|
|||||||
|
const {
|
||||||
|
joinArrayIndexPath,
|
||||||
|
joinArrayTemplatePath,
|
||||||
|
joinPropertyPath,
|
||||||
|
splitObjectPath
|
||||||
|
} = require("./configPath");
|
||||||
|
const {ValidationMessageKeys} = require("./localizationKeys");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the repository's minimal config-schema subset into a recursive tree.
|
* Parse the repository's minimal config-schema subset into a recursive tree.
|
||||||
* The parser intentionally mirrors the same high-level contract used by the
|
* The parser intentionally mirrors the same high-level contract used by the
|
||||||
@ -137,7 +145,7 @@ function extractYamlComments(text) {
|
|||||||
|
|
||||||
const itemIndex = currentContext.nextIndex || 0;
|
const itemIndex = currentContext.nextIndex || 0;
|
||||||
currentContext.nextIndex = itemIndex + 1;
|
currentContext.nextIndex = itemIndex + 1;
|
||||||
const itemPath = `${currentContext.path}[${itemIndex}]`;
|
const itemPath = joinArrayIndexPath(currentContext.path, itemIndex);
|
||||||
assignPendingComments(comments, itemPath, pendingComments);
|
assignPendingComments(comments, itemPath, pendingComments);
|
||||||
pendingComments = [];
|
pendingComments = [];
|
||||||
|
|
||||||
@ -150,37 +158,37 @@ function extractYamlComments(text) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inlineObjectMatch = /^([A-Za-z0-9_]+):(.*)$/u.exec(rest);
|
const inlineObjectMapping = parseYamlMappingText(rest);
|
||||||
if (!inlineObjectMatch) {
|
if (!inlineObjectMapping) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemObjectContext = {indent: indent + 2, type: "object", path: itemPath, nextIndex: 0};
|
const itemObjectContext = {indent: indent + 2, type: "object", path: itemPath, nextIndex: 0};
|
||||||
stack.push(itemObjectContext);
|
stack.push(itemObjectContext);
|
||||||
|
|
||||||
const key = inlineObjectMatch[1];
|
const key = inlineObjectMapping.key;
|
||||||
const parsedValue = splitYamlValueAndInlineComment(inlineObjectMatch[2].trim());
|
const parsedValue = splitYamlValueAndInlineComment(inlineObjectMapping.rawValue.trim());
|
||||||
if (parsedValue.comment) {
|
if (parsedValue.comment) {
|
||||||
comments[`${itemPath}.${key}`] = parsedValue.comment;
|
comments[joinPropertyPath(itemPath, key)] = parsedValue.comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextLine = findNextMeaningfulLine(lines, index + 1);
|
const nextLine = findNextMeaningfulLine(lines, index + 1);
|
||||||
if (parsedValue.value.length === 0 && nextLine && nextLine.indent > indent) {
|
if (parsedValue.value.length === 0 && nextLine && nextLine.indent > indent) {
|
||||||
stack.push(createContextForChild(`${itemPath}.${key}`, nextLine));
|
stack.push(createContextForChild(joinPropertyPath(itemPath, key), nextLine));
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = /^([A-Za-z0-9_]+):(.*)$/u.exec(trimmed);
|
const mapping = parseYamlMappingText(trimmed);
|
||||||
if (!match) {
|
if (!mapping) {
|
||||||
pendingComments = [];
|
pendingComments = [];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = match[1];
|
const key = mapping.key;
|
||||||
const valueInfo = splitYamlValueAndInlineComment(match[2].trim());
|
const valueInfo = splitYamlValueAndInlineComment(mapping.rawValue.trim());
|
||||||
const currentPath = currentContext.path ? `${currentContext.path}.${key}` : key;
|
const currentPath = joinPropertyPath(currentContext.path, key);
|
||||||
assignPendingComments(comments, currentPath, pendingComments);
|
assignPendingComments(comments, currentPath, pendingComments);
|
||||||
pendingComments = [];
|
pendingComments = [];
|
||||||
|
|
||||||
@ -282,16 +290,16 @@ function applyFormUpdates(originalYaml, updates) {
|
|||||||
const commentUpdates = updates.comments || {};
|
const commentUpdates = updates.comments || {};
|
||||||
|
|
||||||
for (const [path, value] of Object.entries(scalarUpdates)) {
|
for (const [path, value] of Object.entries(scalarUpdates)) {
|
||||||
setNodeAtPath(root, path.split("."), createScalarNode(String(value)));
|
setNodeAtPath(root, splitObjectPath(path), createScalarNode(String(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [path, values] of Object.entries(arrayUpdates)) {
|
for (const [path, values] of Object.entries(arrayUpdates)) {
|
||||||
setNodeAtPath(root, path.split("."), createArrayNode(
|
setNodeAtPath(root, splitObjectPath(path), createArrayNode(
|
||||||
(values || []).map((item) => createScalarNode(String(item)))));
|
(values || []).map((item) => createScalarNode(String(item)))));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [path, items] of Object.entries(objectArrayUpdates)) {
|
for (const [path, items] of Object.entries(objectArrayUpdates)) {
|
||||||
setNodeAtPath(root, path.split("."), createArrayNode(
|
setNodeAtPath(root, splitObjectPath(path), createArrayNode(
|
||||||
(items || []).map((item) => createNodeFromFormValue(item))));
|
(items || []).map((item) => createNodeFromFormValue(item))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,7 +448,7 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
: [];
|
: [];
|
||||||
const properties = {};
|
const properties = {};
|
||||||
for (const [key, propertyNode] of Object.entries(value.properties || {})) {
|
for (const [key, propertyNode] of Object.entries(value.properties || {})) {
|
||||||
properties[key] = parseSchemaNode(propertyNode, combinePath(displayPath, key));
|
properties[key] = parseSchemaNode(propertyNode, joinPropertyPath(displayPath, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -455,7 +463,7 @@ function parseSchemaNode(rawNode, displayPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === "array") {
|
if (type === "array") {
|
||||||
const itemNode = parseSchemaNode(value.items || {}, `${displayPath}[]`);
|
const itemNode = parseSchemaNode(value.items || {}, joinArrayTemplatePath(displayPath));
|
||||||
return {
|
return {
|
||||||
type: "array",
|
type: "array",
|
||||||
displayPath,
|
displayPath,
|
||||||
@ -497,7 +505,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
if (!yamlNode || yamlNode.kind !== "array") {
|
if (!yamlNode || yamlNode.kind !== "array") {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage("expectedArray", localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.expectedArray, localizer, {
|
||||||
displayPath
|
displayPath
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@ -505,7 +513,12 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
||||||
validateNode(schemaNode.items, yamlNode.items[index], `${displayPath}[${index}]`, diagnostics, localizer);
|
validateNode(
|
||||||
|
schemaNode.items,
|
||||||
|
yamlNode.items[index],
|
||||||
|
joinArrayIndexPath(displayPath, index),
|
||||||
|
diagnostics,
|
||||||
|
localizer);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -513,7 +526,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
if (!yamlNode || yamlNode.kind !== "scalar") {
|
if (!yamlNode || yamlNode.kind !== "scalar") {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage("expectedScalarShape", localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.expectedScalarShape, localizer, {
|
||||||
displayPath,
|
displayPath,
|
||||||
schemaType: schemaNode.type,
|
schemaType: schemaNode.type,
|
||||||
yamlKind: yamlNode ? yamlNode.kind : "missing"
|
yamlKind: yamlNode ? yamlNode.kind : "missing"
|
||||||
@ -525,7 +538,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
if (!isScalarCompatible(schemaNode.type, yamlNode.value)) {
|
if (!isScalarCompatible(schemaNode.type, yamlNode.value)) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage("expectedScalarValue", localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.expectedScalarValue, localizer, {
|
||||||
displayPath,
|
displayPath,
|
||||||
schemaType: schemaNode.type
|
schemaType: schemaNode.type
|
||||||
})
|
})
|
||||||
@ -538,7 +551,7 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
!schemaNode.enumValues.includes(unquoteScalar(yamlNode.value))) {
|
!schemaNode.enumValues.includes(unquoteScalar(yamlNode.value))) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage("enumMismatch", localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.enumMismatch, localizer, {
|
||||||
displayPath,
|
displayPath,
|
||||||
values: schemaNode.enumValues.join(", ")
|
values: schemaNode.enumValues.join(", ")
|
||||||
})
|
})
|
||||||
@ -557,10 +570,16 @@ function validateNode(schemaNode, yamlNode, displayPath, diagnostics, localizer)
|
|||||||
*/
|
*/
|
||||||
function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) {
|
function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, localizer) {
|
||||||
if (!yamlNode || yamlNode.kind !== "object") {
|
if (!yamlNode || yamlNode.kind !== "object") {
|
||||||
const subject = displayPath.length === 0 ? "Root object" : `Property '${displayPath}'`;
|
const subject = displayPath.length === 0
|
||||||
|
? localizer && localizer.isChinese
|
||||||
|
? "根对象应为对象。"
|
||||||
|
: "Root object is expected to be an object."
|
||||||
|
: localizer && localizer.isChinese
|
||||||
|
? `属性“${displayPath}”应为对象。`
|
||||||
|
: `Property '${displayPath}' is expected to be an object.`;
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage("expectedObject", localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.expectedObject, localizer, {
|
||||||
subject,
|
subject,
|
||||||
displayPath
|
displayPath
|
||||||
})
|
})
|
||||||
@ -572,8 +591,8 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
if (!yamlNode.map.has(requiredProperty)) {
|
if (!yamlNode.map.has(requiredProperty)) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage("missingRequired", localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.missingRequired, localizer, {
|
||||||
displayPath: combinePath(displayPath, requiredProperty)
|
displayPath: joinPropertyPath(displayPath, requiredProperty)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -583,8 +602,8 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
if (!Object.prototype.hasOwnProperty.call(schemaNode.properties, entry.key)) {
|
if (!Object.prototype.hasOwnProperty.call(schemaNode.properties, entry.key)) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
severity: "error",
|
severity: "error",
|
||||||
message: localizeValidationMessage("unknownProperty", localizer, {
|
message: localizeValidationMessage(ValidationMessageKeys.unknownProperty, localizer, {
|
||||||
displayPath: combinePath(displayPath, entry.key)
|
displayPath: joinPropertyPath(displayPath, entry.key)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -593,7 +612,7 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
validateNode(
|
validateNode(
|
||||||
schemaNode.properties[entry.key],
|
schemaNode.properties[entry.key],
|
||||||
entry.node,
|
entry.node,
|
||||||
combinePath(displayPath, entry.key),
|
joinPropertyPath(displayPath, entry.key),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
localizer);
|
localizer);
|
||||||
}
|
}
|
||||||
@ -602,29 +621,31 @@ function validateObjectNode(schemaNode, yamlNode, displayPath, diagnostics, loca
|
|||||||
/**
|
/**
|
||||||
* Format one validation message in either English or Simplified Chinese.
|
* Format one validation message in either English or Simplified Chinese.
|
||||||
*
|
*
|
||||||
* @param {"expectedArray" | "expectedScalarShape" | "expectedScalarValue" | "enumMismatch" | "expectedObject" | "missingRequired" | "unknownProperty"} key Message key.
|
* @param {string} key Message key.
|
||||||
* @param {{isChinese?: boolean} | undefined} localizer Optional runtime localizer.
|
* @param {{isChinese?: boolean} | undefined} localizer Optional runtime localizer.
|
||||||
* @param {Record<string, string>} params Message parameters.
|
* @param {Record<string, string>} params Message parameters.
|
||||||
* @returns {string} Localized validation message.
|
* @returns {string} Localized validation message.
|
||||||
*/
|
*/
|
||||||
function localizeValidationMessage(key, localizer, params) {
|
function localizeValidationMessage(key, localizer, params) {
|
||||||
|
if (localizer && typeof localizer.t === "function") {
|
||||||
|
return localizer.t(key, params);
|
||||||
|
}
|
||||||
|
|
||||||
if (localizer && localizer.isChinese) {
|
if (localizer && localizer.isChinese) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "expectedArray":
|
case ValidationMessageKeys.expectedArray:
|
||||||
return `属性“${params.displayPath}”应为数组。`;
|
return `属性“${params.displayPath}”应为数组。`;
|
||||||
case "expectedScalarShape":
|
case ValidationMessageKeys.expectedScalarShape:
|
||||||
return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前 YAML 结构是“${params.yamlKind}”。`;
|
return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前 YAML 结构是“${params.yamlKind}”。`;
|
||||||
case "expectedScalarValue":
|
case ValidationMessageKeys.expectedScalarValue:
|
||||||
return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前标量值不兼容。`;
|
return `属性“${params.displayPath}”应为“${params.schemaType}”,但当前标量值不兼容。`;
|
||||||
case "enumMismatch":
|
case ValidationMessageKeys.enumMismatch:
|
||||||
return `属性“${params.displayPath}”必须是以下值之一:${params.values}。`;
|
return `属性“${params.displayPath}”必须是以下值之一:${params.values}。`;
|
||||||
case "expectedObject":
|
case ValidationMessageKeys.expectedObject:
|
||||||
return params.displayPath && params.displayPath.length > 0
|
return params.subject;
|
||||||
? `属性“${params.displayPath}”应为对象。`
|
case ValidationMessageKeys.missingRequired:
|
||||||
: "根对象应为对象。";
|
|
||||||
case "missingRequired":
|
|
||||||
return `缺少必填属性“${params.displayPath}”。`;
|
return `缺少必填属性“${params.displayPath}”。`;
|
||||||
case "unknownProperty":
|
case ValidationMessageKeys.unknownProperty:
|
||||||
return `属性“${params.displayPath}”未在匹配的 schema 中声明。`;
|
return `属性“${params.displayPath}”未在匹配的 schema 中声明。`;
|
||||||
default:
|
default:
|
||||||
return key;
|
return key;
|
||||||
@ -632,19 +653,19 @@ function localizeValidationMessage(key, localizer, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "expectedArray":
|
case ValidationMessageKeys.expectedArray:
|
||||||
return `Property '${params.displayPath}' is expected to be an array.`;
|
return `Property '${params.displayPath}' is expected to be an array.`;
|
||||||
case "expectedScalarShape":
|
case ValidationMessageKeys.expectedScalarShape:
|
||||||
return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current YAML shape is '${params.yamlKind}'.`;
|
return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current YAML shape is '${params.yamlKind}'.`;
|
||||||
case "expectedScalarValue":
|
case ValidationMessageKeys.expectedScalarValue:
|
||||||
return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current scalar value is incompatible.`;
|
return `Property '${params.displayPath}' is expected to be '${params.schemaType}', but the current scalar value is incompatible.`;
|
||||||
case "enumMismatch":
|
case ValidationMessageKeys.enumMismatch:
|
||||||
return `Property '${params.displayPath}' must be one of: ${params.values}.`;
|
return `Property '${params.displayPath}' must be one of: ${params.values}.`;
|
||||||
case "expectedObject":
|
case ValidationMessageKeys.expectedObject:
|
||||||
return `${params.subject} is expected to be an object.`;
|
return params.subject;
|
||||||
case "missingRequired":
|
case ValidationMessageKeys.missingRequired:
|
||||||
return `Required property '${params.displayPath}' is missing.`;
|
return `Required property '${params.displayPath}' is missing.`;
|
||||||
case "unknownProperty":
|
case ValidationMessageKeys.unknownProperty:
|
||||||
return `Property '${params.displayPath}' is not declared in the matching schema.`;
|
return `Property '${params.displayPath}' is not declared in the matching schema.`;
|
||||||
default:
|
default:
|
||||||
return key;
|
return key;
|
||||||
@ -719,14 +740,14 @@ function parseMapping(tokens, state, indent) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = /^([A-Za-z0-9_]+):(.*)$/u.exec(token.text);
|
const mapping = parseYamlMappingText(token.text);
|
||||||
if (!match) {
|
if (!mapping) {
|
||||||
state.index += 1;
|
state.index += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = match[1];
|
const key = mapping.key;
|
||||||
const rawValue = match[2].trim();
|
const rawValue = mapping.rawValue.trim();
|
||||||
state.index += 1;
|
state.index += 1;
|
||||||
|
|
||||||
let node;
|
let node;
|
||||||
@ -774,7 +795,7 @@ function parseSequence(tokens, state, indent) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^[A-Za-z0-9_]+:/u.test(rest)) {
|
if (parseYamlMappingText(rest)) {
|
||||||
items.push(parseInlineObjectItem(tokens, state, indent, rest));
|
items.push(parseInlineObjectItem(tokens, state, indent, rest));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -894,7 +915,7 @@ function renderYaml(node, indent = 0, currentPath = "", commentMap = {}) {
|
|||||||
function renderObjectNode(node, indent, currentPath, commentMap) {
|
function renderObjectNode(node, indent, currentPath, commentMap) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
for (const entry of node.entries) {
|
for (const entry of node.entries) {
|
||||||
const entryPath = currentPath ? `${currentPath}.${entry.key}` : entry.key;
|
const entryPath = joinPropertyPath(currentPath, entry.key);
|
||||||
if (commentMap[entryPath]) {
|
if (commentMap[entryPath]) {
|
||||||
lines.push(...renderYamlComments(commentMap[entryPath], indent));
|
lines.push(...renderYamlComments(commentMap[entryPath], indent));
|
||||||
}
|
}
|
||||||
@ -927,7 +948,7 @@ function renderArrayNode(node, indent, currentPath, commentMap) {
|
|||||||
const lines = [];
|
const lines = [];
|
||||||
for (let index = 0; index < node.items.length; index += 1) {
|
for (let index = 0; index < node.items.length; index += 1) {
|
||||||
const item = node.items[index];
|
const item = node.items[index];
|
||||||
const itemPath = `${currentPath}[${index}]`;
|
const itemPath = joinArrayIndexPath(currentPath, index);
|
||||||
if (commentMap[itemPath]) {
|
if (commentMap[itemPath]) {
|
||||||
lines.push(...renderYamlComments(commentMap[itemPath], indent));
|
lines.push(...renderYamlComments(commentMap[itemPath], indent));
|
||||||
}
|
}
|
||||||
@ -1041,7 +1062,7 @@ function collectSchemaComments(schemaNode, currentPath, commentMap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, propertySchema] of Object.entries(schemaNode.properties || {})) {
|
for (const [key, propertySchema] of Object.entries(schemaNode.properties || {})) {
|
||||||
const propertyPath = currentPath ? `${currentPath}.${key}` : key;
|
const propertyPath = joinPropertyPath(currentPath, key);
|
||||||
if (propertySchema.description) {
|
if (propertySchema.description) {
|
||||||
commentMap[propertyPath] = propertySchema.description;
|
commentMap[propertyPath] = propertySchema.description;
|
||||||
}
|
}
|
||||||
@ -1052,7 +1073,7 @@ function collectSchemaComments(schemaNode, currentPath, commentMap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (propertySchema.type === "array" && propertySchema.items.type === "object") {
|
if (propertySchema.type === "array" && propertySchema.items.type === "object") {
|
||||||
collectSchemaComments(propertySchema.items, `${propertyPath}[0]`, commentMap);
|
collectSchemaComments(propertySchema.items, joinArrayIndexPath(propertyPath, 0), commentMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1201,14 +1222,66 @@ function splitYamlValueAndInlineComment(rawValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine a parent path with one child segment.
|
* Parse one YAML mapping entry such as `key: value` or `"complex key": value`.
|
||||||
*
|
*
|
||||||
* @param {string} parentPath Parent path.
|
* @param {string} text Raw YAML line text without leading indentation.
|
||||||
* @param {string} key Child key.
|
* @returns {{key: string, rawValue: string} | undefined} Parsed mapping entry.
|
||||||
* @returns {string} Combined path.
|
|
||||||
*/
|
*/
|
||||||
function combinePath(parentPath, key) {
|
function parseYamlMappingText(text) {
|
||||||
return parentPath && parentPath !== "<root>" ? `${parentPath}.${key}` : key;
|
const separatorIndex = findYamlKeyValueSeparator(text);
|
||||||
|
if (separatorIndex < 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawKey = text.slice(0, separatorIndex).trim();
|
||||||
|
if (rawKey.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: normalizeYamlKey(rawKey),
|
||||||
|
rawValue: text.slice(separatorIndex + 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first `:` that acts as a YAML key/value separator.
|
||||||
|
*
|
||||||
|
* @param {string} text Raw YAML line text without leading indentation.
|
||||||
|
* @returns {number} Separator index, or -1 when not found.
|
||||||
|
*/
|
||||||
|
function findYamlKeyValueSeparator(text) {
|
||||||
|
let inSingleQuote = false;
|
||||||
|
let inDoubleQuote = false;
|
||||||
|
|
||||||
|
for (let index = 0; index < text.length; index += 1) {
|
||||||
|
const character = text[index];
|
||||||
|
if (character === "'" && !inDoubleQuote) {
|
||||||
|
inSingleQuote = !inSingleQuote;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "\"" && !inSingleQuote) {
|
||||||
|
inDoubleQuote = !inDoubleQuote;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === ":" && !inSingleQuote && !inDoubleQuote) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a YAML key token into the logical key name used in the form model.
|
||||||
|
*
|
||||||
|
* @param {string} rawKey Raw YAML key token.
|
||||||
|
* @returns {string} Normalized key name.
|
||||||
|
*/
|
||||||
|
function normalizeYamlKey(rawKey) {
|
||||||
|
return unquoteScalar(rawKey.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -12,6 +12,12 @@ const {
|
|||||||
unquoteScalar,
|
unquoteScalar,
|
||||||
validateParsedConfig
|
validateParsedConfig
|
||||||
} = require("./configValidation");
|
} = require("./configValidation");
|
||||||
|
const {
|
||||||
|
isTemplatePath,
|
||||||
|
joinArrayIndexPath,
|
||||||
|
joinArrayTemplatePath,
|
||||||
|
joinPropertyPath
|
||||||
|
} = require("./configPath");
|
||||||
const {createLocalizer} = require("./localization");
|
const {createLocalizer} = require("./localization");
|
||||||
|
|
||||||
const localizer = createLocalizer(vscode.env.language);
|
const localizer = createLocalizer(vscode.env.language);
|
||||||
@ -353,11 +359,11 @@ async function openFormPreview(item, diagnostics) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const yamlText = await fs.promises.readFile(configUri.fsPath, "utf8");
|
let latestYamlText = await fs.promises.readFile(configUri.fsPath, "utf8");
|
||||||
const parsedYaml = parseTopLevelYaml(yamlText);
|
const parsedYaml = parseTopLevelYaml(latestYamlText);
|
||||||
const commentLookup = extractYamlComments(yamlText);
|
const commentLookup = extractYamlComments(latestYamlText);
|
||||||
const schemaInfo = await loadSchemaInfoForConfig(configUri, workspaceRoot);
|
const schemaInfo = await loadSchemaInfoForConfig(configUri, workspaceRoot);
|
||||||
const canInitializeFromSchema = schemaInfo.exists && yamlText.trim().length === 0;
|
const canInitializeFromSchema = schemaInfo.exists && latestYamlText.trim().length === 0;
|
||||||
|
|
||||||
const panel = vscode.window.createWebviewPanel(
|
const panel = vscode.window.createWebviewPanel(
|
||||||
"gframeworkConfigFormPreview",
|
"gframeworkConfigFormPreview",
|
||||||
@ -376,7 +382,7 @@ async function openFormPreview(item, diagnostics) {
|
|||||||
|
|
||||||
panel.webview.onDidReceiveMessage(async (message) => {
|
panel.webview.onDidReceiveMessage(async (message) => {
|
||||||
if (message.type === "save") {
|
if (message.type === "save") {
|
||||||
const latestYamlText = await fs.promises.readFile(configUri.fsPath, "utf8");
|
latestYamlText = await fs.promises.readFile(configUri.fsPath, "utf8");
|
||||||
const updatedYaml = applyFormUpdates(latestYamlText, {
|
const updatedYaml = applyFormUpdates(latestYamlText, {
|
||||||
scalars: message.scalars || {},
|
scalars: message.scalars || {},
|
||||||
arrays: parseArrayFieldPayload(message.arrays || {}),
|
arrays: parseArrayFieldPayload(message.arrays || {}),
|
||||||
@ -402,17 +408,30 @@ async function openFormPreview(item, diagnostics) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmLabel = localizer.t("button.initializeFromSchemaConfirm");
|
||||||
|
const cancelLabel = localizer.t("button.cancel");
|
||||||
|
const userChoice = await vscode.window.showWarningMessage(
|
||||||
|
localizer.t("message.initializeFromSchemaConfirm"),
|
||||||
|
{modal: true},
|
||||||
|
confirmLabel,
|
||||||
|
cancelLabel);
|
||||||
|
|
||||||
|
if (userChoice !== confirmLabel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sampleYaml = createSampleConfigYaml(schemaInfo);
|
const sampleYaml = createSampleConfigYaml(schemaInfo);
|
||||||
await fs.promises.writeFile(configUri.fsPath, sampleYaml, "utf8");
|
await fs.promises.writeFile(configUri.fsPath, sampleYaml, "utf8");
|
||||||
const document = await vscode.workspace.openTextDocument(configUri);
|
const document = await vscode.workspace.openTextDocument(configUri);
|
||||||
await document.save();
|
await document.save();
|
||||||
|
latestYamlText = sampleYaml;
|
||||||
await validateConfigFile(configUri, diagnostics);
|
await validateConfigFile(configUri, diagnostics);
|
||||||
panel.webview.html = renderFormHtml(
|
panel.webview.html = renderFormHtml(
|
||||||
path.basename(configUri.fsPath),
|
path.basename(configUri.fsPath),
|
||||||
schemaInfo,
|
schemaInfo,
|
||||||
parseTopLevelYaml(sampleYaml),
|
parseTopLevelYaml(latestYamlText),
|
||||||
{
|
{
|
||||||
commentLookup: extractYamlComments(sampleYaml),
|
commentLookup: extractYamlComments(latestYamlText),
|
||||||
canInitializeFromSchema: false
|
canInitializeFromSchema: false
|
||||||
});
|
});
|
||||||
void vscode.window.showInformationMessage(localizer.t("message.formInitialized"));
|
void vscode.window.showInformationMessage(localizer.t("message.formInitialized"));
|
||||||
@ -1178,7 +1197,7 @@ function renderYamlCommentBlock(field) {
|
|||||||
*/
|
*/
|
||||||
function renderCommentEditor(field) {
|
function renderCommentEditor(field) {
|
||||||
const commentPath = field.displayPath || field.path;
|
const commentPath = field.displayPath || field.path;
|
||||||
if (commentPath.includes("[]")) {
|
if (isTemplatePath(commentPath)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1273,7 +1292,7 @@ function collectFormFields(schemaNode, yamlNode, currentPath, depth, fields, uns
|
|||||||
const requiredSet = new Set(Array.isArray(schemaNode.required) ? schemaNode.required : []);
|
const requiredSet = new Set(Array.isArray(schemaNode.required) ? schemaNode.required : []);
|
||||||
|
|
||||||
for (const [key, propertySchema] of Object.entries(schemaNode.properties || {})) {
|
for (const [key, propertySchema] of Object.entries(schemaNode.properties || {})) {
|
||||||
const propertyPath = currentPath ? `${currentPath}.${key}` : key;
|
const propertyPath = joinPropertyPath(currentPath, key);
|
||||||
const label = propertySchema.title || key;
|
const label = propertySchema.title || key;
|
||||||
const propertyValue = yamlMap.get(key);
|
const propertyValue = yamlMap.get(key);
|
||||||
|
|
||||||
@ -1317,7 +1336,7 @@ function collectFormFields(schemaNode, yamlNode, currentPath, depth, fields, uns
|
|||||||
propertySchema.items,
|
propertySchema.items,
|
||||||
undefined,
|
undefined,
|
||||||
"",
|
"",
|
||||||
`${propertyPath}[]`,
|
joinArrayTemplatePath(propertyPath),
|
||||||
depth + 1,
|
depth + 1,
|
||||||
itemFieldsTemplate,
|
itemFieldsTemplate,
|
||||||
unsupported,
|
unsupported,
|
||||||
@ -1386,7 +1405,7 @@ function buildObjectArrayItemModels(itemSchema, yamlNode, propertyPath, depth, u
|
|||||||
const items = [];
|
const items = [];
|
||||||
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
for (let index = 0; index < yamlNode.items.length; index += 1) {
|
||||||
const itemNode = yamlNode.items[index];
|
const itemNode = yamlNode.items[index];
|
||||||
const itemPath = `${propertyPath}[${index}]`;
|
const itemPath = joinArrayIndexPath(propertyPath, index);
|
||||||
if (!itemNode || itemNode.kind !== "object") {
|
if (!itemNode || itemNode.kind !== "object") {
|
||||||
unsupported.push({
|
unsupported.push({
|
||||||
path: itemPath,
|
path: itemPath,
|
||||||
@ -1437,8 +1456,8 @@ function collectObjectArrayItemFields(schemaNode, yamlNode, localPath, displayPa
|
|||||||
const requiredSet = new Set(Array.isArray(schemaNode.required) ? schemaNode.required : []);
|
const requiredSet = new Set(Array.isArray(schemaNode.required) ? schemaNode.required : []);
|
||||||
|
|
||||||
for (const [key, propertySchema] of Object.entries(schemaNode.properties || {})) {
|
for (const [key, propertySchema] of Object.entries(schemaNode.properties || {})) {
|
||||||
const itemLocalPath = localPath ? `${localPath}.${key}` : key;
|
const itemLocalPath = joinPropertyPath(localPath, key);
|
||||||
const itemDisplayPath = `${displayPath}.${key}`;
|
const itemDisplayPath = joinPropertyPath(displayPath, key);
|
||||||
const label = propertySchema.title || key;
|
const label = propertySchema.title || key;
|
||||||
const propertyValue = yamlMap.get(key);
|
const propertyValue = yamlMap.get(key);
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
const {ValidationMessageKeys} = require("./localizationKeys");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a tiny in-process localizer for the extension runtime and webview.
|
* Create a tiny in-process localizer for the extension runtime and webview.
|
||||||
* VS Code contribution points use package.nls files, while runtime strings are
|
* VS Code contribution points use package.nls files, while runtime strings are
|
||||||
@ -10,12 +12,26 @@
|
|||||||
function createLocalizer(language) {
|
function createLocalizer(language) {
|
||||||
const normalizedLanguage = String(language || "en").toLowerCase();
|
const normalizedLanguage = String(language || "en").toLowerCase();
|
||||||
const isChinese = normalizedLanguage.startsWith("zh");
|
const isChinese = normalizedLanguage.startsWith("zh");
|
||||||
const languageTag = isChinese ? "zh-CN" : "en";
|
const isTraditionalChinese =
|
||||||
const dictionary = isChinese ? zhCnMessages : enMessages;
|
normalizedLanguage === "zh-tw" ||
|
||||||
|
normalizedLanguage === "zh-hk" ||
|
||||||
|
normalizedLanguage === "zh-mo" ||
|
||||||
|
normalizedLanguage.startsWith("zh-hant");
|
||||||
|
const isSimplifiedChinese = isChinese && !isTraditionalChinese;
|
||||||
|
const languageTag = isTraditionalChinese
|
||||||
|
? normalizedLanguage
|
||||||
|
: isSimplifiedChinese
|
||||||
|
? "zh-CN"
|
||||||
|
: "en";
|
||||||
|
const dictionary = isTraditionalChinese
|
||||||
|
? enMessages
|
||||||
|
: isSimplifiedChinese
|
||||||
|
? zhCnMessages
|
||||||
|
: enMessages;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
languageTag,
|
languageTag,
|
||||||
isChinese,
|
isChinese: isSimplifiedChinese,
|
||||||
t(key, params) {
|
t(key, params) {
|
||||||
const template = dictionary[key] || enMessages[key] || key;
|
const template = dictionary[key] || enMessages[key] || key;
|
||||||
return template.replace(/\{([A-Za-z0-9_]+)\}/gu, (match, token) => {
|
return template.replace(/\{([A-Za-z0-9_]+)\}/gu, (match, token) => {
|
||||||
@ -38,6 +54,7 @@ const enMessages = {
|
|||||||
"message.schemaNotFound": "Matching schema file was not found.",
|
"message.schemaNotFound": "Matching schema file was not found.",
|
||||||
"message.formSaved": "Config file saved from form preview.",
|
"message.formSaved": "Config file saved from form preview.",
|
||||||
"message.formInitialized": "Example config initialized from the schema.",
|
"message.formInitialized": "Example config initialized from the schema.",
|
||||||
|
"message.initializeFromSchemaConfirm": "Initializing from the schema will replace the current configuration and may discard unsaved form changes. Do you want to continue?",
|
||||||
"message.noYamlFilesInDomain": "No YAML config files were found in the selected domain.",
|
"message.noYamlFilesInDomain": "No YAML config files were found in the selected domain.",
|
||||||
"message.batchEditNeedsSchema": "Batch edit requires a matching schema file for the selected domain.",
|
"message.batchEditNeedsSchema": "Batch edit requires a matching schema file for the selected domain.",
|
||||||
"message.batchEditNoEditableFields": "No top-level scalar or scalar-array fields were found in the matching schema.",
|
"message.batchEditNoEditableFields": "No top-level scalar or scalar-array fields were found in the matching schema.",
|
||||||
@ -55,6 +72,8 @@ const enMessages = {
|
|||||||
"detail.refTable": "ref: {refTable}",
|
"detail.refTable": "ref: {refTable}",
|
||||||
"detail.arrayType": "array<{itemType}>",
|
"detail.arrayType": "array<{itemType}>",
|
||||||
"detail.default": "default",
|
"detail.default": "default",
|
||||||
|
"button.cancel": "Cancel",
|
||||||
|
"button.initializeFromSchemaConfirm": "Initialize from schema",
|
||||||
"input.batchArray.title": "Batch Edit Array: {field}",
|
"input.batchArray.title": "Batch Edit Array: {field}",
|
||||||
"input.batchArray.prompt": "Enter comma-separated items for '{fieldKey}' (expected array<{itemType}>). Leave empty to clear the array.",
|
"input.batchArray.prompt": "Enter comma-separated items for '{fieldKey}' (expected array<{itemType}>). Leave empty to clear the array.",
|
||||||
"input.batchArray.placeholder.allowedItems": "Allowed items: {values}",
|
"input.batchArray.placeholder.allowedItems": "Allowed items: {values}",
|
||||||
@ -90,7 +109,14 @@ const enMessages = {
|
|||||||
"webview.unsupported.array": "Unsupported array shapes are currently raw-YAML-only in the form preview.",
|
"webview.unsupported.array": "Unsupported array shapes are currently raw-YAML-only in the form preview.",
|
||||||
"webview.unsupported.type": "{type} fields are currently raw-YAML-only.",
|
"webview.unsupported.type": "{type} fields are currently raw-YAML-only.",
|
||||||
"webview.unsupported.objectArrayMixed": "Object-array items must be mappings. Use raw YAML if the current file mixes scalar and object items.",
|
"webview.unsupported.objectArrayMixed": "Object-array items must be mappings. Use raw YAML if the current file mixes scalar and object items.",
|
||||||
"webview.unsupported.nestedObjectArray": "Nested object-array fields are currently raw-YAML-only inside the object-array editor."
|
"webview.unsupported.nestedObjectArray": "Nested object-array fields are currently raw-YAML-only inside the object-array editor.",
|
||||||
|
[ValidationMessageKeys.enumMismatch]: "Property '{displayPath}' must be one of: {values}.",
|
||||||
|
[ValidationMessageKeys.expectedArray]: "Property '{displayPath}' is expected to be an array.",
|
||||||
|
[ValidationMessageKeys.expectedObject]: "{subject} is expected to be an object.",
|
||||||
|
[ValidationMessageKeys.expectedScalarShape]: "Property '{displayPath}' is expected to be '{schemaType}', but the current YAML shape is '{yamlKind}'.",
|
||||||
|
[ValidationMessageKeys.expectedScalarValue]: "Property '{displayPath}' is expected to be '{schemaType}', but the current scalar value is incompatible.",
|
||||||
|
[ValidationMessageKeys.missingRequired]: "Required property '{displayPath}' is missing.",
|
||||||
|
[ValidationMessageKeys.unknownProperty]: "Property '{displayPath}' is not declared in the matching schema."
|
||||||
};
|
};
|
||||||
|
|
||||||
const zhCnMessages = {
|
const zhCnMessages = {
|
||||||
@ -102,6 +128,7 @@ const zhCnMessages = {
|
|||||||
"message.schemaNotFound": "未找到匹配的 schema 文件。",
|
"message.schemaNotFound": "未找到匹配的 schema 文件。",
|
||||||
"message.formSaved": "已从表单预览保存配置文件。",
|
"message.formSaved": "已从表单预览保存配置文件。",
|
||||||
"message.formInitialized": "已根据 schema 初始化示例配置。",
|
"message.formInitialized": "已根据 schema 初始化示例配置。",
|
||||||
|
"message.initializeFromSchemaConfirm": "从 schema 初始化会替换当前配置,并且可能丢失尚未保存的表单修改。是否继续?",
|
||||||
"message.noYamlFilesInDomain": "所选配置域中没有找到 YAML 配置文件。",
|
"message.noYamlFilesInDomain": "所选配置域中没有找到 YAML 配置文件。",
|
||||||
"message.batchEditNeedsSchema": "批量编辑要求该配置域存在匹配的 schema 文件。",
|
"message.batchEditNeedsSchema": "批量编辑要求该配置域存在匹配的 schema 文件。",
|
||||||
"message.batchEditNoEditableFields": "匹配的 schema 中没有可批量编辑的顶层标量字段或标量数组字段。",
|
"message.batchEditNoEditableFields": "匹配的 schema 中没有可批量编辑的顶层标量字段或标量数组字段。",
|
||||||
@ -119,6 +146,8 @@ const zhCnMessages = {
|
|||||||
"detail.refTable": "引用表:{refTable}",
|
"detail.refTable": "引用表:{refTable}",
|
||||||
"detail.arrayType": "数组<{itemType}>",
|
"detail.arrayType": "数组<{itemType}>",
|
||||||
"detail.default": "默认值",
|
"detail.default": "默认值",
|
||||||
|
"button.cancel": "取消",
|
||||||
|
"button.initializeFromSchemaConfirm": "从 schema 初始化",
|
||||||
"input.batchArray.title": "批量编辑数组:{field}",
|
"input.batchArray.title": "批量编辑数组:{field}",
|
||||||
"input.batchArray.prompt": "请输入“{fieldKey}”的逗号分隔项(期望类型:数组<{itemType}>)。留空表示清空数组。",
|
"input.batchArray.prompt": "请输入“{fieldKey}”的逗号分隔项(期望类型:数组<{itemType}>)。留空表示清空数组。",
|
||||||
"input.batchArray.placeholder.allowedItems": "允许项:{values}",
|
"input.batchArray.placeholder.allowedItems": "允许项:{values}",
|
||||||
@ -154,7 +183,14 @@ const zhCnMessages = {
|
|||||||
"webview.unsupported.array": "当前表单预览暂不支持这种数组结构,请改用原始 YAML。",
|
"webview.unsupported.array": "当前表单预览暂不支持这种数组结构,请改用原始 YAML。",
|
||||||
"webview.unsupported.type": "当前表单预览暂不支持 {type} 字段,请改用原始 YAML。",
|
"webview.unsupported.type": "当前表单预览暂不支持 {type} 字段,请改用原始 YAML。",
|
||||||
"webview.unsupported.objectArrayMixed": "对象数组中的每一项都必须是映射对象。如果当前文件混用了标量项和对象项,请改用原始 YAML。",
|
"webview.unsupported.objectArrayMixed": "对象数组中的每一项都必须是映射对象。如果当前文件混用了标量项和对象项,请改用原始 YAML。",
|
||||||
"webview.unsupported.nestedObjectArray": "对象数组编辑器内暂不支持更深层的对象数组字段,请改用原始 YAML。"
|
"webview.unsupported.nestedObjectArray": "对象数组编辑器内暂不支持更深层的对象数组字段,请改用原始 YAML。",
|
||||||
|
[ValidationMessageKeys.enumMismatch]: "属性“{displayPath}”必须是以下值之一:{values}。",
|
||||||
|
[ValidationMessageKeys.expectedArray]: "属性“{displayPath}”应为数组。",
|
||||||
|
[ValidationMessageKeys.expectedObject]: "{subject}",
|
||||||
|
[ValidationMessageKeys.expectedScalarShape]: "属性“{displayPath}”应为“{schemaType}”,但当前 YAML 结构是“{yamlKind}”。",
|
||||||
|
[ValidationMessageKeys.expectedScalarValue]: "属性“{displayPath}”应为“{schemaType}”,但当前标量值不兼容。",
|
||||||
|
[ValidationMessageKeys.missingRequired]: "缺少必填属性“{displayPath}”。",
|
||||||
|
[ValidationMessageKeys.unknownProperty]: "属性“{displayPath}”未在匹配的 schema 中声明。"
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
13
tools/gframework-config-tool/src/localizationKeys.js
Normal file
13
tools/gframework-config-tool/src/localizationKeys.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const ValidationMessageKeys = Object.freeze({
|
||||||
|
enumMismatch: "validation.enumMismatch",
|
||||||
|
expectedArray: "validation.expectedArray",
|
||||||
|
expectedObject: "validation.expectedObject",
|
||||||
|
expectedScalarShape: "validation.expectedScalarShape",
|
||||||
|
expectedScalarValue: "validation.expectedScalarValue",
|
||||||
|
missingRequired: "validation.missingRequired",
|
||||||
|
unknownProperty: "validation.unknownProperty"
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ValidationMessageKeys
|
||||||
|
};
|
||||||
@ -85,6 +85,20 @@ phases:
|
|||||||
assert.equal(yaml.map.get("phases").items[0].map.get("wave").value, "1");
|
assert.equal(yaml.map.get("phases").items[0].map.get("wave").value, "1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parseTopLevelYaml should keep complex mapping keys", () => {
|
||||||
|
const yaml = parseTopLevelYaml(`
|
||||||
|
my-key: slime
|
||||||
|
"complex key": value
|
||||||
|
root:
|
||||||
|
item.id: potion
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.equal(yaml.kind, "object");
|
||||||
|
assert.equal(yaml.map.get("my-key").value, "slime");
|
||||||
|
assert.equal(yaml.map.get("complex key").value, "value");
|
||||||
|
assert.equal(yaml.map.get("root").map.get("item.id").value, "potion");
|
||||||
|
});
|
||||||
|
|
||||||
test("validateParsedConfig should report missing and unknown nested properties", () => {
|
test("validateParsedConfig should report missing and unknown nested properties", () => {
|
||||||
const schema = parseSchemaContent(`
|
const schema = parseSchemaContent(`
|
||||||
{
|
{
|
||||||
@ -312,6 +326,22 @@ skills:
|
|||||||
assert.equal(comments["skills[0].id"], "Skill id note");
|
assert.equal(comments["skills[0].id"], "Skill id note");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("extractYamlComments should keep comments for complex YAML keys", () => {
|
||||||
|
const comments = extractYamlComments(`
|
||||||
|
# Dashed key comment
|
||||||
|
my-key: Slime
|
||||||
|
# Quoted key comment
|
||||||
|
"complex key": value
|
||||||
|
root:
|
||||||
|
# Dotted key comment
|
||||||
|
item.id: potion
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.equal(comments["my-key"], "Dashed key comment");
|
||||||
|
assert.equal(comments["complex key"], "Quoted key comment");
|
||||||
|
assert.equal(comments["root.item.id"], "Dotted key comment");
|
||||||
|
});
|
||||||
|
|
||||||
test("applyFormUpdates should preserve and update YAML comments", () => {
|
test("applyFormUpdates should preserve and update YAML comments", () => {
|
||||||
const updated = applyFormUpdates(
|
const updated = applyFormUpdates(
|
||||||
[
|
[
|
||||||
|
|||||||
@ -23,3 +23,14 @@ test("createLocalizer should switch to Simplified Chinese for zh languages", ()
|
|||||||
localizer.t("message.batchEditUpdated", {count: 2, domain: "monster"}),
|
localizer.t("message.batchEditUpdated", {count: 2, domain: "monster"}),
|
||||||
"已在“monster”中批量更新 2 个配置文件。");
|
"已在“monster”中批量更新 2 个配置文件。");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("createLocalizer should fall back to English for Traditional Chinese locales", () => {
|
||||||
|
const localizer = createLocalizer("zh-TW");
|
||||||
|
|
||||||
|
assert.equal(localizer.languageTag, "zh-tw");
|
||||||
|
assert.equal(localizer.isChinese, false);
|
||||||
|
assert.equal(localizer.t("webview.button.save"), "Save Form");
|
||||||
|
assert.equal(
|
||||||
|
localizer.t("message.batchEditUpdated", {count: 2, domain: "monster"}),
|
||||||
|
"Batch updated 2 config file(s) in 'monster'.");
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user