estools/esquery

Feature Request: Add a method for converting a selector object to a string

StyleShit opened this issue · 1 comments

Currently, the parsing is a one-way ticket, you can take a string and convert it to a selector object.
I propose that we'll have a way to go the other way around - taking a selector and converting it to a string.

Roughly something like this:

const filter = 'MethodDefinition[value.typeParameters]';
const selector = esquery.parse(filter);
const unparsed = esquery.unparse(selector);

filter === unparsed; // true

(not sure about the name though)

I'm willing to send a PR for this if this is something that you think can be done properly by someone who's unfamiliar with the codebase (yet 😄)

auvred commented

It's actually not that hard

const esquery = require("esquery");

function stringify(selector) {
  switch (selector.type) {
    case "wildcard":
      return "*";
    case "identifier":
      return selector.value;
    case "field":
      return "." + selector.name.split(".");
    case "matches":
      return ":matches(" + selector.selectors.map(stringify).join(", ") + ")";
    case "compound":
      return selector.selectors.map(stringify).join("");
    case "not":
      return ":not(" + selector.selectors.map(stringify).join(", ") + ")";
    case "has":
      return ":has(" + selector.selectors.map(stringify).join(", ") + ")";
    case "child":
      return stringify(selector.left) + " > " + stringify(selector.right);
    case "descendant":
      return stringify(selector.left) + " " + stringify(selector.right);
    case "attribute": {
      const parts = ["[", selector.name];
      if (selector.operator) {
        parts.push(selector.operator);
        if (selector.value.type === "regexp") {
          parts.push(selector.value.value.toString());
        } else if (selector.value.type === "literal") {
          parts.push(`${selector.value.value}`);
        } else if (selector.value.type === "type") {
          parts.push("type(" + selector.value.value + ")");
        }
      }
      return [...parts, "]"].join("");
    }
    case "sibling":
      return stringify(selector.left) + " ~ " + stringify(selector.right);
    case "adjacent":
      return stringify(selector.left) + " + " + stringify(selector.right);
    case "nth-child":
      return ":nth-child(" + selector.index.value + ")";
    case "nth-last-child":
      return ":nth-last-child(" + selector.index.value + ")";
    case "class":
      return ":" + selector.name;
  }
}

console.log(stringify(esquery.parse("aaa.bbb[ccc=111]:statement > eee:matches(* ~ ttt + ddd :not(ee, yy[a])), *:first-child")));
// :matches(aaa.bbb[ccc=111]:statement > eee:matches(* ~ ttt + ddd :not(ee, yy[a])), *:nth-child(1))

But it will replace top level matches with :matches and first-child/last-child with :nth-child(...). Also it will remove all extra spaces. (limitations of AST-based codegen)