jclyo1 commited on
Commit
18d463f
1 Parent(s): da55d71
Files changed (3) hide show
  1. static/index.html +353 -58
  2. static/json_viewer.css +71 -0
  3. static/json_viewer.js +269 -0
static/index.html CHANGED
@@ -24,6 +24,10 @@
24
  type="module"
25
  src="https://display.truepic.com/truepic_display.es.js"
26
  ></script>
 
 
 
 
27
  </head>
28
  <body>
29
  <div class="container-fluid mt-2" id="head">
@@ -83,10 +87,18 @@
83
  <p>Ornare a est accumsan et platea quis rhoncus.</p>
84
  </div>
85
  <div class="col flex-grow-0">
86
- <a id="download-button" class="btn btn-outline-primary">Download image <img src="images/download_icon.svg" /></a>
 
 
87
  </div>
88
  <div class="col flex-grow-0">
89
- <button type="button" id="goto-verify-button" class="btn btn-outline-primary">Go to verify tab <img src="images/link_icon.svg" /></button>
 
 
 
 
 
 
90
  </div>
91
  </div>
92
  </div>
@@ -94,26 +106,116 @@
94
  <div class="display-verify">
95
  <div class="row mt-5 pb-4" id="original-image">
96
  <div class="col flex-grow-0">
97
- <img id="uploaded-image" class="thumbnail" />
98
  </div>
99
  <div class="col">
100
- <strong>Uploaded image</strong><br/>
101
- Content Credentials <span id="contentCredentialResults"></span><br/>
 
102
  Digital watermark <span id="digitalWatermarkResults"></span>
103
  </div>
104
  </div>
105
  <div class="mt-3" id="resultLabel">
106
- <strong>Result</strong>
107
- </div>
108
- <div class="image-container">
109
- <img src="images/placeholder.png" class="placeholder" />
 
 
 
 
 
110
  <img
111
  src="images/spinner.svg"
112
  class="spinner"
113
  style="display: none"
114
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  </div>
116
-
117
  </div>
118
  </div>
119
  <div class="col right-column">
@@ -178,7 +280,12 @@
178
  <div class="form-group mb-3">
179
  <label>Upload image</label>
180
  <div class="custom-select">
181
- <input type="file" class="form-control" name="fileUpload" id="fileUpload" />
 
 
 
 
 
182
  </div>
183
  </div>
184
 
@@ -228,13 +335,23 @@
228
  const imageGenForm = document.querySelector(".image-gen-form");
229
  const imagePrompt = document.getElementById("prompt");
230
  const model = document.getElementById("model");
231
- const generateImageContainer = document.querySelector(".display-generate .image-container");
 
 
232
  const generateActionMenu = document.querySelector(".action-menu");
233
- const verifyImageContainer = document.querySelector(".display-verify .image-container");
 
 
234
  const uploadedImageContainer = document.querySelector("#original-image");
235
  const downloadButton = document.getElementById("download-button");
 
 
 
 
 
 
 
236
 
237
-
238
  generateTab.addEventListener("click", (event) => {
239
  event.target.classList.add("active");
240
  verifyTab.classList.remove("active");
@@ -251,13 +368,15 @@
251
  setVerifyElementsDisplay("block");
252
  });
253
 
254
- document.getElementById('goto-verify-button').addEventListener("click", (event) => {
255
- verifyTab.classList.add("active");
256
- generateTab.classList.remove("active");
 
 
257
 
258
- setVerifyElementsDisplay('block');
259
- setGenerateElementsDisplay('none');
260
- });
261
 
