diff --git a/change/@minecraft-api-docs-generator-efb1c0e7-576a-46cd-9e12-3f19323ef8b5.json b/change/@minecraft-api-docs-generator-efb1c0e7-576a-46cd-9e12-3f19323ef8b5.json
new file mode 100644
index 00000000..f409af93
--- /dev/null
+++ b/change/@minecraft-api-docs-generator-efb1c0e7-576a-46cd-9e12-3f19323ef8b5.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "Updated MS Docs Commands documentation to support examples",
+ "packageName": "@minecraft/api-docs-generator",
+ "email": "jake@xbox.com",
+ "dependentChangeType": "patch"
+}
diff --git a/change/@minecraft-markup-generators-plugin-cba35642-23cd-4760-be4c-75b9eb620273.json b/change/@minecraft-markup-generators-plugin-cba35642-23cd-4760-be4c-75b9eb620273.json
new file mode 100644
index 00000000..16a43037
--- /dev/null
+++ b/change/@minecraft-markup-generators-plugin-cba35642-23cd-4760-be4c-75b9eb620273.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "Updated MS Docs Commands documentation to support examples",
+ "packageName": "@minecraft/markup-generators-plugin",
+ "email": "jake@xbox.com",
+ "dependentChangeType": "patch"
+}
diff --git a/tools/api-docs-generator-test-snapshots/test/general/__snapshots__/general.spec.ts.snap b/tools/api-docs-generator-test-snapshots/test/general/__snapshots__/general.spec.ts.snap
index bf61ac2f..862fcc62 100644
--- a/tools/api-docs-generator-test-snapshots/test/general/__snapshots__/general.spec.ts.snap
+++ b/tools/api-docs-generator-test-snapshots/test/general/__snapshots__/general.spec.ts.snap
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`General > Properly generates documentation for the input > # Of Files Generated 1`] = `42`;
+exports[`General > Properly generates documentation for the input > # Of Files Generated 1`] = `45`;
exports[`General > Properly generates documentation for the input > msdocs/blocks/block_properties/diamonds_stored.md 1`] = `
"---
@@ -101,6 +101,14 @@ exports[`General > Properly generates documentation for the input > msdocs/comma
href: commands.md
- name: testcommand
href: commands/testcommand.md
+- name: Command argument types
+ items:
+ - name: int
+ href: types/int.md
+ - name: position
+ href: types/position.md
+ - name: target
+ href: types/target.md
"
`;
@@ -121,9 +129,9 @@ description: List and descriptions of Minecraft commands and enums
# List and summary of command argument types and enums:
|Type |Description |Is Enum? |
|----------|-----------|-----------|
-| int | A basic number | |
-| position | | |
-| target | | |
+| [int](./types/int.md) | A basic number | |
+| [position](./types/position.md) | | |
+| [target](./types/target.md) | | |
"
`;
@@ -139,47 +147,68 @@ description: Description and usage of the testcommand command
# \`/testcommand\` Command
A fake command for testing documentation generation
-
-
- | Aliases |
- /notrealcommand, /fakecommand |
-
-
- | Permission Level |
- Any |
-
-
- | Requires Cheats? |
- Yes |
-
-
+| | |
+| --- | --- |
+| **Aliases** | \`/notrealcommand\`, \`/fakecommand\` |
+| **Permission Level** | Any |
+| **Requires Cheats** | Yes |
+
+## Syntax Overview
+
+\`/testcommand add [amount: int]\`
+
+\`/testcommand remove [amount: int]\`
+
+\`/testcommand set \`
+
+\`/testcommand set \`
## Usage
### \`/testcommand add [amount: int]\`
Adds an amount to a target
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+| \`target\` | [target](../types/target.md) | Required | A target entity |
+| \`amount\` | [int](../types/int.md) | Optional | |
+
### \`/testcommand remove [amount: int]\`
Removes an amount from a target
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+| \`target\` | [target](../types/target.md) | Required | A target entity |
+| \`amount\` | [int](../types/int.md) | Optional | |
+
### Set based on Position
\`/testcommand set \`
Sets an area to a biome centered on a position
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+| \`center: x y z\` | [position](../types/position.md) | Required | The position that the biome will be centered on |
+| \`biome\` | TestBiomeEnum | Required | |
+
### Set based on Target
\`/testcommand set \`
Sets an area to a biome centered on a target
-## Arguments
-- \`amount\`: int
-- \`biome\`: TestBiomeEnum
-- \`center: x y z\`: position
-The position that the biome will be centered on
-- \`center\`: target
-The target entity that the biome will be centered on
-- \`target\`: target
-A target entity
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+| \`center\` | [target](../types/target.md) | Required | The target entity that the biome will be centered on |
+| \`biome\` | TestBiomeEnum | Required | |
+
+## Arguments Reference
+
+| Argument | Type | Description |
+| --- | --- | --- |
+| \`amount\` | [int](../types/int.md) | |
+| \`biome\` | TestBiomeEnum | |
+| \`center: x y z\` | [position](../types/position.md) | The position that the biome will be centered on |
+| \`center\` | [target](../types/target.md) | The target entity that the biome will be centered on |
+| \`target\` | [target](../types/target.md) | A target entity |
## Enums
### \`TestBiomeEnum\`
@@ -197,6 +226,47 @@ The plains biome.
"
`;
+exports[`General > Properly generates documentation for the input > msdocs/commands/types/int.md 1`] = `
+"---
+# DO NOT TOUCH — This file was automatically generated by @minecraft/api-docs-generator, to report problems file an issue at https://github.com/Mojang/minecraft-scripting-libraries
+author: jakeshirley
+ms.author: jashir
+ms.service: minecraft-bedrock-edition
+title: int Command Argument Type
+description: Description of the int command argument type
+---
+# \`int\` Command Argument Type
+A basic number
+
+"
+`;
+
+exports[`General > Properly generates documentation for the input > msdocs/commands/types/position.md 1`] = `
+"---
+# DO NOT TOUCH — This file was automatically generated by @minecraft/api-docs-generator, to report problems file an issue at https://github.com/Mojang/minecraft-scripting-libraries
+author: jakeshirley
+ms.author: jashir
+ms.service: minecraft-bedrock-edition
+title: position Command Argument Type
+description: Description of the position command argument type
+---
+# \`position\` Command Argument Type
+"
+`;
+
+exports[`General > Properly generates documentation for the input > msdocs/commands/types/target.md 1`] = `
+"---
+# DO NOT TOUCH — This file was automatically generated by @minecraft/api-docs-generator, to report problems file an issue at https://github.com/Mojang/minecraft-scripting-libraries
+author: jakeshirley
+ms.author: jashir
+ms.service: minecraft-bedrock-edition
+title: target Command Argument Type
+description: Description of the target command argument type
+---
+# \`target\` Command Argument Type
+"
+`;
+
exports[`General > Properly generates documentation for the input > msdocs/scriptapi/TOC.yml 1`] = `
" - name: test-module2
items:
diff --git a/tools/api-docs-generator/src/Context.ts b/tools/api-docs-generator/src/Context.ts
index 9adf9f1e..cf03c672 100644
--- a/tools/api-docs-generator/src/Context.ts
+++ b/tools/api-docs-generator/src/Context.ts
@@ -291,7 +291,7 @@ export class GeneratorContext {
if (config.documentationDirectory) {
const docsPath = path.resolve(config.documentationDirectory);
- const loader = new FileLoader(docsPath, ['.json', '.js', '.ts']);
+ const loader = new FileLoader(docsPath, ['.json', '.js', '.ts', '.mcfunction']);
if (loader.loaded()) {
this.documentationFileLoader = loader;
} else {
diff --git a/tools/api-docs-generator/src/filters/CommandFilters.ts b/tools/api-docs-generator/src/filters/CommandFilters.ts
index 10b80720..8037b418 100644
--- a/tools/api-docs-generator/src/filters/CommandFilters.ts
+++ b/tools/api-docs-generator/src/filters/CommandFilters.ts
@@ -110,7 +110,7 @@ function formatCommandArgumentTypes(moduleJson: MinecraftCommandModule) {
typeJson.name = enumJson.name;
typeJson.is_enum = true;
- if (enumJson.command_references.length > 1) {
+ if (enumJson.values.length > 1 && enumJson.command_references.length > 1) {
typeJson.has_link = true;
}
if (enumJson.values.length === 1) {
@@ -121,6 +121,7 @@ function formatCommandArgumentTypes(moduleJson: MinecraftCommandModule) {
}
}
if (!typeJson.is_enum) {
+ typeJson.has_link = true;
const mappedTypeName = commandTypeNameMappings[typeJson.name];
if (mappedTypeName) {
typeJson.name = mappedTypeName;
diff --git a/tools/api-docs-generator/src/filters/CommonFilters.ts b/tools/api-docs-generator/src/filters/CommonFilters.ts
index c482e59a..7f57f122 100644
--- a/tools/api-docs-generator/src/filters/CommonFilters.ts
+++ b/tools/api-docs-generator/src/filters/CommonFilters.ts
@@ -32,6 +32,8 @@ import {
BlockPropertyDocsValidator,
CommandDocsValidator,
CommandEnumDocsValidator,
+ CommandExampleDocsData,
+ CommandExampleDocsValidator,
CommonDocsDescriptionValidator,
ScriptCommonDocsValidator,
ScriptFunctionDocsValidator,
@@ -671,14 +673,9 @@ function addExamples(
for (const examplePath of exampleFilePaths) {
const exampleName = path.basename(examplePath);
- const exampleFileData = fileLoader.readFile(examplePath);
- const exampleFileStrings: string[] = exampleFileData
- .toString()
- .split('\n')
- .map(line => line.replace(/\n|\r/, ''));
- if (exampleFileStrings.at(-1) === '') {
- exampleFileStrings.splice(-1); // Remove newline at end of example file
- }
+ const exampleFileData = fileLoader.readFileAsString(examplePath);
+ const exampleFileStrings =
+ exampleFileData.length === 0 ? [] : utils.normalizeExampleText(exampleFileData).split('\n');
const escapedExampleFileStrings = exampleFileStrings.map(line =>
line.replace(/\/\*/g, `/\\*`).replace(/\*\//g, `*\\/`)
@@ -1024,6 +1021,69 @@ function addCommandTypeDescriptions(
}
}
+function addCommandExamples(fileLoader: FileLoader, commandJson: MinecraftCommand, commandFolderPath: string): void {
+ commandJson.command_examples = [];
+ for (const overload of commandJson.overloads) {
+ overload.overload_examples = [];
+ }
+
+ const examplesDirectory = path.join(commandFolderPath, '_examples');
+ const exampleFiles = utils
+ .getFiles(fileLoader.joinToRoot(examplesDirectory))
+ .filter(filename => fileLoader.canLoadFile(path.join(examplesDirectory, filename)))
+ .sort();
+ const sourceFiles = exampleFiles.filter(filename => path.extname(filename) === '.mcfunction');
+ for (const filename of exampleFiles.filter(filename => path.extname(filename) === '.json')) {
+ const sourceName = `${path.basename(filename, '.json')}.mcfunction`;
+ if (!sourceFiles.includes(sourceName)) {
+ log.error(
+ `Command example metadata '${path.join(examplesDirectory, filename)}' has no matching '${sourceName}' file.`
+ );
+ }
+ }
+
+ for (const filename of sourceFiles) {
+ const examplePath = path.join(examplesDirectory, filename);
+ const infoPath = path.join(examplesDirectory, `${path.basename(filename, '.mcfunction')}.json`);
+ let info: CommandExampleDocsData = {};
+ if (fileLoader.canLoadFile(infoPath)) {
+ const parsedInfo = parseJsonSafe(fileLoader, infoPath, CommandExampleDocsValidator);
+ if (!parsedInfo) {
+ continue;
+ }
+ info = parsedInfo;
+ }
+ if (info.title !== undefined && !info.title.trim()) {
+ log.error(`Command example metadata '${infoPath}' has an empty title.`);
+ continue;
+ }
+ const overload =
+ info.overload === undefined
+ ? undefined
+ : commandJson.overloads.find(overload => Number(overload.name) === info.overload);
+ if (info.overload !== undefined && !overload) {
+ log.error(`Command example metadata '${infoPath}' references unknown overload '${info.overload}'.`);
+ continue;
+ }
+
+ const code = utils.normalizeExampleText(fileLoader.readFileAsString(examplePath));
+ if (!code.trim()) {
+ log.error(`Command example file '${examplePath}' is empty.`);
+ continue;
+ }
+ const examples = overload ? overload.overload_examples : commandJson.command_examples;
+ examples.push({
+ title: info.title ?? filename,
+ code,
+ description: info.description ? splitStringByNewline(info.description) : [],
+ });
+ commandJson.has_comments = true;
+ if (overload) {
+ overload.has_comments = true;
+ }
+ }
+}
+
function addCommandDescriptions(fileLoader: FileLoader, commandJson: MinecraftCommand, moduleFolderPath: string) {
commandJson.has_comments = false;
@@ -1040,17 +1100,21 @@ function addCommandDescriptions(fileLoader: FileLoader, commandJson: MinecraftCo
if (infoJson.overloads) {
for (const overloadJson of commandJson.overloads) {
const overloadInfoJson = infoJson.overloads.find(o => Number(overloadJson.name) === o.id);
- if (overloadInfoJson && overloadInfoJson.description) {
- overloadJson.has_comments = true;
- overloadJson.overload_description = splitStringByNewline(overloadInfoJson.description);
- if (overloadInfoJson.header) {
- overloadJson.overload_header = overloadInfoJson.header;
+ if (overloadInfoJson) {
+ overloadJson.overload_header = overloadInfoJson.header;
+ if (overloadInfoJson.description) {
+ overloadJson.has_comments = true;
+ overloadJson.overload_description = splitStringByNewline(overloadInfoJson.description);
}
}
}
}
if (infoJson.arguments) {
- for (const argumentJson of commandJson.arguments) {
+ const argumentsToDocument = new Set([
+ ...commandJson.arguments,
+ ...commandJson.overloads.flatMap(overload => overload.params),
+ ]);
+ for (const argumentJson of argumentsToDocument) {
const argInfoJson = infoJson.arguments.find(a => argumentJson.directory_name === a.name);
if (argInfoJson && argInfoJson.description) {
argumentJson.has_comments = true;
@@ -1060,6 +1124,8 @@ function addCommandDescriptions(fileLoader: FileLoader, commandJson: MinecraftCo
}
}
+ addCommandExamples(fileLoader, commandJson, commandFolderPath);
+
if (commandJson.command_enums) {
for (const enumJson of commandJson.command_enums) {
addCommandEnumDescriptions(fileLoader, enumJson, commandFolderPath);
diff --git a/tools/api-docs-generator/src/modules/MinecraftCommandModule.ts b/tools/api-docs-generator/src/modules/MinecraftCommandModule.ts
index e6aef6e5..306d472c 100644
--- a/tools/api-docs-generator/src/modules/MinecraftCommandModule.ts
+++ b/tools/api-docs-generator/src/modules/MinecraftCommandModule.ts
@@ -71,6 +71,13 @@ export const MinecraftCommandArgumentRecord = Record({
});
export type MinecraftCommandArgument = Static;
+export const MinecraftCommandExampleRecord = Record({
+ title: String,
+ code: String,
+ description: Optional(Array(String)),
+});
+export type MinecraftCommandExample = Static;
+
export const MinecraftCommandOverloadRecord = Record({
name: String,
params: Array(MinecraftCommandArgumentRecord),
@@ -79,6 +86,7 @@ export const MinecraftCommandOverloadRecord = Record({
has_comments: Optional(Boolean),
overload_description: Optional(Array(String)),
overload_header: Optional(String),
+ overload_examples: Optional(Array(MinecraftCommandExampleRecord)),
});
export type MinecraftCommandOverload = Static;
@@ -100,6 +108,7 @@ export const MinecraftCommandRecord = Record({
has_comments: Optional(Boolean),
command_description: Optional(Array(String)),
+ command_examples: Optional(Array(MinecraftCommandExampleRecord)),
});
export type MinecraftCommand = Static;
diff --git a/tools/api-docs-generator/src/modules/MinecraftDocsTypes.ts b/tools/api-docs-generator/src/modules/MinecraftDocsTypes.ts
index fcdbe4d2..3d73d901 100644
--- a/tools/api-docs-generator/src/modules/MinecraftDocsTypes.ts
+++ b/tools/api-docs-generator/src/modules/MinecraftDocsTypes.ts
@@ -83,6 +83,15 @@ export type ScriptNestedFunctionDocsData = Static;
+export const CommandExampleDocsValidator = Intersect(
+ CommonDocsDescriptionValidator,
+ Record({
+ title: Optional(String),
+ overload: Optional(Number),
+ })
+);
+export type CommandExampleDocsData = Static;
+
export const CommandDocsValidator = Intersect(
CommonDocsDescriptionValidator,
Record({
diff --git a/tools/api-docs-generator/src/test/normalizeExampleText.test.ts b/tools/api-docs-generator/src/test/normalizeExampleText.test.ts
new file mode 100644
index 00000000..55d212a5
--- /dev/null
+++ b/tools/api-docs-generator/src/test/normalizeExampleText.test.ts
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+
+import { FileLoader } from '../FileLoader';
+import { CommonFilters } from '../filters';
+import { MinecraftRelease } from '../MinecraftRelease';
+import { MinecraftScriptModule } from '../modules/MinecraftScriptModule';
+import { normalizeExampleText } from '../utilities';
+
+describe('normalizeExampleText', () => {
+ it.each([
+ { name: 'text without a final newline', input: 'first\nsecond', expected: 'first\nsecond' },
+ { name: 'a leading BOM', input: '\uFEFFfirst', expected: 'first' },
+ { name: 'an embedded BOM', input: 'first\uFEFFsecond', expected: 'first\uFEFFsecond' },
+ { name: 'LF line endings', input: 'first\nsecond\n', expected: 'first\nsecond' },
+ { name: 'CRLF line endings', input: 'first\r\nsecond\r\n', expected: 'first\nsecond' },
+ { name: 'CR line endings', input: 'first\rsecond\r', expected: 'first\nsecond' },
+ { name: 'mixed line endings', input: 'first\r\nsecond\rthird\n', expected: 'first\nsecond\nthird' },
+ { name: 'leading blank lines', input: '\n\nfirst\n', expected: '\n\nfirst' },
+ { name: 'extra trailing blank lines', input: 'first\n\n\n', expected: 'first\n\n' },
+ { name: 'indentation and trailing spaces', input: '\tfirst \n second \n', expected: '\tfirst \n second ' },
+ { name: 'empty input', input: '', expected: '' },
+ { name: 'a newline only', input: '\n', expected: '' },
+ { name: 'a BOM only', input: '\uFEFF', expected: '' },
+ { name: 'spaces only', input: ' ', expected: ' ' },
+ ])('handles $name', ({ input, expected }) => {
+ expect(normalizeExampleText(input)).toBe(expected);
+ });
+});
+
+describe('Script example normalization', () => {
+ let directory: string;
+
+ beforeEach(() => {
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'script-example-normalization-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(directory, { recursive: true, force: true });
+ });
+
+ it.each([
+ {
+ name: 'LF input with whitespace and a blank line',
+ input: '/* comment */\n const value = 1; \n\n',
+ expected: ['/* comment */', ' const value = 1; ', ''],
+ },
+ {
+ name: 'a BOM and mixed line endings',
+ input: '\uFEFF/* comment */\rconst first = 1;\r\nconst second = 2;\n',
+ expected: ['/* comment */', 'const first = 1;', 'const second = 2;'],
+ },
+ { name: 'empty input', input: '', expected: [] },
+ { name: 'one blank line', input: '\n', expected: [''] },
+ { name: 'two blank lines', input: '\r\n\r\n', expected: ['', ''] },
+ ])('normalizes local and shared examples with $name', ({ input, expected }) => {
+ const module: MinecraftScriptModule = {
+ name: 'test-module',
+ uuid: '411a0018-bbd6-43a5-96eb-ce7c4162f491',
+ version: '1.0.0',
+ minecraft_version: '1.0.0',
+ module_type: 'script',
+ };
+ const moduleDirectory = path.join(directory, module.name);
+ fs.mkdirSync(path.join(moduleDirectory, '_examples'), { recursive: true });
+ fs.mkdirSync(path.join(moduleDirectory, '_shared_examples'));
+ fs.writeFileSync(path.join(moduleDirectory, '_examples', 'local.ts'), input);
+ fs.writeFileSync(path.join(moduleDirectory, '_shared_examples', 'shared.ts'), input);
+ fs.writeFileSync(path.join(moduleDirectory, '_example_files.json'), JSON.stringify(['shared.ts']));
+
+ const release = new MinecraftRelease('1.0.0');
+ release.script_modules = [module];
+ const loader = new FileLoader(directory, ['.json', '.ts']);
+ for (const [, filter] of CommonFilters.filters) {
+ filter([release], loader);
+ }
+
+ expect(module.examples.map(example => example.name)).toEqual(['local.ts', 'shared.ts']);
+ for (const example of module.examples) {
+ expect(example.code.text).toEqual(expected);
+ expect(example.code.escaped_text).toEqual(
+ expected.map(line => line.replace(/\/\*/g, '/\\*').replace(/\*\//g, '*\\/'))
+ );
+ }
+ });
+});
diff --git a/tools/api-docs-generator/src/utilities/FormatMarkdownTableCell.ts b/tools/api-docs-generator/src/utilities/FormatMarkdownTableCell.ts
new file mode 100644
index 00000000..46aa7a6d
--- /dev/null
+++ b/tools/api-docs-generator/src/utilities/FormatMarkdownTableCell.ts
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/**
+ * Converts LF/CRLF line breaks to HTML breaks and escapes unescaped table delimiters.
+ */
+export function formatMarkdownTableCell(text: string): string {
+ return text.replace(/\r?\n/g, '
').replace(/(? {
+ let directory: string;
+ let give: string;
+ let other: string;
+ let outputDirectory: string;
+
+ const parameter = (name: string, type: string, optional = false): MinecraftCommandArgument => ({
+ name,
+ is_optional: optional,
+ type: { name: type },
+ });
+
+ beforeAll(async () => {
+ directory = fs.mkdtempSync(path.join(os.tmpdir(), 'command-docs-'));
+ outputDirectory = path.join(directory, 'output');
+ const docsDirectory = path.join(directory, 'docs');
+ const writeInfo = (segments: string[], data: unknown) => {
+ const filename = path.join(docsDirectory, 'test-commands', ...segments, 'info.json');
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
+ fs.writeFileSync(filename, JSON.stringify(data));
+ };
+ writeInfo(['commands', 'give'], {
+ description: 'Give an item.',
+ overloads: [
+ {
+ id: 1,
+ header: 'Give items',
+ },
+ ],
+ arguments: [
+ { name: 'player', description: 'A **player** or `selector`.' },
+ { name: 'amount-int', description: ['Use `1|2`.', 'Default: `1`.'] },
+ { name: 'amount-string', description: 'A textual amount.' },
+ ],
+ });
+ const examplesDirectory = path.join(docsDirectory, 'test-commands', 'commands', 'give', '_examples');
+ fs.mkdirSync(examplesDirectory);
+ fs.writeFileSync(path.join(examplesDirectory, 'building-materials.mcfunction'), '/give @s stone 64\n');
+ fs.writeFileSync(
+ path.join(examplesDirectory, 'building-materials.json'),
+ JSON.stringify({ title: 'Command example' })
+ );
+ fs.writeFileSync(
+ path.join(examplesDirectory, 'components.mcfunction'),
+ '/give @p diamond_pickaxe 1 0 {"minecraft:can_destroy":{"blocks":["stone"]}}\r\n/give @s torch\r\n'
+ );
+ fs.writeFileSync(
+ path.join(examplesDirectory, 'components.json'),
+ JSON.stringify({
+ title: 'Component example',
+ description: ['Keep the JSON unchanged.', 'Run this command:'],
+ overload: 1,
+ })
+ );
+ writeInfo(['command_types', 'target'], { description: 'Select a player using `@s`.' });
+ writeInfo(['command_types', 'int'], { description: 'An integer.' });
+ const module: MinecraftCommandModule = {
+ name: 'test-commands',
+ module_type: 'commands',
+ minecraft_version: '1.0.0',
+ commands: [
+ {
+ name: 'give',
+ description: 'Raw description.',
+ aliases: [{ name: 'gift' }, { name: 'present' }],
+ permission_level: 1,
+ requires_cheats: true,
+ overloads: [
+ {
+ name: '1',
+ params: [
+ parameter('player', 'SELECTION'),
+ parameter('itemName', 'ITEM'),
+ parameter('amount', 'INT', true),
+ parameter('components', 'JSON_OBJECT', true),
+ ],
+ },
+ {
+ name: '2',
+ params: [parameter('player', 'SELECTION', true), parameter('amount', 'INT', true)],
+ },
+ {
+ name: '3',
+ params: [
+ parameter('amount', 'ID'),
+ parameter('mode', 'LOCALMODE'),
+ parameter('keyword', 'KEYWORD', true),
+ parameter('objective', 'SCOREBOARDOBJECTIVES', true),
+ ],
+ },
+ ],
+ },
+ {
+ name: 'other',
+ description: 'Metadata-only command.',
+ permission_level: 0,
+ requires_cheats: false,
+ overloads: [
+ { name: '1', params: [] },
+ { name: '2', params: [parameter('mode', 'KEYWORD')] },
+ {
+ name: '3',
+ params: [
+ parameter('mode', 'KEYWORD', true),
+ parameter('item', 'ITEM'),
+ parameter('value', 'WILDCARDINT', true),
+ parameter('objective', 'SCOREBOARDOBJECTIVES', true),
+ ],
+ },
+ ],
+ },
+ ],
+ command_enums: [
+ { name: 'Item', values: [{ value: 'stone' }, { value: 'torch' }] },
+ { name: 'Keyword', values: [{ value: 'reset' }] },
+ { name: 'LocalMode', values: [{ value: 'first' }, { value: 'second' }] },
+ { name: 'ScoreboardObjectives', values: [] },
+ ],
+ };
+ const release = new MinecraftRelease('1.0.0');
+ release.command_modules = [module];
+ const context = await GeneratorContext.Init({
+ inputDirectory: directory,
+ outputDirectory,
+ documentationDirectory: docsDirectory,
+ ignoreConfig: true,
+ generatorsToRun: ['msdocs'],
+ log: { level: 'error' },
+ });
+ for (const [, filter] of CommonFilters.filters) {
+ filter([release], context.documentationFileLoader);
+ }
+ await new MSDocsMarkdownGenerator().generateFiles(context, [release], outputDirectory);
+ give = fs
+ .readFileSync(path.join(outputDirectory, 'commands', 'commands', 'give.md'), 'utf8')
+ .replace(/\r\n/g, '\n');
+ other = fs
+ .readFileSync(path.join(outputDirectory, 'commands', 'commands', 'other.md'), 'utf8')
+ .replace(/\r\n/g, '\n');
+ });
+
+ afterAll(() => {
+ if (directory) {
+ fs.rmSync(directory, { recursive: true, force: true });
+ }
+ });
+
+ it('renders syntax, metadata, and usage tables in parameter order', () => {
+ expect(give).toContain('| **Aliases** | `/gift`, `/present` |');
+ expect(give).toContain('| **Permission Level** | Game Directors |');
+ expect(give).toContain('| **Requires Cheats** | Yes |');
+ expect(give).toContain('## Syntax Overview');
+ expect(give).toContain('`/give [amount: int] [components: json]`');
+ expect(give).toContain('### Give items');
+ expect(give).toContain('| Argument | Type | Required | Description |');
+ expect(give).toContain('| `player` | [target](../types/target.md) | Required | A **player** or `selector`. |');
+ expect(give).toContain('| `player` | [target](../types/target.md) | Optional | A **player** or `selector`. |');
+ expect(give.indexOf('| `player`')).toBeLessThan(give.indexOf('| `itemName`'));
+ expect(give.indexOf('| `itemName`')).toBeLessThan(give.indexOf('| `amount`'));
+ });
+
+ it('documents every overload and escapes table descriptions without losing Markdown', () => {
+ expect(give.match(/\| Optional \| Use `1\\\|2`\.
Default: `1`\. \|/g)).toHaveLength(2);
+ expect(give).toContain('| `amount` | [string](../types/string.md) | Required | A textual amount. |');
+ const reference = give.slice(give.indexOf('## Arguments Reference'));
+ expect(reference).toContain('| `amount` | [int](../types/int.md) | Use `1\\|2`.
Default: `1`. |');
+ expect(reference).toContain('| `amount` | [string](../types/string.md) | A textual amount. |');
+ expect(reference.indexOf('| `amount`')).toBeLessThan(reference.indexOf('| `player`'));
+ });
+
+ it('renders command and overload examples once, retaining JSON and multiple lines', () => {
+ expect(give.match(/## Examples/g)).toHaveLength(1);
+ expect(give.match(/\*\*Examples:\*\*/g)).toHaveLength(1);
+ expect(give.match(/\/give @s stone 64/g)).toHaveLength(1);
+ expect(give).toContain('### Command example\n\n```text\n/give @s stone 64\n```');
+ expect(give).toContain('Keep the JSON unchanged.\n\nRun this command:');
+ expect(give).toContain(
+ '```text\n/give @p diamond_pickaxe 1 0 {"minecraft:can_destroy":{"blocks":["stone"]}}\n/give @s torch\n```'
+ );
+ });
+
+ it('preserves metadata-only commands, literal syntax, optional keywords, and inline enums', () => {
+ expect(other).toContain('Metadata-only command.');
+ expect(other).toContain('| **Requires Cheats** | No |');
+ expect(other).not.toContain('Examples');
+ expect(other).toContain('`/other reset`');
+ expect(other).toContain(
+ '`/other [reset: Keyword] [value: wildcard int] [objective: ScoreboardObjectives]`'
+ );
+ expect(other.match(/\| Argument \| Type \| Required \| Description \|/g)).toHaveLength(1);
+ expect(other).toContain('| `reset` | Keyword | Optional | |');
+ expect(give).toContain('### `LocalMode`');
+ expect(give).toContain('| `mode` | LocalMode | Required | |');
+ expect(give).toContain('| `objective` | ScoreboardObjectives | Optional | |');
+ });
+
+ it('generates primitive type pages and keeps every argument link resolvable', () => {
+ const commandDirectory = path.join(outputDirectory, 'commands', 'commands');
+ for (const markdown of [give, other]) {
+ for (const match of markdown.matchAll(/\]\((\.\.\/(?:types|enums)\/[^)]+)\)/g)) {
+ expect(fs.existsSync(path.resolve(commandDirectory, decodeURIComponent(match[1])))).toBe(true);
+ }
+ }
+ expect(fs.readFileSync(path.join(outputDirectory, 'commands', 'types', 'target.md'), 'utf8')).toContain(
+ 'Select a player using `@s`.'
+ );
+ expect(other).toContain('[wildcard int](../types/wildcard%20int.md)');
+ expect(fs.readFileSync(path.join(outputDirectory, 'commands', 'commands.md'), 'utf8')).toContain(
+ '[wildcard int](./types/wildcard%20int.md)'
+ );
+ expect(fs.readFileSync(path.join(outputDirectory, 'commands', 'TOC.yml'), 'utf8')).toContain(
+ 'href: types/target.md'
+ );
+ });
+
+ it('adds a section TOC before enum values without listing individual values', () => {
+ const markdown = fs
+ .readFileSync(path.join(outputDirectory, 'commands', 'enums', 'Item.md'), 'utf8')
+ .replace(/\r\n/g, '\n');
+ const contents = markdown.slice(markdown.indexOf('## Contents'), markdown.indexOf('## Values'));
+ expect(contents).toBe('## Contents\n\n- [Values](#values)\n- [References](#references)\n\n');
+ expect(markdown).toContain('## Values\n- `stone`\n- `torch`');
+ expect(markdown).toContain('## References');
+ expect(markdown).toContain('- [give](../commands/give.md)');
+ });
+
+ it.each([{ commandReferences: undefined }, { commandReferences: [] }])(
+ 'omits the References TOC entry when references are $commandReferences',
+ ({ commandReferences }) => {
+ const template = fs.readFileSync(
+ path.join(__dirname, '..', 'templates', 'msdocs', 'commands', 'enum.mustache'),
+ 'utf8'
+ );
+ const markdown = mustache.render(template, {
+ enum_name: 'Test',
+ values: [{ value: 'first' }, { value: 'second' }],
+ command_references: commandReferences,
+ });
+ expect(markdown).toContain('- [Values](#values)');
+ expect(markdown).toContain('## Values');
+ expect(markdown).not.toContain('- [References](#references)');
+ expect(markdown).not.toContain('## References');
+ }
+ );
+
+ it('accepts existing documentation and validates optional example sidecars', () => {
+ expect(CommandDocsValidator.guard({ description: 'Existing docs.', overloads: [{ id: 1 }] })).toBe(true);
+ expect(CommandExampleDocsValidator.guard({})).toBe(true);
+ expect(
+ CommandExampleDocsValidator.guard({
+ title: 'Example',
+ description: 'A description.',
+ overload: 1,
+ })
+ ).toBe(true);
+ expect(CommandExampleDocsValidator.guard({ title: 42 })).toBe(false);
+ expect(CommandExampleDocsValidator.guard({ overload: '1' })).toBe(false);
+ });
+});
diff --git a/tools/markup-generators-plugin/src/generators/MSDocsMarkdownGenerator.ts b/tools/markup-generators-plugin/src/generators/MSDocsMarkdownGenerator.ts
index 5e479fe6..509b8933 100644
--- a/tools/markup-generators-plugin/src/generators/MSDocsMarkdownGenerator.ts
+++ b/tools/markup-generators-plugin/src/generators/MSDocsMarkdownGenerator.ts
@@ -18,6 +18,8 @@ import {
MinecraftBlockProperty,
MinecraftClass,
MinecraftCommand,
+ MinecraftCommandArgument,
+ MinecraftCommandArgumentType,
MinecraftCommandEnum,
MinecraftCommandModule,
MinecraftEnum,
@@ -386,9 +388,27 @@ export class MSDocsMarkdownGenerator implements MarkupGenerator {
outputDirectory: string
): void {
const msdocsTemplateFileData = mdTemplateFiles.readFileAsString('commands/command.mustache');
- const msdocsProcessedData = mustache.render(msdocsTemplateFileData, commandJson, {
+ const formatArgument = (argument: MinecraftCommandArgument) => ({
+ ...argument,
+ table_description: Utils.formatMarkdownTableCell((argument.argument_description ?? []).join('\n')),
+ table_type: argument.type.has_link
+ ? `[${argument.type.name}](../${argument.type.is_enum ? 'enums' : 'types'}/${encodeURIComponent(argument.type.name)}.md)`
+ : argument.type.name,
+ });
+ const commandView = {
+ ...commandJson,
+ arguments: commandJson.arguments?.map(formatArgument),
+ overloads: commandJson.overloads?.map(overload => ({
+ ...overload,
+ usage_arguments: overload.params
+ .filter(argument => !argument.type.is_keyword || argument.is_optional)
+ .map(formatArgument),
+ })),
+ };
+ const msdocsProcessedData = mustache.render(msdocsTemplateFileData, commandView, {
overload: mdTemplateFiles.readFileAsString('commands/overload.mustache'),
parameter: mdTemplateFiles.readFileAsString('commands/parameter.mustache'),
+ example: mdTemplateFiles.readFileAsString('commands/example.mustache'),
inline_enum: mdTemplateFiles.readFileAsString('commands/inline_enum.mustache'),
default_metadata: mdTemplateFiles.readFileAsString('commands/default_metadata.mustache'),
});
@@ -411,14 +431,30 @@ export class MSDocsMarkdownGenerator implements MarkupGenerator {
fs.writeFileSync(msdocsEnumFilePath, msdocsProcessedData);
}
+ private generateCommandTypeFiles(
+ typeJson: MinecraftCommandArgumentType,
+ mdTemplateFiles: FileLoader,
+ outputDirectory: string
+ ): void {
+ const msdocsProcessedData = mustache.render(
+ mdTemplateFiles.readFileAsString('commands/type.mustache'),
+ typeJson,
+ { default_metadata: mdTemplateFiles.readFileAsString('commands/default_metadata.mustache') }
+ );
+ const msdocsTypeFilePath = path.join(outputDirectory, 'types', `${typeJson.name}.md`);
+ fs.mkdirSync(path.dirname(msdocsTypeFilePath), { recursive: true });
+ fs.writeFileSync(msdocsTypeFilePath, msdocsProcessedData);
+ }
+
private generateCommandsTableOfContents(
- commandsJson: MinecraftCommand[],
+ moduleJson: MinecraftCommandModule,
mdTemplateFiles: FileLoader,
outputDirectory: string
): void {
const msdocsTemplateFileData = mdTemplateFiles.readFileAsString('commands/toc.mustache');
const msdocsProcessedData = mustache.render(msdocsTemplateFileData, {
- commands: commandsJson,
+ commands: moduleJson.commands,
+ command_types: moduleJson.command_types,
});
const msdocsModuleFilePath = path.join(outputDirectory, `TOC.yml`);
fs.mkdirSync(path.dirname(msdocsModuleFilePath), { recursive: true });
@@ -436,7 +472,10 @@ export class MSDocsMarkdownGenerator implements MarkupGenerator {
{
commands: moduleJson.commands,
command_enums: moduleJson.command_enums,
- command_types: moduleJson.command_types,
+ command_types: moduleJson.command_types?.map(type => ({
+ ...type,
+ type_path: encodeURIComponent(type.name),
+ })),
},
{
default_metadata: mdTemplateFiles.readFileAsString('commands/default_metadata.mustache'),
@@ -568,9 +607,13 @@ export class MSDocsMarkdownGenerator implements MarkupGenerator {
this.generateCommandEnumFiles(enumJson, mdTemplateFiles, commandsMSDocsOutputPath);
}
+ for (const typeJson of moduleJson.command_types ?? []) {
+ this.generateCommandTypeFiles(typeJson, mdTemplateFiles, commandsMSDocsOutputPath);
+ }
+
this.generateCommandsSummaryFile(moduleJson, mdTemplateFiles, commandsMSDocsOutputPath);
- this.generateCommandsTableOfContents(moduleJson.commands, mdTemplateFiles, commandsMSDocsOutputPath);
+ this.generateCommandsTableOfContents(moduleJson, mdTemplateFiles, commandsMSDocsOutputPath);
}
///
diff --git a/tools/markup-generators-plugin/templates/msdocs/commands/command.mustache b/tools/markup-generators-plugin/templates/msdocs/commands/command.mustache
index 27022c3f..df5bba45 100644
--- a/tools/markup-generators-plugin/templates/msdocs/commands/command.mustache
+++ b/tools/markup-generators-plugin/templates/msdocs/commands/command.mustache
@@ -12,35 +12,43 @@ description: Description and usage of the {{{command_name}}} command
{{{description}}}
{{/command_description}}
-
+| | |
+| --- | --- |
{{#aliases.length}}
-
- | Aliases |
- {{#aliases}}/{{{name}}}{{^is_last}}, {{/is_last}}{{/aliases}} |
-
+| **Aliases** | {{#aliases}}`/{{{name}}}`{{^is_last}}, {{/is_last}}{{/aliases}} |
{{/aliases.length}}
-
- | Permission Level |
- {{permission_level_name}} |
-
-
- | Requires Cheats? |
- {{#requires_cheats}}Yes{{/requires_cheats}}{{^requires_cheats}}No{{/requires_cheats}} |
-
-
+| **Permission Level** | {{permission_level_name}} |
+| **Requires Cheats** | {{#requires_cheats}}Yes{{/requires_cheats}}{{^requires_cheats}}No{{/requires_cheats}} |
+{{#overloads.length}}
+
+## Syntax Overview
+{{#overloads}}
+
+`/{{{command_name}}}{{#params}} {{> parameter}}{{/params}}`
+{{/overloads}}
## Usage
{{#overloads}}
{{> overload}}
{{/overloads}}
+{{/overloads.length}}
+{{#command_examples.length}}
+
+## Examples
+{{#command_examples}}
+
+### {{{title}}}
+{{> example}}
+{{/command_examples}}
+{{/command_examples.length}}
{{#arguments.length}}
-## Arguments
+## Arguments Reference
+
+| Argument | Type | Description |
+| --- | --- | --- |
{{#arguments}}
-- `{{{param_name}}}{{#type.syntax}}: {{{.}}}{{/type.syntax}}`: {{#type.has_link}}[{{{type.name}}}]{{#type.is_enum}}(../enums/{{{type.name}}}.md){{/type.is_enum}}{{^type.is_enum}}(../types/{{{type.name}}}.md){{/type.is_enum}}{{/type.has_link}}{{^type.has_link}}{{{type.name}}}{{/type.has_link}}
-{{#argument_description}}
-{{{.}}}
-{{/argument_description}}
+| `{{{param_name}}}{{#type.syntax}}: {{{.}}}{{/type.syntax}}` | {{{table_type}}} | {{{table_description}}} |
{{/arguments}}
{{/arguments.length}}
{{#command_enums.length}}
diff --git a/tools/markup-generators-plugin/templates/msdocs/commands/enum.mustache b/tools/markup-generators-plugin/templates/msdocs/commands/enum.mustache
index 18a0ce59..a17b32d6 100644
--- a/tools/markup-generators-plugin/templates/msdocs/commands/enum.mustache
+++ b/tools/markup-generators-plugin/templates/msdocs/commands/enum.mustache
@@ -8,6 +8,13 @@ description: Description and values of the {{{enum_name}}} enum
{{{.}}}
{{/enum_description}}
+## Contents
+
+- [Values](#values)
+{{#command_references.length}}
+- [References](#references)
+{{/command_references.length}}
+
## Values
{{#values}}
- `{{{value}}}`
diff --git a/tools/markup-generators-plugin/templates/msdocs/commands/example.mustache b/tools/markup-generators-plugin/templates/msdocs/commands/example.mustache
new file mode 100644
index 00000000..e4a537ad
--- /dev/null
+++ b/tools/markup-generators-plugin/templates/msdocs/commands/example.mustache
@@ -0,0 +1,8 @@
+
+{{#description}}
+{{{.}}}
+
+{{/description}}
+```text
+{{{code}}}
+```
diff --git a/tools/markup-generators-plugin/templates/msdocs/commands/overload.mustache b/tools/markup-generators-plugin/templates/msdocs/commands/overload.mustache
index c0516f3c..22e1b8ac 100644
--- a/tools/markup-generators-plugin/templates/msdocs/commands/overload.mustache
+++ b/tools/markup-generators-plugin/templates/msdocs/commands/overload.mustache
@@ -6,6 +6,23 @@
{{#overload_description}}
{{{.}}}
{{/overload_description}}
+{{#usage_arguments.length}}
+
+| Argument | Type | Required | Description |
+| --- | --- | --- | --- |
+{{#usage_arguments}}
+| `{{{param_name}}}{{#type.syntax}}: {{{.}}}{{/type.syntax}}` | {{{table_type}}} | {{#is_optional}}Optional{{/is_optional}}{{^is_optional}}Required{{/is_optional}} | {{{table_description}}} |
+{{/usage_arguments}}
+{{/usage_arguments.length}}
+{{#overload_examples.length}}
+
+**Examples:**
+{{#overload_examples}}
+
+{{{title}}}
+{{> example}}
+{{/overload_examples}}
+{{/overload_examples.length}}
{{^is_last}}
{{/is_last}}
\ No newline at end of file
diff --git a/tools/markup-generators-plugin/templates/msdocs/commands/summary.mustache b/tools/markup-generators-plugin/templates/msdocs/commands/summary.mustache
index eaa5cf25..45517142 100644
--- a/tools/markup-generators-plugin/templates/msdocs/commands/summary.mustache
+++ b/tools/markup-generators-plugin/templates/msdocs/commands/summary.mustache
@@ -14,7 +14,7 @@ description: List and descriptions of Minecraft commands and enums
|Type |Description |Is Enum? |
|----------|-----------|-----------|
{{#command_types}}
-| {{{name}}} | {{type_description}} | |
+| [{{{name}}}](./types/{{{type_path}}}.md) | {{type_description}} | |
{{/command_types}}
{{#command_enums}}
| [`{{{name}}}`](./enums/{{{name}}}.md) | {{enum_description}} | Yes |
diff --git a/tools/markup-generators-plugin/templates/msdocs/commands/toc.mustache b/tools/markup-generators-plugin/templates/msdocs/commands/toc.mustache
index 10bcaff4..b375dc65 100644
--- a/tools/markup-generators-plugin/templates/msdocs/commands/toc.mustache
+++ b/tools/markup-generators-plugin/templates/msdocs/commands/toc.mustache
@@ -4,3 +4,11 @@
- name: {{{name}}}
href: commands/{{{name}}}.md
{{/commands}}
+{{#command_types.length}}
+- name: Command argument types
+ items:
+{{#command_types}}
+ - name: {{{name}}}
+ href: types/{{{name}}}.md
+{{/command_types}}
+{{/command_types.length}}
diff --git a/tools/markup-generators-plugin/templates/msdocs/commands/type.mustache b/tools/markup-generators-plugin/templates/msdocs/commands/type.mustache
new file mode 100644
index 00000000..0106c2b5
--- /dev/null
+++ b/tools/markup-generators-plugin/templates/msdocs/commands/type.mustache
@@ -0,0 +1,10 @@
+---
+{{ >default_metadata }}
+title: {{{type_name}}} Command Argument Type
+description: Description of the {{{type_name}}} command argument type
+---
+# `{{{type_name}}}` Command Argument Type
+{{#type_description}}
+{{{.}}}
+
+{{/type_description}}