Skip to content

Commit 0ad1e05

Browse files
authored
feat: disallow elements or modifiers including delimiters (#241)
Existing code (e.g. `b("foo-bar")`) can occur errors. To avoid errors, you can set the option: `strict: false`.
1 parent 43936a8 commit 0ad1e05

File tree

3 files changed

+104
-12
lines changed

3 files changed

+104
-12
lines changed

README.md

+25
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ b("element", ["mod1", null, "mod3"]);
6666
| [`modifierDelimiter`](#modifierdelimiter) | `string` | `"--"` |
6767
| [`namespace`](#namespace) | `string`, `string[]` | `""` |
6868
| [`namespaceDelimiter`](#namespacedelimiter) | `string` | `"-"` |
69+
| [`strict`](#strict) | `boolean` | `true` |
6970

7071
### `elementDelimiter`
7172

@@ -134,6 +135,29 @@ b("element", { mod1: true, mod2: true });
134135
//=> "block__element block__element--mod1 block__element--mod2"
135136
```
136137

138+
### `strict`
139+
140+
When you set `true` to this option, given elements or modifiers are checked.
141+
And if the check fails, then an runtime error is thrown.
142+
143+
For example, when setting `true`, the following code throws an error.
144+
145+
```ts
146+
const b = block("foo", { strict: true });
147+
b("element__");
148+
b({ modifier--: true });
149+
```
150+
151+
When setting `false`, the following code throws no errors.
152+
153+
```ts
154+
const b = block("foo", { strict: false });
155+
b("element__");
156+
//=> foo__element__
157+
b({ modifier_: true });
158+
//=> foo__modifier_
159+
```
160+
137161
### `setup()`
138162

139163
Change default options.
@@ -146,6 +170,7 @@ setup({
146170
modifierDelimiter: "-",
147171
namespace: "ns",
148172
namespaceDelimiter: "---",
173+
strict: false,
149174
});
150175

151176
const b = block("block");

index.ts

+45-11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ interface Options {
33
modifierDelimiter: string;
44
namespace: string | string[];
55
namespaceDelimiter: string;
6+
strict: boolean;
67
}
78

89
type PartialOptions = Partial<Options>;
@@ -12,13 +13,15 @@ const defaultOptions: Options = {
1213
modifierDelimiter: "--",
1314
namespace: "",
1415
namespaceDelimiter: "-",
16+
strict: true,
1517
};
1618

1719
export function setup({
1820
elementDelimiter,
1921
modifierDelimiter,
2022
namespace,
2123
namespaceDelimiter,
24+
strict,
2225
}: PartialOptions): void {
2326
if (elementDelimiter) {
2427
defaultOptions.elementDelimiter = elementDelimiter;
@@ -32,6 +35,9 @@ export function setup({
3235
if (namespaceDelimiter) {
3336
defaultOptions.namespaceDelimiter = namespaceDelimiter;
3437
}
38+
if (typeof strict === "boolean") {
39+
defaultOptions.strict = strict;
40+
}
3541
}
3642

3743
type Modifiers =
@@ -42,8 +48,22 @@ type Modifiers =
4248

4349
type BemBlockFunction = (elementOrModifiers?: string | Modifiers, modifiers?: Modifiers) => string;
4450

51+
const uniqueChars = (list: string[]): string[] =>
52+
list
53+
.join("")
54+
.split("")
55+
.filter((value, index, self) => self.indexOf(value) === index);
56+
57+
const includesChars = (str: string, chars: string[]): boolean =>
58+
chars.some(char => str.includes(char));
59+
60+
const invalidMessage = (subject: string, subjectValue: string, delimiters: string[]): string => {
61+
const delims = `"${delimiters.join('", "')}"`;
62+
return `The ${subject} ("${subjectValue}") must not use the characters contained within the delimiters (${delims}).`;
63+
};
64+
4565
export default function bem(block: string, options: PartialOptions = {}): BemBlockFunction {
46-
const { elementDelimiter, modifierDelimiter, namespace, namespaceDelimiter } = {
66+
const { elementDelimiter, modifierDelimiter, namespace, namespaceDelimiter, strict } = {
4767
...defaultOptions,
4868
...options,
4969
};
@@ -53,32 +73,46 @@ export default function bem(block: string, options: PartialOptions = {}): BemBlo
5373
.filter(Boolean) // compact
5474
.reduce((joined, ns) => joined + `${ns}${namespaceDelimiter}`, "");
5575

56-
const baseBlock = `${namespaces}${block}`;
76+
const namespaceBlock = `${namespaces}${block}`;
77+
78+
const delimiters = strict ? [namespaceDelimiter, elementDelimiter, modifierDelimiter] : [];
79+
const delimiterChars = strict ? uniqueChars(delimiters) : [];
5780

5881
return function bemBlock(elementOrModifiers, modifiers) {
5982
if (!elementOrModifiers) {
60-
return baseBlock;
83+
return namespaceBlock;
6184
}
6285

63-
const base =
64-
typeof elementOrModifiers === "string"
65-
? `${baseBlock}${elementDelimiter}${elementOrModifiers}`
66-
: baseBlock;
86+
const element = typeof elementOrModifiers === "string" ? elementOrModifiers : null;
87+
88+
if (strict && element && includesChars(element, delimiterChars)) {
89+
throw new Error(invalidMessage("element", element, delimiters));
90+
}
91+
92+
const base = element ? `${namespaceBlock}${elementDelimiter}${element}` : namespaceBlock;
93+
6794
const mods = typeof elementOrModifiers === "string" ? modifiers : elementOrModifiers;
6895

6996
if (!mods) {
7097
return base;
7198
}
7299

73-
const reducer = (result: string, modifier: string | null | undefined): string =>
74-
modifier ? `${result} ${base}${modifierDelimiter}${modifier}` : result;
100+
const addModifiers = (className: string, modifier: string | null | undefined): string => {
101+
if (modifier) {
102+
if (strict && includesChars(modifier, delimiterChars)) {
103+
throw new Error(invalidMessage("modifier", modifier, delimiters));
104+
}
105+
return `${className} ${base}${modifierDelimiter}${modifier}`;
106+
}
107+
return className;
108+
};
75109

76110
if (Array.isArray(mods)) {
77-
return mods.reduce(reducer, base);
111+
return mods.reduce(addModifiers, base);
78112
}
79113

80114
return Object.keys(mods)
81115
.filter(mod => mods[mod])
82-
.reduce(reducer, base);
116+
.reduce(addModifiers, base);
83117
};
84118
}

test.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const testCases = [
106106
modifierDelimiter: "-",
107107
namespace: "ns",
108108
namespaceDelimiter: "---",
109+
strict: true,
109110
});
110111
return block("block");
111112
},
@@ -185,6 +186,37 @@ testCases.forEach(({ description, tested, expectations }) => {
185186
});
186187
});
187188

189+
test("invalid arguments", t => {
190+
const b = block("invalid", {
191+
namespaceDelimiter: "-",
192+
elementDelimiter: "__",
193+
modifierDelimiter: "--",
194+
});
195+
196+
const expectedError = (subject: string, value: string): RegExp =>
197+
new RegExp(
198+
`^Error: The ${subject} \\("${value}"\\) must not use the characters contained within the delimiters \\("-", "__", "--"\\)\\.$`
199+
);
200+
201+
t.test("element is invalid", assert => {
202+
assert.throws(() => b("element--"), expectedError("element", "element--"));
203+
assert.throws(() => b("element_"), expectedError("element", "element_"));
204+
assert.throws(() => b("---element"), expectedError("element", "---element"));
205+
assert.throws(() => b("-_element"), expectedError("element", "-_element"));
206+
assert.throws(() => b("ele-me_nt"), expectedError("element", "ele-me_nt"));
207+
assert.end();
208+
});
209+
210+
t.test("modifier is invalid", assert => {
211+
assert.throws(() => b(["modifier--"]), expectedError("modifier", "modifier--"));
212+
assert.throws(() => b(["modifier_"]), expectedError("modifier", "modifier_"));
213+
assert.throws(() => b(["---modifier"]), expectedError("modifier", "---modifier"));
214+
assert.throws(() => b(["-_modifier"]), expectedError("modifier", "-_modifier"));
215+
assert.throws(() => b(["mod-ifi_er"]), expectedError("modifier", "mod-ifi_er"));
216+
assert.end();
217+
});
218+
});
219+
188220
// `setup()` test must be at last
189221
test("`setup()` additional case", t => {
190222
t.test("overrides options which was setup", assert => {
@@ -193,8 +225,9 @@ test("`setup()` additional case", t => {
193225
modifierDelimiter: "/",
194226
namespace: "n",
195227
namespaceDelimiter: "=",
228+
strict: false,
196229
});
197-
assert.is(b("element", { mod: true }), "n=block:element n=block:element/mod");
230+
assert.is(b("element:", { mod: true }), "n=block:element: n=block:element:/mod");
198231
assert.end();
199232
});
200233

0 commit comments

Comments
 (0)