UNPKG

6.77 kBJavaScriptView Raw
1/**
2 * @module glob
3 */
4
5"use strict";
6
7const fs = require("fs");
8const 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 */
16const 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 */
27const 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 */
103const 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 */
130const 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 */
161const 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 */
177const 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
192module.exports = { test, walk };