@ericrisco/rsc 3.0.21 → 3.0.22
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/manifest.json +1 -1
- package/package.json +1 -1
- package/targets/copy-rules.mjs +39 -19
package/manifest.json
CHANGED
package/package.json
CHANGED
package/targets/copy-rules.mjs
CHANGED
|
@@ -14,27 +14,40 @@
|
|
|
14
14
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
15
15
|
import { join, relative } from 'node:path';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// A rule is a DECLARATION, not a mention. 3.0.21 read any sentence that contained "prohibido" and any
|
|
18
|
+
// backticked token near it, and on a real wiki that turned file names, table cells and words like
|
|
19
|
+
// "add" or "review" into banned phrases: documentation ABOUT rules became rules. So the keyword has to
|
|
20
|
+
// open the line or sentence, code spans are never phrases in prose, tables are skipped, and process
|
|
21
|
+
// records (ftd/, sdd/) are not policy.
|
|
22
|
+
const BAN = String.raw`(?:prohib\p{L}*|vetad\p{L}*|forbidden|banned|never (?:use|say|write)|nunca (?:uses|usar|digas|decir|escribas)|no (?:uses|digas|escribas)|do not (?:use|say|write)|don'?t (?:use|say|write)|blocklist|denylist)`;
|
|
23
|
+
const LEAD = String.raw`^[\s>*_¡¿("“«-]*(?:(?:queda|quedan|esta|está|estan|están|it is|it's|is|are|strictly|totalmente|terminantemente|absolutamente|words|phrases|terms|palabras|frases|términos|terminos|expresiones)\s+){0,2}`;
|
|
24
|
+
const DECLARES = new RegExp(LEAD + BAN, 'iu');
|
|
18
25
|
const BAN_HEADING = new RegExp(BAN, 'iu');
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
26
|
+
const BAN_COLON = new RegExp(String.raw`^\s*(?:[-*+]\s+)?\**\s*(?:(?:queda|quedan|esta|está|it is|words|phrases|terms|palabras|frases|términos|terminos|expresiones)\s+){0,2}${BAN}[\p{L} ]{0,25}:\**\s*(.*)$`, 'iu');
|
|
27
|
+
const PROSE_QUOTES = /"([^"\n]{1,80})"|“([^”\n]{1,80})”|«([^»\n]{1,80})»/gu;
|
|
28
|
+
const LIST_QUOTES = /"([^"\n]{1,80})"|“([^”\n]{1,80})”|«([^»\n]{1,80})»|`([^`\n]{1,80})`/gu;
|
|
29
|
+
const STOP = new Set(['y', 'o', 'e', 'u', 'a', 'de', 'la', 'el', 'en', 'and', 'or', 'the', 'of', 'to', 'in']);
|
|
30
|
+
const SKIP_DIRS = new Set(['ftd', 'sdd', 'raw']);
|
|
31
|
+
// The guard checks TEXT. A visual-identity ban ("no illustrations of people") read as copy would forbid
|
|
32
|
+
// the word "people" in every answer, so pages and sections about visuals are not copy policy.
|
|
33
|
+
const VISUAL = /visual|design|dise[nñ]o|colou?r|logo|imagen|image|ilustraci|tipograf|typograph|paleta|palette/iu;
|
|
22
34
|
|
|
23
35
|
export const normalize = (s) => String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase();
|
|
24
36
|
|
|
25
37
|
// A trailing * is the explicit wildcard, so only a markdown bold pair is stripped from the end.
|
|
26
|
-
const clean = (s) => s.replace(/^[\s"“«`'
|
|
38
|
+
const clean = (s) => s.replace(/\*\*/gu, '').replace(/^[\s"“«`'_]+/u, '').replace(/^(?:y|and|o|or)\s+/iu, '').replace(/[\s"”»`'_.;:,]+$/u, '').trim();
|
|
27
39
|
|
|
28
|
-
function quotedIn(text) {
|
|
40
|
+
function quotedIn(text, re) {
|
|
29
41
|
const out = [];
|
|
30
|
-
for (const m of text.matchAll(
|
|
42
|
+
for (const m of text.matchAll(re)) out.push(m[1] ?? m[2] ?? m[3] ?? m[4]);
|
|
31
43
|
return out;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
function listItems(text) {
|
|
35
|
-
const q = quotedIn(text);
|
|
47
|
+
const q = quotedIn(text, LIST_QUOTES);
|
|
36
48
|
if (q.length) return q;
|
|
37
|
-
|
|
49
|
+
// Commas only: "cerebros, robots o personas" split on "o" would ban the word "personas".
|
|
50
|
+
return text.split(/[,;]/u).map(clean).filter(Boolean);
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
/** Banned phrases declared in one markdown document. */
|
|
@@ -42,28 +55,34 @@ export function extractRules(markdown, source = '') {
|
|
|
42
55
|
const found = [];
|
|
43
56
|
const add = (phrase, line) => {
|
|
44
57
|
const p = clean(phrase);
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
const letters = normalize(p).replace(/[^\p{L}]/gu, '');
|
|
59
|
+
// A ban is a word or a short phrase: not a sentence that happened to be quoted, not a stray "y".
|
|
60
|
+
if (p && p.split(/\s+/).length <= 6 && letters.length >= 2 && !STOP.has(normalize(p))) found.push({ phrase: p, source, line });
|
|
47
61
|
};
|
|
48
62
|
const lines = String(markdown).split('\n');
|
|
49
|
-
let inFence = false, banSection = false, para = [], paraStart = 0;
|
|
63
|
+
let inFence = false, banSection = false, visualSection = false, para = [], paraStart = 0, colonList = null;
|
|
50
64
|
const flush = () => {
|
|
65
|
+
if (colonList) { for (const item of listItems(colonList.text)) add(item, colonList.line); colonList = null; }
|
|
51
66
|
if (!para.length) return;
|
|
52
67
|
const text = para.join(' ');
|
|
53
|
-
for (const sentence of text.split(/(?<=[.!?])\s+(?=[\p{Lu}¿¡"
|
|
54
|
-
if (
|
|
68
|
+
for (const sentence of text.split(/(?<=[.!?])\s+(?=[\p{Lu}¿¡"“«*])/u)) {
|
|
69
|
+
if (DECLARES.test(sentence)) for (const q of quotedIn(sentence, PROSE_QUOTES)) add(q, paraStart + 1);
|
|
55
70
|
}
|
|
56
71
|
para = [];
|
|
57
72
|
};
|
|
58
73
|
lines.forEach((raw, i) => {
|
|
59
74
|
if (/^\s*```/.test(raw)) { flush(); inFence = !inFence; return; }
|
|
60
75
|
if (inFence) return;
|
|
76
|
+
if (/^\s*\|/.test(raw)) { flush(); return; }
|
|
61
77
|
const heading = raw.match(/^#{1,6}\s+(.*)$/);
|
|
62
|
-
if (heading) { flush(); banSection = BAN_HEADING.test(heading[1]); return; }
|
|
78
|
+
if (heading) { flush(); visualSection = VISUAL.test(heading[1]); banSection = !visualSection && BAN_HEADING.test(heading[1]) && heading[1].split(/\s+/).length <= 6; return; }
|
|
79
|
+
if (visualSection) return;
|
|
63
80
|
if (!raw.trim()) { flush(); return; }
|
|
64
|
-
const colon = raw.match(BAN_COLON);
|
|
65
|
-
if (colon && !quotedIn(raw.slice(0, raw.indexOf(':'))).length) { flush(); for (const item of listItems(colon[1])) add(item, i + 1); return; }
|
|
66
81
|
const bullet = raw.match(/^\s*(?:[-*+]|\d+[.)])\s+(.*)$/);
|
|
82
|
+
// A "Vetado: a, b," list may wrap onto the next lines; it runs until a blank line or a new item.
|
|
83
|
+
if (colonList && !bullet) { colonList.text += ' ' + raw.trim(); const end = colonList.text.search(/[.!?](?=\s+\p{Lu}|\s*$)/u); if (end >= 0) { colonList.text = colonList.text.slice(0, end); flush(); } return; }
|
|
84
|
+
const colon = raw.match(BAN_COLON);
|
|
85
|
+
if (colon) { flush(); colonList = { text: colon[1], line: i + 1 }; const end = colonList.text.search(/[.!?](?=\s+\p{Lu}|\s*$)/u); if (end >= 0) { colonList.text = colonList.text.slice(0, end); flush(); } return; }
|
|
67
86
|
if (bullet && banSection) { flush(); for (const item of listItems(bullet[1])) add(item, i + 1); return; }
|
|
68
87
|
if (bullet) { flush(); para = [bullet[1]]; paraStart = i; flush(); return; }
|
|
69
88
|
if (!para.length) paraStart = i;
|
|
@@ -84,8 +103,9 @@ export function loadRules(root, { maxFiles = 3000 } = {}) {
|
|
|
84
103
|
if (files.length >= maxFiles || name.startsWith('.')) continue;
|
|
85
104
|
const full = join(dir, name);
|
|
86
105
|
let st; try { st = statSync(full); } catch { continue; }
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
// Archived pages (_archive/, archive/) are history, not the rules in force.
|
|
107
|
+
if (st.isDirectory()) { if ((dir !== base || !SKIP_DIRS.has(name)) && !name.startsWith('_') && !/^archive/i.test(name)) walk(full); }
|
|
108
|
+
else if (name.endsWith('.md') && !VISUAL.test(name)) files.push(full);
|
|
89
109
|
}
|
|
90
110
|
};
|
|
91
111
|
walk(base);
|