@kumu/hydra
Advanced tools
Comparing version 0.0.0-kumu.27 to 0.0.0-kumu.28
{ | ||
"name": "@kumu/hydra", | ||
"version": "0.0.0-kumu.27", | ||
"version": "0.0.0-kumu.28", | ||
"type": "module", | ||
@@ -5,0 +5,0 @@ "types": "./dist/index.d.ts", |
@@ -1277,3 +1277,2 @@ import { Graph } from "./graph"; | ||
* Don't write to this directly, instead use `setGroupLabelOffset`. | ||
* @internal | ||
*/ | ||
@@ -1280,0 +1279,0 @@ readonly labelOffset: Point; |
@@ -20,3 +20,8 @@ /** | ||
export { data, scale, categorize } from "./style"; | ||
export { parseSelector, parseSelectorOrThrow } from "./selectors"; | ||
export { | ||
parseSelector, | ||
parseSelectorOrThrow, | ||
joinSelectors, | ||
SelectorJoin, | ||
} from "./selectors"; | ||
export type { Entity, NodeData, EdgeData, GroupData } from "./entity"; | ||
@@ -23,0 +28,0 @@ |
@@ -8,2 +8,4 @@ import { assert, expect, test, vi } from "vitest"; | ||
walk, | ||
SelectorJoin, | ||
joinSelectors, | ||
} from "./selectors"; | ||
@@ -421,1 +423,113 @@ import { Edge, EdgeData, Entity, Node, NodeData } from "./entity"; | ||
}); | ||
test(":is selector", () => { | ||
let graph = createTestGraph(); | ||
let children = select(graph, ":is(#bob)"); | ||
expect(children).toEqual(["bob"]); | ||
}); | ||
test(":is selectors combined", () => { | ||
let graph = createTestGraph(); | ||
let children = select(graph, ":is(#bob):is([type=person])"); | ||
expect(children).toEqual(["bob"]); | ||
}); | ||
test("joining simple selectors with OR", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.Or, ["#bob", "#cat"]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["bob", "cat"]); | ||
}); | ||
test("joining simple selectors with AND", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.And, [":node", "#cat"]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat"]); | ||
}); | ||
test("joining type selectors with AND", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.And, ["person", "#bob"]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["bob"]); | ||
}); | ||
test("joining type selectors with OR", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.Or, ["animal", "#cat"]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat", "dog", "moose"]); | ||
}); | ||
test("joining list selectors with AND", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.And, ["#cat,toy", "animal"]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat"]); | ||
}); | ||
test("joining list selectors with OR", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.Or, ["#cat,toy", "animal"]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat", "dog", "wool", "moose"]); | ||
}); | ||
test("joining traversal selectors with AND", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.And, [ | ||
"#cat", | ||
"person likes animal", | ||
]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat"]); | ||
}); | ||
test("joining traversal selectors with OR", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.Or, [ | ||
"#cat", | ||
"person < likes < animal", | ||
]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat", "dog"]); | ||
}); | ||
test("joining universal selectors with AND", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.And, ["*", "animal"]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat", "dog", "moose"]); | ||
}); | ||
test("joining universal selectors with OR", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.Or, ["*", "toy"]); | ||
let children = select(graph, selector); | ||
expect(children.length).toEqual(graph.size()); | ||
}); | ||
test("joining mixed selectors with AND", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.And, [ | ||
"animal", | ||
":node", | ||
":not(person)", | ||
"#bob likes *", | ||
"[id!=wool]", | ||
]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["cat", "dog"]); | ||
}); | ||
test("joining mixed selectors with OR", () => { | ||
let graph = createTestGraph(); | ||
let selector = joinSelectors(SelectorJoin.Or, [ | ||
"#cat", | ||
":node:not(animal)", | ||
"#bob < likes < animal", | ||
]); | ||
let children = select(graph, selector); | ||
expect(children).toEqual(["bob", "cat", "dog", "wool"]); | ||
}); |
@@ -102,2 +102,5 @@ import { | ||
return testChildSelector(selector, entity, graph); | ||
case "is": | ||
assert(selector.subtree, ":is selector needs an argument!"); | ||
return testSelector(selector.subtree, entity, graph); | ||
case "from": | ||
@@ -430,1 +433,74 @@ assert(selector.subtree, ":from selector needs an argument!"); | ||
} | ||
export enum SelectorJoin { | ||
And = "and", | ||
Or = "or", | ||
} | ||
/** | ||
* Join multiple selectors together with AND or OR semantics. | ||
* | ||
* For example: | ||
* - `.cat OR .dog` becomes `.cat,.dog` | ||
* - `.cat AND .dog` becomes `.cat.dog` | ||
* | ||
* More complicated selectors such as lists and traversals will be wrapped or | ||
* translated before they're joined to ensure that resulting selector is | ||
* correct. | ||
* | ||
* - `person OR .dev` will become `[type=person].dev` | ||
* - `.a, .b AND .c` will become `:is(.a, .b).c` | ||
* | ||
* @param join The operation to use for joining. | ||
* @param selectors The selectors to join. | ||
* @returns The newly joined selector. | ||
*/ | ||
export function joinSelectors(join: SelectorJoin, selectors: string[]): string { | ||
if (join === SelectorJoin.Or) { | ||
return selectors.join(", "); | ||
} | ||
return selectors | ||
.map((selector) => { | ||
let ast = parseSelector(selector); | ||
// We couldn't parse this selector, so leave it out of the joined one | ||
// otherwise the joined one will also be unparseable. | ||
if (!ast) return ""; | ||
// Combining universal selector with anything is meaningless. | ||
if (selector === "*") { | ||
return ""; | ||
} | ||
// Joining a selector onto the end of a list or a traversal will only | ||
// apply the selector to the final selector in the list. | ||
// | ||
// Example | ||
// #a, .b AND :not(c) | ||
// | ||
// Wrong | ||
// #a, .b:not(c)) | ||
// | ||
// Correct (distribute verbosely) | ||
// #a:not(c), .b:not(c)) | ||
// | ||
// Correct (wrap the list in :is) | ||
// :is(#a, .b):not(c) | ||
if (ast.type === "list" || ast.type === "complex") { | ||
return `:is(${selector})`; | ||
} | ||
// Type selectors don't have a prefix, which means they join badly | ||
// "person" AND "org" becomes "personorg" which is incorrect. | ||
// | ||
// Instead, we'll just convert them into the more explicit attribute | ||
// selector form, which joins neatly. | ||
if (ast.type === "type") { | ||
return `[type=${selector}]`; | ||
} | ||
return selector; | ||
}) | ||
.join(""); | ||
} |
@@ -718,3 +718,3 @@ import { Core } from "./core"; | ||
node.style.padding = scale * resolve(core, style.padding, node.data, parseFloat); | ||
node.style.radius = size / 2 + node.style.padding ?? node.style.radius; | ||
node.style.radius = size / 2 + node.style.padding; | ||
node.style.margin = scale * resolve(core, style.margin, node.data, parseFloat); | ||
@@ -721,0 +721,0 @@ node.style.shape = resolve(core, style.shape, node.data, parseNodeShape) ?? node.style.shape; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
1127479
19549