molstar
Version:
A comprehensive macromolecular library.
193 lines (192 loc) • 7.66 kB
JavaScript
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { canonicalJsonString } from '../../../../mol-util/json';
import { addParamDefaults } from './params-schema';
import { getParams } from './tree-schema';
/** Run DFS (depth-first search) algorithm on a rooted tree.
* Runs `visit` function when a node is discovered (before visiting any descendants).
* Runs `postVisit` function when leaving a node (after all descendants have been visited). */
export function dfs(root, visit, postVisit) {
return _dfs(root, undefined, visit, postVisit);
}
function _dfs(root, parent, visit, postVisit) {
var _a;
if (visit)
visit(root, parent);
for (const child of (_a = root.children) !== null && _a !== void 0 ? _a : []) {
_dfs(child, root, visit, postVisit);
}
if (postVisit)
postVisit(root, parent);
}
/** Convert a tree into a pretty-printed string. */
export function treeToString(tree) {
let level = 0;
const lines = [];
dfs(tree, node => lines.push(' '.repeat(level++) + nodeToString(node)), node => level--);
return lines.join('\n');
}
function nodeToString(node) {
var _a;
return `- ${node.kind} ${formatObject((_a = node.params) !== null && _a !== void 0 ? _a : {})}${formatCustomProps(node.custom)}${formatRef(node.ref)}`;
}
/** Convert object to a human-friendly string (similar to JSON.stringify but without quoting keys) */
export function formatObject(obj) {
if (!obj)
return 'undefined';
return JSON.stringify(obj).replace(/,("\w+":)/g, ', $1').replace(/"(\w+)":/g, '$1: ');
}
/** Return human-friendly string with node custom properties, if any */
function formatCustomProps(customProps) {
if (!customProps || Object.keys(customProps).length === 0)
return '';
return `, custom: ${formatObject(customProps)}`;
}
/** Return human-friendly string with node ref, if any */
function formatRef(ref) {
if (ref === undefined)
return '';
return `, ref: "${ref}"`;
}
/** Create a copy of a tree node, ignoring children. */
export function copyNodeWithoutChildren(node) {
return {
kind: node.kind,
params: node.params ? { ...node.params } : undefined,
custom: node.custom ? { ...node.custom } : undefined,
ref: node.ref,
};
}
/** Create a copy of a tree node, including a shallow copy of children. */
export function copyNode(node) {
return {
kind: node.kind,
params: node.params ? { ...node.params } : undefined,
custom: node.custom ? { ...node.custom } : undefined,
ref: node.ref,
children: node.children ? [...node.children] : undefined,
};
}
/** Create a deep copy of a tree. */
export function copyTree(root) {
return convertTree(root, {});
}
/** Apply a set of conversion rules to a tree to change to a different schema. */
export function convertTree(root, conversions) {
const mapping = new Map();
let convertedRoot;
dfs(root, (node, parent) => {
var _a, _b;
var _c;
const conversion = conversions[node.kind];
if (conversion) {
const convertidos = conversion(node, parent);
if (!parent && convertidos.length === 0)
throw new Error('Cannot convert root to empty path');
let convParent = parent ? mapping.get(parent) : undefined;
for (const conv of convertidos) {
if (convParent) {
((_a = convParent.children) !== null && _a !== void 0 ? _a : (convParent.children = [])).push(conv);
}
else {
convertedRoot = conv;
}
convParent = conv;
}
mapping.set(node, convParent);
}
else {
const converted = copyNodeWithoutChildren(node);
if (parent) {
((_b = (_c = mapping.get(parent)).children) !== null && _b !== void 0 ? _b : (_c.children = [])).push(converted);
}
else {
convertedRoot = converted;
}
mapping.set(node, converted);
}
});
return convertedRoot;
}
/** Create a copy of the tree where twins (siblings of the same kind with the same params) are merged into one node.
* Applies only to the node kinds listed in `condenseNodes` (or all if undefined) except node kinds in `skipNodes`. */
export function condenseTree(root, condenseNodes, skipNodes) {
const map = new Map();
const result = copyTree(root);
dfs(result, node => {
var _a, _b, _c;
map.clear();
const newChildren = [];
for (const child of (_a = node.children) !== null && _a !== void 0 ? _a : []) {
let twin = undefined;
const doApply = (!condenseNodes || condenseNodes.has(child.kind)) && !(skipNodes === null || skipNodes === void 0 ? void 0 : skipNodes.has(child.kind));
if (doApply) {
const key = child.kind + canonicalJsonString(getParams(child));
twin = map.get(key);
if (!twin)
map.set(key, child);
}
if (twin) {
((_b = twin.children) !== null && _b !== void 0 ? _b : (twin.children = [])).push(...(_c = child.children) !== null && _c !== void 0 ? _c : []);
}
else {
newChildren.push(child);
}
}
node.children = newChildren;
});
return result;
}
/** Create a copy of the tree where missing optional params for each node are added based on `defaults`. */
export function addDefaults(tree, treeSchema) {
const rules = {};
for (const kind in treeSchema.nodes) {
rules[kind] = node => [{
kind: node.kind,
params: addParamDefaults(treeSchema.nodes[kind].params, node.params),
custom: node.custom,
ref: node.ref,
}];
}
return convertTree(tree, rules);
}
/** Resolve any URI params in a tree, in place. URI params are those listed in `uriParamNames`.
* Relative URIs are treated as relative to `baseUri`, which can in turn be relative to the window URL (if available). */
export function resolveUris(tree, baseUri, uriParamNames) {
dfs(tree, node => {
const params = node.params;
if (!params)
return;
for (const name in params) {
if (uriParamNames.includes(name)) {
const uri = params[name];
if (typeof uri === 'string') {
params[name] = resolveUri(uri, baseUri, windowUrl());
}
}
}
});
}
/** Resolve a sequence of URI references (relative URIs), where each reference is either absolute or relative to the next one
* (i.e. the last one is the base URI). Skip any `undefined`.
* E.g. `resolveUri('./unexpected.png', '/spanish/inquisition/expectations.html', 'https://example.org/spam/spam/spam')`
* returns `'https://example.org/spanish/inquisition/unexpected.png'`. */
function resolveUri(...refs) {
let result = undefined;
for (const ref of refs.reverse()) {
if (ref !== undefined) {
if (result === undefined)
result = ref;
else
result = new URL(ref, result).href;
}
}
return result;
}
/** Return URL of the current page when running in a browser; `undefined` when running in Node. */
function windowUrl() {
return (typeof window !== 'undefined') ? window.location.href : undefined;
}