From 9cc010e8c44c4f25b7d0b107751e385f7e0ec356 Mon Sep 17 00:00:00 2001
From: Ed Mackey <elm19087@gmail.com>
Date: Mon, 16 Aug 2021 14:27:23 -0400
Subject: [PATCH] Add a commandline option for the **MUST** keyword.

---
 README.md                           |   1 +
 bin/wetzel.js                       |   6 +-
 lib/generateMarkdown.js             |  20 +-
 lib/style.js                        |  24 +-
 test/test-golden/example-keyword.md |  43 +++
 test/test-golden/nested-keyword.md  | 417 ++++++++++++++++++++++++++++
 test/test-schemas/index.json        |   3 +-
 7 files changed, 502 insertions(+), 12 deletions(-)
 create mode 100644 test/test-golden/example-keyword.md
 create mode 100644 test/test-golden/nested-keyword.md

diff --git a/README.md b/README.md
index c0c2682..8ba1da6 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,7 @@ There's also a version [published on npm](https://www.npmjs.com/package/wetzel).
 
 * The `-l` option specifies the starting header level.
 * The `-c` option lets you specify a custom symbol to place in front of required properties.
+* The `-k` option replaces the word `must` with a specified keyword, such as `**MUST**`.
 * The `-p` option lets you specify the relative path that should be used when referencing the schema, relative to where you store the documentation.
 * The `-s` option lets you specify the path string that should be used when loading the schema reference paths.
 * The `-e` option writes an additional output file that embeds the full text of JSON schemas (AsciiDoctor mode only).
