@aitrien/motifspec 1.13.0 → 1.13.3
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/CHANGELOG.md +275 -257
- package/LICENSE +21 -21
- package/README.md +128 -150
- package/dist/backdrop.html +1 -1
- package/dist/chroma.css +36 -36
- package/dist/codex.css +1145 -1145
- package/dist/codex.js +126 -126
- package/dist/contrast.md +101 -101
- package/dist/fonts.css +3 -3
- package/dist/icons.svg +18 -18
- package/dist/plate.html +1 -1
- package/dist/plate.js +292 -292
- package/dist/theme-init.js +1 -1
- package/dist/tokens.css +466 -466
- package/dist/tokens.js +3330 -3330
- package/dist/tokens.json +3330 -3330
- package/docs/components/_template.md +37 -37
- package/docs/components/accession-label.md +106 -106
- package/docs/components/backdrop.md +83 -83
- package/docs/components/button-and-link.md +87 -87
- package/docs/components/callout.md +75 -75
- package/docs/components/code-block.md +109 -109
- package/docs/components/data-table.md +73 -73
- package/docs/components/figure.md +68 -68
- package/docs/components/heading-anchor.md +76 -76
- package/docs/components/icon.md +63 -63
- package/docs/components/ornament.md +77 -77
- package/docs/components/pagination.md +78 -78
- package/docs/components/post-list-item.md +91 -91
- package/docs/components/project-card.md +82 -82
- package/docs/components/prose.md +122 -122
- package/docs/components/site-footer.md +99 -99
- package/docs/components/site-header.md +133 -133
- package/docs/components/specimen-plate.md +92 -92
- package/docs/components/table-of-contents.md +83 -83
- package/docs/components/tag.md +70 -70
- package/docs/components/tooltip.md +67 -67
- package/docs/hugo.md +132 -120
- package/docs/syntax-highlighting.md +55 -55
- package/docs/templates.md +21 -30
- package/fonts/OFL-jetbrains-mono.txt +93 -93
- package/fonts/OFL-national-park.txt +93 -93
- package/fonts/OFL-yarndings-12.txt +93 -93
- package/icons/LICENSE +202 -202
- package/icons/manifest.json +75 -75
- package/layouts/_markup/render-blockquote.html +30 -30
- package/layouts/_markup/render-codeblock.html +18 -18
- package/layouts/_markup/render-heading.html +2 -2
- package/layouts/_markup/render-image.html +14 -14
- package/layouts/_markup/render-link.html +14 -14
- package/layouts/_markup/render-table.html +31 -31
- package/layouts/_partials/motifspec/accession/id.html +2 -2
- package/layouts/_partials/motifspec/accession/label.html +18 -18
- package/layouts/_partials/motifspec/accession/status.html +2 -2
- package/layouts/_partials/motifspec/article.html +24 -24
- package/layouts/_partials/motifspec/back-link.html +2 -2
- package/layouts/_partials/motifspec/body-end.html +5 -5
- package/layouts/_partials/motifspec/body-start.html +5 -5
- package/layouts/_partials/motifspec/figure.html +14 -14
- package/layouts/_partials/motifspec/filed-under.html +5 -5
- package/layouts/_partials/motifspec/h2s.html +7 -7
- package/layouts/_partials/motifspec/head.html +15 -15
- package/layouts/_partials/motifspec/pagination-entries.html +34 -34
- package/layouts/_partials/motifspec/pagination-pages.html +30 -30
- package/layouts/_partials/motifspec/post-list-item.html +15 -15
- package/layouts/_partials/motifspec/project-card.html +23 -23
- package/layouts/_partials/motifspec/section-heading.html +3 -3
- package/layouts/_partials/motifspec/site-header.html +27 -27
- package/layouts/_partials/motifspec/table-of-contents.html +21 -21
- package/layouts/_partials/motifspec/tag-filter.html +22 -22
- package/layouts/_partials/motifspec/tags.html +8 -8
- package/layouts/_partials/motifspec/tokens.html +9 -9
- package/package.json +68 -68
- package/scripts/check.mjs +153 -153
- package/scripts/lib.mjs +176 -176
- package/tokens/README.md +3 -3
- package/tokens/border.json +63 -63
- package/tokens/color.json +1902 -1902
- package/tokens/motion.json +207 -207
- package/tokens/radius.json +53 -53
- package/tokens/size.json +378 -378
- package/tokens/space.json +289 -289
- package/tokens/typography.json +859 -859
package/scripts/check.mjs
CHANGED
|
@@ -1,153 +1,153 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// The MotifSpec rule checker: one implementation for this repo's templates (npm run audit), for sites that use
|
|
3
|
-
// MotifSpec (their CI runs this file from the pinned module) and for
|
|
4
|
-
//
|
|
5
|
-
// node scripts/check.mjs [project-root] check a consuming project's layouts/ and content/
|
|
6
|
-
// node scripts/check.mjs [project-root] --dirs src,app check other folders instead (a non-Hugo project)
|
|
7
|
-
// node scripts/check.mjs [project-root] --json print the problems as JSON
|
|
8
|
-
// npx motifspec-check [same options] the same, from the npm package
|
|
9
|
-
//
|
|
10
|
-
// Rules come from the files of this release: class names from dist/codex.css, tokens from dist/tokens.json, icons
|
|
11
|
-
// from icons/manifest.json. A site therefore checks against the version it has pinned, not against main.
|
|
12
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
13
|
-
import { extname, join, relative, resolve } from 'node:path';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { ROOT } from './lib.mjs';
|
|
16
|
-
|
|
17
|
-
export const RULES = {
|
|
18
|
-
stylesheet: 'Styles come only from MotifSpec. Remove the stylesheet; use components and tokens, or propose an addition to MotifSpec.',
|
|
19
|
-
'style-attribute': 'No style attributes. Use a component class from docs/components/*.md.',
|
|
20
|
-
'raw-colour': 'No raw colours. Use a semantic colour token through a component class, or var(--cx-color-…) in CSS.',
|
|
21
|
-
'raw-unit': 'No raw sizes or durations. Use a semantic space, size, radius, border or motion token.',
|
|
22
|
-
'primitive-token': 'Primitive tokens are not for use outside MotifSpec. Use the semantic token that aliases it.',
|
|
23
|
-
'unknown-token': 'Not a MotifSpec token. Check the name in dist/tokens.json.',
|
|
24
|
-
'deprecated-token': 'This token is deprecated (see CHANGELOG.md). Use its replacement.',
|
|
25
|
-
'unknown-class': 'Not a MotifSpec class. Copy the markup from docs/components/*.md; don\'t invent cx- classes.',
|
|
26
|
-
'unknown-icon': 'Not an approved icon. Use a key from icons/manifest.json, for its listed role.',
|
|
27
|
-
separator: 'Separate inline metadata with <span class="cx-sep" aria-hidden="true">|</span>, never "·".',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const read = (...p) => readFileSync(join(ROOT, ...p), 'utf8');
|
|
31
|
-
let data;
|
|
32
|
-
function load() {
|
|
33
|
-
if (data) return data;
|
|
34
|
-
const tokens = JSON.parse(read('dist', 'tokens.json')).tokens;
|
|
35
|
-
const sizes = JSON.parse(read('tokens', 'size.json'));
|
|
36
|
-
data = {
|
|
37
|
-
// Classes the stylesheet styles, plus hooks the shipped scripts look for (such as cx-code__copy-label).
|
|
38
|
-
classes: new Set([
|
|
39
|
-
...read('dist', 'codex.css').match(/\.cx-[a-z0-9_-]+/g).map((c) => c.slice(1)),
|
|
40
|
-
...['codex.js', 'plate.js'].flatMap((f) => read('dist', f).match(/(?<=[.'"\s])cx-[a-z0-9_-]+/g) ?? []),
|
|
41
|
-
]),
|
|
42
|
-
icons: new Set(Object.keys(JSON.parse(read('icons', 'manifest.json')).icons)),
|
|
43
|
-
tokens: new Map(tokens.map((t) => [t.css, t])),
|
|
44
|
-
breakpoints: new Set(Object.entries(sizes.primitive.size.breakpoint).filter(([k]) => !k.startsWith('$')).map(([, t]) => `${t.$value.value}${t.$value.unit}`)),
|
|
45
|
-
};
|
|
46
|
-
return data;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// A hex colour that isn't part of a fragment link such as href="#cx-icon-add" or an HTML entity.
|
|
50
|
-
const RAW_COLOR = /(?<![\w&/."-])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\w-])|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/g;
|
|
51
|
-
// A number with a length or time unit. Zero is allowed (0px is not a size).
|
|
52
|
-
const RAW_UNIT = /(?<![\w.#-])-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|svh|dvh|lvh|cqi|cqw|pt|pc|cm|mm|in|ms|s)\b/g;
|
|
53
|
-
const STYLE_ATTR = /\sstyle\s*=/g;
|
|
54
|
-
const TOKEN_REF = /--cx-[a-z0-9-]+/g;
|
|
55
|
-
const CLASS_ATTR = /\s(?:class|className)\s*=\s*["']([^"']*)["']/g;
|
|
56
|
-
const ICON_REF = /#cx-icon-([a-z0-9-]*)/g;
|
|
57
|
-
const TEMPLATE = /\{\{.*?\}\}/g; // Hugo actions; replaced by a placeholder so a class or icon built from them is skipped
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Check one file's text. kind: "html" (templates and markup), "md" (content) or "css".
|
|
61
|
-
* Returns [{ file, line, rule, match, message }].
|
|
62
|
-
*/
|
|
63
|
-
export function checkText(text, { file = '<input>', kind = 'html' } = {}) {
|
|
64
|
-
const { classes, icons, tokens, breakpoints } = load();
|
|
65
|
-
const problems = [];
|
|
66
|
-
const add = (line, rule, match) => problems.push({ file, line, rule, match, message: RULES[rule] });
|
|
67
|
-
text.split('\n').forEach((raw, i) => {
|
|
68
|
-
const n = i + 1;
|
|
69
|
-
// In CSS, a breakpoint value in an @media condition is the build's own output and allowed.
|
|
70
|
-
const line = kind === 'css' && /@media/.test(raw) ? raw.replace(/(\d+(?:\.\d+)?)(rem|px|em)\b/g, (v) => (breakpoints.has(v) ? 'breakpoint' : v)) : raw;
|
|
71
|
-
if (kind !== 'css') for (const m of line.matchAll(STYLE_ATTR)) add(n, 'style-attribute', m[0].trim());
|
|
72
|
-
for (const m of line.matchAll(RAW_COLOR)) add(n, 'raw-colour', m[0]);
|
|
73
|
-
for (const m of line.matchAll(RAW_UNIT)) if (!/^-?0+(?:\.0+)?[a-z]+$/.test(m[0])) add(n, 'raw-unit', m[0]);
|
|
74
|
-
for (const [name] of line.matchAll(TOKEN_REF)) {
|
|
75
|
-
if (name.startsWith('--cx-primitive-')) add(n, 'primitive-token', name);
|
|
76
|
-
else if (!tokens.has(name)) add(n, 'unknown-token', name);
|
|
77
|
-
else if (tokens.get(name).deprecated) add(n, 'deprecated-token', name);
|
|
78
|
-
}
|
|
79
|
-
if (kind !== 'css') {
|
|
80
|
-
if (line.includes('·')) add(n, 'separator', '·');
|
|
81
|
-
const plain = line.replace(TEMPLATE, '§');
|
|
82
|
-
for (const m of plain.matchAll(CLASS_ATTR)) {
|
|
83
|
-
for (const c of m[1].split(/\s+/)) if (c.startsWith('cx-') && !c.includes('§') && !classes.has(c)) add(n, 'unknown-class', c);
|
|
84
|
-
}
|
|
85
|
-
for (const m of plain.matchAll(ICON_REF)) {
|
|
86
|
-
if (m[1] && plain[m.index + m[0].length] !== '§' && !icons.has(m[1])) add(n, 'unknown-icon', `cx-icon-${m[1]}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
return problems;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const KIND = {
|
|
94
|
-
'.html': 'html', '.xml': 'html', '.jsx': 'html', '.tsx': 'html', '.vue': 'html', '.svelte': 'html', '.astro': 'html',
|
|
95
|
-
'.txt': 'md', '.md': 'md', '.mdx': 'md', '.json': 'md', '.css': 'css',
|
|
96
|
-
};
|
|
97
|
-
// Build output and dependencies, which contain MotifSpec's own CSS.
|
|
98
|
-
const SKIP = new Set(['.git', 'public', 'resources', 'node_modules', 'dist', 'build', '.next', '.astro', '.svelte-kit']);
|
|
99
|
-
function* walk(dir) {
|
|
100
|
-
for (const name of readdirSync(dir)) {
|
|
101
|
-
if (SKIP.has(name)) continue;
|
|
102
|
-
const p = join(dir, name);
|
|
103
|
-
if (statSync(p).isDirectory()) yield* walk(p); else yield p;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Check a project that uses MotifSpec: no stylesheets anywhere, and every file in `dirs` checked by checkText. */
|
|
108
|
-
export function checkProject(root, { dirs = ['layouts', 'content'] } = {}) {
|
|
109
|
-
const problems = [];
|
|
110
|
-
for (const file of walk(root)) {
|
|
111
|
-
if (['.css', '.scss', '.sass', '.less'].includes(extname(file))) {
|
|
112
|
-
problems.push({ file: relative(root, file), line: 0, rule: 'stylesheet', match: extname(file), message: RULES.stylesheet });
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
for (const dir of dirs) {
|
|
116
|
-
const abs = join(root, dir);
|
|
117
|
-
if (!existsSync(abs)) continue;
|
|
118
|
-
for (const file of walk(abs)) {
|
|
119
|
-
const kind = KIND[extname(file)];
|
|
120
|
-
if (kind && kind !== 'css') problems.push(...checkText(readFileSync(file, 'utf8'), { file: relative(root, file), kind }));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return problems;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function format(p) {
|
|
127
|
-
return `${p.file}${p.line ? `:${p.line}` : ''}: [${p.rule}] "${p.match}". ${p.message}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Run as a command: directly, or through npm's bin symlink (node_modules/.bin/motifspec-check).
|
|
131
|
-
const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
132
|
-
if (isMain) {
|
|
133
|
-
const args = process.argv.slice(2);
|
|
134
|
-
const at = args.indexOf('--dirs');
|
|
135
|
-
const dirs = at >= 0 ? args[at + 1]?.split(',').filter(Boolean) : undefined;
|
|
136
|
-
if (at >= 0 && !dirs?.length) { console.error('--dirs needs a comma-separated list, e.g. --dirs src,app'); process.exit(2); }
|
|
137
|
-
const root = resolve(args.find((a, i) => !a.startsWith('--') && (at < 0 || i !== at + 1)) ?? '.');
|
|
138
|
-
if (!existsSync(root)) { console.error(`No such folder: ${root}`); process.exit(2); }
|
|
139
|
-
const problems = checkProject(root, dirs ? { dirs } : undefined);
|
|
140
|
-
const version = JSON.parse(read('dist', 'tokens.json')).version;
|
|
141
|
-
if (args.includes('--json')) {
|
|
142
|
-
console.log(JSON.stringify({ version, problems }, null, 2));
|
|
143
|
-
} else if (problems.length) {
|
|
144
|
-
for (const p of problems) {
|
|
145
|
-
console.error(`✗ ${format(p)}`);
|
|
146
|
-
if (process.env.GITHUB_ACTIONS) console.log(`::error file=${p.file},line=${p.line || 1},title=MotifSpec ${p.rule}::${p.message}`);
|
|
147
|
-
}
|
|
148
|
-
console.error(`${problems.length} problem(s), checked against MotifSpec ${version}.`);
|
|
149
|
-
} else {
|
|
150
|
-
console.log(`No problems in ${relative(process.cwd(), root) || '.'}: checked against MotifSpec ${version}.`);
|
|
151
|
-
}
|
|
152
|
-
process.exit(problems.length ? 1 : 0);
|
|
153
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The MotifSpec rule checker: one implementation for this repo's templates (npm run audit), for sites that use
|
|
3
|
+
// MotifSpec (their CI runs this file from the pinned module) and for any tool that imports checkText.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/check.mjs [project-root] check a consuming project's layouts/ and content/
|
|
6
|
+
// node scripts/check.mjs [project-root] --dirs src,app check other folders instead (a non-Hugo project)
|
|
7
|
+
// node scripts/check.mjs [project-root] --json print the problems as JSON
|
|
8
|
+
// npx motifspec-check [same options] the same, from the npm package
|
|
9
|
+
//
|
|
10
|
+
// Rules come from the files of this release: class names from dist/codex.css, tokens from dist/tokens.json, icons
|
|
11
|
+
// from icons/manifest.json. A site therefore checks against the version it has pinned, not against main.
|
|
12
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
13
|
+
import { extname, join, relative, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { ROOT } from './lib.mjs';
|
|
16
|
+
|
|
17
|
+
export const RULES = {
|
|
18
|
+
stylesheet: 'Styles come only from MotifSpec. Remove the stylesheet; use components and tokens, or propose an addition to MotifSpec.',
|
|
19
|
+
'style-attribute': 'No style attributes. Use a component class from docs/components/*.md.',
|
|
20
|
+
'raw-colour': 'No raw colours. Use a semantic colour token through a component class, or var(--cx-color-…) in CSS.',
|
|
21
|
+
'raw-unit': 'No raw sizes or durations. Use a semantic space, size, radius, border or motion token.',
|
|
22
|
+
'primitive-token': 'Primitive tokens are not for use outside MotifSpec. Use the semantic token that aliases it.',
|
|
23
|
+
'unknown-token': 'Not a MotifSpec token. Check the name in dist/tokens.json.',
|
|
24
|
+
'deprecated-token': 'This token is deprecated (see CHANGELOG.md). Use its replacement.',
|
|
25
|
+
'unknown-class': 'Not a MotifSpec class. Copy the markup from docs/components/*.md; don\'t invent cx- classes.',
|
|
26
|
+
'unknown-icon': 'Not an approved icon. Use a key from icons/manifest.json, for its listed role.',
|
|
27
|
+
separator: 'Separate inline metadata with <span class="cx-sep" aria-hidden="true">|</span>, never "·".',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const read = (...p) => readFileSync(join(ROOT, ...p), 'utf8');
|
|
31
|
+
let data;
|
|
32
|
+
function load() {
|
|
33
|
+
if (data) return data;
|
|
34
|
+
const tokens = JSON.parse(read('dist', 'tokens.json')).tokens;
|
|
35
|
+
const sizes = JSON.parse(read('tokens', 'size.json'));
|
|
36
|
+
data = {
|
|
37
|
+
// Classes the stylesheet styles, plus hooks the shipped scripts look for (such as cx-code__copy-label).
|
|
38
|
+
classes: new Set([
|
|
39
|
+
...read('dist', 'codex.css').match(/\.cx-[a-z0-9_-]+/g).map((c) => c.slice(1)),
|
|
40
|
+
...['codex.js', 'plate.js'].flatMap((f) => read('dist', f).match(/(?<=[.'"\s])cx-[a-z0-9_-]+/g) ?? []),
|
|
41
|
+
]),
|
|
42
|
+
icons: new Set(Object.keys(JSON.parse(read('icons', 'manifest.json')).icons)),
|
|
43
|
+
tokens: new Map(tokens.map((t) => [t.css, t])),
|
|
44
|
+
breakpoints: new Set(Object.entries(sizes.primitive.size.breakpoint).filter(([k]) => !k.startsWith('$')).map(([, t]) => `${t.$value.value}${t.$value.unit}`)),
|
|
45
|
+
};
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A hex colour that isn't part of a fragment link such as href="#cx-icon-add" or an HTML entity.
|
|
50
|
+
const RAW_COLOR = /(?<![\w&/."-])#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![\w-])|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/g;
|
|
51
|
+
// A number with a length or time unit. Zero is allowed (0px is not a size).
|
|
52
|
+
const RAW_UNIT = /(?<![\w.#-])-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em|ch|ex|vw|vh|vmin|vmax|svh|dvh|lvh|cqi|cqw|pt|pc|cm|mm|in|ms|s)\b/g;
|
|
53
|
+
const STYLE_ATTR = /\sstyle\s*=/g;
|
|
54
|
+
const TOKEN_REF = /--cx-[a-z0-9-]+/g;
|
|
55
|
+
const CLASS_ATTR = /\s(?:class|className)\s*=\s*["']([^"']*)["']/g;
|
|
56
|
+
const ICON_REF = /#cx-icon-([a-z0-9-]*)/g;
|
|
57
|
+
const TEMPLATE = /\{\{.*?\}\}/g; // Hugo actions; replaced by a placeholder so a class or icon built from them is skipped
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check one file's text. kind: "html" (templates and markup), "md" (content) or "css".
|
|
61
|
+
* Returns [{ file, line, rule, match, message }].
|
|
62
|
+
*/
|
|
63
|
+
export function checkText(text, { file = '<input>', kind = 'html' } = {}) {
|
|
64
|
+
const { classes, icons, tokens, breakpoints } = load();
|
|
65
|
+
const problems = [];
|
|
66
|
+
const add = (line, rule, match) => problems.push({ file, line, rule, match, message: RULES[rule] });
|
|
67
|
+
text.split('\n').forEach((raw, i) => {
|
|
68
|
+
const n = i + 1;
|
|
69
|
+
// In CSS, a breakpoint value in an @media condition is the build's own output and allowed.
|
|
70
|
+
const line = kind === 'css' && /@media/.test(raw) ? raw.replace(/(\d+(?:\.\d+)?)(rem|px|em)\b/g, (v) => (breakpoints.has(v) ? 'breakpoint' : v)) : raw;
|
|
71
|
+
if (kind !== 'css') for (const m of line.matchAll(STYLE_ATTR)) add(n, 'style-attribute', m[0].trim());
|
|
72
|
+
for (const m of line.matchAll(RAW_COLOR)) add(n, 'raw-colour', m[0]);
|
|
73
|
+
for (const m of line.matchAll(RAW_UNIT)) if (!/^-?0+(?:\.0+)?[a-z]+$/.test(m[0])) add(n, 'raw-unit', m[0]);
|
|
74
|
+
for (const [name] of line.matchAll(TOKEN_REF)) {
|
|
75
|
+
if (name.startsWith('--cx-primitive-')) add(n, 'primitive-token', name);
|
|
76
|
+
else if (!tokens.has(name)) add(n, 'unknown-token', name);
|
|
77
|
+
else if (tokens.get(name).deprecated) add(n, 'deprecated-token', name);
|
|
78
|
+
}
|
|
79
|
+
if (kind !== 'css') {
|
|
80
|
+
if (line.includes('·')) add(n, 'separator', '·');
|
|
81
|
+
const plain = line.replace(TEMPLATE, '§');
|
|
82
|
+
for (const m of plain.matchAll(CLASS_ATTR)) {
|
|
83
|
+
for (const c of m[1].split(/\s+/)) if (c.startsWith('cx-') && !c.includes('§') && !classes.has(c)) add(n, 'unknown-class', c);
|
|
84
|
+
}
|
|
85
|
+
for (const m of plain.matchAll(ICON_REF)) {
|
|
86
|
+
if (m[1] && plain[m.index + m[0].length] !== '§' && !icons.has(m[1])) add(n, 'unknown-icon', `cx-icon-${m[1]}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return problems;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const KIND = {
|
|
94
|
+
'.html': 'html', '.xml': 'html', '.jsx': 'html', '.tsx': 'html', '.vue': 'html', '.svelte': 'html', '.astro': 'html',
|
|
95
|
+
'.txt': 'md', '.md': 'md', '.mdx': 'md', '.json': 'md', '.css': 'css',
|
|
96
|
+
};
|
|
97
|
+
// Build output and dependencies, which contain MotifSpec's own CSS.
|
|
98
|
+
const SKIP = new Set(['.git', 'public', 'resources', 'node_modules', 'dist', 'build', '.next', '.astro', '.svelte-kit']);
|
|
99
|
+
function* walk(dir) {
|
|
100
|
+
for (const name of readdirSync(dir)) {
|
|
101
|
+
if (SKIP.has(name)) continue;
|
|
102
|
+
const p = join(dir, name);
|
|
103
|
+
if (statSync(p).isDirectory()) yield* walk(p); else yield p;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Check a project that uses MotifSpec: no stylesheets anywhere, and every file in `dirs` checked by checkText. */
|
|
108
|
+
export function checkProject(root, { dirs = ['layouts', 'content'] } = {}) {
|
|
109
|
+
const problems = [];
|
|
110
|
+
for (const file of walk(root)) {
|
|
111
|
+
if (['.css', '.scss', '.sass', '.less'].includes(extname(file))) {
|
|
112
|
+
problems.push({ file: relative(root, file), line: 0, rule: 'stylesheet', match: extname(file), message: RULES.stylesheet });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const dir of dirs) {
|
|
116
|
+
const abs = join(root, dir);
|
|
117
|
+
if (!existsSync(abs)) continue;
|
|
118
|
+
for (const file of walk(abs)) {
|
|
119
|
+
const kind = KIND[extname(file)];
|
|
120
|
+
if (kind && kind !== 'css') problems.push(...checkText(readFileSync(file, 'utf8'), { file: relative(root, file), kind }));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return problems;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function format(p) {
|
|
127
|
+
return `${p.file}${p.line ? `:${p.line}` : ''}: [${p.rule}] "${p.match}". ${p.message}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Run as a command: directly, or through npm's bin symlink (node_modules/.bin/motifspec-check).
|
|
131
|
+
const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
132
|
+
if (isMain) {
|
|
133
|
+
const args = process.argv.slice(2);
|
|
134
|
+
const at = args.indexOf('--dirs');
|
|
135
|
+
const dirs = at >= 0 ? args[at + 1]?.split(',').filter(Boolean) : undefined;
|
|
136
|
+
if (at >= 0 && !dirs?.length) { console.error('--dirs needs a comma-separated list, e.g. --dirs src,app'); process.exit(2); }
|
|
137
|
+
const root = resolve(args.find((a, i) => !a.startsWith('--') && (at < 0 || i !== at + 1)) ?? '.');
|
|
138
|
+
if (!existsSync(root)) { console.error(`No such folder: ${root}`); process.exit(2); }
|
|
139
|
+
const problems = checkProject(root, dirs ? { dirs } : undefined);
|
|
140
|
+
const version = JSON.parse(read('dist', 'tokens.json')).version;
|
|
141
|
+
if (args.includes('--json')) {
|
|
142
|
+
console.log(JSON.stringify({ version, problems }, null, 2));
|
|
143
|
+
} else if (problems.length) {
|
|
144
|
+
for (const p of problems) {
|
|
145
|
+
console.error(`✗ ${format(p)}`);
|
|
146
|
+
if (process.env.GITHUB_ACTIONS) console.log(`::error file=${p.file},line=${p.line || 1},title=MotifSpec ${p.rule}::${p.message}`);
|
|
147
|
+
}
|
|
148
|
+
console.error(`${problems.length} problem(s), checked against MotifSpec ${version}.`);
|
|
149
|
+
} else {
|
|
150
|
+
console.log(`No problems in ${relative(process.cwd(), root) || '.'}: checked against MotifSpec ${version}.`);
|
|
151
|
+
}
|
|
152
|
+
process.exit(problems.length ? 1 : 0);
|
|
153
|
+
}
|