262
  function setGenerateElementsDisplay(displayStatus) {
263
  displayGenerate.forEach((item) => {
@@ -277,20 +396,22 @@
277
  });
278
 
279
  function submitForm() {
280
- const file = document.getElementById("fileUpload").files[0];
281
 
282
  let fileReader = new FileReader();
283
  fileReader.readAsDataURL(file);
284
- fileReader.onload = function (){
285
- document.getElementById("uploaded-image").setAttribute('src', fileReader.result);
286
- }
 
 
 
 
 
287
 
288
- placeholder = document.querySelector('.display-verify .placeholder');
289
- spinner = document.querySelector('.display-verify .spinner');
290
-
291
  if (placeholder) placeholder.remove();
292
  if (document.getElementById("result"))
293
- document.getElementById("result").remove(); // JCL make sure to remove the correct one
294
  spinner.style.display = "block";
295
 
296
  const formData = new FormData(uploadForm);
@@ -311,24 +432,48 @@
311
  .then((data) => {
312
  console.log(data);
313
 
314
- console.log("contains_c2pa", data.contains_c2pa);
315
- console.log("contains_watermark", data.contains_watermark);
316
- console.log("result_media", data.result_media);
317
-
318
- document.getElementById("contentCredentialResults").innerHTML = data.contains_c2pa;
319
- document.getElementById("digitalWatermarkResults").innerHTML = data.contains_watermark;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
  uploadedImageContainer.style.display = "flex";
322
- document.getElementById('resultLabel').style.display = "block";
323
 
324
- document.querySelector('.display-verify .spinner').style.display = "none";
 
325
 
326
- if (data.result_media != 'n/a') {
327
  const path = "/" + data.result_media;
328
 
329
  var truepicDisplay = document.createElement("truepic-display");
330
-
331
- /*
332
  truepicDisplay.addEventListener(
333
  "validate",
334
  setVerificationOutputFromValidation
@@ -337,8 +482,6 @@
337
  "validate",
338
  setCertificateOutputFromValidation
339
  );
340
- */
341
-
342
 
343
  truepicDisplay.setAttribute("id", "result");
344
  truepicDisplay.setAttribute("active", "");
@@ -368,19 +511,17 @@
368
 
369
  imageGenForm.addEventListener("submit", async (event) => {
370
  event.preventDefault();
371
-
372
- placeholder = document.querySelector('.display-generate .placeholder');
373
- spinner = document.querySelector('.display-generate .spinner');
374
  generateActionMenu.style.display = "none";
375
 
376
- // verificationDetails.style.display = "none";
377
  // parameters.style.display = "none";
378
- // downloadLink.style.display = "none";
379
 
380
  try {
381
  if (placeholder) placeholder.remove();
382
  if (document.getElementById("result"))
383
- document.getElementById("result").remove(); // JCL make sure to remove the correct one
384
 
385
  spinner.style.display = "block";
386
 
@@ -388,16 +529,6 @@
388
  const path = "/" + resp;
389
 
390
  var truepicDisplay = document.createElement("truepic-display");
391
- /*
392
- truepicDisplay.addEventListener(
393
- "validate",
394
- setVerificationOutputFromValidation
395
- );
396
- truepicDisplay.addEventListener(
397
- "validate",
398
- setCertificateOutputFromValidation
399
- );
400
- */
401
 
402
  truepicDisplay.setAttribute("id", "result");
403
  truepicDisplay.setAttribute("active", "");
@@ -421,10 +552,174 @@
421
  promptParam.innerHTML = textGenInput.value;
422
  parameters.style.display = "block";
423
  */
424
-
425
  } catch (err) {
426
  console.error(err);
427
  }
428
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  </script>
430
  </html>
 
24
  type="module"
25
  src="https://display.truepic.com/truepic_display.es.js"
26
  ></script>
27
+
28
+ <link rel="stylesheet" href="json_viewer.css" />
29
+ <script src="https://unpkg.com/@peculiar/x509"></script>
30
+ <script type="text/javascript" src="json_viewer.js"></script>
31
  </head>
32
  <body>
33
  <div class="container-fluid mt-2" id="head">
 
87
  <p>Ornare a est accumsan et platea quis rhoncus.</p>
88
  </div>
89
  <div class="col flex-grow-0">
90
+ <a id="download-button" class="btn btn-outline-primary"
91
+ >Download image <img src="images/download_icon.svg"
92
+ /></a>
93
  </div>
94
  <div class="col flex-grow-0">
95
+ <button
96
+ type="button"
97
+ id="goto-verify-button"
98
+ class="btn btn-outline-primary"
99
+ >
100
+ Go to verify tab <img src="images/link_icon.svg" />
101
+ </button>
102
  </div>
103
  </div>
104
  </div>
 
106
  <div class="display-verify">
107
  <div class="row mt-5 pb-4" id="original-image">
108
  <div class="col flex-grow-0">
109
+ <img id="uploaded-image" class="thumbnail" />
110
  </div>
111
  <div class="col">
112
+ <strong>Uploaded image</strong><br />
113
+ Content Credentials <span id="contentCredentialResults"></span
114
+ ><br />
115
  Digital watermark <span id="digitalWatermarkResults"></span>
116
  </div>
117
  </div>
118
  <div class="mt-3" id="resultLabel">
119
+ <strong>Result</strong>
120
+ <div
121
+ class="alert alert-secondary"
122
+ role="alert"
123
+ id="verifyResultDescription"
124
+ ></div>
125
+ </div>
126
+ <div class="image-container">
127
+ <img src="images/placeholder.png" class="placeholder" />
128
  <img
129
  src="images/spinner.svg"
130
  class="spinner"
131
  style="display: none"
132
  />
133
+ </div>
134
+ <h3>Verification Output</h3>
135
+ <div id="verification-output"></div>
136
+
137
+ <h3>Certificates</h3>
138
+ <div class="certificate" id="certificate-output">
139
+ <p>
140
+ Details about the certificate used for signing.<br />
141
+ Truepic maintains the first and only purpose-built C2PA
142
+ certificate authority.
143
+ </p>
144
+
145
+ <p>
146
+ <strong style="font-weight: 600">Certificate chain</strong>
147
+ </p>
148
+ <ul id="certificate-list"></ul>
149
+ <p><strong style="font-weight: 600">Details</strong></p>
150
+ <div class="details">
151
+ <strong>Basic info</strong>
152
+ <div>
153
+ <label>Type</label>
154
+ X.509 Certificate
155
+ </div>
156
+ <div>
157
+ <label>Serial Number</label>
158
+ <div class="serialNumber"></div>
159
+ </div>
160
+ <div>
161
+ <label>Issued</label>
162
+ <div class="issued"></div>
163
+ </div>
164
+ <div>
165
+ <label>Expired</label>
166
+ <div class="expired"></div>
167
+ </div>
168
+ <strong>Subject</strong>
169
+ <div>
170
+ <label>Common Name</label>
171
+ <div class="subjectCommonName"></div>
172
+ </div>
173
+ <div>
174
+ <label>Organization</label>
175
+ <div class="subjectOrganization"></div>
176
+ </div>
177
+ <div>
178
+ <label>Organization Unit</label>
179
+ <div class="subjectOrganizationUnit"></div>
180
+ </div>
181
+ <div>
182
+ <label>Country</label>
183
+ <div class="subjectCountry"></div>
184
+ </div>
185
+
186
+ <strong>Issuer</strong>
187
+ <div>
188
+ <label>Common Name</label>
189
+ <div class="issuerCommonName"></div>
190
+ </div>
191
+ <div>
192
+ <label>Organization</label>
193
+ <div class="issuerOrganization"></div>
194
+ </div>
195
+ <div>
196
+ <label>Organization Unit</label>
197
+ <div class="issuerOrganizationUnit"></div>
198
+ </div>
199
+ <div>
200
+ <label>Country</label>
201
+ <div class="issuerCountry"></div>
202
+ </div>
203
+ <strong>Public Key Info</strong>
204
+
205
+ <div>
206
+ <label>Algorithm</label>
207
+ <div class="algorithm"></div>
208
+ </div>
209
+ <div id="modulusContainer">
210
+ <label>Modulus</label>
211
+ <div class="modulus"></div>
212
+ </div>
213
+ <div id="curveContainer">
214
+ <label>Curve</label>
215
+ <div class="namedCurve"></div>
216
+ </div>
217
  </div>
218
+ </div>
219
  </div>
220
  </div>
221
  <div class="col right-column">
 
280
  <div class="form-group mb-3">
281
  <label>Upload image</label>
282
  <div class="custom-select">
283
+ <input
284
+ type="file"
285
+ class="form-control"
286
+ name="fileUpload"
287
+ id="fileUpload"
288
+ />
289
  </div>
290
  </div>
291
 
 
335
  const imageGenForm = document.querySelector(".image-gen-form");
336
  const imagePrompt = document.getElementById("prompt");
337
  const model = document.getElementById("model");
338
+ const generateImageContainer = document.querySelector(
339
+ ".display-generate .image-container"
340
+ );
341
  const generateActionMenu = document.querySelector(".action-menu");
342
+ const verifyImageContainer = document.querySelector(
343
+ ".display-verify .image-container"
344
+ );
345
  const uploadedImageContainer = document.querySelector("#original-image");
346
  const downloadButton = document.getElementById("download-button");
347
+ const verifyResultDescription = document.getElementById(
348
+ "verifyResultDescription"
349
+ );
350
+ const verificationOutput = document.getElementById("verification-output");
351
+ const certificateList = document.getElementById("certificate-list");
352
+ var certificates = [];
353
+
354
 
 
355
  generateTab.addEventListener("click", (event) => {
356
  event.target.classList.add("active");
357
  verifyTab.classList.remove("active");
 
368
  setVerifyElementsDisplay("block");
369
  });
370
 
371
+ document
372
+ .getElementById("goto-verify-button")
373
+ .addEventListener("click", (event) => {
374
+ verifyTab.classList.add("active");
375
+ generateTab.classList.remove("active");
376
 
377
+ setVerifyElementsDisplay("block");
378
+ setGenerateElementsDisplay("none");
379
+ });
380
 
381
  function setGenerateElementsDisplay(displayStatus) {
382
  displayGenerate.forEach((item) => {
 
396
  });
397
 
398
  function submitForm() {
399
+ const file = document.getElementById("fileUpload").files[0];
400
 
401
  let fileReader = new FileReader();
402
  fileReader.readAsDataURL(file);
403
+ fileReader.onload = function () {
404
+ document
405
+ .getElementById("uploaded-image")
406
+ .setAttribute("src", fileReader.result);
407
+ };
408
+
409
+ placeholder = document.querySelector(".display-verify .placeholder");
410
+ spinner = document.querySelector(".display-verify .spinner");
411
 
 
 
 
412
  if (placeholder) placeholder.remove();
413
  if (document.getElementById("result"))
414
+ document.getElementById("result").remove(); // JCL make sure to remove the correct one
415
  spinner.style.display = "block";
416
 
417
  const formData = new FormData(uploadForm);
 
432
  .then((data) => {
433
  console.log(data);
434
 
435
+ document.getElementById("contentCredentialResults").innerHTML =
436
+ data.contains_c2pa;
437
+ document.getElementById("digitalWatermarkResults").innerHTML =
438
+ data.contains_watermark;
439
+
440
+ if (
441
+ data.contains_c2pa == "true" &&
442
+ data.contains_watermark == "true"
443
+ ) {
444
+ verifyResultDescription.innerHTML =
445
+ "Your image contains content credentials, which are displayed below.";
446
+ } else if (
447
+ data.contains_c2pa == "false" &&
448
+ data.contains_watermark == "true"
449
+ ) {
450
+ verifyResultDescription.innerHTML =
451
+ "The watermark was found, but image modifications were also detected. The last untampered, signed version on file is displayed.";
452
+ } else if (
453
+ data.contains_c2pa == "true" &&
454
+ data.contains_watermark == "false"
455
+ ) {
456
+ verifyResultDescription.innerHTML =
457
+ "Your image contains content credentials, which are displayed below. A watermark was not detected.";
458
+ } else if (
459
+ data.contains_c2pa == "false" &&
460
+ data.contains_watermark == "false"
461
+ ) {
462
+ verifyResultDescription.innerHTML =
463
+ "Nothing to show: this image contains neither content credentials, nor a watermark.";
464
+ }
465
 
466
  uploadedImageContainer.style.display = "flex";
467
+ document.getElementById("resultLabel").style.display = "block";
468
 
469
+ document.querySelector(".display-verify .spinner").style.display =
470
+ "none";
471
 
472
+ if (data.result_media != "n/a") {
473
  const path = "/" + data.result_media;
474
 
475
  var truepicDisplay = document.createElement("truepic-display");
476
+
 
477
  truepicDisplay.addEventListener(
478
  "validate",
479
  setVerificationOutputFromValidation
 
482
  "validate",
483
  setCertificateOutputFromValidation
484
  );
 
 
485
 
486
  truepicDisplay.setAttribute("id", "result");
487
  truepicDisplay.setAttribute("active", "");
 
511
 
512
  imageGenForm.addEventListener("submit", async (event) => {
513
  event.preventDefault();
514
+
515
+ placeholder = document.querySelector(".display-generate .placeholder");
516
+ spinner = document.querySelector(".display-generate .spinner");
517
  generateActionMenu.style.display = "none";
518
 
 
519
  // parameters.style.display = "none";
 
520
 
521
  try {
522
  if (placeholder) placeholder.remove();
523
  if (document.getElementById("result"))
524
+ document.getElementById("result").remove(); // JCL make sure to remove the correct one
525
 
526
  spinner.style.display = "block";
527
 
 
529
  const path = "/" + resp;
530
 
531
  var truepicDisplay = document.createElement("truepic-display");
 
 
 
 
 
 
 
 
 
 
532
 
533
  truepicDisplay.setAttribute("id", "result");
534
  truepicDisplay.setAttribute("active", "");
 
552
  promptParam.innerHTML = textGenInput.value;
553
  parameters.style.display = "block";
554
  */
 
555
  } catch (err) {
556
  console.error(err);
557
  }
558
  });
559
+
560
+ function setVerificationOutputFromValidation(event) {
561
+ //verificationDetails.style.display = "block";
562
+ return setVerificationOutput(event.detail.manifestStore.toJSON());
563
+ }
564
+
565
+ function setCertificateOutputFromValidation(event) {
566
+ return setCertificateOutput(event.detail.manifestStore);
567
+ }
568
+
569
+ function setVerificationOutput(output = null) {
570
+ verificationOutput.innerHTML = "";
571
+
572
+ if (!output) {
573
+ return;
574
+ }
575
+
576
+ const viewer = new JSONViewer();
577
+
578
+ verificationOutput.appendChild(viewer.getContainer());
579
+
580
+ viewer.showJSON(output);
581
+ }
582
+
583
+ function setCertificateOutput(manifestStore = null) {
584
+ const certificate = manifestStore?.activeManifest?.certificate;
585
+
586
+ if (!certificate) {
587
+ return;
588
+ }
589
+
590
+ certificates = [
591
+ {
592
+ der: certificate.der,
593
+ name: certificate.subjectName,
594
+ decoded: new x509.X509Certificate(certificate.der),
595
+ },
596
+ ...certificate.chain.map((certificate) => ({
597
+ der: certificate.der,
598
+ decoded: new x509.X509Certificate(certificate.der),
599
+ })),
600
+ ];
601
+
602
+ certificates.forEach((certificate) => {
603
+ certificate.transformed = transformCert(certificate.decoded);
604
+ });
605
+
606
+ console.log("certificates", certificates);
607
+
608
+ certificateList.innerHTML = "";
609
+
610
+ certificates.forEach((certificate, index) => {
611
+ var li = document.createElement("li");
612
+ if (index == 0) li.classList.add("active");
613
+ li.appendChild(
614
+ document.createTextNode(certificate.transformed.subjectCommonName)
615
+ );
616
+ li.addEventListener("click", function (e) {
617
+ setCertificate(index);
618
+ const lis = document.querySelectorAll("#certificate-list li");
619
+
620
+ lis.forEach((element) => {
621
+ element.classList.remove("active");
622
+ });
623
+
624
+ this.classList.add("active");
625
+ });
626
+
627
+ certificateList.appendChild(li);
628
+ });
629
+
630
+ setCertificate(0);
631
+ }
632
+
633
+ function transformCert(certificate) {
634
+ const {
635
+ issuer,
636
+ subject,
637
+ notAfter: expired,
638
+ notBefore: issued,
639
+ serialNumber,
640
+ publicKey: {
641
+ algorithm: {
642
+ name: algorithm,
643
+ modulusLength: modulus,
644
+ namedCurve: namedCurve,
645
+ },
646
+ },
647
+ } = certificate;
648
+
649
+ const parsedSubject = parseCertificateValues(subject);
650
+ const parsedIssuer = parseCertificateValues(issuer);
651
+
652
+ return {
653
+ issuerCommonName: parsedIssuer["CN"],
654
+ issuerOrganizationUnit: parsedIssuer["OU"],
655
+ issuerOrganization: parsedIssuer["O"],
656
+ issuerCountry: parsedIssuer["C"],
657
+ subjectCommonName: parsedSubject["CN"],
658
+ subjectOrganizationUnit: parsedSubject["OU"],
659
+ subjectOrganization: parsedSubject["O"],
660
+ subjectCountry: parsedSubject["C"],
661
+ issued,
662
+ expired,
663
+ serialNumber,
664
+ algorithm,
665
+ modulus,
666
+ namedCurve,
667
+ };
668
+ }
669
+
670
+ function parseCertificateValues(input) {
671
+ const params = new URLSearchParams(input.replaceAll(",", "&"));
672
+ const responses = {};
673
+
674
+ for (const entry of params.entries()) {
675
+ responses[entry[0].trim()] = entry[1];
676
+ }
677
+
678
+ return responses;
679
+ }
680
+
681
+ function setCertificate(ind) {
682
+ const certificate = certificates[ind].transformed;
683
+
684
+ document.querySelector(".details .issuerCommonName").innerHTML =
685
+ certificate.issuerCommonName;
686
+ document.querySelector(".details .issuerOrganizationUnit").innerHTML =
687
+ certificate.issuerOrganizationUnit;
688
+ document.querySelector(".details .issuerOrganization").innerHTML =
689
+ certificate.issuerOrganization;
690
+ document.querySelector(".details .issuerCountry").innerHTML =
691
+ certificate.issuerCountry;
692
+ document.querySelector(".details .subjectCommonName").innerHTML =
693
+ certificate.subjectCommonName;
694
+ document.querySelector(".details .subjectOrganizationUnit").innerHTML =
695
+ certificate.subjectOrganizationUnit;
696
+ document.querySelector(".details .subjectOrganization").innerHTML =
697
+ certificate.subjectOrganization;
698
+ document.querySelector(".details .subjectCountry").innerHTML =
699
+ certificate.subjectCountry;
700
+ document.querySelector(".details .issued").innerHTML = certificate.issued;
701
+ document.querySelector(".details .expired").innerHTML =
702
+ certificate.expired;
703
+ document.querySelector(".details .serialNumber").innerHTML =
704
+ certificate.serialNumber;
705
+ document.querySelector(".details .algorithm").innerHTML =
706
+ certificate.algorithm;
707
+
708
+ if (certificate.modulus !== undefined) {
709
+ document.querySelector(".details .modulus").innerHTML =
710
+ certificate.modulus;
711
+ document.querySelector("#modulusContainers").style.display = "block";
712
+ } else {
713
+ document.querySelector("#modulusContainers").style.display = "none";
714
+ }
715
+
716
+ if (certificate.namedCurve !== undefined) {
717
+ document.querySelector(".details .namedCurve").innerHTML =
718
+ certificate.namedCurve;
719
+ document.querySelector("#curveContainer").style.display = "block";
720
+ } else {
721
+ document.querySelector("#curveContainer").style.display = "none";
722
+ }
723
+ }
724
  </script>
725
  </html>
static/json_viewer.css ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .json-viewer {
2
+ color: #000;
3
+ padding-left: 20px;
4
+ }
5
+
6
+ .json-viewer ul {
7
+ list-style-type: none;
8
+ margin: 0;
9
+ margin: 0 0 0 1px;
10
+ border-left: 1px dotted #ccc;
11
+ padding-left: 2em;
12
+ }
13
+
14
+ .json-viewer .hide {
15
+ display: none;
16
+ }
17
+
18
+ .json-viewer .type-string {
19
+ color: #0b7500;
20
+ }
21
+
22
+ .json-viewer .type-date {
23
+ color: #cb7500;
24
+ }
25
+
26
+ .json-viewer .type-boolean {
27
+ color: #1a01cc;
28
+ font-weight: bold;
29
+ }
30
+
31
+ .json-viewer .type-number {
32
+ color: #1a01cc;
33
+ }
34
+
35
+ .json-viewer .type-null,
36
+ .json-viewer .type-undefined {
37
+ color: #90a;
38
+ }
39
+
40
+ .json-viewer a.list-link {
41
+ color: #000;
42
+ text-decoration: none;
43
+ position: relative;
44
+ }
45
+
46
+ .json-viewer a.list-link:before {
47
+ color: #aaa;
48
+ content: "\25BC";
49
+ position: absolute;
50
+ display: inline-block;
51
+ width: 1em;
52
+ left: -1em;
53
+ }
54
+
55
+ .json-viewer a.list-link.collapsed:before {
56
+ content: "\25B6";
57
+ }
58
+
59
+ .json-viewer a.list-link.empty:before {
60
+ content: "";
61
+ }
62
+
63
+ .json-viewer .items-ph {
64
+ color: #aaa;
65
+ padding: 0 1em;
66
+ }
67
+
68
+ .json-viewer .items-ph:hover {
69
+ text-decoration: underline;
70
+ }
71
+
static/json_viewer.js ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * JSONViewer - by Roman Makudera 2016 (c) MIT licence.
3
+ */
4
+ var JSONViewer = (function(document) {
5
+ var Object_prototype_toString = ({}).toString;
6
+ var DatePrototypeAsString = Object_prototype_toString.call(new Date);
7
+
8
+ /** @constructor */
9
+ function JSONViewer() {
10
+ this._dom_container = document.createElement("pre");
11
+ this._dom_container.classList.add("json-viewer");
12
+ };
13
+
14
+ /**
15
+ * Visualise JSON object.
16
+ *
17
+ * @param {Object|Array} json Input value
18
+ * @param {Number} [inputMaxLvl] Process only to max level, where 0..n, -1 unlimited
19
+ * @param {Number} [inputColAt] Collapse at level, where 0..n, -1 unlimited
20
+ */
21
+ JSONViewer.prototype.showJSON = function(jsonValue, inputMaxLvl, inputColAt) {
22
+ // Process only to maxLvl, where 0..n, -1 unlimited
23
+ var maxLvl = typeof inputMaxLvl === "number" ? inputMaxLvl : -1; // max level
24
+ // Collapse at level colAt, where 0..n, -1 unlimited
25
+ var colAt = typeof inputColAt === "number" ? inputColAt : -1; // collapse at
26
+
27
+ this._dom_container.innerHTML = "";
28
+ walkJSONTree(this._dom_container, jsonValue, maxLvl, colAt, 0);
29
+ };
30
+
31
+ /**
32
+ * Get container with pre object - this container is used for visualise JSON data.
33
+ *
34
+ * @return {Element}
35
+ */
36
+ JSONViewer.prototype.getContainer = function() {
37
+ return this._dom_container;
38
+ };
39
+
40
+ /**
41
+ * Recursive walk for input value.
42
+ *
43
+ * @param {Element} outputParent is the Element that will contain the new DOM
44
+ * @param {Object|Array} value Input value
45
+ * @param {Number} maxLvl Process only to max level, where 0..n, -1 unlimited
46
+ * @param {Number} colAt Collapse at level, where 0..n, -1 unlimited
47
+ * @param {Number} lvl Current level
48
+ */
49
+ function walkJSONTree(outputParent, value, maxLvl, colAt, lvl) {
50
+ var isDate = Object_prototype_toString.call(value) === DatePrototypeAsString;
51
+ var realValue = !isDate && typeof value === "object" && value !== null && "toJSON" in value ? value.toJSON() : value;
52
+ if (typeof realValue === "object" && realValue !== null && !isDate) {
53
+ var isMaxLvl = maxLvl >= 0 && lvl >= maxLvl;
54
+ var isCollapse = colAt >= 0 && lvl >= colAt;
55
+
56
+ var isArray = Array.isArray(realValue);
57
+ var items = isArray ? realValue : Object.keys(realValue);
58
+
59
+ if (lvl === 0) {
60
+ // root level
61
+ var rootCount = _createItemsCount(items.length);
62
+ // hide/show
63
+ var rootLink = _createLink(isArray ? "[" : "{");
64
+
65
+ if (items.length) {
66
+ rootLink.addEventListener("click", function() {
67
+ if (isMaxLvl) return;
68
+
69
+ rootLink.classList.toggle("collapsed");
70
+ rootCount.classList.toggle("hide");
71
+
72
+ // main list
73
+ outputParent.querySelector("ul").classList.toggle("hide");
74
+ });
75
+
76
+ if (isCollapse) {
77
+ rootLink.classList.add("collapsed");
78
+ rootCount.classList.remove("hide");
79
+ }
80
+ }
81
+ else {
82
+ rootLink.classList.add("empty");
83
+ }
84
+
85
+ rootLink.appendChild(rootCount);
86
+ outputParent.appendChild(rootLink); // output the rootLink
87
+ }
88
+
89
+ if (items.length && !isMaxLvl) {
90
+ var len = items.length - 1;
91
+ var ulList = document.createElement("ul");
92
+ ulList.setAttribute("data-level", lvl);
93
+ ulList.classList.add("type-" + (isArray ? "array" : "object"));
94
+
95
+ items.forEach(function(key, ind) {
96
+ var item = isArray ? key : value[key];
97
+ var li = document.createElement("li");
98
+
99
+ if (typeof item === "object") {
100
+ // null && date
101
+ if (!item || item instanceof Date) {
102
+ li.appendChild(document.createTextNode(isArray ? "" : key + ": "));
103
+ li.appendChild(createSimpleViewOf(item ? item : null, true));
104
+ }
105
+ // array & object
106
+ else {
107
+ var itemIsArray = Array.isArray(item);
108
+ var itemLen = itemIsArray ? item.length : Object.keys(item).length;
109
+
110
+ // empty
111
+ if (!itemLen) {
112
+ li.appendChild(document.createTextNode(key + ": " + (itemIsArray ? "[]" : "{}")));
113
+ }
114
+ else {
115
+ // 1+ items
116
+ var itemTitle = (typeof key === "string" ? key + ": " : "") + (itemIsArray ? "[" : "{");
117
+ var itemLink = _createLink(itemTitle);
118
+ var itemsCount = _createItemsCount(itemLen);
119
+
120
+ // maxLvl - only text, no link
121
+ if (maxLvl >= 0 && lvl + 1 >= maxLvl) {
122
+ li.appendChild(document.createTextNode(itemTitle));
123
+ }
124
+ else {
125
+ itemLink.appendChild(itemsCount);
126
+ li.appendChild(itemLink);
127
+ }
128
+
129
+ walkJSONTree(li, item, maxLvl, colAt, lvl + 1);
130
+ li.appendChild(document.createTextNode(itemIsArray ? "]" : "}"));
131
+
132
+ var list = li.querySelector("ul");
133
+ var itemLinkCb = function() {
134
+ itemLink.classList.toggle("collapsed");
135
+ itemsCount.classList.toggle("hide");
136
+ list.classList.toggle("hide");
137
+ };
138
+
139
+ // hide/show
140
+ itemLink.addEventListener("click", itemLinkCb);
141
+
142
+ // collapse lower level
143
+ if (colAt >= 0 && lvl + 1 >= colAt) {
144
+ itemLinkCb();
145
+ }
146
+ }
147
+ }
148
+ }
149
+ // simple values
150
+ else {
151
+ // object keys with key:
152
+ if (!isArray) {
153
+ li.appendChild(document.createTextNode(key + ": "));
154
+ }
155
+
156
+ // recursive
157
+ walkJSONTree(li, item, maxLvl, colAt, lvl + 1);
158
+ }
159
+
160
+ // add comma to the end
161
+ if (ind < len) {
162
+ li.appendChild(document.createTextNode(","));
163
+ }
164
+
165
+ ulList.appendChild(li);
166
+ }, this);
167
+
168
+ outputParent.appendChild(ulList); // output ulList
169
+ }
170
+ else if (items.length && isMaxLvl) {
171
+ var itemsCount = _createItemsCount(items.length);
172
+ itemsCount.classList.remove("hide");
173
+
174
+ outputParent.appendChild(itemsCount); // output itemsCount
175
+ }
176
+
177
+ if (lvl === 0) {
178
+ // empty root
179
+ if (!items.length) {
180
+ var itemsCount = _createItemsCount(0);
181
+ itemsCount.classList.remove("hide");
182
+
183
+ outputParent.appendChild(itemsCount); // output itemsCount
184
+ }
185
+
186
+ // root cover
187
+ outputParent.appendChild(document.createTextNode(isArray ? "]" : "}"));
188
+
189
+ // collapse
190
+ if (isCollapse) {
191
+ outputParent.querySelector("ul").classList.add("hide");
192
+ }
193
+ }
194
+ } else {
195
+ // simple values
196
+ outputParent.appendChild( createSimpleViewOf(value, isDate) );
197
+ }
198
+ };
199
+
200
+ /**
201
+ * Create simple value (no object|array).
202
+ *
203
+ * @param {Number|String|null|undefined|Date} value Input value
204
+ * @return {Element}
205
+ */
206
+ function createSimpleViewOf(value, isDate) {
207
+ var spanEl = document.createElement("span");
208
+ var type = typeof value;
209
+ var asText = "" + value;
210
+
211
+ if (type === "string") {
212
+ asText = '"' + value + '"';
213
+ } else if (value === null) {
214
+ type = "null";
215
+ //asText = "null";
216
+ } else if (isDate) {
217
+ type = "date";
218
+ asText = value.toLocaleString();
219
+ }
220
+
221
+ spanEl.className = "type-" + type;
222
+ spanEl.textContent = asText;
223
+
224
+ return spanEl;
225
+ };
226
+
227
+ /**
228
+ * Create items count element.
229
+ *
230
+ * @param {Number} count Items count
231
+ * @return {Element}
232
+ */
233
+ function _createItemsCount(count) {
234
+ var itemsCount = document.createElement("span");
235
+ itemsCount.className = "items-ph hide";
236
+ itemsCount.innerHTML = _getItemsTitle(count);
237
+
238
+ return itemsCount;
239
+ };
240
+
241
+ /**
242
+ * Create clickable link.
243
+ *
244
+ * @param {String} title Link title
245
+ * @return {Element}
246
+ */
247
+ function _createLink(title) {
248
+ var linkEl = document.createElement("a");
249
+ linkEl.classList.add("list-link");
250
+ linkEl.href = "javascript:void(0)";
251
+ linkEl.innerHTML = title || "";
252
+
253
+ return linkEl;
254
+ };
255
+
256
+ /**
257
+ * Get correct item|s title for count.
258
+ *
259
+ * @param {Number} count Items count
260
+ * @return {String}
261
+ */
262
+ function _getItemsTitle(count) {
263
+ var itemsTxt = count > 1 || count === 0 ? "items" : "item";
264
+
265
+ return (count + " " + itemsTxt);
266
+ };
267
+
268
+ return JSONViewer;
269
+ })(document);