@casomoltd/tooling 0.39.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/README.md CHANGED
@@ -15,7 +15,7 @@ Shared linting, formatting, commit config, and CLI tools for Casomo Ltd's repos.
15
15
  | `@casomoltd/tooling/knip` | Knip config for unused exports/dependencies |
16
16
  | `@casomoltd/tooling/jscpd` | Copy-paste detection config |
17
17
  | `@casomoltd/tooling/readability` | `scoreText()` — readability scoring for page content |
18
- | `@casomoltd/tooling/identity` | `CASOMO_ORGANIZATION`, the schema.org `Organization`, logo included, that every Casomo site names as publisher in its JSON-LD. App code imports it at build time, so a site that installs only production dependencies at runtime lists the package under `dependencies` |
18
+ | `@casomoltd/tooling/identity` | `CASOMO_ORGANIZATION`, the schema.org `Organization`, logo included, that every Casomo site names as publisher in its JSON-LD; and the company's public registrations, `COMPANY_REGISTER`, `ICO_REGISTER` and `REGISTERED_OFFICE` ([types](identity/index.d.mts)). App code imports it at build time, so a site that installs only production dependencies at runtime lists the package under `dependencies` |
19
19
  | `@casomoltd/tooling/buttondown` | For a repo using the `buttondown` command: the shared email blocks and skins (`createEmailBlocks`, `emailBaseCss`, `composeWebSkin`), `tokenReader()`, `loadSettings()` and `buildPayload()`, and `EMAIL_CHROME_HIDES` and `WEB_CHROME_HIDES` ([docs/buttondown.md](docs/buttondown.md)) |
20
20
 
21
21
  ### Bin commands
@@ -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 < 4; depth += 1) {
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
- /** What precedes a date, as a reader sees it: tags dropped, a generous
98
- * window, because a label and its value are separated by a cell boundary
99
- * and whatever styling that cell carries. */
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(-120).replace(NON_WORD, "")
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
- for (const rule of rules) {
161
- for (const [i, line] of lines.entries()) {
162
- for (const m of line.matchAll(rule.re)) {
163
- if (rule.allow?.(m[0])) continue;
164
- if (rule.context && !rule.context(line.slice(0, m.index))) continue;
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
  };
@@ -13,3 +13,16 @@ export interface CasomoOrganization {
13
13
  }
14
14
 
15
15
  export declare const CASOMO_ORGANIZATION: CasomoOrganization;
16
+
17
+ /** An entry on a public register: its number, and the page that shows it. */
18
+ export interface Registration {
19
+ readonly number: string;
20
+ readonly url: string;
21
+ }
22
+
23
+ /** The company's Companies House record. */
24
+ export declare const COMPANY_REGISTER: Registration;
25
+ /** The company's registration with the ICO, as a data controller. */
26
+ export declare const ICO_REGISTER: Registration;
27
+ /** The company's registered office, as Companies House holds it. */
28
+ export declare const REGISTERED_OFFICE: string;
@@ -3,4 +3,9 @@
3
3
  * lives in organization.mjs, where the typecheck can read it against the
4
4
  * declaration here; beside a .d.mts, TypeScript would skip it.
5
5
  */
6
- export {CASOMO_ORGANIZATION} from "./organization.mjs";
6
+ export {
7
+ CASOMO_ORGANIZATION,
8
+ COMPANY_REGISTER,
9
+ ICO_REGISTER,
10
+ REGISTERED_OFFICE,
11
+ } from "./organization.mjs";
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Casomo Ltd as search engines should see it: the one schema.org
3
- * `Organization` every Casomo site names in its JSON-LD. Each site imports
4
- * this rather than typing its own, so no two sites can disagree about who
5
- * publishes them, and `sameAs` ties them all to the same profiles.
2
+ * Casomo Ltd's public identity, which every Casomo site imports rather than
3
+ * typing its own, so no two sites can disagree about the company. Here are the
4
+ * schema.org `Organization` every site names in its JSON-LD, whose `sameAs`
5
+ * ties them all to the same profiles, and the company's entries on the public
6
+ * registers, each by its number and the page that shows it.
6
7
  *
7
- * Public facts only: the company's name, its mark and the public addresses it
8
- * is known by. A person, such as an article's author, stays with the site.
8
+ * Public facts only: the company's name, its mark, the public addresses it
9
+ * is known by, and its public registrations. A person, such as an article's
10
+ * author, stays with the site.
9
11
  */
10
12
  const SITE = "https://casomoltd.com";
11
13
 
@@ -25,3 +27,29 @@ export const CASOMO_ORGANIZATION = Object.freeze({
25
27
  "https://www.linkedin.com/company/111967567/",
26
28
  ]),
27
29
  });
30
+
31
+ const COMPANY_NUMBER = "15030496";
32
+ /**
33
+ * The company's Companies House record.
34
+ * @type {import("./index.d.mts").Registration}
35
+ */
36
+ export const COMPANY_REGISTER = Object.freeze({
37
+ number: COMPANY_NUMBER,
38
+ url:
39
+ "https://find-and-update.company-information.service.gov.uk"
40
+ + `/company/${COMPANY_NUMBER}`,
41
+ });
42
+
43
+ const ICO_NUMBER = "ZC219565";
44
+ /**
45
+ * The company's registration with the ICO, as a data controller.
46
+ * @type {import("./index.d.mts").Registration}
47
+ */
48
+ export const ICO_REGISTER = Object.freeze({
49
+ number: ICO_NUMBER,
50
+ url: `https://ico.org.uk/ESDWebPages/Entry/${ICO_NUMBER}`,
51
+ });
52
+
53
+ /** The company's registered office, as Companies House holds it. */
54
+ export const REGISTERED_OFFICE =
55
+ "71-75 Shelton Street, Covent Garden, London, WC2H 9JQ";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casomoltd/tooling",
3
- "version": "0.39.0",
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": {