diff --git a/bin/wetzel.js b/bin/wetzel.js
index 801b797..0f8fc30 100755
--- a/bin/wetzel.js
+++ b/bin/wetzel.js
@@ -12,11 +12,12 @@ if (!defined(argv._[0]) || defined(argv.h) || defined(argv.help)) {
     var help = 'Usage: node ' + path.basename(__filename) + ' [path-to-json-schema-file] [OPTIONS]\n' +
         '  -l,  --headerLevel        Top-level header. Default: 1\n' +
         '  -c,  --checkmark          Symbol for required properties. Default: &#10003;\n' +
+        '  -k,  --keyword            Use a particular keyword in place of "must", for example "**MUST**".\n' +
         '  -p,  --schemaPath         The path string that should be used when generating the schema reference paths.\n' +
         '  -s,  --searchPath         The path string that should be used when loading the schema reference paths.\n' +
         '  -e,  --embedOutput        The output path for a document that embeds JSON schemas directly (AsciiDoctor only).\n' +
-        '  -m,  --outputMode         The output mode, Markdown (the default) or AsciiDoctor (a).' +
-        '  -n,  --noTOC              Skip writing the Table of Contents.' +
+        '  -m,  --outputMode         The output mode, Markdown (the default) or AsciiDoctor (a).\n' +
+        '  -n,  --noTOC              Skip writing the Table of Contents.\n' +
         '  -a,  --autoLink           Aggressively auto-inter-link types referenced in descriptions.\n' +
         '                                Add =cqo to auto-link types that are in code-quotes only.\n' +
         '  -i                        An array of schema filenames (no paths) that should not get their own\n' +
@@ -73,6 +74,7 @@ var options = {
     writeTOC: !defaultValue(defaultValue(argv.n, argv.noTOC), false),
     headerLevel: defaultValue(defaultValue(argv.l, argv.headerLevel), 1),
     checkmark: defaultValue(defaultValue(argv.c, argv.checkmark), null),
+    mustKeyword: defaultValue(defaultValue(argv.k, argv.keyword), null),
     schemaRelativeBasePath: defaultValue(defaultValue(argv.p, argv.schemaPath), null),
     embedMode: enums.embedMode.none,
     debug: defaultValue(defaultValue(argv.d, argv.debug), null),
diff --git a/lib/generateMarkdown.js b/lib/generateMarkdown.js
index 6cc1728..9303e8c 100644
--- a/lib/generateMarkdown.js
+++ b/lib/generateMarkdown.js
@@ -30,6 +30,10 @@ function generateMarkdown(options) {
         style.setCheckmark(options.checkmark);
     }
 
+    if (defined(options.mustKeyword)) {
+        style.setMustKeyword(options.mustKeyword);
+    }
+
     // Verify JSON Schema version
     var schemaRef = schema.$schema;
     var resolved = null;
@@ -278,9 +282,11 @@ function createPropertiesDetails(schema, title, headerLevel, knownTypes, autoLin
 
             md += style.bulletItem(style.propertyDetails('Type') + ': ' + summary.formattedType, 0);
 
+            var eachElementInTheArrayMust = 'Each element in the array' + style.mustKeyword;
+
             var uniqueItems = property.uniqueItems;
             if (defined(uniqueItems) && uniqueItems) {
-                md += style.bulletItem('Each element in the array must be unique.', 1);
+                md += style.bulletItem(eachElementInTheArrayMust + 'be unique.', 1);
             }
 
             // TODO: items is a full schema
@@ -293,21 +299,21 @@ function createPropertiesDetails(schema, title, headerLevel, knownTypes, autoLin
                 var maxString = itemsExclusiveMaximum ? 'less than' : 'less than or equal to';
 
                 if (defined(items.minimum) && defined(items.maximum)) {
-                    md += style.bulletItem('Each element in the array must be ' + minString + ' ' +
+                    md += style.bulletItem(eachElementInTheArrayMust + 'be ' + minString + ' ' +
                         style.minMax(items.minimum) + ' and ' + maxString + ' ' + style.minMax(items.maximum) + '.', 1);
                 } else if (defined(items.minimum)) {
-                    md += style.bulletItem('Each element in the array must be ' + minString + ' ' + style.minMax(items.minimum) + '.', 1);
+                    md += style.bulletItem(eachElementInTheArrayMust + 'be ' + minString + ' ' + style.minMax(items.minimum) + '.', 1);
                 } else if (defined(items.maximum)) {
-                    md += style.bulletItem('Each element in the array must be ' + maxString + ' ' + style.minMax(items.maximum) + '.', 1);
+                    md += style.bulletItem(eachElementInTheArrayMust + 'be ' + maxString + ' ' + style.minMax(items.maximum) + '.', 1);
                 }
 
                 if (defined(items.minLength) && defined(items.maxLength)) {
-                    md += style.bulletItem('Each element in the array must have length between ' + style.minMax(items.minLength) +
+                    md += style.bulletItem(eachElementInTheArrayMust + 'have length between ' + style.minMax(items.minLength) +
                         ' and ' + style.minMax(items.maxLength) + '.', 1);
                 } else if (defined(items.minLength)) {
-                    md += style.bulletItem('Each element in the array must have length greater than or equal to ' + style.minMax(items.minLength) + '.', 1);
+                    md += style.bulletItem(eachElementInTheArrayMust + 'have length greater than or equal to ' + style.minMax(items.minLength) + '.', 1);
                 } else if (defined(items.maxLength)) {
-                    md += style.bulletItem('Each element in the array must have length less than or equal to ' + style.minMax(items.maxLength) + '.', 1);
+                    md += style.bulletItem(eachElementInTheArrayMust + 'have length less than or equal to ' + style.minMax(items.maxLength) + '.', 1);
                 }
 
                 var itemsString = getEnumString(items, type, 2);
diff --git a/lib/style.js b/lib/style.js
index a51837b..b98eef1 100644
--- a/lib/style.js
+++ b/lib/style.js
@@ -10,6 +10,8 @@ module.exports = {
 
     setCheckmark: setCheckmark,
 
+    setMustKeyword: setMustKeyword,
+
     getHeaderMarkdown: getHeaderMarkdown,
 
     getSectionMarkdown: getSectionMarkdown,
@@ -123,9 +125,14 @@ module.exports = {
     embedJsonSchema: embedJsonSchema,
 
     /**
-    * @property {string} The markdown string used for displaying the icon used to indicate a value is required.
+    * @property {string} requiredIcon - The markdown string used for displaying the icon used to indicate a value is required.
     */
-    requiredIcon: ' &#10003; '
+    requiredIcon: ' &#10003; ',
+
+    /**
+     * @property {string} mustKeyword - The keyword used when a condition must be true.
+     */
+    mustKeyword: ' must ',
 };
 
 const REFERENCE = "reference-";
@@ -160,6 +167,19 @@ function setCheckmark(checkmark) {
     }
 }
 
+/**
+ * @function setMustKeyword
+ * Set the keyword used in place of the word "must".
+ * @param {string} mustKeyword The keyword used when a condition must be true.
+ */
+function setMustKeyword(mustKeyword) {
+    if (mustKeyword.length > 0) {
+        module.exports.mustKeyword = ' ' + mustKeyword + ' ';
+    } else {
+        module.exports.mustKeyword = ' must ';
+    }
+}
+
 /**
 * @function getHeaderMarkdown
 * Gets the markdown syntax for the start of a header.
diff --git a/test/test-golden/example-keyword.md b/test/test-golden/example-keyword.md
new file mode 100644
index 0000000..e9114dc
--- /dev/null
+++ b/test/test-golden/example-keyword.md
@@ -0,0 +1,43 @@
+# Objects
+* [`example`](#reference-example) (root object)
+
+
+---------------------------------------
+<a name="reference-example"></a>
+## example
+
+Example description.
+
+**`example` Properties**
+
+|   |Type|Description|Required|
+|---|---|---|---|
+|**byteOffset**|`integer`|The offset relative to the start of the buffer in bytes.|No, default: `0`|
+|**type**|`string`|Specifies if the elements are scalars, vectors, or matrices.| &#10003; Yes|
+
+Additional properties are not allowed.
+
+### example.byteOffset
+
+The offset relative to the start of the buffer in bytes.
+
+* **Type**: `integer`
+* **Required**: No, default: `0`
+* **Minimum**: ` >= 0`
+
+### example.type
+
+Specifies if the elements are scalars, vectors, or matrices.
+
+* **Type**: `string`
+* **Required**:  &#10003; Yes
+* **Allowed values**:
+   * `"SCALAR"`
+   * `"VEC2"`
+   * `"VEC3"`
+   * `"VEC4"`
+   * `"MAT2"`
+   * `"MAT3"`
+   * `"MAT4"`
+
+
diff --git a/test/test-golden/nested-keyword.md b/test/test-golden/nested-keyword.md
new file mode 100644
index 0000000..1480ac8
--- /dev/null
+++ b/test/test-golden/nested-keyword.md
@@ -0,0 +1,417 @@
+# Objects
+* [`Buffer View`](#reference-bufferview)
+* [`Extension`](#reference-extension)
+* [`Extras`](#reference-extras)
+* [`Image`](#reference-image)
+* [`Material`](#reference-material)
+   * [`PBR Metallic Roughness`](#reference-material-pbrmetallicroughness)
+* [`nestedTest`](#reference-nestedtest) (root object)
+
+
+---------------------------------------
+<a name="reference-bufferview"></a>
+## Buffer View
+
+A view into a buffer.
+
+**`Buffer View` Properties**
+
+|   |Type|Description|Required|
+|---|---|---|---|
+|**byteOffset**|`integer`|The offset into the buffer in bytes.|No, default: `0`|
+|**byteLength**|`integer`|The length of the bufferView in bytes.| &#10003; Yes|
+|**byteStride**|`integer`|The stride, in bytes.|No|
+|**target**|`integer`|This is a test of some enums.|No|
+|**name**|`string`|The user-defined name of this object.|No|
+|**extensions**|`extension`|Dictionary object with extension-specific objects.|No|
+|**extras**|`extras`|Application-specific data.|No|
+
+Additional properties are allowed.
+
+### bufferView.byteOffset
+
+The offset into the buffer in bytes.
+
+* **Type**: `integer`
+* **Required**: No, default: `0`
+* **Minimum**: ` >= 0`
+
+### bufferView.byteLength
+
+The length of the bufferView in bytes.
+
+* **Type**: `integer`
+* **Required**:  &#10003; Yes
+* **Minimum**: ` >= 1`
+
+### bufferView.byteStride
+
+The stride, in bytes, between vertex attributes.  This is the detailed description of the property.
+
+* **Type**: `integer`
+* **Required**: No
+* **Minimum**: ` >= 4`
+* **Maximum**: ` <= 252`
+* **Related WebGL functions**: `vertexAttribPointer()` stride parameter
+
+### bufferView.target
+
+This is a test of some enums.
+
+* **Type**: `integer`
+* **Required**: No
+* **Allowed values**:
+   * `34962` ARRAY_BUFFER
+   * `34963` ELEMENT_ARRAY_BUFFER
+* **Related WebGL functions**: `bindBuffer()`
+
+### bufferView.name
+
+The user-defined name of this object.  This is the detailed description of the property.
+
+* **Type**: `string`
+* **Required**: No
+
+### bufferView.extensions
+
+Dictionary object with extension-specific objects.
+
+* **Type**: `extension`
+* **Required**: No
+* **Type of each property**: Extension
+
+### bufferView.extras
+
+Application-specific data.
+
+* **Type**: `extras`
+* **Required**: No
+
+
+
+
+---------------------------------------
+<a name="reference-extension"></a>
+## Extension
+
+Dictionary object with extension-specific objects.
+
+Additional properties are allowed.
+
+
+
+
+---------------------------------------
+<a name="reference-extras"></a>
+## Extras
+
+Application-specific data.
+
+**Implementation Note:** Although extras may have any type, it is common for applications to store and access custom data as key/value pairs. As best practice, extras should be an Object rather than a primitive value for best portability.
+
+
+
+---------------------------------------
+<a name="reference-image"></a>
+## Image
+
+Image data used to create a texture. Image can be referenced by URI or `bufferView` index. `mimeType` is required in the latter case.
+
+**`Image` Properties**
+
+|   |Type|Description|Required|
+|---|---|---|---|
+|**uri**|`string`|The uri of the image.|No|
+|**mimeType**|`string`|The image's MIME type. Required if `bufferView` is defined.|No|
+|**bufferView**|`integer`|The index of the bufferView that contains the image. Use this instead of the image's uri property.|No|
+|**fraction**|`number`|A number that must be between zero and one.|No|
+|**name**|`string`|The user-defined name of this object.|No|
+|**extensions**|`extension`|Dictionary object with extension-specific objects.|No|
+|**extras**|`extras`|Application-specific data.|No|
+
+Additional properties are allowed.
+
+### image.uri
+
+The uri of the image.  This is the detailed description of the property.
+
+* **Type**: `string`
+* **Required**: No
+* **Format**: uriref
+
+### image.mimeType
+
+The image's MIME type. Required if `bufferView` is defined.
+
+* **Type**: `string`
+* **Required**: No
+* **Allowed values**:
+   * `"image/jpeg"`
+   * `"image/png"`
+
+### image.bufferView
+
+The index of the bufferView that contains the image. Use this instead of the image's uri property.
+
+* **Type**: `integer`
+* **Required**: No
+* **Minimum**: ` >= 0`
+
+### image.fraction
+
+A number that must be between zero and one.
+
+* **Type**: `number`
+* **Required**: No
+* **Minimum**: ` > 0`
+* **Maximum**: ` < 1`
+
+### image.name
+
+The user-defined name of this object.  This is the detailed description of the property.
+
+* **Type**: `string`
+* **Required**: No
+
+### image.extensions
+
+Dictionary object with extension-specific objects.
+
+* **Type**: `extension`
+* **Required**: No
+* **Type of each property**: Extension
+
+### image.extras
+
+Application-specific data.
+
+* **Type**: `extras`
+* **Required**: No
+
+
+
+
+---------------------------------------
+<a name="reference-material"></a>
+## Material
+
+The material appearance of a primitive.
+
+**`Material` Properties**
+
+|   |Type|Description|Required|
+|---|---|---|---|
+|**name**|`string`|The user-defined name of this object.|No|
+|**extensions**|`extension`|Dictionary object with extension-specific objects.|No|
+|**extras**|`extras`|Application-specific data.|No|
+|**pbrMetallicRoughness**|`material.pbrMetallicRoughness`|A set of parameter values that are used to define the metallic-roughness material model from Physically-Based Rendering (PBR) methodology. When not specified, all the default values of `pbrMetallicRoughness` apply.|No|
+|**emissiveFactor**|`number` `[3]`|The emissive color of the material.|No, default: `[0,0,0]`|
+|**alphaMode**|`string`|The alpha rendering mode of the material.|No, default: `"OPAQUE"`|
+|**alphaCutoff**|`number`|The alpha cutoff value of the material.|No, default: `0.5`|
+|**doubleSided**|`boolean`|Specifies whether the material is double sided.|No, default: `false`|
+
+Additional properties are allowed.
+
+### material.name
+
+The user-defined name of this object.  This is the detailed description of the property.
+
+* **Type**: `string`
+* **Required**: No
+
+### material.extensions
+
+Dictionary object with extension-specific objects.
+
+* **Type**: `extension`
+* **Required**: No
+* **Type of each property**: Extension
+
+### material.extras
+
+Application-specific data.
+
+* **Type**: `extras`
+* **Required**: No
+
+### material.pbrMetallicRoughness
+
+A set of parameter values that are used to define the metallic-roughness material model from Physically-Based Rendering (PBR) methodology. When not specified, all the default values of `pbrMetallicRoughness` apply.
+
+* **Type**: `material.pbrMetallicRoughness`
+* **Required**: No
+
+### material.emissiveFactor
+
+The RGB components of the emissive color of the material. This is the detailed description of the property.
+
+* **Type**: `number` `[3]`
+   * Each element in the array **MUST** be greater than or equal to `0` and less than or equal to `1`.
+* **Required**: No, default: `[0,0,0]`
+
+### material.alphaMode
+
+The material's alpha rendering mode enumeration specifying the interpretation of the alpha value of the main factor and texture.
+
+* **Type**: `string`
+* **Required**: No, default: `"OPAQUE"`
+* **Allowed values**:
+   * `"OPAQUE"` The alpha value is ignored and the rendered output is fully opaque.
+   * `"MASK"` The rendered output is either fully opaque or fully transparent depending on the alpha value and the specified alpha cutoff value.
+   * `"BLEND"` The alpha value is used to composite the source and destination areas.
+
+### material.alphaCutoff
+
+Specifies the cutoff threshold when in `MASK` mode. This is the detailed description of the property.
+
+* **Type**: `number`
+* **Required**: No, default: `0.5`
+* **Minimum**: ` >= 0`
+
+### material.doubleSided
+
+Specifies whether the material is double sided. This is the detailed description of the property.
+
+* **Type**: `boolean`
+* **Required**: No, default: `false`
+
+
+
+
+---------------------------------------
+<a name="reference-material-pbrmetallicroughness"></a>
+## Material PBR Metallic Roughness
+
+A set of parameter values that are used to define the metallic-roughness material model from Physically-Based Rendering (PBR) methodology.
+
+**`Material PBR Metallic Roughness` Properties**
+
+|   |Type|Description|Required|
+|---|---|---|---|
+|**baseColorFactor**|`number` `[4]`|The material's base color factor.|No, default: `[1,1,1,1]`|
+|**metallicFactor**|`number`|The metalness of the material.|No, default: `1`|
+|**roughnessFactor**|`number`|The roughness of the material.|No, default: `1`|
+|**extensions**|`extension`|Dictionary object with extension-specific objects.|No|
+|**extras**|`extras`|Application-specific data.|No|
+
+Additional properties are allowed.
+
+### material.pbrMetallicRoughness.baseColorFactor
+
+The RGBA components of the base color of the material. This is the detailed description of the property.
+
+* **Type**: `number` `[4]`
+   * Each element in the array **MUST** be greater than or equal to `0` and less than or equal to `1`.
+* **Required**: No, default: `[1,1,1,1]`
+
+### material.pbrMetallicRoughness.metallicFactor
+
+The metalness of the material. This is the detailed description of the property.
+
+* **Type**: `number`
+* **Required**: No, default: `1`
+* **Minimum**: ` >= 0`
+* **Maximum**: ` <= 1`
+
+### material.pbrMetallicRoughness.roughnessFactor
+
+The roughness of the material. This is the detailed description of the property.
+
+* **Type**: `number`
+* **Required**: No, default: `1`
+* **Minimum**: ` >= 0`
+* **Maximum**: ` <= 1`
+
+### material.pbrMetallicRoughness.extensions
+
+Dictionary object with extension-specific objects.
+
+* **Type**: `extension`
+* **Required**: No
+* **Type of each property**: Extension
+
+### material.pbrMetallicRoughness.extras
+
+Application-specific data.
+
+* **Type**: `extras`
+* **Required**: No
+
+
+
+
+---------------------------------------
+<a name="reference-nestedtest"></a>
+## nestedTest
+
+The root object for a nestedTest asset.
+
+**`nestedTest` Properties**
+
+|   |Type|Description|Required|
+|---|---|---|---|
+|**bufferViews**|`bufferView` `[1-*]`|An array of bufferViews.| &#10003; Yes|
+|**materials**|`material` `[1-*]`|An array of materials.|No|
+|**images**|`image` `[1-*]`|An array of images.|No|
+|**version**|`string`|A version string with a specific pattern.|No|
+|**uri**|`string`|A string that should reference a URI.|No|
+|**extensions**|`extension`|Dictionary object with extension-specific objects.|No|
+|**extras**|`extras`|Application-specific data.|No|
+
+Additional properties are allowed.
+
+### nestedTest.bufferViews
+
+An array of bufferViews.  This is the detailed description of the property.
+
+* **Type**: `bufferView` `[1-*]`
+* **Required**:  &#10003; Yes
+
+### nestedTest.materials
+
+An array of materials.  This is the detailed description of the property.
+
+* **Type**: `material` `[1-*]`
+* **Required**: No
+
+### nestedTest.images
+
+An array of images.  This is the detailed description of the property.
+
+* **Type**: `image` `[1-*]`
+* **Required**: No
+
+### nestedTest.version
+
+A version string with a specific pattern.
+
+* **Type**: `string`
+* **Required**: No
+* **Pattern**: `^[0-9]+\.[0-9]+$`
+
+### nestedTest.uri
+
+A string that should reference a URI.  This is the detailed description of the property.
+
+* **Type**: `string`
+* **Required**: No
+* **Format**: uriref
+
+### nestedTest.extensions
+
+Dictionary object with extension-specific objects.
+
+* **Type**: `extension`
+* **Required**: No
+* **Type of each property**: Extension
+
+### nestedTest.extras
+
+Application-specific data.
+
+* **Type**: `extras`
+* **Required**: No
+
+
+
+
diff --git a/test/test-schemas/index.json b/test/test-schemas/index.json
index 0382a30..9636bd4 100644
--- a/test/test-schemas/index.json
+++ b/test/test-schemas/index.json
@@ -6,7 +6,8 @@
         "linked.adoc": "-l2 -a=cqo -m=a -p schema --checkmark \"icon:check[]\"",
         "remote.md": "-n -a=cqo -p \"https://www.khronos.org/wetzel/just/testing/schema\"",
         "remote.adoc": "-n -a=cqo -m=a -p \"https://www.khronos.org/wetzel/just/testing/schema\"",
-        "embed.adoc,embedJSON.adoc": "-n -a=cqo -m=a -p schema -e {EMBED}"
+        "embed.adoc,embedJSON.adoc": "-n -a=cqo -m=a -p schema -e {EMBED}",
+        "keyword.md": "-k \"**MUST**\""
     },
     "schemas": [{
         "name": "example",