Преглед изворни кода

Node properties view in table format (#88) (#66)

Lutz Roeder пре 8 година
родитељ
комит
71a0832c36
15 измењених фајлова са 620 додато и 277 уклоњено
  1. 4 1
      setup.py
  2. 8 3
      src/caffe-model.js
  3. 14 3
      src/caffe2-model.js
  4. 45 44
      src/coreml-model.js
  5. 1 1
      src/keras-model.js
  6. 20 0
      src/mxnet-model.js
  7. 2 1
      src/mxnet-operator.json
  8. 5 5
      src/onnx-model.js
  9. 10 10
      src/tf-model.js
  10. 2 0
      src/view-browser.html
  11. 2 0
      src/view-electron.html
  12. 136 0
      src/view-node.css
  13. 339 0
      src/view-node.js
  14. 0 188
      src/view-template.js
  15. 32 21
      src/view.js

+ 4 - 1
setup.py

@@ -80,7 +80,10 @@ setuptools.setup(
             'caffe2-model.js', 'caffe2-operator.json', 'caffe2.js',
             'mxnet-model.js', 'mxnet-operator.json',
             'view-browser.html', 'view-browser.js',
-            'view.js', 'view.css', 'view-render.css', 'view-render.js', 'view-template.js'
+            'view-render.css', 'view-render.js',
+            'view-node.css', 'view-node.js',
+            'view-template.js',
+            'view.js', 'view.css'
         ]
     },
     install_requires=[],

+ 8 - 3
src/caffe-model.js

@@ -172,7 +172,7 @@ class CaffeNode {
                     }
                 });
                 layer.layer.blobs.forEach((blob) => {
-                    this._initializers.push(new CaffeTensor(blob));
+                    this._initializers.push(new CaffeTensor(blob, 'Initializer'));
                 });
                 break;
             case 1:
@@ -193,7 +193,7 @@ class CaffeNode {
                     }
                 });
                 layer.blobs.forEach((blob) => {
-                    this._initializers.push(new CaffeTensor(blob));
+                    this._initializers.push(new CaffeTensor(blob, 'Initializer'));
                 });
                 break;
         }
