1 | /**
|
2 | * @module glob
|
3 | */
|
4 |
|
5 | ;
|
6 |
|
7 | const fs = require("fs");
|
8 | const path = require("path");
|
9 |
|
10 | /**
|
11 | * Protège les caractères spéciaux des expressions rationnelles.
|
12 | *
|
13 | * @param {string} pattern Les caractères.
|
14 | * @returns {string} Les caractères protégés.
|
15 | */
|
16 | const sanitize = function (pattern) {
|
17 | return pattern.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
18 | };
|
19 |
|
20 | /**
|
21 | * Transforme un patron en expression rationnelle.
|
22 | *
|
23 | * @param {string} pattern Le patron.
|
24 | * @returns {object.<string, *>} La marque pour la négation et l'expression
|
25 | * rationnelle.
|
26 | */
|
27 | const compile = function (pattern) {
|
28 | const negate = pattern.startsWith("!");
|
29 | const glob = pattern.replace(/^!/u, "");
|
30 | let regexp = glob.startsWith("/") ? "^"
|
31 | : "^(.*/)?";
|
32 |
|
33 | for (let i = 0; i < glob.length; ++i) {
|
34 | if ("/" === glob[i]) {
|
35 | if ("/**/" === glob.substring(i, i + 4)) {
|
36 | regexp += "/(.*/)?";
|
37 | i += 3;
|
38 | } else if ("/**" === glob.substring(i, i + 3)) {
|
39 | if (glob.length === i + 3) {
|
40 | regexp += "/.*";
|
41 | i += 2;
|
42 | } else {
|
43 | throw new Error(pattern + ": '**' not followed by a" +
|
44 | " slash.");
|
45 | }
|
46 | } else {
|
47 | regexp += "/";
|
48 | }
|
49 | } else if ("*" === glob[i]) {
|
50 | // Si c'est le dernier caractère ou qu'il n'est pas suivi par une
|
51 | // étoile.
|
52 | if (glob.length === i + 1 || "*" !== glob[i + 1]) {
|
53 | regexp += "[^/]*";
|
54 | // Sinon : ce n'est pas le dernier caractère et le prochain est
|
55 | // une étoile.
|
56 | } else if (0 === i) {
|
57 | regexp += ".*";
|
58 | ++i;
|
59 | } else {
|
60 | throw new Error(pattern + ": '**' not preceded by a slash.");
|
61 | }
|
62 | } else if ("?" === glob[i]) {
|
63 | regexp += "[^/]";
|
64 | } else if ("[" === glob[i]) {
|
65 | const closing = glob.indexOf("]", i);
|
66 | if (-1 === closing) {
|
67 | throw new Error(pattern + ": ']' missing.");
|
68 | }
|
69 | regexp += "[" + sanitize(glob.substring(i + 1, closing)) + "]";
|
70 | i = closing;
|
71 | } else {
|
72 | regexp += sanitize(glob[i]);
|
73 | }
|
74 | }
|
75 |
|
76 | if (negate) {
|
77 | if (regexp.endsWith("/")) {
|
78 | regexp += ".*";
|
79 | } else {
|
80 | regexp += "(/.*)?";
|
81 | }
|
82 | } else if (!regexp.endsWith("/")) {
|
83 | regexp += "/?";
|
84 | }
|
85 | regexp += "$";
|
86 |
|
87 | return { negate, "regexp": new RegExp(regexp, "u") };
|
88 | };
|
89 |
|
90 | /**
|
91 | * Teste si un fichier respecte un des patrons.
|
92 | *
|
93 | * @param {string} file L’adresse du fichier qui sera vérifié.
|
94 | * @param {Array.<string>} patterns La liste des patrons.
|
95 | * @param {string} root L’adresse du répertoire où se trouve le
|
96 | * dossier <code>.metalint/</code>.
|
97 | * @param {boolean} directory La marque indiquant si le fichier est un
|
98 | * répertoire.
|
99 | * @returns {string} <code>"MATCHED"</code> si le fichier respecte au moins un
|
100 | * patron ; <code>"NEGATE"</code> si le fichier ne respecte
|
101 | * pas un patron négatif ; sinon <code>"NONE"</code>.
|
102 | */
|
103 | const exec = function (file, patterns, root, directory) {
|
104 | const relative = "/" + path.relative(root, path.join(process.cwd(), file)) +
|
105 | (directory ? "/" : "");
|
106 |
|
107 | let result = "NONE";
|
108 | for (const pattern of patterns) {
|
109 | if (pattern.negate) {
|
110 | if (pattern.regexp.test(relative)) {
|
111 | return "NEGATE";
|
112 | }
|
113 | } else if ("NONE" === result && pattern.regexp.test(relative)) {
|
114 | result = "MATCHED";
|
115 | }
|
116 | }
|
117 | return result;
|
118 | };
|
119 |
|
120 | /**
|
121 | * Récupère toute l’arborescence des fichiers respectant un des patrons.
|
122 | *
|
123 | * @param {string} base Le fichier / répertoire servant de racine
|
124 | * pour l’arborescence.
|
125 | * @param {Array.<string>} patterns La liste des patrons compilés.
|
126 | * @param {string} root L’adresse du répertoire où se trouve le
|
127 | * dossier <code>.metalint/</code>.
|
128 | * @returns {Array.<string>} La liste des fichiers respectant un des patrons.
|
129 | */
|
130 | const deep = function (base, patterns, root) {
|
131 | const directory = fs.lstatSync(base).isDirectory();
|
132 | const result = exec(base, patterns, root, directory);
|
133 | if ("NEGATE" === result) {
|
134 | return [];
|
135 | }
|
136 |
|
137 | const files = [];
|
138 | if ("MATCHED" === result) {
|
139 | files.push(base);
|
140 | }
|
141 | if (directory) {
|
142 | for (const file of fs.readdirSync(base)) {
|
143 | files.push(...deep(path.join(base, file), patterns, root));
|
144 | }
|
145 | }
|
146 | return files;
|
147 | };
|
148 |
|
149 | /**
|
150 | * Vérifie si un fichier respecte un des patrons.
|
151 | *
|
152 | * @param {string} file L’adresse du fichier qui sera vérifié.
|
153 | * @param {Array.<string>} patterns La liste des patrons.
|
154 | * @param {string} root L’adresse du répertoire où se trouve le
|
155 | * dossier <code>.metalint/</code>.
|
156 | * @param {boolean} directory La marque indiquant si le fichier est un
|
157 | * répertoire.
|
158 | * @returns {boolean} <code>true</code> si le fichier respectent au moins un
|
159 | * patron ; sinon <code>false</code>.
|
160 | */
|
161 | const test = function (file, patterns, root, directory) {
|
162 | return "MATCHED" === exec(file, patterns.map(compile), root, directory);
|
163 | };
|
164 |
|
165 | /**
|
166 | * Récupère toute l’arborescence des fichiers respectant un des patrons.
|
167 | *
|
168 | * @param {string} bases La liste des fichiers / répertoires
|
169 | * servant de racine pour
|
170 | * l’arborescence.
|
171 | * @param {(Array.<string>|string)} patterns La liste des patrons.
|
172 | * @param {string} root L’adresse du répertoire où se
|
173 | * trouve le dossier
|
174 | * <code>.metalint/</code>.
|
175 | * @returns {Array.<string>} La liste des fichiers respectant un des patrons.
|
176 | */
|
177 | const walk = function (bases, patterns, root) {
|
178 | const compileds = Array.isArray(patterns) ? patterns.map(compile)
|
179 | : [patterns].map(compile);
|
180 |
|
181 | if (0 === bases.length) {
|
182 | return deep(".", compileds, root);
|
183 | }
|
184 |
|
185 | const files = [];
|
186 | for (const base of bases) {
|
187 | files.push(...deep(base, compileds, root));
|
188 | }
|
189 | return files;
|
190 | };
|
191 |
|
192 | module.exports = { test, walk };
|