@casomoltd/tooling 0.40.0 → 0.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/check-personal-data.mjs +37 -27
- package/package.json +2 -2
|
@@ -66,6 +66,9 @@ const FIELD_SUFFIX = /(?:value|val|text|node|display|content)$/;
|
|
|
66
66
|
/** A key that names a birth. */
|
|
67
67
|
const BIRTH_KEY = /(?:dob|dateofbirth|birthdate|born)$/;
|
|
68
68
|
|
|
69
|
+
/** How many nested field-name layers a lead-in may carry before the key. */
|
|
70
|
+
const MAX_FIELD_SUFFIXES = 4;
|
|
71
|
+
|
|
69
72
|
/** The constructor a source file writes a date with. `new Date(1983, 6, 14)`
|
|
70
73
|
* matches neither the printed nor the ISO rule, and the words before it end
|
|
71
74
|
* `newdate` rather than a field name, so both halves would be blind at once
|
|
@@ -83,7 +86,7 @@ const DATE_CALL = /(?:new\s*)?date\($/;
|
|
|
83
86
|
*/
|
|
84
87
|
function namesABirth(lead) {
|
|
85
88
|
let rest = lead;
|
|
86
|
-
for (let depth = 0; depth <
|
|
89
|
+
for (let depth = 0; depth < MAX_FIELD_SUFFIXES; depth += 1) {
|
|
87
90
|
if (BIRTH_KEY.test(rest)) return true;
|
|
88
91
|
const trimmed = rest.replace(FIELD_SUFFIX, "");
|
|
89
92
|
if (trimmed === rest) return false;
|
|
@@ -94,11 +97,14 @@ function namesABirth(lead) {
|
|
|
94
97
|
|
|
95
98
|
const NON_WORD = /[^a-z0-9]/gi;
|
|
96
99
|
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
+
/** How far back a label may sit: generous, because a label and its value
|
|
101
|
+
* are separated by a cell boundary and whatever styling that cell carries. */
|
|
102
|
+
const LEAD_IN_CHARS = 120;
|
|
103
|
+
|
|
104
|
+
/** What precedes a date, as a reader sees it: tags dropped, within
|
|
105
|
+
* LEAD_IN_CHARS. */
|
|
100
106
|
const leadIn = (before) =>
|
|
101
|
-
before.replace(/<[^<>]*>/g, " ").slice(-
|
|
107
|
+
before.replace(/<[^<>]*>/g, " ").slice(-LEAD_IN_CHARS).replace(NON_WORD, "")
|
|
102
108
|
.toLowerCase();
|
|
103
109
|
|
|
104
110
|
/** A real address ends in a letters-only top-level domain; without this a
|
|
@@ -155,31 +161,34 @@ export function identityRules(allowEmail = []) {
|
|
|
155
161
|
|
|
156
162
|
/** Every finding in one file's text, against the rules and the names. */
|
|
157
163
|
export function findPersonalData(text, rules, names = []) {
|
|
158
|
-
const hits = [];
|
|
159
164
|
const lines = text.split("\n");
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
hits.push({line: i + 1, rule: rule.name, text: m[0]});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
for (const name of names) {
|
|
170
|
-
// No `g`: `.test()` on a global regex advances `lastIndex`, and a name
|
|
171
|
-
// on consecutive lines would be reported on every other one.
|
|
172
|
-
const re = new RegExp(
|
|
173
|
-
`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
174
|
-
for (const [i, line] of lines.entries()) {
|
|
175
|
-
if (re.test(line)) {
|
|
176
|
-
hits.push({line: i + 1, rule: "name on the local denylist", text: name});
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return hits;
|
|
165
|
+
return [
|
|
166
|
+
...rules.flatMap((rule) =>
|
|
167
|
+
lines.flatMap((line, i) => ruleHitsOnLine(rule, line, i + 1))),
|
|
168
|
+
...names.flatMap((name) => nameHits(lines, name)),
|
|
169
|
+
];
|
|
181
170
|
}
|
|
182
171
|
|
|
172
|
+
/** One rule's findings on one line: each match its `allow` does not
|
|
173
|
+
* excuse and its `context`, if it has one, confirms. */
|
|
174
|
+
const ruleHitsOnLine = (rule, line, lineNumber) =>
|
|
175
|
+
[...line.matchAll(rule.re)]
|
|
176
|
+
.filter((m) => !rule.allow?.(m[0]))
|
|
177
|
+
.filter((m) => !rule.context || rule.context(line.slice(0, m.index)))
|
|
178
|
+
.map((m) => ({line: lineNumber, rule: rule.name, text: m[0]}));
|
|
179
|
+
|
|
180
|
+
/** Each line naming someone on the local denylist. */
|
|
181
|
+
const nameHits = (lines, name) => {
|
|
182
|
+
// No `g`: `.test()` on a global regex advances `lastIndex`, and a name on
|
|
183
|
+
// consecutive lines would be reported on every other one.
|
|
184
|
+
const re = new RegExp(
|
|
185
|
+
`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
186
|
+
return lines.flatMap((line, i) =>
|
|
187
|
+
re.test(line)
|
|
188
|
+
? [{line: i + 1, rule: "name on the local denylist", text: name}]
|
|
189
|
+
: []);
|
|
190
|
+
};
|
|
191
|
+
|
|
183
192
|
/* Tracked AND untracked-but-not-ignored: the moment this exists for is the
|
|
184
193
|
* first commit of a new directory, and a tracked-only listing is blind to
|
|
185
194
|
* exactly that. Ignored files are left out on purpose; they are the working
|
|
@@ -263,6 +272,7 @@ const isEntryPoint = () => {
|
|
|
263
272
|
try {
|
|
264
273
|
return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
265
274
|
} catch {
|
|
275
|
+
// No resolvable argv[1] (imported by a test, or a bare REPL): not the entry.
|
|
266
276
|
return false;
|
|
267
277
|
}
|
|
268
278
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@casomoltd/tooling",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.1",
|
|
4
4
|
"description": "Casomo's shared dev config: ESLint, Prettier, commitlint, knip & jscpd presets, dev CLIs (git hooks, readability, screenshots, design specs), and the build-time data and helpers sites import (the Buttondown engine, the shared Organization)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -108,10 +108,10 @@
|
|
|
108
108
|
"typescript-eslint": "^8.72.0"
|
|
109
109
|
},
|
|
110
110
|
"overrides": {
|
|
111
|
-
"minimatch": ">=11.0.1",
|
|
112
111
|
"remark-lint-frontmatter-schema": {
|
|
113
112
|
"@apidevtools/json-schema-ref-parser": "^11.1.1",
|
|
114
113
|
"ajv": "^8.18.0",
|
|
114
|
+
"minimatch": "^9.0.7",
|
|
115
115
|
"yaml": "^2.8.3"
|
|
116
116
|
}
|
|
117
117
|
}
|