234 lines
12 KiB
JavaScript
234 lines
12 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
if (document.contentType !== 'application/json' || document.querySelector('.json-viewer')) {
|
|
return;
|
|
}
|
|
|
|
var CSS = `:root{color-scheme:light dark;--jv-bg:#fff;--jv-fg:#24292f;--jv-muted:#6e7781;--jv-border:#d0d7de;--jv-guide:#d8dee4;--jv-hover:#f3f4f6;--jv-control-bg:#f6f8fa;--jv-control-hover:#eaeef2;--jv-string:#16803a;--jv-number:#0969da;--jv-boolean:#bc4c00;--jv-null:#6e7781;--jv-key:#8250df;--jv-match:#fff3a3}*{box-sizing:border-box}html,body{margin:0;min-height:100%;background:var(--jv-bg);color:var(--jv-fg)}body.json-viewer-page{font-family:Menlo,Consolas,"Liberation Mono",monospace;font-size:14px;line-height:1.55}.json-viewer{min-height:100vh}.json-viewer__toolbar{position:sticky;z-index:10;top:0;display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--jv-border);background:var(--jv-bg);box-shadow:0 1px 4px rgb(0 0 0/8%)}.json-viewer__title{margin-right:auto;font-weight:700}.json-viewer__button,.json-viewer__search{min-height:32px;border:1px solid var(--jv-border);border-radius:6px;background:var(--jv-control-bg);color:var(--jv-fg);font:inherit}.json-viewer__button{padding:4px 10px;cursor:pointer}.json-viewer__button:hover{background:var(--jv-control-hover)}.json-viewer__button:focus-visible,.json-viewer__search:focus-visible{outline:2px solid var(--jv-number);outline-offset:1px}.json-viewer__helper-apps{display:flex;align-items:center;gap:6px}.json-viewer__helper-label{color:var(--jv-muted);font-size:12px}.json-viewer__helper-link{display:inline-flex;align-items:center;text-decoration:none}.json-viewer__search{width:min(280px,50vw);padding:5px 10px}.json-viewer__status{min-width:9em;color:var(--jv-muted);font-size:12px}.json-viewer__tree{overflow-x:auto;overflow-y:visible;padding:14px 16px 40px}.json-viewer__node{min-width:0}.json-viewer__children{margin-left:8px;padding-left:17px;border-left:1px solid var(--jv-guide)}.json-viewer__node.is-collapsed>.json-viewer__children{display:none}.json-viewer__row{display:flex;align-items:baseline;min-height:23px;padding:1px 7px 1px 2px;border-radius:4px;white-space:pre}.json-viewer__row:hover{background:var(--jv-hover)}.json-viewer__node.is-filtered-out{display:none}.json-viewer__node.is-match>.json-viewer__row{background:var(--jv-match)}.json-viewer__toggle{width:18px;flex:0 0 18px;padding:0;border:0;background:transparent;color:var(--jv-muted);cursor:pointer;font:inherit;line-height:1}.json-viewer__toggle:before{content:"▼"}.json-viewer__node.is-collapsed>.json-viewer__row>.json-viewer__toggle:before{content:"▶"}.json-viewer__toggle--empty{cursor:default}.json-viewer__toggle--empty:before{content:""}.json-viewer__key{color:var(--jv-key)}.json-viewer__key:after{content:":";color:var(--jv-fg)}.json-viewer__punctuation{color:var(--jv-fg)}.json-viewer__string{min-width:0;color:var(--jv-string);white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;word-break:break-all}.json-viewer__number{color:var(--jv-number)}.json-viewer__boolean{color:var(--jv-boolean);font-weight:600}.json-viewer__null{color:var(--jv-null);font-style:italic}.json-viewer__badge{display:none;margin-left:7px;padding:0 6px;border:1px solid var(--jv-border);border-radius:999px;color:var(--jv-muted);font-size:11px;line-height:17px}.json-viewer__node.is-collapsed>.json-viewer__row>.json-viewer__badge{display:inline}.json-viewer__preview{display:none;margin-left:7px;color:var(--jv-muted)}.json-viewer__node.is-collapsed>.json-viewer__row>.json-viewer__preview{display:inline}@media(prefers-color-scheme:dark){:root{--jv-bg:#1e1e1e;--jv-fg:#d4d4d4;--jv-muted:#9b9b9b;--jv-border:#454545;--jv-guide:#3f3f46;--jv-hover:#2a2d2e;--jv-control-bg:#2d2d30;--jv-control-hover:#3a3d41;--jv-string:#6a9955;--jv-number:#569cd6;--jv-boolean:#ce9178;--jv-null:#9b9b9b;--jv-key:#9cdcfe;--jv-match:#514b20}}@media(max-width:700px){.json-viewer__title{width:100%}.json-viewer__search{order:2;width:100%}.json-viewer__status{order:3}.json-viewer__helper-apps{width:100%;flex-wrap:wrap}}`;
|
|
|
|
function element(tag, className, text) {
|
|
var node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
function valueType(value) {
|
|
if (value === null) return 'null';
|
|
return typeof value;
|
|
}
|
|
|
|
function primitive(value) {
|
|
var type = valueType(value);
|
|
var text = type === 'string' ? JSON.stringify(value) : String(value);
|
|
return element('span', 'json-viewer__' + type, text);
|
|
}
|
|
|
|
function countLabel(value) {
|
|
var count = Array.isArray(value) ? value.length : Object.keys(value).length;
|
|
return count + ' ' + (Array.isArray(value) ? (count === 1 ? 'item' : 'items') : (count === 1 ? 'key' : 'keys'));
|
|
}
|
|
|
|
function buildNode(value, key, path) {
|
|
var compound = value !== null && typeof value === 'object';
|
|
var node = element('div', 'json-viewer__node');
|
|
node.dataset.key = key === null ? '' : String(key).toLocaleLowerCase();
|
|
node.dataset.path = path.toLocaleLowerCase();
|
|
|
|
var row = element('div', 'json-viewer__row');
|
|
var toggle = element('button', 'json-viewer__toggle' + (compound ? '' : ' json-viewer__toggle--empty'));
|
|
toggle.type = 'button';
|
|
toggle.tabIndex = compound ? 0 : -1;
|
|
toggle.setAttribute('aria-label', compound ? 'Collapse node' : 'Value');
|
|
row.appendChild(toggle);
|
|
|
|
if (key !== null) {
|
|
row.appendChild(element('span', 'json-viewer__key', JSON.stringify(String(key))));
|
|
row.appendChild(document.createTextNode(' '));
|
|
}
|
|
|
|
if (!compound) {
|
|
row.appendChild(primitive(value));
|
|
node.appendChild(row);
|
|
return node;
|
|
}
|
|
|
|
var isArray = Array.isArray(value);
|
|
var opening = isArray ? '[' : '{';
|
|
var closing = isArray ? ']' : '}';
|
|
row.appendChild(element('span', 'json-viewer__punctuation', opening));
|
|
row.appendChild(element('span', 'json-viewer__badge', countLabel(value)));
|
|
row.appendChild(element('span', 'json-viewer__preview', '… ' + closing));
|
|
node.appendChild(row);
|
|
|
|
var children = element('div', 'json-viewer__children');
|
|
var entries = isArray ? value.map(function (item, index) { return [index, item]; }) : Object.keys(value).map(function (itemKey) { return [itemKey, value[itemKey]]; });
|
|
entries.forEach(function (entry) {
|
|
var childPath = path ? path + '.' + entry[0] : String(entry[0]);
|
|
children.appendChild(buildNode(entry[1], isArray ? null : entry[0], childPath));
|
|
});
|
|
children.appendChild(element('div', 'json-viewer__row json-viewer__punctuation', ' ' + closing));
|
|
node.appendChild(children);
|
|
|
|
toggle.addEventListener('click', function () {
|
|
var collapsed = node.classList.toggle('is-collapsed');
|
|
toggle.setAttribute('aria-label', collapsed ? 'Expand node' : 'Collapse node');
|
|
});
|
|
return node;
|
|
}
|
|
|
|
function setAllCollapsed(root, collapsed) {
|
|
root.querySelectorAll('.json-viewer__node').forEach(function (node) {
|
|
if (node.querySelector(':scope > .json-viewer__children')) {
|
|
node.classList.toggle('is-collapsed', collapsed);
|
|
var button = node.querySelector(':scope > .json-viewer__row > .json-viewer__toggle');
|
|
if (button) button.setAttribute('aria-label', collapsed ? 'Expand node' : 'Collapse node');
|
|
}
|
|
});
|
|
}
|
|
|
|
function copyText(text, button) {
|
|
var copied = function () {
|
|
var old = button.textContent;
|
|
button.textContent = 'Copied!';
|
|
window.setTimeout(function () { button.textContent = old; }, 1200);
|
|
};
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(text).then(copied).catch(function () { fallbackCopy(text, copied); });
|
|
} else {
|
|
fallbackCopy(text, copied);
|
|
}
|
|
}
|
|
|
|
function fallbackCopy(text, done) {
|
|
var area = element('textarea');
|
|
area.value = text;
|
|
area.style.position = 'fixed';
|
|
area.style.opacity = '0';
|
|
document.body.appendChild(area);
|
|
area.select();
|
|
try { document.execCommand('copy'); done(); } catch (ignore) { /* no-op */ }
|
|
area.remove();
|
|
}
|
|
|
|
function addNostrHelperApps(toolbar) {
|
|
var match = location.href.match(/^nostr:\/\/([^/?#]+)/i);
|
|
if (!match || match[1] === 'helper-apps') return;
|
|
|
|
var entity = decodeURIComponent(match[1]);
|
|
fetch('nostr://' + encodeURIComponent(entity) + '/helper-apps')
|
|
.then(function (response) {
|
|
if (!response.ok) throw new Error('Unable to load Nostr helper apps');
|
|
return response.json();
|
|
})
|
|
.then(function (apps) {
|
|
if (!Array.isArray(apps) || !apps.length) return;
|
|
var group = element('span', 'json-viewer__helper-apps');
|
|
group.appendChild(element('span', 'json-viewer__helper-label', 'Open in:'));
|
|
apps.forEach(function (app) {
|
|
if (!app || typeof app.name !== 'string' || typeof app.url_template !== 'string') return;
|
|
var url = app.url_template.split('{entity}').join(entity);
|
|
var link = element('a', 'json-viewer__button json-viewer__helper-link', app.name);
|
|
link.href = url;
|
|
link.target = '_blank';
|
|
link.rel = 'noopener noreferrer';
|
|
link.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
window.open(url, '_blank', 'noopener');
|
|
});
|
|
group.appendChild(link);
|
|
});
|
|
if (group.children.length > 1) toolbar.appendChild(group);
|
|
})
|
|
.catch(function () { /* Keep the standard JSON toolbar on endpoint failure. */ });
|
|
}
|
|
|
|
var source = document.querySelector('body > pre') || document.querySelector('pre');
|
|
if (!source) return;
|
|
|
|
var raw = source.textContent;
|
|
var data;
|
|
try {
|
|
data = JSON.parse(raw);
|
|
} catch (error) {
|
|
return;
|
|
}
|
|
|
|
var style = element('style');
|
|
style.id = 'sovereign-json-viewer-styles';
|
|
style.textContent = CSS;
|
|
(document.head || document.documentElement).appendChild(style);
|
|
|
|
var viewer = element('main', 'json-viewer');
|
|
var toolbar = element('div', 'json-viewer__toolbar');
|
|
toolbar.appendChild(element('span', 'json-viewer__title', 'JSON Viewer'));
|
|
|
|
var copyButton = element('button', 'json-viewer__button', 'Copy JSON');
|
|
copyButton.type = 'button';
|
|
copyButton.addEventListener('click', function () { copyText(raw, copyButton); });
|
|
toolbar.appendChild(copyButton);
|
|
|
|
var collapseButton = element('button', 'json-viewer__button', 'Collapse all');
|
|
collapseButton.type = 'button';
|
|
toolbar.appendChild(collapseButton);
|
|
|
|
var expandButton = element('button', 'json-viewer__button', 'Expand all');
|
|
expandButton.type = 'button';
|
|
toolbar.appendChild(expandButton);
|
|
|
|
addNostrHelperApps(toolbar);
|
|
|
|
var search = element('input', 'json-viewer__search');
|
|
search.type = 'search';
|
|
search.placeholder = 'Filter keys…';
|
|
search.setAttribute('aria-label', 'Filter JSON keys');
|
|
toolbar.appendChild(search);
|
|
|
|
var status = element('span', 'json-viewer__status');
|
|
status.setAttribute('aria-live', 'polite');
|
|
toolbar.appendChild(status);
|
|
viewer.appendChild(toolbar);
|
|
|
|
var tree = element('div', 'json-viewer__tree');
|
|
tree.setAttribute('role', 'tree');
|
|
tree.appendChild(buildNode(data, null, ''));
|
|
viewer.appendChild(tree);
|
|
|
|
collapseButton.addEventListener('click', function () { setAllCollapsed(tree, true); });
|
|
expandButton.addEventListener('click', function () { setAllCollapsed(tree, false); });
|
|
|
|
search.addEventListener('input', function () {
|
|
var query = search.value.trim().toLocaleLowerCase();
|
|
var nodes = Array.prototype.slice.call(tree.querySelectorAll('.json-viewer__node'));
|
|
var matches = 0;
|
|
nodes.forEach(function (node) {
|
|
node.classList.remove('is-filtered-out', 'is-match');
|
|
});
|
|
if (!query) {
|
|
status.textContent = '';
|
|
return;
|
|
}
|
|
nodes.forEach(function (node) {
|
|
var directMatch = node.dataset.key.indexOf(query) !== -1;
|
|
node.classList.toggle('is-match', directMatch);
|
|
if (directMatch) {
|
|
matches += 1;
|
|
var parent = node.parentElement;
|
|
while (parent && parent !== tree) {
|
|
if (parent.classList.contains('json-viewer__node')) parent.classList.remove('is-collapsed');
|
|
parent = parent.parentElement;
|
|
}
|
|
}
|
|
});
|
|
nodes.forEach(function (node) {
|
|
var visible = node.classList.contains('is-match') || !!node.querySelector('.json-viewer__node.is-match');
|
|
node.classList.toggle('is-filtered-out', !visible);
|
|
});
|
|
status.textContent = matches + (matches === 1 ? ' matching key' : ' matching keys');
|
|
});
|
|
|
|
document.body.className = 'json-viewer-page';
|
|
document.body.replaceChildren(viewer);
|
|
})();
|