@@ -313,8 +313,9 @@ class CaffeAttribute {
 
 class CaffeTensor {
 
-    constructor(blob) {
+    constructor(blob, kind) {
         this._blob = blob;
+        this._kind = kind;
 
         if (blob.hasOwnProperty('num') && blob.hasOwnProperty('channels') &&
             blob.hasOwnProperty('width') && blob.hasOwnProperty('height')) {
@@ -347,6 +348,10 @@ class CaffeTensor {
         }
     }
 
+    get kind() {
+        return this._kind;
+    }
+
     get type() {
         return this._type + JSON.stringify(this._shape);
     }

+ 14 - 3
src/caffe2-model.js

@@ -131,6 +131,9 @@ class Caffe2Node {
         if (op.name) {
             this._name = op.name;
         }
+        if (op.engine) {
+            this._device = op.engine;
+        }
         this._operator = op.type;
         this._inputs = op.input;
         this._outputs = op.output;
@@ -146,7 +149,7 @@ class Caffe2Node {
                 var initializer = initializerMap[input];
                 if (initializer) {
                     delete initializerMap[input];
-                    this._initializers[input] = new Caffe2Tensor();
+                    this._initializers[input] = new Caffe2Tensor('Initializer');
                 }
             }
         });
@@ -156,6 +159,10 @@ class Caffe2Node {
         return this._name ? this._name : '';
     }
 
+    get device() {
+        return this._device ? this._device : '';
+    }
+
     get operator() {
         return this._operator;
     }
@@ -225,9 +232,13 @@ class Caffe2Attribute {
 }
 
 class Caffe2Tensor {
-    
-    constructor() {
 
+    constructor(kind) {
+        this._kind = kind;
+    }
+
+    get kind() {
+        return this._kind;
     }
 }
 

+ 45 - 44
src/coreml-model.js

@@ -351,7 +351,7 @@ class CoreMLNode {
             var input = {
                 name: initializer.name,
                 connections: [ { 
-                    id: initializer.id, 
+                    id: '',
                     type: initializer.type,
                     initializer: initializer, } ]
             };
@@ -386,60 +386,60 @@ class CoreMLNode {
                     weightsShape[0] = data.kernelChannels;
                     weightsShape[1] = Math.floor(data.outputChannels / (data.nGroups != 0 ? data.nGroups : 1));
                 }    
-                this._initializers.push(new CoreMLTensor('weights', weightsShape, data.weights));
+                this._initializers.push(new CoreMLTensor('Weights', 'weights', weightsShape, data.weights));
                 if (data.hasBias) {
-                    this._initializers.push(new CoreMLTensor('bias', [ data.bias.floatValue.length ], data.bias));
+                    this._initializers.push(new CoreMLTensor('Weights', 'bias', [ data.bias.floatValue.length ], data.bias));
                 }
                 return { 'weights': true, 'bias': data.hasBias };
             case 'innerProduct':
-                this._initializers.push(new CoreMLTensor('weights', [ data.outputChannels, data.inputChannels ], data.weights));
+                this._initializers.push(new CoreMLTensor('Weights', 'weights', [ data.outputChannels, data.inputChannels ], data.weights));
                 if (data.hasBias) {
-                    this._initializers.push(new CoreMLTensor('bias', [ data.outputChannels ], data.bias));
+                    this._initializers.push(new CoreMLTensor('Weights', 'bias', [ data.outputChannels ], data.bias));
                 }
                 return { 'weights': true, 'bias': data.hasBias };
             case 'batchnorm':
-                this._initializers.push(new CoreMLTensor('gamma', [ data.channels ], data.gamma));
-                this._initializers.push(new CoreMLTensor('beta', [ data.channels ], data.beta));
+                this._initializers.push(new CoreMLTensor('Weights', 'gamma', [ data.channels ], data.gamma));
+                this._initializers.push(new CoreMLTensor('Weights', 'beta', [ data.channels ], data.beta));
                 if (data.mean) {
-                    this._initializers.push(new CoreMLTensor('mean', [ data.channels ], data.mean));
+                    this._initializers.push(new CoreMLTensor('Weights', 'mean', [ data.channels ], data.mean));
                 }
                 if (data.variance) {
-                    this._initializers.push(new CoreMLTensor('variance', [ data.channels ], data.variance));
+                    this._initializers.push(new CoreMLTensor('Weights', 'variance', [ data.channels ], data.variance));
                 }
                 return { 'gamma': true, 'beta': true, 'mean': true, 'variance': true };
             case 'embedding':
-                this._initializers.push(new CoreMLTensor('weights', [ data.inputDim, data.outputChannels ], data.weights));
+                this._initializers.push(new CoreMLTensor('Weights', 'weights', [ data.inputDim, data.outputChannels ], data.weights));
                 return { 'weights': true };
             case 'loadConstant':    
-                this._initializers.push(new CoreMLTensor('data', data.shape, data.data));            
+                this._initializers.push(new CoreMLTensor('Weights', 'data', data.shape, data.data));            
                 return { 'data': true };
             case 'scale':
-                this._initializers.push(new CoreMLTensor('scale', data.shapeScale, data.scale));
+                this._initializers.push(new CoreMLTensor('Weights', 'scale', data.shapeScale, data.scale));
                 if (data.hasBias) {
-                    this._initializers.push(new CoreMLTensor('bias', data.shapeBias, data.bias));
+                    this._initializers.push(new CoreMLTensor('Weights', 'bias', data.shapeBias, data.bias));
                 }
                 return { 'scale': true, 'bias': data.hasBias };
             case 'bias':
-                this._initializers.push(new CoreMLTensor('bias', data.shapeBias, data.bias));
+                this._initializers.push(new CoreMLTensor('Weights', 'bias', data.shapeBias, data.bias));
                 return { 'bias': true };
             case 'simpleRecurrentLayer':
-                this._initializers.push(new CoreMLTensor('weights', null, data.weightMatrix));
-                this._initializers.push(new CoreMLTensor('recurrent', null, data.recursionMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'weights', null, data.weightMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'recurrent', null, data.recursionMatrix));
                 if (data.hasBiasVectors) {
-                    this._initializers.push(new CoreMLTensor('bias', null, data.biasVector));
+                    this._initializers.push(new CoreMLTensor('Weights', 'bias', null, data.biasVector));
                 }
                 return { 'weightMatrix': true, 'recursionMatrix': true, 'biasVector': data.hasBiasVectors };
             case 'gru':
-                this._initializers.push(new CoreMLTensor('updateGateWeightMatrix', null, data.updateGateWeightMatrix));
-                this._initializers.push(new CoreMLTensor('resetGateWeightMatrix', null, data.resetGateWeightMatrix));
-                this._initializers.push(new CoreMLTensor('outputGateWeightMatrix', null, data.outputGateWeightMatrix));
-                this._initializers.push(new CoreMLTensor('updateGateRecursionMatrix', null, data.updateGateRecursionMatrix));
-                this._initializers.push(new CoreMLTensor('resetGateRecursionMatrix', null, data.resetGateRecursionMatrix));
-                this._initializers.push(new CoreMLTensor('outputGateRecursionMatrix', null, data.outputGateRecursionMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'updateGateWeightMatrix', null, data.updateGateWeightMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'resetGateWeightMatrix', null, data.resetGateWeightMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'outputGateWeightMatrix', null, data.outputGateWeightMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'updateGateRecursionMatrix', null, data.updateGateRecursionMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'resetGateRecursionMatrix', null, data.resetGateRecursionMatrix));
+                this._initializers.push(new CoreMLTensor('Weights', 'outputGateRecursionMatrix', null, data.outputGateRecursionMatrix));
                 if (data.hasBiasVectors) {
-                    this._initializers.push(new CoreMLTensor('updateGateBiasVector', null, data.updateGateBiasVector));
-                    this._initializers.push(new CoreMLTensor('resetGateBiasVector', null, data.resetGateBiasVector));
-                    this._initializers.push(new CoreMLTensor('outputGateBiasVector', null, data.outputGateBiasVector));
+                    this._initializers.push(new CoreMLTensor('Weights', 'updateGateBiasVector', null, data.updateGateBiasVector));
+                    this._initializers.push(new CoreMLTensor('Weights', 'resetGateBiasVector', null, data.resetGateBiasVector));
+                    this._initializers.push(new CoreMLTensor('Weights', 'outputGateBiasVector', null, data.outputGateBiasVector));
                 }  
                 return {
                     'updateGateWeightMatrix': true, 'resetGateWeightMatrix': true, 'outputGateWeightMatrix': true, 
@@ -453,24 +453,24 @@ class CoreMLNode {
                 for (var i = 0; i < count; i++) {
                     var weights = count == 1 ? data.weightParams : data.weightParams[i];
                     var suffix = (i == 0) ? '' : '_rev';
-                    this._initializers.push(new CoreMLTensor('inputGateWeightMatrix' + suffix, matrixShape, weights.inputGateWeightMatrix));
-                    this._initializers.push(new CoreMLTensor('forgetGateWeightMatrix' + suffix, matrixShape, weights.forgetGateWeightMatrix));
-                    this._initializers.push(new CoreMLTensor('blockInputWeightMatrix' + suffix, matrixShape, weights.blockInputWeightMatrix));
-                    this._initializers.push(new CoreMLTensor('outputGateWeightMatrix' + suffix, matrixShape, weights.outputGateWeightMatrix));
-                    this._initializers.push(new CoreMLTensor('inputGateRecursionMatrix' + suffix, matrixShape, weights.inputGateRecursionMatrix));
-                    this._initializers.push(new CoreMLTensor('forgetGateRecursionMatrix' + suffix, matrixShape,weights.forgetGateRecursionMatrix));
-                    this._initializers.push(new CoreMLTensor('blockInputRecursionMatrix' + suffix, matrixShape, weights.blockInputRecursionMatrix));
-                    this._initializers.push(new CoreMLTensor('outputGateRecursionMatrix' + suffix, matrixShape, weights.outputGateRecursionMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'inputGateWeightMatrix' + suffix, matrixShape, weights.inputGateWeightMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'forgetGateWeightMatrix' + suffix, matrixShape, weights.forgetGateWeightMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'blockInputWeightMatrix' + suffix, matrixShape, weights.blockInputWeightMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'outputGateWeightMatrix' + suffix, matrixShape, weights.outputGateWeightMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'inputGateRecursionMatrix' + suffix, matrixShape, weights.inputGateRecursionMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'forgetGateRecursionMatrix' + suffix, matrixShape,weights.forgetGateRecursionMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'blockInputRecursionMatrix' + suffix, matrixShape, weights.blockInputRecursionMatrix));
+                    this._initializers.push(new CoreMLTensor('Weights', 'outputGateRecursionMatrix' + suffix, matrixShape, weights.outputGateRecursionMatrix));
                     if (data.params.hasBiasVectors) {
-                        this._initializers.push(new CoreMLTensor('inputGateBiasVector' + suffix, vectorShape, weights.inputGateBiasVector));
-                        this._initializers.push(new CoreMLTensor('forgetGateBiasVector' + suffix, vectorShape, weights.forgetGateBiasVector));
-                        this._initializers.push(new CoreMLTensor('blockInputBiasVector' + suffix, vectorShape, weights.blockInputBiasVector));
-                        this._initializers.push(new CoreMLTensor('outputGateBiasVector' + suffix, vectorShape, weights.outputGateBiasVector));
+                        this._initializers.push(new CoreMLTensor('Weights', 'inputGateBiasVector' + suffix, vectorShape, weights.inputGateBiasVector));
+                        this._initializers.push(new CoreMLTensor('Weights', 'forgetGateBiasVector' + suffix, vectorShape, weights.forgetGateBiasVector));
+                        this._initializers.push(new CoreMLTensor('Weights', 'blockInputBiasVector' + suffix, vectorShape, weights.blockInputBiasVector));
+                        this._initializers.push(new CoreMLTensor('Weights', 'outputGateBiasVector' + suffix, vectorShape, weights.outputGateBiasVector));
                     }
                     if (data.params.hasPeepholeVectors) {
-                        this._initializers.push(new CoreMLTensor('inputGatePeepholeVector' + suffix, vectorShape, weights.inputGatePeepholeVector));
-                        this._initializers.push(new CoreMLTensor('forgetGatePeepholeVector' + suffix, vectorShape, weights.forgetGatePeepholeVector));
-                        this._initializers.push(new CoreMLTensor('outputGatePeepholeVector' + suffix, vectorShape, weights.outputGatePeepholeVector));
+                        this._initializers.push(new CoreMLTensor('Weights', 'inputGatePeepholeVector' + suffix, vectorShape, weights.inputGatePeepholeVector));
+                        this._initializers.push(new CoreMLTensor('Weights', 'forgetGatePeepholeVector' + suffix, vectorShape, weights.forgetGatePeepholeVector));
+                        this._initializers.push(new CoreMLTensor('Weights', 'outputGatePeepholeVector' + suffix, vectorShape, weights.outputGatePeepholeVector));
                     }
                 }
                 return { 'weightParams': true };
@@ -505,7 +505,8 @@ class CoreMLAttribute {
 
 class CoreMLTensor {
 
-    constructor(name, shape, data) {
+    constructor(kind, name, shape, data) {
+        this._kind = kind;
         this._name = name;
         this._shape = shape;
         this._type = null;
@@ -535,8 +536,8 @@ class CoreMLTensor {
         return this._name;
     }
 
-    get title() {
-        return 'Initializer';
+    get kind() {
+        return this._kind;
     }
 
     get type() {

+ 1 - 1
src/keras-model.js

@@ -476,7 +476,7 @@ class KerasTensor {
         this._variable = variable;
     }
 
-    get title() {
+    get kind() {
         return 'Initializer';
     }
 

+ 20 - 0
src/mxnet-model.js

@@ -215,6 +215,7 @@ class MXNetNode {
             input.connections.forEach((connection) => {
                 var initializer = this._initializers[connection.id];
                 if (initializer) {
+                    connection.type = initializer.type;
                     connection.initializer = initializer;
                 }
             });
@@ -259,11 +260,30 @@ class MXNetTensor {
     
     constructor(json) {
         this._json = json;
+        this._type = '';
+        var attrs = this._json.attrs; 
+        if (attrs) {
+            var dtype = attrs.__dtype__;
+            var shape = attrs.__shape__;
+            if (dtype && shape) {
+                dtype = dtype.replace('0', 'float');
+                shape = shape.split(' ').join('').replace('(', '[').replace(')', ']');
+                this._type = dtype + shape;
+            }
+        }
     }
 
     get name() {
         return this._json.name;
     }
+
+    get kind() {
+        return 'Initializer';
+    }
+
+    get type() {
+        return this._type;
+    }
 }
 
 class MXNetOperatorMetadata {

+ 2 - 1
src/mxnet-operator.json

@@ -45,7 +45,8 @@
           "name": "weight"
         },
         {
-          "name": "bias"
+          "name": "bias",
+          "option": "optional"
         }
       ]
     }

+ 5 - 5
src/onnx-model.js

@@ -393,11 +393,11 @@ class OnnxAttribute {
 
 class OnnxTensor {
 
-    constructor(tensor, id, title) {
+    constructor(tensor, id, kind) {
         this._tensor = tensor;
         this._id = id;
-        if (title) {
-            this._title = title;
+        if (kind) {
+            this._kind = kind;
         }
     }
 
@@ -409,8 +409,8 @@ class OnnxTensor {
         return this._tensor.name ? this._tensor.name : this._id; 
     }
 
-    get title() {
-        return this._title ? this._title : null;
+    get kind() {
+        return this._kind ? this._kind : null;
     }
 
     get type() {

+ 10 - 10
src/tf-model.js

@@ -509,12 +509,12 @@ class TensorFlowAttribute {
 
 class TensorFlowIdentityTensor {
     
-    constructor(tensor, id, name, title) {
+    constructor(tensor, id, name, kind) {
         this._tensor = tensor;
         this._id = id;
         this._name = name;
-        if (title) {
-            this._title = title;
+        if (kind) {
+            this._kind = kind;
         }
     }
 
@@ -526,8 +526,8 @@ class TensorFlowIdentityTensor {
         return this._name;
     }
 
-    get title() {
-        return this._title;
+    get kind() {
+        return this._kind;
     }
 
     get type() {
@@ -541,12 +541,12 @@ class TensorFlowIdentityTensor {
 
 class TensorFlowTensor {
 
-    constructor(tensor, id, name, title) {
+    constructor(tensor, id, name, kind) {
         this._tensor = tensor;
         this._id = id;
         this._name = name;
-        if (title) {
-            this._title = title;
+        if (kind) {
+            this._kind = kind;
         }
     }
 
@@ -562,8 +562,8 @@ class TensorFlowTensor {
         return TensorFlowTensor.formatTensorType(this._tensor);
     }
 
-    get title() {
-        return this._title;
+    get kind() {
+        return this._kind;
     }
 
     get value() {

+ 2 - 0
src/view-browser.html

@@ -5,6 +5,7 @@
 <!-- meta -->
 <link rel='stylesheet' type='text/css' href='open-sans.css'>
 <link rel='stylesheet' type='text/css' href='view-render.css'>
+<link rel='stylesheet' type='text/css' href='view-node.css'>
 <link rel='stylesheet' type='text/css' href='view.css'>
 <link rel='shortcut icon' type='image/x-icon' href='favicon.ico' />
 <link rel='icon' type='image/png' href='icon.png' />
@@ -50,6 +51,7 @@
 <script type='text/javascript' src='view-template.js'></script>
 <script type='text/javascript' src='view-browser.js'></script>
 <script type='text/javascript' src='view-render.js'></script>
+<script type='text/javascript' src='view-node.js'></script>
 <script type='text/javascript' src='view.js'></script>
 </body>
 </html>

+ 2 - 0
src/view-electron.html

@@ -3,6 +3,7 @@
 <meta charset='utf-8'>
 <link rel='stylesheet' type='text/css' href='../node_modules/npm-font-open-sans/open-sans.css'>
 <link rel='stylesheet' type='text/css' href='view-render.css'>
+<link rel='stylesheet' type='text/css' href='view-node.css'>
 <link rel='stylesheet' type='text/css' href='view.css'>
 </head>
 <body>
@@ -40,6 +41,7 @@
 <script type='text/javascript' src='view-template.js'></script>
 <script type='text/javascript' src='view-electron.js'></script>
 <script type='text/javascript' src='view-render.js'></script>
+<script type='text/javascript' src='view-node.js'></script>
 <script type='text/javascript' src='view.js'></script>
 </body>
 </html>

+ 136 - 0
src/view-node.css

@@ -0,0 +1,136 @@
+
+.node-view-title {
+    font-weight: 600;
+    font-size: 14px;
+    line-height: 1.25;
+    border-bottom: 1px solid #eaecef;
+    padding-bottom: 0.3em;
+    margin-top: 0;
+    margin-bottom: 16px;
+    user-select: none; 
+    -webkit-user-select: none; 
+    -moz-user-select: none;
+    cursor: default;
+}
+
+.node-view-header {
+    font-weight: 600;
+    font-size: 12px;
+    line-height: 1.25;
+    margin-top: 16px;
+    margin-bottom: 16px;
+    border-bottom: 1px solid #eaecef;
+    display: block;
+    user-select: none; 
+    -webkit-user-select: none; 
+    -moz-user-select: none;
+    cursor: default;
+}
+
+.node-view-documentation-button {
+    display: inline-block;
+    color: #888;
+    text-align: center;
+    vertical-align: middle;
+    font-weight: 600;
+    width: 12px;
+    height: 12px;
+    font-size: 10px;
+    line-height: 12px;
+    border-radius: 50%;
+    transform: translateY(-1px);
+    padding: 1px;
+    background: transparent;
+    border: 1px solid #aaa;
+    text-decoration: none;
+    cursor: hand;
+    user-select: none; 
+    -webkit-user-select: none; 
+    -moz-user-select: none;
+}
+
+.node-view-documentation-button:hover {
+    color: #333;
+    border: 1px solid #333;
+}
+
+.node-view-item {
+    margin-bottom: 6px;
+    display: table-row;
+}
+
+.node-view-item-name {
+    font-size: 10px;
+    min-width: 95px;
+    max-width: 95px;
+    padding-right: 5px;
+    display: table-cell;
+}
+
+.node-view-item-name input {
+    font-family: inherit;
+    font-size: inherit;
+    color: inherit;
+    background-color:
+    inherit; width: 100%;
+    text-align: right;
+    margin: 0;
+    padding: 0;
+    border: 0;
+    outline: none;
+    text-overflow: ellipsis;
+}
+
+.node-view-item-value-list {
+    overflow: auto;
+    width: 100%;
+    display: table-cell;
+    margin: 0;
+    padding: 0;
+}
+
+.node-view-item-value {
+    font-size: 10px;
+    background-color: rgba(27, 31, 35, 0.05);
+    border-radius: 8px;
+    border: 1px solid rgba(27, 31, 35, 0.05);
+    margin-top: 3px;
+    margin-bottom: 3px;
+}
+
+.node-view-item-value b {
+    font-weight: 600;
+}
+
+.node-view-item-value code {
+    font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; 
+}
+
+.node-view-item-value pre {
+    font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; 
+    margin: 0;
+}
+
+.node-view-item-value-line {
+    padding: 4px 6px 4px 6px;
+}
+
+.node-view-item-value-line-border {
+    padding: 4px 6px 4px 6px;
+    border-top: 1px solid rgba(27, 31, 35, 0.05);
+}
+
+.node-view-item-value-expander {
+    font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; 
+    float: right;
+    color: #aaa;
+    cursor: hand;
+    user-select: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    padding: 4px 6px 4px 6px;
+}
+
+.node-view-item-value-expander:hover {
+    color: #000;
+}

+ 339 - 0
src/view-node.js

@@ -0,0 +1,339 @@
+/*jshint esversion: 6 */
+
+class NodeView {
+
+    constructor(node, documentationHandler) {
+        this._node = node;
+        this._documentationHandler = documentationHandler;
+        this._elements = [];
+        this._attributes = [];
+        this._inputs = [];
+        this._outputs = [];
+
+        var operatorElement = document.createElement('div');
+        operatorElement.className = 'node-view-title';
+        operatorElement.innerText = node.operator + ' ';
+        this._elements.push(operatorElement);
+
+        if (node.documentation) {
+            var documentationButton = document.createElement('a');
+            documentationButton.className = 'node-view-documentation-button';
+            documentationButton.innerText = '?';
+            documentationButton.addEventListener('click', (e) => {
+                this._documentationHandler();
+            });
+            operatorElement.appendChild(documentationButton);
+        }
+
+        if (node.name) {
+            this.addProperty('name', new NodeViewItemContent(node.name));
+        }
+
+        if (node.domain) {
+            this.addProperty('domain', new NodeViewItemContent(node.domain));
+        }
+
+        if (node.description) {
+            this.addProperty('description', new NodeViewItemContent(node.description));
+        }
+
+        if (node.device) {
+            this.addProperty('device', new NodeViewItemContent(node.device));
+        }
+
+        var attributes = node.attributes;
+        if (attributes && node.attributes.length > 0) {
+            this.addHeader('Attributes');
+            attributes.forEach((attribute) => {
+                this.addAttribute(attribute.name, attribute);
+            });
+        }
+
+        var inputs = node.inputs;
+        if (inputs && node.inputs.length > 0) {
+            this.addHeader('Inputs');
+            inputs.forEach((input) => {
+                this.addInput(input.name, input);
+            });
+          }
+
+        var outputs = node.outputs;
+        if (outputs && node.outputs.length > 0) {
+            this.addHeader('Outputs');
+            outputs.forEach((output) => {
+                this.addOutput(output.name, output);
+            });
+        }
+
+        var divider = document.createElement('div');
+        divider.setAttribute('style', 'margin-bottom: 20px');
+        this._elements.push(divider);
+    }
+
+    get elements() {
+        return this._elements;
+    }
+
+    addHeader(title) {
+        var headerElement = document.createElement('div');
+        headerElement.className = 'node-view-header';
+        headerElement.innerText = title;
+        this._elements.push(headerElement);
+    }
+
+    addProperty(name, value) {
+        var item = new NodeViewItem(name, value);
+        this._elements.push(item.element);
+    }
+
+    addAttribute(name, attribute) {
+        var item = new NodeViewItem(name, new NodeViewItemAttribute(attribute));
+        this._attributes.push(item);
+        this._elements.push(item.element);
+    }
+
+    addInput(name, input) {
+        if (input.connections.length > 0) {
+            var item = new NodeViewItem(name, new NodeViewItemList(input));
+            this._inputs.push(item);
+            this._elements.push(item.element);
+        }
+    }
+
+    addOutput(name, output) {
+        if (output.connections.length > 0) {
+            var item = new NodeViewItem(name, new NodeViewItemList(output));
+            this._outputs.push(item);
+            this._elements.push(item.element);
+        }
+    }
+
+    toggleInput(name) {
+        this._inputs.forEach((input) => {
+            if (name == input.name) {
+                input.toggle();
+            }
+        });
+    }
+}
+
+class NodeViewItem {
+    constructor(name, value) {
+        this._name = name;
+        this._value = value;
+
+        var itemName = document.createElement('div');
+        itemName.className = 'node-view-item-name';
+
+        var inputName = document.createElement('input');
+        inputName.setAttribute('type', 'text');
+        inputName.setAttribute('value', name);
+        inputName.setAttribute('title', name);
+        itemName.appendChild(inputName);
+
+        var itemValueList = document.createElement('div');
+        itemValueList.className = 'node-view-item-value-list';
+
+        value.elements.forEach((element) => {
+            itemValueList.appendChild(element);
+        });
+
+        this._element = document.createElement('div');
+        this._element.className = 'node-view-item';
+        this._element.appendChild(itemName);
+        this._element.appendChild(itemValueList);
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get element() {
+        return this._element;
+    }
+
+    toggle() {
+        this._value.toggle();
+    }
+}
+
+class NodeViewItemContent {
+    constructor(value) {
+        var line = document.createElement('div');
+        line.className = 'node-view-item-value-line';
+        line.innerHTML = '<code>' + value + '</code>';
+        var element = document.createElement('div');
+        element.className = 'node-view-item-value';
+        element.appendChild(line);
+        this._elements = [ element ];
+    }
+
+    get elements() {
+        return this._elements;
+    }
+
+    toggle() {
+    }
+}
+
+class NodeViewItemAttribute {
+
+    constructor(attribute) {
+        this._attribute = attribute;
+        this._element = document.createElement('div');
+        this._element.className = 'node-view-item-value';
+
+        if (attribute.type) {
+            this._expander = document.createElement('div');
+            this._expander.className = 'node-view-item-value-expander';
+            this._expander.innerText = '+';
+            this._expander.addEventListener('click', (e) => {
+                this.toggle();
+            });
+            this._element.appendChild(this._expander);
+        }
+
+        var value = this._attribute.value;
+        var valueLine = document.createElement('div');
+        valueLine.className = 'node-view-item-value-line';
+        valueLine.innerHTML = '<code>' + (value ? value : '&nbsp;') + '</code>';
+        this._element.appendChild(valueLine);
+    }
+
+    get elements() {
+        return [ this._element ];
+    }
+
+    toggle() {
+        if (this._expander.innerText == '+') {
+            this._expander.innerText = '-';
+
+            var typeLine = document.createElement('div');
+            typeLine.className = 'node-view-item-value-line-border';
+            typeLine.innerHTML = 'type: ' + '<code><b>' + this._attribute.type + '</b></code>';
+            this._element.appendChild(typeLine);
+        }
+        else {
+            this._expander.innerText = '+';
+            while (this._element.childElementCount > 2) {
+                this._element.removeChild(this._element.lastChild);
+            }
+        }
+    }
+}
+
+class NodeViewItemList {
+
+    constructor(list) {
+        this._list = list;
+        this._elements = [];
+        this._items = [];
+        list.connections.forEach((connection) => {
+            var item = new NodeViewItemConnection(connection);
+            this._items.push(item);
+            this._elements.push(item.element);
+        });
+    }
+
+    get elements() {
+        return this._elements;
+    }
+
+    toggle() {
+        this._items.forEach((item) => {
+            item.toggle();
+        });
+    }
+}
+
+class NodeViewItemConnection {
+    constructor(connection) {
+        this._connection = connection;
+        this._element = document.createElement('div');
+        this._element.className = 'node-view-item-value';
+
+        var initializer = connection.initializer;
+        if (!initializer) {
+            this._element.style.backgroundColor = '#f4f4f4';
+        }
+
+        var type = connection.type;
+        if (type || initializer) {
+            this._expander = document.createElement('div');
+            this._expander.className = 'node-view-item-value-expander';
+            this._expander.innerText = '+';
+            this._expander.addEventListener('click', (e) => {
+                this.toggle();
+            });
+            this._element.appendChild(this._expander);
+        }
+
+        this._hasId = this._connection.id ? true : false;
+        if (this._hasId) {
+            var idLine = document.createElement('div');
+            idLine.className = 'node-view-item-value-line';
+            idLine.innerHTML = 'id: <b>' + this._connection.id + '</b>';
+            this._element.appendChild(idLine);
+        }
+        else {
+            var kindLine = document.createElement('div');
+            kindLine.className = 'node-view-item-value-line';
+            kindLine.innerHTML = 'kind: <b>' + initializer.kind + '</b>';
+            this._element.appendChild(kindLine);
+        }
+    }
+
+    get element() {
+        return this._element;
+    }
+
+    toggle() {
+        if (this._expander) {
+            if (this._expander.innerText == '+') {
+                this._expander.innerText = '-';
+    
+                var initializer = this._connection.initializer;
+                if (initializer && this._hasId) {
+                    var kind = initializer.kind;
+                    if (kind) {
+                        var kindLine = document.createElement('div');
+                        kindLine.className = 'node-view-item-value-line-border';
+                        kindLine.innerHTML = 'kind: ' + '<b>' + kind + '</b>';
+                        this._element.appendChild(kindLine);
+                    }
+                }
+    
+                var type = this._connection.type;
+                if (type) {
+                    var typeLine = document.createElement('div');
+                    typeLine.className = 'node-view-item-value-line-border';
+                    typeLine.innerHTML = 'type: ' + '<code><b>' + type + '</b></code>';
+                    this._element.appendChild(typeLine);
+                }
+    
+                if (initializer) {
+                    var quantization = initializer.quantization;
+                    if (quantization) {
+                        var quantizationLine = document.createElement('div');
+                        quantizationLine.className = 'node-view-item-value-line-border';
+                        quantizationLine.innerHTML = 'quantization: ' + '<code><b>' + quantization + '</b></code>';
+                        this._element.appendChild(quantizationLine);   
+                    }
+                    var value = initializer.value;
+                    if (value) {
+                        var valueLine = document.createElement('div');
+                        valueLine.className = 'node-view-item-value-line-border';
+                        valueLine.innerHTML = '<pre>' + value + '</pre>';
+                        this._element.appendChild(valueLine);
+                    }   
+                }
+            }
+            else {
+                this._expander.innerText = '+';
+                while (this._element.childElementCount > 2) {
+                    this._element.removeChild(this._element.lastChild);
+                }
+            }
+        }
+    }
+}

+ 0 - 188
src/view-template.js

@@ -1,60 +1,5 @@
 /*jshint esversion: 6 */
 
-var inputTemplate = `
-<style type='text/css'>
-.inputs { font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 12px; line-height: 1.5; margin: 0; }
-.input { margin-bottom: 20px; }
-.input b { font-weight: 600; }
-.input h1 { font-weight: 600; font-size: 14px; line-height: 1.25; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; margin-top: 0; margin-bottom: 16px; }
-.input code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 10px; background-color: rgba(27, 31, 35, 0.05); padding: 0.2em 0.4em; margin: 2px 0 2px 0; border-radius: 3px; }
-.input pre { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 11px; padding: 8px 12px 8px 12px; overflow: auto; line-height: 1.45; margin: 2px 0 2px 0; background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; white-space: pre-wrap; word-wrap: break-word; }
-.connection { margin-top: 8px; background-color: rgba(27, 31, 35, 0.05); border-radius: 8px; border: 1px solid rgba(27, 31, 35, 0.05); }
-.connection-field { font-size: 10px; padding: 4px 8px 4px 8px; }
-.connection-border { border-top: 1px solid rgba(27, 31, 35, 0.05); }
-.connection code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 10px; background-color: rgba(0, 0, 0, 0); padding: 0; margin: 0; border: 0; }
-.connection pre { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 10px; background-color: rgba(0, 0, 0, 0); margin: 0; padding: 4px 8px 4px 8px; border: 0; }
-</style>
-<div class='inputs'>
-<div class='input'>
-<b>{{{name}}}{{#if type}}: {{/if}}</b>{{#if type}}<code><b>{{{type}}}</b></code>{{/if}}<br>
-{{#connections}}
-<div class='connection'>
-<div class='connection-field'>
-connection: <b>{{{id}}}</b>
-{{#if initializer}}
-{{#if initializer.title}}
-<div style='float: right;'>{{initializer.title}}</div>
-{{/if}}
-{{/if}}
-</div>
-{{#if initializer.description}}
-<div class='connection-border' />
-<div class='connection-field' />
-{{{initializer.description}}}
-</div>
-{{/if}}
-{{#if type}}
-<div class='connection-border' />
-<div class='connection-field'>
-type: <code><b>{{{type}}}</b></code>
-</div>
-{{/if}}
-{{#if initializer.quantization}}
-<div class='connection-border' />
-<div class='connection-field'>
-quantization: <code>{{{initializer.quantization}}}</code>
-</div>
-{{/if}}
-{{#if initializer.value}}
-<div class='connection-border' />
-<pre>{{{initializer.value}}}</pre>
-{{/if}}
-</div>
-{{/connections}}
-</div>
-</div>
-`;
-
 var operatorTemplate = `
 <style type='text/css'>
 .documentation { font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 12px; line-height: 1.5; margin: 0; }
@@ -238,136 +183,3 @@ var summaryTemplate = `
 {{/graphs}}
 </div>
 `;
-
-var nodeTemplate = `
-<style type='text/css'>
-.node-summary h1 { font-weight: 600; font-size: 14px; line-height: 1.25; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; margin-top: 0; margin-bottom: 16px; }
-.node-summary h2 { font-weight: 600; font-size: 12px; line-height: 1.25; margin-bottom: 16px; border-bottom: 1px solid #eaecef; }
-.node-summary h3 { font-weight: 600; font-size: 12px; line-height: 1.25; }
-.node-summary .documentation-button { display: inline-block; text-align: center; vertical-align: middle; font-weight: 600; width: 12px; height: 12px; font-size: 10px; line-height: 12px; border-radius: 50%; transform: translateY(-1px); padding: 1px; color: #888; background: transparent; border: 1px solid #aaa; text-decoration: none; }
-.node-summary .documentation-button:hover { color: #f6f6f6; background: #aaa; border-color: #aaa; text-decoration: none; }
-.node-summary .node-group { font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 12px; line-height: 1.5; margin: 0; }
-.node-summary .node-group .node-item { margin-bottom: 20px; }
-.node-summary .node-group .node-item b { font-weight: 600; }
-.node-summary .node-group .node-item code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-weight: 600; font-size: 10px; background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; padding: 0.2em 0.4em; margin: 0; }
-.node-summary .node-group .node-item pre { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 10px; padding: 8px 12px 8px 12px; overflow: auto; line-height: 1.45; background-color: rgba(27, 31, 35, 0.05); border-radius: 8px; border: 1px solid rgba(27, 31, 35, 0.05); white-space: pre-wrap; word-wrap: break-word; padding: 4px 8px 4px 8px; }
-.node-summary .node-group .node-item .group { margin-top: 8px; background-color: rgba(27, 31, 35, 0.05); border-radius: 8px; border: 1px solid rgba(27, 31, 35, 0.04); }
-.node-summary .node-group .node-item .group-property { font-size: 10px; padding: 4px 8px 4px 8px; }
-.node-summary .node-group .node-item .group-border { border-top: 1px solid rgba(27, 31, 35, 0.04); }
-.node-summary .node-group .node-item .group code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 10px; background-color: rgba(0, 0, 0, 0); padding: 0; margin: 0; border: 0; }
-.node-summary .node-group .node-item .group pre { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 10px; background-color: rgba(0, 0, 0, 0); margin: 0; padding: 4px 8px 4px 8px; border: 0; }
-
-</style>
-<div class='node-summary'>
-<div class='node-group'>
-{{#if operator}}
-<h1>{{{operator}}}{{#if documentation}} <a id='documentation-button' class='documentation-button'>?</a>{{/if}}</h1>
-{{/if}}
-{{#if name}}
-<div class='node-item'><b>name</b><br><pre>{{{name}}}</pre></div>
-{{/if}}
-{{#if description}}
-<div class='node-item'><b>description</b><br><pre>{{{description}}}</pre></div>
-{{/if}}
-{{#if domain}}
-<div class='node-item'><b>domain</b><br><pre>{{{domain}}}</pre></div>
-{{/if}}
-</div>
-
-{{#if attributes}}
-<h2>Attributes</h2>
-{{/if}}
-<div class='node-group'>
-{{#attributes}}
-<div class='node-item'>    
-<b>{{{name}}}{{#if type}}: {{/if}}</b>{{#if type}}<code>{{{type}}}</code>{{/if}}<br>
-{{#if description}}
-{{{description}}}
-{{/if}}
-<pre>{{{value}}}</pre>
-</div>
-{{/attributes}}
-</div>
-
-{{#if inputs}}
-<h2>Inputs</h2>
-<div class='node-group'>
-{{#inputs}}
-<div class='node-item'>
-<b>{{{name}}}{{#if type}}: {{/if}}</b>{{#if type}}<code>{{{type}}}</code>{{/if}}
-{{#connections}}
-<div class='group'>
-<div class='group-property'>
-connection: <b>{{{id}}}</b>
-{{#if initializer}}
-{{#if initializer.title}}
-<div style='float: right;'>{{initializer.title}}</div>
-{{/if}}
-{{/if}}
-</div>
-{{#if type}}
-<div class='group-border'></div>
-<div class='group-property'>
-type: <code><b>{{{type}}}</b></code>
-</div>
-{{/if}}
-</div>
-{{/connections}}
-</div>
-{{/inputs}}
-</div>
-{{/if}}
-
-{{#if outputs}}
-<h2>Outputs</h2>
-<div class='node-group'>
-{{#outputs}}
-<div class='node-item'>
-<b>{{{name}}}{{#if type}}: {{/if}}</b>{{#if type}}<code><b>{{{type}}}</b></code>{{/if}}
-{{#connections}}
-<div class='group'>
-<div class='group-property'>
-connection: <b>{{{id}}}</b>
-</div>
-{{#if type}}
-<div class='group-border'></div>
-<div class='group-property'>
-type: <code><b>{{{type}}}</b></code>
-</div>
-{{/if}}
-</div>
-{{/connections}}
-</div>
-{{/outputs}}
-</div>
-{{/if}}
-
-{{#if dependencies}}
-<h2>Control Dependencies</h2>
-<div class='node-group'>
-<div class='node-item'>
-{{#dependencies}}
-<div class='group'>
-<div class='group-property'>
-connection: <b>{{{id}}}</b>
-</div>
-{{#if name}}
-<div class='group-border'></div>
-<div class='group-property'>
-name: <b>{{{name}}}</b>
-</div>
-{{/if}}
-{{#if operator}}
-<div class='group-border'></div>
-<div class='group-property'>
-operator: <b>{{{operator}}}</b>
-</div>
-{{/if}}
-</div>
-{{/dependencies}}
-</div>
-</div>
-{{/if}}
-
-</div>
-`;

+ 32 - 21
src/view.js

@@ -288,7 +288,7 @@ class View {
                     if (!input.hidden) {
                         var types = input.connections.map(connection => connection.type ? connection.type : '').join('\n');
                         formatter.addItem(input.name, [ inputClass ], types, () => {
-                            this.showNodeInput(input);
+                            this.showNodeInput(node, input);
                         });    
                     }
     
@@ -531,19 +531,25 @@ class View {
     
     showNode(node) {
         if (node) {
-            var template = Handlebars.compile(nodeTemplate, 'utf-8');
-            var data = template(node);
-            this._sidebar.open(data, 'Node');
-    
-            var documentationButton = document.getElementById('documentation-button');
-            if (documentationButton) {
-                documentationButton.addEventListener('click', () => { 
-                    this.showDocumentation(node);
-                });
-            }
+            var documentationHandler = () => {
+                this.showDocumentation(node);
+            };
+            var view = new NodeView(node, documentationHandler);
+            this._sidebar.open(view.elements, 'Node');
         }
     }
-    
+
+    showNodeInput(node, input) {
+        if (input) {
+            var documentationHandler = () => {
+                this.showDocumentation(node);
+            };
+            var view = new NodeView(node, documentationHandler);
+            view.toggleInput(input.name);
+            this._sidebar.open(view.elements, 'Node');
+        }
+    }
+
     showDocumentation(node) {
         var documentation = node.documentation;
         if (documentation) {
@@ -563,14 +569,6 @@ class View {
             }
         }
     }
-
-    showNodeInput(input) {
-        if (input) {
-            var template = Handlebars.compile(inputTemplate, 'utf-8');
-            var data = template(input);
-            this._sidebar.open(data, 'Node Input');
-        }
-    }
 }
 
 class Sidebar {
@@ -606,7 +604,20 @@ class Sidebar {
             closeButtonElement.addEventListener('click', this._closeSidebarHandler);
             closeButtonElement.style.color = '#818181';
             contentElement.style.height = window.innerHeight - 60;
-            contentElement.innerHTML = content;
+            while (contentElement.firstChild) {
+                contentElement.removeChild(contentElement.firstChild);
+            }
+            if (typeof content == 'string') {
+                contentElement.innerHTML = content;
+            }
+            else if (content instanceof Array) {
+                content.forEach((element) => {
+                    contentElement.appendChild(element);
+                });
+            }
+            else {
+                contentElement.appendChild(content);
+            }
             sidebarElement.style.width = width ? width : '500px';    
             if (width && width.endsWith('%')) {
                 contentElement.style.width = '100%';