@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
417 lines (416 loc) âĸ 18.3 kB
JavaScript
;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationDocMarkdownTop = void 0;
/**
* ARCHITECTURE DOCUMENTATION: ValidationRulesMarkdownGenerator
* ==============================================================
*
* This module generates detailed markdown documentation for MCTools validation rules.
* Unlike the general FormMarkdownDocumentationGenerator, this is specifically designed
* to explain validation rules in a user-friendly way that helps creators understand:
*
* 1. What each validation rule checks for
* 2. Why the rule matters (the problem it catches)
* 3. How to fix issues when the rule triggers
* 4. Technical details for advanced users
*
* ## Input Structure
*
* Validation form files (mctoolsval/*.form.json) contain:
* - Form-level: id, title, description, technicalDescription
* - Field-level (each field = one validation rule):
* - id: The topic ID (numeric string like "100", "110")
* - title: Rule name (e.g., "Format Version Undefined")
* - description: What the rule checks
* - howToUse: How to fix the issue
* - technicalDescription: Technical details
* - messageType: error | warning | recommendation | info
* - matchedValues: Auto-fix actions available
* - note, note2, note3: Additional notes
*
* ## Output Structure
*
* For each validation category (form file), generates:
* 1. Overview page with all rules listed
* 2. Detailed sections for each rule with:
* - Rule ID and severity badge
* - Description of what's being checked
* - "How to Fix" section (from howToUse)
* - Technical details (from technicalDescription)
* - Auto-fix availability indicator
*
* ## Related Files
*
* - FormMarkdownDocumentationGenerator.ts: General form documentation (parent class)
* - IFormDefinition.ts: Form structure interface
* - IField.ts: Field structure including messageType, howToUse
* - public/data/forms/mctoolsval/: Validation rule form definitions
*/
const Utilities_1 = __importDefault(require("../core/Utilities"));
const StorageUtilities_1 = __importDefault(require("../storage/StorageUtilities"));
exports.ValidationDocMarkdownTop = `---
author: mammerla
ms.author: mikeam
title: "{0}"
description: "{1}"
ai-usage: ai-assisted
ms.service: minecraft-bedrock-edition
ms.date: 02/11/2025
---
`;
const SEVERITY_MAP = {
error: { label: "Error", emoji: "đ´", cssClass: "error" },
warning: { label: "Warning", emoji: "đĄ", cssClass: "warning" },
recommendation: { label: "Recommendation", emoji: "đĩ", cssClass: "recommendation" },
info: { label: "Info", emoji: "âšī¸", cssClass: "info" },
};
/**
* Generates detailed markdown documentation for MCTools validation rules.
* This provides a more user-friendly format than the general form documentation,
* with emphasis on explaining what each rule checks and how to fix issues.
*/
class ValidationRulesMarkdownGenerator {
/**
* Generate validation rule documentation from form JSON files.
*
* @param formJsonInputFolder Folder containing mctoolsval form JSON files
* @param outputFolder Folder to write markdown documentation to
*/
async generateValidationDocs(formJsonInputFolder, outputFolder) {
const formsByPath = {};
await this.loadFormJsonFromFolder(formsByPath, formJsonInputFolder);
// Generate individual rule category pages
await this.exportValidationRuleDocs(formsByPath, outputFolder, "/MCToolsValReference/");
// Generate overview/index page
await this.exportValidationRulesIndex(formsByPath, outputFolder, "/MCToolsValReference/");
}
/**
* Load all form JSON files from a folder into a dictionary.
*/
async loadFormJsonFromFolder(formsByPath, inputFolder) {
if (!inputFolder.isLoaded) {
await inputFolder.load();
}
for (const folderName in inputFolder.folders) {
const folder = inputFolder.folders[folderName];
if (folder) {
await this.loadFormJsonFromFolder(formsByPath, folder);
}
}
for (const fileName in inputFolder.files) {
const file = inputFolder.files[fileName];
if (file && fileName.endsWith(".form.json")) {
if (!file.isContentLoaded) {
await file.loadContent();
}
const jsonO = StorageUtilities_1.default.getJsonObject(file);
if (jsonO) {
formsByPath[file.storageRelativePath] = jsonO;
}
// Unload file content after extracting JSON to save memory during bulk processing
file.unload();
}
}
}
/**
* Export individual validation rule category documentation pages.
*/
async exportValidationRuleDocs(formsByPath, outputFolder, subFolderPath) {
const targetFolder = await outputFolder.ensureFolderFromRelativePath(subFolderPath);
if (!targetFolder) {
return;
}
await targetFolder.ensureExists();
// Filter to only mctoolsval forms
const validationForms = this.getValidationForms(formsByPath);
for (const formPath in validationForms) {
const form = validationForms[formPath];
if (form && form.fields && form.fields.length > 0) {
let baseName = StorageUtilities_1.default.getBaseFromName(StorageUtilities_1.default.getLeafName(formPath));
if (baseName.endsWith(".form")) {
baseName = baseName.substring(0, baseName.length - 5);
}
const fileName = baseName.toLowerCase().replace(/ /g, "-");
const markdownFile = targetFolder.ensureFile(fileName + ".md");
await this.saveValidationRuleDoc(markdownFile, form, baseName);
}
}
}
/**
* Export the validation rules index/overview page.
*/
async exportValidationRulesIndex(formsByPath, outputFolder, subFolderPath) {
const targetFolder = await outputFolder.ensureFolderFromRelativePath(subFolderPath);
if (!targetFolder) {
return;
}
await targetFolder.ensureExists();
const indexFile = targetFolder.ensureFile("ValidationRulesIndex.md");
const content = [];
content.push(Utilities_1.default.stringFormat(exports.ValidationDocMarkdownTop, "MCTools Validation Rules Reference", "Complete reference for all validation rules in Minecraft Creator Tools"));
content.push("# MCTools Validation Rules Reference\n");
content.push("This reference documents all validation rules used by Minecraft Creator Tools to check your add-on content.\n");
content.push("Each rule helps identify potential issues in your Minecraft content before deployment.\n");
content.push("## Validation Categories\n");
content.push("| Category | Description | Rules |\n");
content.push("|:---------|:------------|:------|\n");
const validationForms = this.getValidationForms(formsByPath);
const sortedPaths = Object.keys(validationForms).sort();
for (const formPath of sortedPaths) {
const form = validationForms[formPath];
if (form) {
let baseName = StorageUtilities_1.default.getBaseFromName(StorageUtilities_1.default.getLeafName(formPath));
if (baseName.endsWith(".form")) {
baseName = baseName.substring(0, baseName.length - 5);
}
const title = form.title || Utilities_1.default.humanifyMinecraftName(baseName);
const description = form.description ? this.truncateDescription(form.description, 80) : "";
const ruleCount = form.fields ? form.fields.length : 0;
const linkName = baseName.toLowerCase().replace(/ /g, "-");
content.push(`| [${title}](${linkName}.md) | ${description} | ${ruleCount} |\n`);
}
}
content.push("\n## Understanding Severity Levels\n");
content.push("Validation rules have different severity levels:\n");
content.push("- đ´ **Error**: Must be fixed. These issues will cause problems in Minecraft.\n");
content.push("- đĄ **Warning**: Should be reviewed. These may cause issues or indicate problems.\n");
content.push("- đĩ **Recommendation**: Consider addressing. These are best practices.\n");
content.push("- âšī¸ **Info**: Informational. Aggregated data or neutral status.\n");
indexFile.setContent(content.join(""));
await indexFile.saveContent();
}
/**
* Save a single validation rule category documentation page.
*/
async saveValidationRuleDoc(markdownFile, form, baseName) {
const content = [];
const title = form.title || Utilities_1.default.humanifyMinecraftName(baseName);
content.push(Utilities_1.default.stringFormat(exports.ValidationDocMarkdownTop, `Validation Rules - ${title}`, `Documentation for ${title} validation rules in Minecraft Creator Tools`));
content.push(`# ${title} Validation Rules\n`);
// Form-level description
if (form.description) {
content.push(`${this.sanitizeDescription(form.description)}\n`);
}
// Form-level technical description
if (form.technicalDescription) {
content.push("> [!TIP]\n");
content.push(`> **Technical Details**: ${this.sanitizeDescription(form.technicalDescription)}\n`);
}
// Get ruleset name from baseName (uppercase)
const rulesetName = baseName.toUpperCase();
// Summary table of all rules
content.push("## Rules Summary\n");
content.push("| Rule ID | Rule | Severity | Auto-Fix |\n");
content.push("|:--------|:-----|:---------|:---------|\n");
if (form.fields) {
// Sort fields by ID (numerically if possible)
const sortedFields = [...form.fields].sort((a, b) => {
const aNum = parseInt(a.id, 10);
const bNum = parseInt(b.id, 10);
if (!isNaN(aNum) && !isNaN(bNum)) {
return aNum - bNum;
}
return a.id.localeCompare(b.id);
});
for (const field of sortedFields) {
const severity = this.getSeverityInfo(field.messageType);
const hasAutoFix = this.hasAutoFix(field) ? "â
" : "";
const ruleTitle = field.title || field.id;
const ruleId = `${rulesetName}${field.id}`;
const anchor = ruleId.toLowerCase();
content.push(`| [${ruleId}](#${anchor}) | ${ruleTitle} | ${severity.emoji} ${severity.label} | ${hasAutoFix} |\n`);
}
// Detailed documentation for each rule
content.push("\n---\n");
content.push("## Rule Details\n");
for (const field of sortedFields) {
await this.appendValidationRuleDetail(field, content, rulesetName);
}
}
markdownFile.setContent(content.join(""));
await markdownFile.saveContent();
}
/**
* Append detailed documentation for a single validation rule.
* @param field The field/rule to document
* @param content The content array to append to
* @param rulesetName The uppercase ruleset name (e.g., "SHARING", "JSONF")
*/
async appendValidationRuleDetail(field, content, rulesetName) {
const severity = this.getSeverityInfo(field.messageType);
const ruleTitle = field.title || field.id;
// Use format: ### RULESETNAME<id>
content.push(`\n### ${rulesetName}${field.id}\n`);
content.push(`**${severity.emoji} ${ruleTitle}** \n`);
content.push(`**Severity**: ${severity.label}\n`);
// Description - what does this rule check?
if (field.description) {
content.push(`\n#### What This Checks\n`);
content.push(`${this.sanitizeDescription(field.description)}\n`);
}
// How to fix - this is the key user-focused content
if (field.howToUse) {
content.push(`\n#### How to Fix\n`);
content.push(`${this.sanitizeDescription(field.howToUse)}\n`);
}
// Auto-fix availability
const autoFixActions = this.getAutoFixActions(field);
if (autoFixActions.length > 0) {
content.push(`\n> [!TIP]\n`);
content.push(`> **Auto-Fix Available**: This issue can be automatically fixed.\n`);
for (const action of autoFixActions) {
content.push(`> - ${action}\n`);
}
}
// Technical description for advanced users
if (field.technicalDescription) {
content.push(`\n#### Technical Details\n`);
content.push(`${this.sanitizeDescription(field.technicalDescription)}\n`);
}
// Additional notes
if (field.note) {
content.push(`\n> [!NOTE]\n`);
content.push(`> ${this.sanitizeDescription(field.note)}\n`);
}
if (field.note2) {
content.push(`\n> [!NOTE]\n`);
content.push(`> ${this.sanitizeDescription(field.note2)}\n`);
}
if (field.note3) {
content.push(`\n> [!NOTE]\n`);
content.push(`> ${this.sanitizeDescription(field.note3)}\n`);
}
}
/**
* Filter forms to only include validation rule forms.
*/
getValidationForms(formsByPath) {
const validationForms = {};
for (const formPath in formsByPath) {
// Check if this is a mctoolsval form
if (formPath.toLowerCase().includes("/mctoolsval/") &&
!formPath.includes("index") &&
!formPath.includes("overview")) {
validationForms[formPath] = formsByPath[formPath];
}
}
return validationForms;
}
/**
* Get severity information from messageType.
*/
getSeverityInfo(messageType) {
if (messageType === undefined) {
return SEVERITY_MAP["info"];
}
// Handle numeric messageType (0 = info, 1 = warning, 2 = error, 3 = recommendation)
if (typeof messageType === "number") {
switch (messageType) {
case 0:
return SEVERITY_MAP["info"];
case 1:
return SEVERITY_MAP["warning"];
case 2:
return SEVERITY_MAP["error"];
case 3:
return SEVERITY_MAP["recommendation"];
default:
return SEVERITY_MAP["info"];
}
}
// Handle string messageType
const key = messageType.toLowerCase();
return SEVERITY_MAP[key] || SEVERITY_MAP["info"];
}
/**
* Check if a field has auto-fix capabilities.
* Handles both object-style matchedValues { [key: string]: string } and
* array-style matchedValues from JSON forms [{ updaterId, updaterIndex, action }].
*/
hasAutoFix(field) {
if (!field.matchedValues) {
return false;
}
// Check if it's an object with any keys
if (typeof field.matchedValues === "object") {
return Object.keys(field.matchedValues).length > 0;
}
return false;
}
/**
* Get auto-fix action descriptions from a field.
* Handles both object-style and array-style matchedValues.
*/
getAutoFixActions(field) {
const actions = [];
if (!field.matchedValues) {
return actions;
}
// The matchedValues could be { [key: string]: string } or parsed from JSON as an array
// When loaded from JSON, it might be an array of objects with 'action' property
const matchedValues = field.matchedValues;
if (Array.isArray(matchedValues)) {
// Array style: [{ updaterId, updaterIndex, action }]
for (const item of matchedValues) {
if (item && typeof item === "object" && "action" in item) {
const actionItem = item;
if (actionItem.action) {
actions.push(actionItem.action);
}
}
}
}
else if (typeof matchedValues === "object" && matchedValues !== null) {
// Object style: { [key: string]: string }
for (const key in matchedValues) {
const value = matchedValues[key];
if (value) {
actions.push(value);
}
}
}
return actions;
}
/**
* Create a markdown anchor from rule ID and title.
*/
getAnchor(id, title) {
// Use a combination of emoji representation and title for unique anchor
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-");
}
/**
* Sanitize description text for markdown.
*/
sanitizeDescription(description) {
description = description.trim();
if (description.length > 10 && !description.endsWith(".") && !description.endsWith(":")) {
description += ".";
}
return description;
}
/**
* Truncate a description to a maximum length.
*/
truncateDescription(description, maxLength) {
description = description.trim();
if (description.length <= maxLength) {
return description;
}
// Find a good break point
const truncated = description.substring(0, maxLength);
const lastSpace = truncated.lastIndexOf(" ");
if (lastSpace > maxLength * 0.7) {
return truncated.substring(0, lastSpace) + "...";
}
return truncated + "...";
}
}
exports.default = ValidationRulesMarkdownGenerator;