Skip to content

Commit

Permalink
Add state-remove Tag (#128)
Browse files Browse the repository at this point in the history
* add a remove state tag

* docs

* cleanup

* update comment

* rename + add tests

* remove async callback

* update docs
  • Loading branch information
biniona-mongodb authored Aug 25, 2022
1 parent 43bc647 commit 375b679
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 22 deletions.
11 changes: 11 additions & 0 deletions docs/docs/reference/tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,17 @@ Produces the following output:
With `state-uncomment`, you can create multiple valid end states but only run
one of those states when executing your source code.

## State Remove

The `state-remove` tag removes the spanned range from
Bluehawk output for specified states.

Use the `state-remove` when you want to include
enclosed text in everything except the states you
specify to the tag.

The `state-remove` tag is a block tag.

## Uncomment

The `uncomment` tag removes a single comment from the beginning of
Expand Down
2 changes: 2 additions & 0 deletions src/bluehawk/getBluehawk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ReplaceTag,
RemoveTag,
StateTag,
StateRemoveTag,
UncommentTag,
EmphasizeTag,
tokens,
Expand Down Expand Up @@ -38,6 +39,7 @@ export const getBluehawk = async (): Promise<Bluehawk> => {
ReplaceTag,
SnippetTag,
StateTag,
StateRemoveTag,
StateUncommentTag,
UncommentTag,
EmphasizeTag,
Expand Down
24 changes: 24 additions & 0 deletions src/bluehawk/tags/StateRemoveTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
makeBlockTag,
IdsRequiredAttributesSchema,
IdsRequiredAttributes,
} from "./Tag";
import { RemoveTag } from "./RemoveTag";
import { conditionalForkWithState } from "./StateTag";

export const StateRemoveTag = makeBlockTag<IdsRequiredAttributes>({
name: "state-remove",
description:
"given a state name(s) as tag ids, identifies blocks that should not appear in the given state's version of the file",
attributesSchema: IdsRequiredAttributesSchema,
shorthandArgsAttributeName: "id",
process(request) {
conditionalForkWithState(request);
const { tagNode, document } = request;
const stateAttribute = document.attributes["state"];
// Strip all matching states
if (tagNode.attributes.id.includes(stateAttribute)) {
RemoveTag.process(request);
}
},
});
53 changes: 31 additions & 22 deletions src/bluehawk/tags/StateTag.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { strict as assert } from "assert";
import {
makeBlockTag,
IdsRequiredAttributes,
IdsRequiredAttributesSchema,
} from "./Tag";
import { RemoveTag } from "./RemoveTag";
import { BlockTagNode } from "../parser";
import { ProcessRequest } from "../processor/Processor";

export const StateTag = makeBlockTag<IdsRequiredAttributes>({
name: "state",
Expand All @@ -13,31 +14,39 @@ export const StateTag = makeBlockTag<IdsRequiredAttributes>({
attributesSchema: IdsRequiredAttributesSchema,
shorthandArgsAttributeName: "id",
process(request) {
const { tagNode, fork, document, tagNodes } = request;

conditionalForkWithState(request);
const { tagNode, document } = request;
const stateAttribute = document.attributes["state"];

if (stateAttribute === undefined) {
// We are not processing in a state file, so start one
tagNode.attributes.id.forEach((id: string) => {
fork({
document,
tagNodes,
newModifiers: {
state: id,
},
newAttributes: {
// Set the state attribute for next time StateTag is invoked on the
// new file
state: id,
},
});
});
}

// Strip all other states
if (!tagNode.attributes.id.includes(stateAttribute)) {
RemoveTag.process(request);
}
},
});

/**
If we are not processing in a state file, fork a file for each
state listed in our tag node.
*/
export const conditionalForkWithState = (
request: ProcessRequest<BlockTagNode>
) => {
const { tagNode, fork, document, tagNodes } = request;
const stateAttribute = document.attributes["state"];
if (stateAttribute === undefined) {
tagNode.attributes.id.forEach((id: string) => {
fork({
document,
tagNodes,
newModifiers: {
state: id,
},
newAttributes: {
// Set the state attribute for next time StateTag is invoked on the
// new file
state: id,
},
});
});
}
};
1 change: 1 addition & 0 deletions src/bluehawk/tags/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./RemoveTag";
export * from "./ReplaceTag";
export * from "./SnippetTag";
export * from "./StateTag";
export * from "./StateRemoveTag";
export * from "./UncommentTag";
export * from "./removeMetaRange";
export * from "./EmphasizeTag";
153 changes: 153 additions & 0 deletions src/bluehawk/tags/stateRemoveTag.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { doesNotReject } from "assert";
import { fail } from "yargs";
import { Bluehawk } from "../bluehawk";
import { Document } from "../Document";
import { StateRemoveTag } from "./StateRemoveTag";
import { StateTag } from "./StateTag";

describe("remove state tag", () => {
const bluehawk = new Bluehawk();
bluehawk.registerTag(StateRemoveTag);
bluehawk.registerTag(StateTag);
bluehawk.addLanguage("js", {
languageId: "javascript",
blockComments: [[/\/\*/, /\*\//]],
lineComments: [/\/\/ ?/],
});

it("strips text", async () => {
const input = `t0
// :state-start: s1 s2
t1
// :state-end:
// :state-remove-start: s1
t2
// :state-remove-end:
`;

const source = new Document({
text: input,
path: "test.js",
});

const parseResult = bluehawk.parse(source);
const files = await bluehawk.process(parseResult);
expect(files["test.js"].document.text.toString()).toBe(`t0
t2
`);
expect(files["test.js?state=s1"].document.text.toString()).toBe(`t0
t1
`);
expect(files["test.js?state=s2"].document.text.toString()).toBe(`t0
t1
t2
`);
});

it("nests", async () => {
const input = `1
// :state-remove-start: s1
2
// :state-remove-start: s1 s2
3
// :state-remove-start: s1 s2 s3
4
// :state-remove-end:
5
// :state-remove-end:
6
// :state-remove-end:
7
`;
const source = new Document({
text: input,
path: "test.js",
});
const parseResult = bluehawk.parse(source);
const files = await bluehawk.process(parseResult);

expect(files["test.js"].document.text.toString()).toBe(`1
2
3
4
5
6
7
`);
expect(files["test.js?state=s1"].document.text.toString()).toBe(`1
7
`);
expect(files["test.js?state=s2"].document.text.toString()).toBe(`1
2
6
7
`);
expect(files["test.js?state=s3"].document.text.toString()).toBe(`1
2
3
5
6
7
`);
});
it("behaves well when mixed with state", async () => {
const input = `a
// :state-remove-start: s0
b
// :state-remove-start: s1
c
// :state-start: s2 s3
d
// :state-start: s3
e
// :state-end:
f
// :state-end:
g
// :state-remove-end:
h
// :state-remove-end:
i
`;
const source = new Document({
text: input,
path: "test.js",
});
const parseResult = bluehawk.parse(source);
const files = await bluehawk.process(parseResult);
expect(files["test.js"].document.text.toString()).toBe(`a
b
c
g
h
i
`);
expect(files["test.js?state=s0"].document.text.toString()).toBe(`a
i
`);
expect(files["test.js?state=s1"].document.text.toString()).toBe(`a
b
h
i
`);
expect(files["test.js?state=s2"].document.text.toString()).toBe(`a
b
c
d
f
g
h
i
`);
expect(files["test.js?state=s3"].document.text.toString()).toBe(`a
b
c
d
e
f
g
h
i
`);
});
});

0 comments on commit 375b679

Please sign in to comment.