-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.ts
156 lines (131 loc) · 4.36 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
type Options = {
/**
* A delimiter to split a BEM element. Defaults to `"__"`.
*/
elementDelimiter: string;
/**
* A delimiter to split a BEM modifier. Defaults to `"--"`.
*/
modifierDelimiter: string;
/**
* A namespace to prepend a BEM block. Defaults to `""`.
*/
namespace: string | string[];
/**
* A delimiter to split a namespace. Defaults to `"-"`.
*/
namespaceDelimiter: string;
/**
* A flag to force a BEM convention. Defaults to `true`.
*/
strict: boolean;
};
type PartialOptions = Partial<Options>;
const defaultOptions: Options = {
elementDelimiter: "__",
modifierDelimiter: "--",
namespace: "",
namespaceDelimiter: "-",
strict: true,
};
/**
* Set up the default options.
*
* @param options - Options to control a generated class name.
*/
export function setup({
elementDelimiter,
modifierDelimiter,
namespace,
namespaceDelimiter,
strict,
}: PartialOptions): void {
if (typeof elementDelimiter === "string") {
defaultOptions.elementDelimiter = elementDelimiter;
}
if (typeof modifierDelimiter === "string") {
defaultOptions.modifierDelimiter = modifierDelimiter;
}
if (typeof namespace === "string" || Array.isArray(namespace)) {
defaultOptions.namespace = namespace;
}
if (typeof namespaceDelimiter === "string") {
defaultOptions.namespaceDelimiter = namespaceDelimiter;
}
if (typeof strict === "boolean") {
defaultOptions.strict = strict;
}
}
/**
* BEM modifiers.
*/
type Modifiers = Record<string, boolean | null | undefined> | Array<string | null | undefined>;
/**
* A function to generate a BEM class name.
*
* @param elementOrModifiers - A BEM element or modifiers.
* @param modifiers - BEM modifiers.
* @returns A generated class name.
*/
type BemBlockFunction = (elementOrModifiers?: string | Modifiers, modifiers?: Modifiers) => string;
const uniqueChars = (list: string[]): string[] =>
list
.join("")
.split("")
.filter((value, index, self) => self.indexOf(value) === index);
const includesChars = (str: string, chars: string[]): boolean =>
chars.some((char) => str.includes(char));
const invalidMessage = (subject: string, subjectValue: string, delimiters: string[]): string => {
const delims = `"${delimiters.join('", "')}"`;
return `The ${subject} ("${subjectValue}") must not use the characters contained within the delimiters (${delims}).`;
};
/**
* Creates a function to generate a BEM class name.
*
* @param block - A BEM block name.
* @param options - Options to control a generated class name.
* @returns A function to generate a BEM class name.
*/
export default function bem(block: string, options: PartialOptions = {}): BemBlockFunction {
const { elementDelimiter, modifierDelimiter, namespace, namespaceDelimiter, strict } = {
...defaultOptions,
...options,
};
const namespaces = ([] as string[])
.concat(namespace)
.filter(Boolean) // compact
.reduce((joined, ns) => `${joined}${ns}${namespaceDelimiter}`, "");
const namespaceBlock = `${namespaces}${block}`;
const delimiters = strict ? [namespaceDelimiter, elementDelimiter, modifierDelimiter] : [];
const delimiterChars = strict ? uniqueChars(delimiters) : [];
return function bemBlock(elementOrModifiers, modifiers) {
if (elementOrModifiers == null) {
return namespaceBlock;
}
const element = typeof elementOrModifiers === "string" ? elementOrModifiers : null;
if (strict && element != null && includesChars(element, delimiterChars)) {
throw new Error(invalidMessage("element", element, delimiters));
}
const base =
element == null ? namespaceBlock : `${namespaceBlock}${elementDelimiter}${element}`;
const mods = typeof elementOrModifiers === "string" ? modifiers : elementOrModifiers;
if (mods == null) {
return base;
}
const addModifiers = (className: string, modifier: string | null | undefined): string => {
if (modifier != null && modifier !== "") {
if (strict && includesChars(modifier, delimiterChars)) {
throw new Error(invalidMessage("modifier", modifier, delimiters));
}
return `${className} ${base}${modifierDelimiter}${modifier}`;
}
return className;
};
if (Array.isArray(mods)) {
return mods.reduce(addModifiers, base);
}
return Object.keys(mods)
.filter((mod) => mods[mod])
.reduce(addModifiers, base);
};
}