Skip to content

Commit

Permalink
Merge pull request #41 from tone-row/operate-on-edges
Browse files Browse the repository at this point in the history
operate on edges
  • Loading branch information
rob-gordon committed May 8, 2023
2 parents dc4a75c + 38f8d85 commit d24e1da
Show file tree
Hide file tree
Showing 13 changed files with 1,729 additions and 1,415 deletions.
11 changes: 7 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ jobs:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16.17.0
- uses: pnpm/action-setup@v2.2.2
with:
version: 7.11.0
- name: Install Dependencies
run: pnpm install
version: 8.4.0
run_install: true
- name: Lint
run: pnpm -F graph-selector lint:ci
- name: Run Tests
Expand Down
5 changes: 2 additions & 3 deletions graph-selector/src/getFeatureData.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { featuresRe } from "./regexps";
import { Data, Descriptor } from "./types";

// TODO: not sure if getFeatureData still needs to remove the features from the line and return what's left of the line
export function getFeatureData(_line: string) {
let line = _line.slice(0).trim();
const re =
/(?<replace>(?<id>#[\w-]+)?(?<classes>(\.[a-zA-Z]{1}[\w-]*)*)?(?<attributes>(\[[^\]=]+(?<attributeValue>=['"]?[^'"]+['"]?)?\])*))/g;
let match: RegExpExecArray | null;
let id = "";
let classes = "";
let attributes = "";

while ((match = re.exec(line)) != null) {
while ((match = featuresRe.exec(line)) != null) {
if (!match.groups) continue;
if (!match.groups.replace) break;

Expand Down
103 changes: 103 additions & 0 deletions graph-selector/src/operate/addClassesToEdge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { addClassesToEdge } from "./addClassesToEdge";

describe("addClassesToEdge", () => {
it("should add class to edge", () => {
expect(addClassesToEdge({ line: " edge: foo", classNames: ["bar"] })).toBe(" .bar edge: foo");
});

it("should work if line starts container", () => {
expect(addClassesToEdge({ line: " edge: foo {", classNames: ["bar"] })).toBe(
" .bar edge: foo {",
);
});

it("should work when an ID is present", () => {
expect(addClassesToEdge({ line: " #baz edge: foo", classNames: ["bar"] })).toBe(
" #baz.bar edge: foo",
);
});

it("should work when edge has no label", () => {
expect(addClassesToEdge({ line: " #baz: node", classNames: ["bar"] })).toBe(
" #baz.bar: node",
);
});

it("should work when an ID is present and line starts container", () => {
expect(addClassesToEdge({ line: " #baz edge: foo {", classNames: ["bar"] })).toBe(
" #baz.bar edge: foo {",
);
});

it("should work when the line contains attributes only", () => {
expect(addClassesToEdge({ line: " [foo=bar] edge: foo", classNames: ["bar"] })).toBe(
' .bar[foo="bar"] edge: foo',
);
});

it("should work when the line contains attributes only and line starts container", () => {
expect(addClassesToEdge({ line: " [foo=bar]: foo {", classNames: ["bar"] })).toBe(
' .bar[foo="bar"]: foo {',
);
});

it("should work when the line contains attributes and an ID", () => {
expect(addClassesToEdge({ line: " #baz[foo=bar] edge: foo", classNames: ["bar"] })).toBe(
' #baz.bar[foo="bar"] edge: foo',
);
});

it("should work when the line contains attributes and an ID and line starts container", () => {
expect(addClassesToEdge({ line: " #baz[foo=bar]: foo {", classNames: ["bar"] })).toBe(
' #baz.bar[foo="bar"]: foo {',
);
});

it("should work when the line contains classes only", () => {
expect(addClassesToEdge({ line: " .baz edge: foo", classNames: ["bar"] })).toBe(
" .baz.bar edge: foo",
);
});

it("should work when the line contains classes only and line starts container", () => {
expect(addClassesToEdge({ line: " .baz: foo {", classNames: ["bar"] })).toBe(
" .baz.bar: foo {",
);
});

it("should work when the line contains classes and an ID", () => {
expect(addClassesToEdge({ line: " #baz.baz edge: foo", classNames: ["bar"] })).toBe(
" #baz.baz.bar edge: foo",
);
});

it("should work when the line contains classes and an ID and line starts container", () => {
expect(addClassesToEdge({ line: " #baz.baz: foo {", classNames: ["bar"] })).toBe(
" #baz.baz.bar: foo {",
);
});

it("should work when the line contains classes and attributes", () => {
expect(addClassesToEdge({ line: " .baz[foo=bar] edge: foo", classNames: ["bar"] })).toBe(
' .baz.bar[foo="bar"] edge: foo',
);
});

it("should work when the line contains classes and attributes and line starts container", () => {
expect(addClassesToEdge({ line: " .baz[foo=bar]: foo {", classNames: ["bar"] })).toBe(
' .baz.bar[foo="bar"]: foo {',
);
});

it("should work with multiple classes", () => {
expect(addClassesToEdge({ line: " .baz.baz edge: foo", classNames: ["bar", "foo"] })).toBe(
" .baz.baz.bar.foo edge: foo",
);
});

it("should not add class if class already present", () => {
expect(addClassesToEdge({ line: " .bar edge: foo", classNames: ["bar"] })).toBe(
" .bar edge: foo",
);
});
});
57 changes: 57 additions & 0 deletions graph-selector/src/operate/addClassesToEdge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { featuresRe, getEdgeBreakIndex, getFeaturesIndex } from "../regexps";

import { dataToString } from "./dataToString";
import { getFeatureData } from "../getFeatureData";

export function addClassesToEdge({ line, classNames }: { line: string; classNames: string[] }) {
// remove initial indent
const indent = line.match(/^\s*/)?.[0] || "";
line = line.replace(/^\s*/, "");

// remove container start ("{" as last character)
let containerStart = "";
if (line.endsWith(" {")) {
containerStart = " {";
line = line.slice(0, -2);
}

// pop off edge
let edge = "";
const edgeBreakIndex = getEdgeBreakIndex(line);
if (edgeBreakIndex !== -1) {
edge = line.slice(0, edgeBreakIndex + 1);
line = line.slice(edgeBreakIndex + 1);
}

// separate features and label
const startOfFeatures = getFeaturesIndex(edge);
let features = "";
if (startOfFeatures === 0) {
features = featuresRe.exec(edge)?.[0] || "";
edge = edge.slice(features.length);

// reset regex
featuresRe.lastIndex = 0;
}

if (!features) {
return indent + `.${classNames.join(".")} ` + edge.trim() + line + containerStart;
}

// extract features from string
const { classes, data, id = "" } = getFeatureData(features);

let newFeatureString = " ";
if (id) newFeatureString += `#${id}`;
if (classes) newFeatureString += classes;
let classNameString = "";
// for each class in classNames, if it's not already in classes, add it to the new class string
for (const className of classNames)
if (!classes.includes(className)) classNameString += `.${className}`;
newFeatureString += classNameString;
if (Object.keys(data).length) newFeatureString += dataToString(data);

const edgeWithFeatures = [newFeatureString.trim(), edge.trim()].filter(Boolean).join(" ").trim();

return indent + edgeWithFeatures + line + containerStart;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { addClassesToNode } from "./addClassToNode";
import { addClassesToNode } from "./addClassesToNode";

describe("addClassToNode", () => {
describe("addClassesToNode", () => {
it("should add class to node", () => {
const result = addClassesToNode({
line: " to: my node",
Expand Down
6 changes: 5 additions & 1 deletion graph-selector/src/operate/operate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { addClassesToNode } from "./addClassToNode";
import { addClassesToEdge } from "./addClassesToEdge";
import { addClassesToNode } from "./addClassesToNode";
import { addDataAttributeToNode } from "./addDataAttributeToNode";
import { removeClassesFromEdge } from "./removeClassesFromEdge";
import { removeClassesFromNode } from "./removeClassesFromNode";
import { removeDataAttributeFromNode } from "./removeDataAttributeFromNode";

Expand All @@ -8,6 +10,8 @@ export const operations = {
addClassesToNode,
addDataAttributeToNode,
removeDataAttributeFromNode,
addClassesToEdge,
removeClassesFromEdge,
};

export type OperationKey = keyof typeof operations;
Expand Down
83 changes: 83 additions & 0 deletions graph-selector/src/operate/removeClassesFromEdge.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { removeClassesFromEdge } from "./removeClassesFromEdge";

describe("removeClassesFromEdge", () => {
it("removes a classf rom an edge", () => {
const result = removeClassesFromEdge({
line: ".some-class edge: my node",
classNames: ["some-class"],
});
expect(result).toBe("edge: my node");
});

it("doesn't alter if class isn't present", () => {
const result = removeClassesFromEdge({
line: "edge: my node",
classNames: ["some-class"],
});
expect(result).toBe("edge: my node");
});

it("removes a class from an edge with trailing classes", () => {
const result = removeClassesFromEdge({
line: ".some-class.another-class edge: my node",
classNames: ["some-class"],
});
expect(result).toBe(".another-class edge: my node");
});

it("removes a class from an edge with leading classes", () => {
const result = removeClassesFromEdge({
line: ".some-class.another-class edge: my node",
classNames: ["another-class"],
});
expect(result).toBe(".some-class edge: my node");
});

it("removes a class from an edge with leading and trailing classes", () => {
const result = removeClassesFromEdge({
line: ".some-class.another-class.yet-another-class edge: my node",
classNames: ["another-class"],
});
expect(result).toBe(".some-class.yet-another-class edge: my node");
});

it("removes a class from an edge and doesn't affect indentation", () => {
const result = removeClassesFromEdge({
line: " .some-class edge: my node",
classNames: ["some-class"],
});
expect(result).toBe(" edge: my node");
});

it("removes a class from an edge without affecting the node", () => {
const result = removeClassesFromEdge({
line: " .some-class edge: my node .some-class",
classNames: ["some-class"],
});
expect(result).toBe(" edge: my node .some-class");
});

it("can remove multiple classes at once", () => {
const result = removeClassesFromEdge({
line: ".some-class.another-class edge: my node",
classNames: ["some-class", "another-class"],
});
expect(result).toBe("edge: my node");
});

it("can remove class when no edge label", () => {
const result = removeClassesFromEdge({
line: " .some-class: my node",
classNames: ["some-class"],
});
expect(result).toBe(" my node");
});

it("can remove edge class when starting container line", () => {
const result = removeClassesFromEdge({
line: " .some-class edge: container {",
classNames: ["some-class"],
});
expect(result).toBe(" edge: container {");
});
});
47 changes: 47 additions & 0 deletions graph-selector/src/operate/removeClassesFromEdge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getEdgeBreakIndex } from "../regexps";

export function removeClassesFromEdge({
line,
classNames,
}: {
/** The line text */
line: string;
/** Array of string *without* the dot (.) */
classNames: string[];
}) {
// remove initial indent and store
const indent = line.match(/^\s*/)?.[0];
line = line.replace(/^\s*/, "");

// remove container start ("{" as last character)
let containerStart = "";
if (line.endsWith(" {")) {
containerStart = " {";
line = line.slice(0, -2);
}

// check for unescaped colon that's not at the start of the line
// if it exists, we're dealing with an edge
let edge = "";
const edgeBreakIndex = getEdgeBreakIndex(line);
if (edgeBreakIndex !== -1) {
edge = line.slice(0, edgeBreakIndex + 1);
line = line.slice(edgeBreakIndex + 1);
}

// remove class names from edge
for (const className of classNames) {
edge = edge.replace(new RegExp(`\.${className}`), "");
}

// remove leading whitespace before beginning of line if it exists
edge = edge.replace(/^\s*/, "");

// if the edge is empty and the line begins with a colon,
if (edge === "" && line.startsWith(":")) {
// remove the colon and whitespace
line = line.replace(/^[::]\s*/, "");
}

return indent + edge + line + containerStart;
}
2 changes: 1 addition & 1 deletion graph-selector/src/operate/removeClassesFromNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function removeClassesFromNode({
classNames: string[];
}) {
// remove initial indent and store
const indent = line.match(/^\s*/)?.[0] ?? "";
const indent = line.match(/^\s*/)?.[0];
line = line.replace(/^\s*/, "");

// remove container start ("{" as last character)
Expand Down
7 changes: 7 additions & 0 deletions graph-selector/src/regexps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,10 @@ export const getFeaturesIndex = (line: string) => {
const m = /(^|\s)(#|\.|\[)/.exec(line);
return m?.index ?? line.length;
};

/**
* @description
* This regular expression is used to match the features of a node or edge.
*/
export const featuresRe =
/(?<replace>(?<id>#[\w-]+)?(?<classes>(\.[a-zA-Z]{1}[\w-]*)*)?(?<attributes>(\[[^\]=]+(?<attributeValue>=['"]?[^'"]+['"]?)?\])*))/g;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"devDependencies": {
"husky": "^8.0.0",
"lint-staged": "^13.0.3",
"turbo": "^1.7.0"
"turbo": "^1.9.3"
},
"lint-staged": {
"graph-selector/**/*.{js,ts}": [
Expand Down
Loading

0 comments on commit d24e1da

Please sign in to comment.