From 50866bb5fda5db9f06181ddcc968b9a83cf1b581 Mon Sep 17 00:00:00 2001 From: seveibar Date: Thu, 11 Jul 2024 14:30:10 -0700 Subject: [PATCH] add applySelector to soup-util --- index.ts | 1 + lib/apply-selector.ts | 107 ++++++++++++++++++ ...nvert-abbreviation-to-soup-element-type.ts | 11 ++ package-lock.json | 13 ++- package.json | 3 + 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 lib/apply-selector.ts create mode 100644 lib/convert-abbreviation-to-soup-element-type.ts diff --git a/index.ts b/index.ts index 9309ce4..900825d 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ export * from "./lib/su" export * from "./lib/transform-soup-elements" export * from "./lib/direction-to-vec" +export * from "./lib/apply-selector" export { default as su } from "./lib/su" diff --git a/lib/apply-selector.ts b/lib/apply-selector.ts new file mode 100644 index 0000000..4e33fba --- /dev/null +++ b/lib/apply-selector.ts @@ -0,0 +1,107 @@ +import * as parsel from "parsel-js" +import { convertAbbrToType } from "./convert-abbreviation-to-soup-element-type" +import type { AnySoupElement } from "@tscircuit/soup" + +const filterByType = ( + elements: AnySoupElement[], + type: string +): AnySoupElement[] => { + type = convertAbbrToType(type) + return elements.filter( + (elm) => ("ftype" in elm && elm.ftype === type) || elm.type === type + ) +} + +/** + * Filter elements to match the selector, e.g. to access the left port of a + * resistor you can do ".R1 > port.left" + */ +export const applySelector = ( + elements: AnySoupElement[], + selectorRaw: string +): AnySoupElement[] => { + const selectorAST = parsel.parse(selectorRaw) + return applySelectorAST(elements, selectorAST!) +} + +const doesElmMatchClassName = (elm: AnySoupElement, className: string) => + ("name" in elm && elm.name === className) || + ("port_hints" in elm && elm.port_hints?.includes(className)) + +export const applySelectorAST = ( + elements: AnySoupElement[], + selectorAST: parsel.AST +): AnySoupElement[] => { + switch (selectorAST.type) { + case "complex": { + switch (selectorAST.combinator) { + case " ": // TODO technically should do a deep search + case ">": { + const { left, right } = selectorAST + if (left.type === "class" || left.type === "type") { + // TODO should also check if content matches any element tags + let matchElms: AnySoupElement[] + if (left.type === "class") { + matchElms = elements.filter((elm) => + doesElmMatchClassName(elm, left.name) + ) + } else if (left.type === "type") { + matchElms = filterByType(elements, left.name) + } else { + matchElms = [] + } + + const childrenOfMatchingElms = matchElms.flatMap((matchElm) => + elements.filter( + (elm: any) => + elm[`${matchElm.type}_id`] === + (matchElm as any)[`${matchElm.type}_id`] && elm !== matchElm + ) + ) + return applySelectorAST(childrenOfMatchingElms, right) + } else { + throw new Error(`unsupported selector type "${left.type}" `) + } + } + default: { + throw new Error( + `Couldn't apply selector AST for complex combinator "${selectorAST.combinator}"` + ) + } + } + return [] + } + case "compound": { + const conditionsToMatch = selectorAST.list.map((part) => { + switch (part.type) { + case "class": { + return (elm: any) => doesElmMatchClassName(elm, part.name) + } + case "type": { + const name = convertAbbrToType(part.name) + return (elm: any) => elm.type === name + } + } + }) + + return elements.filter((elm) => + conditionsToMatch.every((condFn) => condFn?.(elm)) + ) + } + case "type": { + return filterByType(elements, selectorAST.name) as AnySoupElement[] + } + case "class": { + return elements.filter((elm) => + doesElmMatchClassName(elm, selectorAST.name) + ) + } + default: { + throw new Error( + `Couldn't apply selector AST for type: "${ + selectorAST.type + }" ${JSON.stringify(selectorAST, null, " ")}` + ) + } + } +} diff --git a/lib/convert-abbreviation-to-soup-element-type.ts b/lib/convert-abbreviation-to-soup-element-type.ts new file mode 100644 index 0000000..cfa6c5f --- /dev/null +++ b/lib/convert-abbreviation-to-soup-element-type.ts @@ -0,0 +1,11 @@ +export const convertAbbrToType = (abbr: string): string => { + switch (abbr) { + case "port": + return "source_port" + case "net": + return "source_net" + case "power": + return "simple_power_source" + } + return abbr +} diff --git a/package-lock.json b/package-lock.json index 2a94148..5c4658f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { "name": "@tscircuit/soup-util", - "version": "0.0.11", + "version": "0.0.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tscircuit/soup-util", - "version": "0.0.11", + "version": "0.0.12", "license": "ISC", + "dependencies": { + "parsel-js": "^1.1.2" + }, "devDependencies": { "@tscircuit/soup": "^0.0.39", "ava": "^6.1.2", @@ -2908,6 +2911,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parsel-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.2.tgz", + "integrity": "sha512-D66DG2nKx4Yoq66TMEyCUHlR2STGqO7vsBrX7tgyS9cfQyO6XD5JyzOiflwmWN6a4wbUAqpmHqmrxlTQVGZcbA==", + "license": "MIT" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", diff --git a/package.json b/package.json index 311bb1e..c43fcc2 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,8 @@ "tsup": "^8.0.2", "typescript": "^5.4.5", "zod": "^3.23.6" + }, + "dependencies": { + "parsel-js": "^1.1.2" } }