diff --git a/README.md b/README.md
index e00eb49..d0ecacf 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,22 @@
# docfx-tools
-A docfx template for package documentation, patching the modern template to provide stylesheets and scripts for rendering custom workflow containers with copy functionality.
+A repository of docfx tools for Bonsai package documentation:
+- Docfx Workflow Container template patching the modern template to provide stylesheets and scripts for rendering custom workflow containers with copy functionality.
+- Docfx API TOC template that groups nodes by operator type in the table of contents(TOC) on API pages.
+- Docfx API template that revamps the API page to enhance user-friendliness.
+- Powershell Scripts that automate several content generation steps for package documentation websites.
-## How to use
+## How to include
-To include this template in a docfx website, first clone this repository as a submodule:
+To include this repo in a docfx website, first clone this repository as a submodule:
```
git submodule add https://github.com/bonsai-rx/docfx-tools bonsai
```
-Then modify `docfx.json` to include the template immediately after the modern template:
+## Using Workflow Container Template
+
+Modify `docfx.json` to include the template immediately after the modern template:
```json
"template": [
@@ -38,12 +44,39 @@ export default {
}
}
```
+## Using API template
+
+Modify `docfx.json` to include the api template in the template section (note both the workflow container and API template have to be added separately).
+
+```json
+"template": [
+ "default",
+ "modern",
+ "bonsai/template",
+ "bonsai/template/api",
+ "template"
+]
+```
+In addition, the images and custom css styles need to be added to the resources section.
-## Powershell Scripts
+```json
+"resource": [
+ {
+ "files": [
+ "logo.svg",
+ "favicon.ico",
+ "images/**",
+ "workflows/**",
+ "bonsai/template/api/images/**",
+ "bonsai/template/api/styles/**"
+ ]
+ }
+]
+```
+To add individual operator workflows for the API pages, open Bonsai, add the operator, and save each individual operator workflow as `OperatorName.bonsai` (case sensitive) in `docs/workflows/operators`.
-This repository also provides helper scripts to automate several content generation steps for package documentation websites.
-### Exporting workflow images
+## Powershell Scripts - Exporting workflow images
Exporting SVG images for all example workflows can be automated by placing all `.bonsai` files in a `workflows` folder and calling the below script pointing to the bin directory to include. A bonsai environment is assumed to be available in the `.bonsai` folder in the repository root.
diff --git a/template/api/ManagedReference.extension.js b/template/api/ManagedReference.extension.js
new file mode 100644
index 0000000..6f43f52
--- /dev/null
+++ b/template/api/ManagedReference.extension.js
@@ -0,0 +1,186 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Define Bonsai operator types in documentation by checking for an explicit Category tag. If the class does not provide one,
+// check the inheritance tree of the class.
+function defineOperatorType(model){
+ const checkForCategory = (category) => model.syntax?.content[0].value.includes(`[WorkflowElementCategory(ElementCategory.${category})]`);
+ const checkInheritance = (inheritance) => model.inheritance?.some(inherited => inherited.uid.includes(inheritance));
+
+ source = checkForCategory('Source') || checkInheritance('Bonsai.Source');
+ sink = checkForCategory('Sink') || checkInheritance('Bonsai.Sink') || checkInheritance('Bonsai.IO.StreamSink') || checkInheritance('Bonsai.IO.FileSink');
+ combinator = checkForCategory('Combinator') || checkInheritance('Bonsai.Combinator') || checkInheritance('Bonsai.WindowCombinator');
+ transform = checkForCategory('Transform') || checkInheritance('Bonsai.Transform') || checkInheritance('Bonsai.Transform');
+
+ let operatorType = {}
+ operatorType.type = sink ? 'sink' : source ? 'source' : transform ? 'transform' : combinator ? 'combinator' : false ;
+
+ return operatorType;
+}
+
+// This function is important for stripping the extra line that is present in some fields
+// replace last instance of '
child.name[0].value.includes('Process') || child.name[0].value.includes('Generate'))
+ .map(child => ({
+ 'description': [child.summary, child.remarks].join(''),
+ 'input': {
+ 'specName': replaceIObservableAndTSource(child.syntax?.parameters[0].type.specName[0].value),
+ 'description': removeBottomMargin([child.syntax?.parameters[0].description, child.syntax?.parameters[0].remarks].join(''))
+ },
+ 'output': {
+ 'specName': replaceIObservableAndTSource(child.syntax.return.type.specName[0].value),
+ 'description': removeBottomMargin([child.syntax.return.description, child.syntax.return.remarks].join('')),
+ }
+ }))
+ .map(item => {
+ // Remove input if it's empty
+ if (!item.input.specName && !item.input.description) {
+ delete item.input;
+ }
+ return item;
+ });
+ return overloads;
+}
+
+// extracts enums so that they can be expanded in the properties table
+function processChildProperty(child, sharedModel) {
+ const enumFields = sharedModel[`~/api/${child.syntax.return.type.uid}.yml`]?.type === 'enum' ?
+ extractEnumData(sharedModel[`~/api/${child.syntax.return.type.uid}.yml`]) :
+ [];
+ return {
+ 'name': child.name[0].value,
+ 'type': child.syntax.return.type.specName[0].value,
+ 'propertyDescription': {
+ 'text': enumFields.length > 0
+ ? [child.summary, child.remarks].join('')
+ : removeBottomMargin([child.summary, child.remarks].join('')),
+ 'hasEnum': enumFields.length > 0,
+ 'enum': enumFields,
+ }
+ }
+}
+
+function extractPropertiesData(model, sharedModel) {
+ return model?.children
+ .filter(child => child.type === 'property' && child.syntax)
+ .map(child => processChildProperty(child, sharedModel));
+}
+
+function extractPropertiesFromInheritedMembersData(model, sharedModel) {
+ return model.inheritedMembers
+ .filter(inheritedMember => inheritedMember.type === 'property')
+ .map(inheritedMember => {
+ return processChildProperty(
+ sharedModel[`~/api/${inheritedMember.parent}.yml`].children.find(inheritedMemberChild => inheritedMemberChild.uid === inheritedMember.uid),
+ sharedModel
+ );
+ });
+}
+
+
+// Properties are usually already listed in declaration order which mirrors Bonsai UI.
+// However a bug in docfx messes up properties that have a numeric endvalue ie Device10 < Device2
+// and this function fixes that.
+function sortPropertiesData(properties) {
+ return properties.sort((a, b) => {
+ const regex = /\D+|\d+$/g;
+
+ // Extract parts for property 'a'
+ const [prefixA, numberA] = a.name.match(regex);
+ const numA = Number(numberA);
+
+ // Extract parts for property 'b'
+ const [prefixB, numberB] = b.name.match(regex);
+ const numB = Number(numberB);
+
+ // If prefix is the same, compare numbers
+ if (prefixA == prefixB) {
+ return numA - numB;
+ }
+ });
+}
+
+// While enum fields can be accessed directly using the mustache template, this function is
+// still important for stripping the extra line that is present in the summary/remarks field
+function extractEnumData(model){
+ return model.children
+ .filter(child => child.type === 'field')
+ .map(child => ({
+ 'field&value': child.syntax.content[0].value,
+ 'enumDescription': removeBottomMargin([child.summary, child.remarks].join(''))
+ }));
+}
+
+/**
+ * This method will be called at the start of exports.transform in ManagedReference.html.primary.js
+ */
+exports.preTransform = function (model) {
+
+ model.bonsai = {};
+
+ model.bonsai.description = [model.summary, model.remarks].join('');
+
+ operatorType = defineOperatorType(model);
+
+ if (operatorType.type){
+ model.bonsai.operatorType = operatorType.type;
+ model.bonsai.showWorkflow = true
+ operators = defineInputsAndOutputs(model);
+ model.bonsai.operators = operators;
+ }
+
+ if (model.type === 'class') {
+ properties = sortPropertiesData([
+ ...extractPropertiesData(model, model.__global._shared),
+ ...extractPropertiesFromInheritedMembersData(model, model.__global._shared),
+ ]);
+ if (properties.length > 0){
+ model.bonsai.hasProperties = true;
+ model.bonsai.properties = properties
+ }
+ }
+
+ else if (model.type === 'enum') {
+ model.bonsai.enumFields = extractEnumData(model);
+ model.bonsai.hasEnumFields = true;
+ }
+ return model;
+}
+
+/**
+ * This method will be called at the end of exports.transform in ManagedReference.html.primary.js
+ */
+exports.postTransform = function (model) {
+ return model;
+}
\ No newline at end of file
diff --git a/template/api/ManagedReference.html.primary.tmpl b/template/api/ManagedReference.html.primary.tmpl
new file mode 100644
index 0000000..5f4ab4f
--- /dev/null
+++ b/template/api/ManagedReference.html.primary.tmpl
@@ -0,0 +1,7 @@
+{{!Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license.}}
+
+{{!master(layout/_master.tmpl)}}
+
+{{>partials/class}}
+
+{{>partials/enum}}
\ No newline at end of file
diff --git a/template/api/ManagedReference.overwrite.js b/template/api/ManagedReference.overwrite.js
new file mode 100644
index 0000000..ff7ef9d
--- /dev/null
+++ b/template/api/ManagedReference.overwrite.js
@@ -0,0 +1,5 @@
+exports.getOptions = function (model) {
+ return {
+ isShared: true
+ };
+}
\ No newline at end of file
diff --git a/template/api/images/combinator-operator.svg b/template/api/images/combinator-operator.svg
new file mode 100644
index 0000000..4a4afac
--- /dev/null
+++ b/template/api/images/combinator-operator.svg
@@ -0,0 +1,67 @@
+
+
diff --git a/template/api/images/right-arrow.svg b/template/api/images/right-arrow.svg
new file mode 100644
index 0000000..85ebb00
--- /dev/null
+++ b/template/api/images/right-arrow.svg
@@ -0,0 +1,87 @@
+
+
+
+
diff --git a/template/api/images/sink-operator.svg b/template/api/images/sink-operator.svg
new file mode 100644
index 0000000..918318c
--- /dev/null
+++ b/template/api/images/sink-operator.svg
@@ -0,0 +1,86 @@
+
+
diff --git a/template/api/images/source-operator.svg b/template/api/images/source-operator.svg
new file mode 100644
index 0000000..f39cb61
--- /dev/null
+++ b/template/api/images/source-operator.svg
@@ -0,0 +1,88 @@
+
+
diff --git a/template/api/images/transform-operator.svg b/template/api/images/transform-operator.svg
new file mode 100644
index 0000000..43846fd
--- /dev/null
+++ b/template/api/images/transform-operator.svg
@@ -0,0 +1,58 @@
+
+
diff --git a/template/api/partials/class.tmpl.partial b/template/api/partials/class.tmpl.partial
new file mode 100644
index 0000000..006577b
--- /dev/null
+++ b/template/api/partials/class.tmpl.partial
@@ -0,0 +1,98 @@
+
+
+{{#isClass}}
+
+
+{{/derivedClasses.0}}
+
+
+{{/isClass}}
\ No newline at end of file
diff --git a/template/api/partials/diagram.tmpl.partial b/template/api/partials/diagram.tmpl.partial
new file mode 100644
index 0000000..e2cb8aa
--- /dev/null
+++ b/template/api/partials/diagram.tmpl.partial
@@ -0,0 +1,83 @@
+{{#bonsai.operators}}
+
+
+
+
{{{description}}}
+
+
+
+
+
+
+
+
+
+ {{#input}}
+
+
{{{specName}}}
+ {{{description}}}
+
+
+
+
+ {{/input}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{{output.specName}}}
+
{{{output.description}}}
+
+
+
+
+
+
+
+{{/bonsai.operators}}
\ No newline at end of file
diff --git a/template/api/partials/enum.tmpl.partial b/template/api/partials/enum.tmpl.partial
new file mode 100644
index 0000000..4755efe
--- /dev/null
+++ b/template/api/partials/enum.tmpl.partial
@@ -0,0 +1,45 @@
+{{#isEnum}}
+
+
+
+
+{{/isEnum}}
\ No newline at end of file
diff --git a/template/api/partials/propertyTables.tmpl.partial b/template/api/partials/propertyTables.tmpl.partial
new file mode 100644
index 0000000..bd47d08
--- /dev/null
+++ b/template/api/partials/propertyTables.tmpl.partial
@@ -0,0 +1,29 @@
+