@webround/editor 0.0.0-stage → 0.3.0
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/dist/blocks.d.ts +90 -0
- package/dist/blocks.js +119 -0
- package/dist/bridge.d.ts +96 -0
- package/dist/bridge.js +970 -0
- package/dist/check/aria.d.ts +19 -0
- package/dist/check/aria.js +106 -0
- package/dist/check/cli.d.ts +3 -0
- package/dist/check/cli.js +30 -0
- package/dist/check/css.d.ts +13 -0
- package/dist/check/css.js +63 -0
- package/dist/check/html.d.ts +8 -0
- package/dist/check/html.js +29 -0
- package/dist/check/index.d.ts +4 -0
- package/dist/check/index.js +5 -0
- package/dist/check/links.d.ts +7 -0
- package/dist/check/links.js +23 -0
- package/dist/check/orphans.d.ts +20 -0
- package/dist/check/orphans.js +57 -0
- package/dist/check/producer-code.d.ts +53 -0
- package/dist/check/producer-code.js +379 -0
- package/dist/check/producer-dom.d.ts +21 -0
- package/dist/check/producer-dom.js +52 -0
- package/dist/check/project.d.ts +20 -0
- package/dist/check/project.js +51 -0
- package/dist/check/rules.d.ts +7 -0
- package/dist/check/rules.js +517 -0
- package/dist/check/types.d.ts +160 -0
- package/dist/check/types.js +22 -0
- package/dist/components/WrAltcha.d.ts +5 -0
- package/dist/components/WrAltcha.js +8 -0
- package/dist/components/WrButton.d.ts +8 -0
- package/dist/components/WrButton.js +34 -0
- package/dist/components/WrForm.d.ts +6 -0
- package/dist/components/WrForm.js +29 -0
- package/dist/components/WrFormPrivacy.d.ts +4 -0
- package/dist/components/WrFormPrivacy.js +7 -0
- package/dist/components/WrFormSuccess.d.ts +4 -0
- package/dist/components/WrFormSuccess.js +12 -0
- package/dist/components/WrIframe.d.ts +12 -0
- package/dist/components/WrIframe.js +30 -0
- package/dist/components/WrImage.d.ts +9 -0
- package/dist/components/WrImage.js +28 -0
- package/dist/components/WrInput.d.ts +8 -0
- package/dist/components/WrInput.js +29 -0
- package/dist/components/WrLink.d.ts +10 -0
- package/dist/components/WrLink.js +38 -0
- package/dist/components/WrNav.d.ts +26 -0
- package/dist/components/WrNav.js +30 -0
- package/dist/components/WrSection.d.ts +11 -0
- package/dist/components/WrSection.js +5 -0
- package/dist/components/WrShared.d.ts +12 -0
- package/dist/components/WrShared.js +20 -0
- package/dist/components/WrText.d.ts +11 -0
- package/dist/components/WrText.js +41 -0
- package/dist/components/WrVideo.d.ts +9 -0
- package/dist/components/WrVideo.js +73 -0
- package/dist/components/text-editor/CodeThemeContext.d.ts +8 -0
- package/dist/components/text-editor/CodeThemeContext.js +10 -0
- package/dist/components/text-editor/ExampleTheme.d.ts +77 -0
- package/dist/components/text-editor/ExampleTheme.js +76 -0
- package/dist/components/text-editor/ToolbarContext.d.ts +17 -0
- package/dist/components/text-editor/ToolbarContext.js +32 -0
- package/dist/components/text-editor/WrTextEditor.d.ts +13 -0
- package/dist/components/text-editor/WrTextEditor.js +48 -0
- package/dist/components/text-editor/common/constants.d.ts +109 -0
- package/dist/components/text-editor/common/constants.js +58 -0
- package/dist/components/text-editor/common/surface.d.ts +3 -0
- package/dist/components/text-editor/common/surface.js +8 -0
- package/dist/components/text-editor/common/utils.d.ts +7 -0
- package/dist/components/text-editor/common/utils.js +78 -0
- package/dist/components/text-editor/hooks/useLexicalCommitOnBlur.d.ts +5 -0
- package/dist/components/text-editor/hooks/useLexicalCommitOnBlur.js +41 -0
- package/dist/components/text-editor/hooks/useLexicalMarkdown.d.ts +33 -0
- package/dist/components/text-editor/hooks/useLexicalMarkdown.js +284 -0
- package/dist/components/text-editor/nodes/CodeBlockNode.d.ts +23 -0
- package/dist/components/text-editor/nodes/CodeBlockNode.js +138 -0
- package/dist/components/text-editor/nodes/HorizontalRuleNode.d.ts +13 -0
- package/dist/components/text-editor/nodes/HorizontalRuleNode.js +22 -0
- package/dist/components/text-editor/plugins/CodeBlockPlugin.d.ts +2 -0
- package/dist/components/text-editor/plugins/CodeBlockPlugin.js +38 -0
- package/dist/components/text-editor/plugins/FontPlugin.d.ts +12 -0
- package/dist/components/text-editor/plugins/FontPlugin.js +114 -0
- package/dist/components/text-editor/plugins/FontSizePlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/FontSizePlugin.js +105 -0
- package/dist/components/text-editor/plugins/FontWeightPlugin.d.ts +6 -0
- package/dist/components/text-editor/plugins/FontWeightPlugin.js +103 -0
- package/dist/components/text-editor/plugins/HTMLTagPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/HTMLTagPlugin.js +82 -0
- package/dist/components/text-editor/plugins/HorizontalRulePlugin.d.ts +2 -0
- package/dist/components/text-editor/plugins/HorizontalRulePlugin.js +35 -0
- package/dist/components/text-editor/plugins/LineSpacingPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/LineSpacingPlugin.js +83 -0
- package/dist/components/text-editor/plugins/LinkPlugin.d.ts +8 -0
- package/dist/components/text-editor/plugins/LinkPlugin.js +183 -0
- package/dist/components/text-editor/plugins/ListToolbarPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/ListToolbarPlugin.js +40 -0
- package/dist/components/text-editor/plugins/LoadContentPlugin.d.ts +4 -0
- package/dist/components/text-editor/plugins/LoadContentPlugin.js +63 -0
- package/dist/components/text-editor/plugins/MarkdownExportPlugin.d.ts +4 -0
- package/dist/components/text-editor/plugins/MarkdownExportPlugin.js +17 -0
- package/dist/components/text-editor/plugins/MarkdownPastePlugin.d.ts +2 -0
- package/dist/components/text-editor/plugins/MarkdownPastePlugin.js +45 -0
- package/dist/components/text-editor/plugins/SanitizePastePlugin.d.ts +3 -0
- package/dist/components/text-editor/plugins/SanitizePastePlugin.js +40 -0
- package/dist/components/text-editor/plugins/TextAlignPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/TextAlignPlugin.js +65 -0
- package/dist/components/text-editor/plugins/TextColorPlugin.d.ts +7 -0
- package/dist/components/text-editor/plugins/TextColorPlugin.js +117 -0
- package/dist/components/text-editor/plugins/ToolbarPlugin.d.ts +10 -0
- package/dist/components/text-editor/plugins/ToolbarPlugin.js +73 -0
- package/dist/components/text-editor/theme-colors.d.ts +11 -0
- package/dist/components/text-editor/theme-colors.js +28 -0
- package/dist/context/WrAppContext.d.ts +35 -0
- package/dist/context/WrAppContext.js +113 -0
- package/dist/context/WrLayout.d.ts +15 -0
- package/dist/context/WrLayout.js +22 -0
- package/dist/context/WrPageContext.d.ts +28 -0
- package/dist/context/WrPageContext.js +282 -0
- package/dist/context/restoreValues.d.ts +2 -0
- package/dist/context/restoreValues.js +6 -0
- package/dist/contrast.d.ts +45 -0
- package/dist/contrast.js +122 -0
- package/dist/drop.d.ts +7 -0
- package/dist/drop.js +51 -0
- package/dist/gaps.d.ts +14 -0
- package/dist/gaps.js +50 -0
- package/dist/hooks/useWrMessage.d.ts +5 -0
- package/dist/hooks/useWrMessage.js +24 -0
- package/dist/hooks/useWrSave.d.ts +2 -0
- package/dist/hooks/useWrSave.js +11 -0
- package/dist/index.css +467 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/interactive.d.ts +8 -0
- package/dist/interactive.js +13 -0
- package/dist/measure-rules.d.ts +34 -0
- package/dist/measure-rules.js +54 -0
- package/dist/measure.d.ts +11 -0
- package/dist/measure.js +139 -0
- package/dist/messages.d.ts +860 -0
- package/dist/messages.js +2 -0
- package/dist/nav.d.ts +5 -0
- package/dist/nav.js +10 -0
- package/dist/operations/animations-store.d.ts +6 -0
- package/dist/operations/animations-store.js +63 -0
- package/dist/operations/cli.d.ts +3 -0
- package/dist/operations/cli.js +52 -0
- package/dist/operations/comments-store.d.ts +17 -0
- package/dist/operations/comments-store.js +62 -0
- package/dist/operations/context.d.ts +12 -0
- package/dist/operations/context.js +35 -0
- package/dist/operations/define.d.ts +81 -0
- package/dist/operations/define.js +86 -0
- package/dist/operations/index.d.ts +6 -0
- package/dist/operations/index.js +6 -0
- package/dist/operations/ops/check.d.ts +5 -0
- package/dist/operations/ops/check.js +53 -0
- package/dist/operations/ops/comments.d.ts +6 -0
- package/dist/operations/ops/comments.js +133 -0
- package/dist/operations/ops/content.d.ts +16 -0
- package/dist/operations/ops/content.js +73 -0
- package/dist/operations/ops/json.d.ts +4 -0
- package/dist/operations/ops/json.js +27 -0
- package/dist/operations/ops/langs.d.ts +4 -0
- package/dist/operations/ops/langs.js +32 -0
- package/dist/operations/ops/nodes.d.ts +8 -0
- package/dist/operations/ops/nodes.js +270 -0
- package/dist/operations/ops/pages.d.ts +8 -0
- package/dist/operations/ops/pages.js +154 -0
- package/dist/operations/ops/project.d.ts +36 -0
- package/dist/operations/ops/project.js +97 -0
- package/dist/operations/ops/seo.d.ts +17 -0
- package/dist/operations/ops/seo.js +358 -0
- package/dist/operations/ops/style.d.ts +76 -0
- package/dist/operations/ops/style.js +268 -0
- package/dist/operations/ops/stylesets.d.ts +9 -0
- package/dist/operations/ops/stylesets.js +145 -0
- package/dist/operations/ops/theme.d.ts +6 -0
- package/dist/operations/ops/theme.js +304 -0
- package/dist/operations/registry.d.ts +8 -0
- package/dist/operations/registry.js +19 -0
- package/dist/operations/stylesets-store.d.ts +50 -0
- package/dist/operations/stylesets-store.js +216 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.js +24 -0
- package/dist/save.d.ts +65 -0
- package/dist/save.js +86 -0
- package/dist/schema.d.ts +69 -0
- package/dist/schema.js +110 -0
- package/dist/scope.d.ts +41 -0
- package/dist/scope.js +44 -0
- package/dist/vite-plugins/ast.d.ts +3 -0
- package/dist/vite-plugins/ast.js +238 -0
- package/dist/vite-plugins/core-swap.d.ts +3 -0
- package/dist/vite-plugins/core-swap.js +57 -0
- package/dist/vite-plugins/create-page.d.ts +4 -0
- package/dist/vite-plugins/create-page.js +59 -0
- package/dist/vite-plugins/guard.d.ts +15 -0
- package/dist/vite-plugins/guard.js +35 -0
- package/dist/vite-plugins/head.d.ts +13 -0
- package/dist/vite-plugins/head.js +53 -0
- package/dist/vite-plugins/index.d.ts +7 -0
- package/dist/vite-plugins/index.js +7 -0
- package/dist/vite-plugins/jsx-tree.d.ts +118 -0
- package/dist/vite-plugins/jsx-tree.js +686 -0
- package/dist/vite-plugins/langs.d.ts +3 -0
- package/dist/vite-plugins/langs.js +44 -0
- package/dist/vite-plugins/operations.d.ts +10 -0
- package/dist/vite-plugins/operations.js +66 -0
- package/dist/vite-plugins/pages-api.d.ts +3 -0
- package/dist/vite-plugins/pages-api.js +112 -0
- package/dist/vite-plugins/pages.d.ts +3 -0
- package/dist/vite-plugins/pages.js +64 -0
- package/dist/vite-plugins/read.d.ts +3 -0
- package/dist/vite-plugins/read.js +45 -0
- package/dist/vite-plugins/seo-files.d.ts +8 -0
- package/dist/vite-plugins/seo-files.js +42 -0
- package/dist/vite-plugins/theme.d.ts +3 -0
- package/dist/vite-plugins/theme.js +207 -0
- package/dist/vite-plugins/update.d.ts +3 -0
- package/dist/vite-plugins/update.js +46 -0
- package/dist/vite-plugins/wr-plugins.d.ts +15 -0
- package/dist/vite-plugins/wr-plugins.js +34 -0
- package/dist/vite-plugins.d.ts +11 -0
- package/dist/vite-plugins.js +518 -0
- package/package.json +96 -5
- package/README.md +0 -3
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
4
|
+
import { WR_COMPONENT_TO_KIND, wrTagOf } from '@webround/core/kinds';
|
|
5
|
+
import { wrAriaLabel, wrLocalized } from '@webround/core/localized';
|
|
6
|
+
import { WR_SHARED_SCOPES, wrPageConfigPath } from '@webround/core/routing';
|
|
7
|
+
import { wrJsonLdProblems, wrSeoPageOf } from '@webround/core/seo';
|
|
8
|
+
import { findPageRoot, syntaxErrors } from '../vite-plugins/jsx-tree.js';
|
|
9
|
+
import { createOperationContext } from '../operations/context.js';
|
|
10
|
+
import { readJsonFile, readLangs, readRoutes, readSeoSite, scopesOfPage, sharedScopesOf } from '../operations/ops/project.js';
|
|
11
|
+
import { multilangFieldsOf } from '../schema.js';
|
|
12
|
+
import { htmlNodes } from './html.js';
|
|
13
|
+
import { findOrphanKeys, idsLabelledInCode } from './orphans.js';
|
|
14
|
+
import { readCss } from './css.js';
|
|
15
|
+
import { stripTags } from './types.js';
|
|
16
|
+
/** A follower for ONE page: the shared components a site declares, none seen yet. The header and the footer are read as the scopes they are, not followed. */
|
|
17
|
+
export function sharedFollower(ctx) {
|
|
18
|
+
const own = Object.values(WR_SHARED_SCOPES).map(scope => scope.name);
|
|
19
|
+
const scopes = sharedScopesOf(ctx).filter(scope => !own.includes(scope.name)).map(scope => ({ name: scope.name, file: scope.file, json: wrPageConfigPath(scope.name) }));
|
|
20
|
+
return { ctx, scopes, seen: new Set(), syntax: [] };
|
|
21
|
+
}
|
|
22
|
+
/** The file (relative to `src/`) the component `tag` is imported from, when it is a relative or `@/` import; null if it is not one of the file's imports. */
|
|
23
|
+
function importedFileOf(el, tag, ctx) {
|
|
24
|
+
const sourceFile = el.getSourceFile();
|
|
25
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
26
|
+
const binds = declaration.getDefaultImport()?.getText() === tag
|
|
27
|
+
|| declaration.getNamedImports().some(named => (named.getAliasNode()?.getText() ?? named.getName()) === tag);
|
|
28
|
+
if (!binds)
|
|
29
|
+
continue;
|
|
30
|
+
const specifier = declaration.getModuleSpecifierValue();
|
|
31
|
+
const base = specifier.startsWith('@/') ? path.join(ctx.srcRoot, specifier.slice(2)) : specifier.startsWith('.') ? path.resolve(path.dirname(sourceFile.getFilePath()), specifier) : null;
|
|
32
|
+
if (!base)
|
|
33
|
+
return null;
|
|
34
|
+
const found = [base, `${base}.tsx`, path.join(base, 'index.tsx')].find(candidate => candidate.endsWith('.tsx') && fs.existsSync(candidate));
|
|
35
|
+
if (!found)
|
|
36
|
+
return null;
|
|
37
|
+
const relative = path.relative(ctx.srcRoot, found).split(path.sep).join('/');
|
|
38
|
+
return relative.startsWith('..') ? null : relative;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** What a shared component the page uses holds, read from its own file: nothing if `tag` is not one, or it was already read for this page. */
|
|
43
|
+
function sharedNodesOf(el, tag, options) {
|
|
44
|
+
const follow = options.follow;
|
|
45
|
+
if (!follow)
|
|
46
|
+
return [];
|
|
47
|
+
const file = importedFileOf(el, tag, follow.ctx);
|
|
48
|
+
const scope = file ? follow.scopes.find(candidate => candidate.file === file) : undefined;
|
|
49
|
+
if (!scope || follow.seen.has(scope.name))
|
|
50
|
+
return [];
|
|
51
|
+
const read = checkScopeFromCode(follow.ctx, scope, { includeHidden: options.includeHidden, lang: options.lang, follow });
|
|
52
|
+
follow.syntax.push(...read.syntax);
|
|
53
|
+
return read.nodes;
|
|
54
|
+
}
|
|
55
|
+
/** What the visitor of a language reads (the default one's when it is not translated: the components do the same). */
|
|
56
|
+
const inLang = (value, options) => wrLocalized(value, options.lang ?? 'default');
|
|
57
|
+
// What stands for text the code decides at run time: it is not empty, we just cannot read it
|
|
58
|
+
const DYNAMIC_TEXT = '{…}';
|
|
59
|
+
const isJsx = (n) => n.isKind(SyntaxKind.JsxElement) || n.isKind(SyntaxKind.JsxSelfClosingElement);
|
|
60
|
+
function attrOf(node, name) {
|
|
61
|
+
const opening = node.isKind(SyntaxKind.JsxElement) ? node.getOpeningElement() : node;
|
|
62
|
+
const attr = opening.getAttribute(name);
|
|
63
|
+
if (!attr || !attr.isKind(SyntaxKind.JsxAttribute))
|
|
64
|
+
return undefined;
|
|
65
|
+
const init = attr.getInitializer();
|
|
66
|
+
if (!init)
|
|
67
|
+
return true;
|
|
68
|
+
switch (init.getKind()) {
|
|
69
|
+
case SyntaxKind.StringLiteral:
|
|
70
|
+
return init.getLiteralValue();
|
|
71
|
+
case SyntaxKind.JsxExpression: {
|
|
72
|
+
const expr = init.getExpression();
|
|
73
|
+
switch (expr?.getKind()) {
|
|
74
|
+
case SyntaxKind.StringLiteral:
|
|
75
|
+
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
76
|
+
return expr.getLiteralValue();
|
|
77
|
+
case SyntaxKind.NumericLiteral:
|
|
78
|
+
return expr.getText();
|
|
79
|
+
case SyntaxKind.PrefixUnaryExpression:
|
|
80
|
+
return /^-\s*\d+(\.\d+)?$/.test(expr.getText()) ? expr.getText().replace(/\s+/g, '') : DYNAMIC_TEXT;
|
|
81
|
+
case SyntaxKind.TrueKeyword:
|
|
82
|
+
return true;
|
|
83
|
+
case SyntaxKind.FalseKeyword:
|
|
84
|
+
return 'false';
|
|
85
|
+
default:
|
|
86
|
+
return DYNAMIC_TEXT; // set by an expression: present, value unknown
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const str = (v) => typeof v === 'string' ? v : undefined;
|
|
94
|
+
/** The JSX directly under a node, looking through `{cond && <X/>}`, fragments and the like. */
|
|
95
|
+
function jsxUnder(node, out = []) {
|
|
96
|
+
node.forEachChild(child => {
|
|
97
|
+
if (isJsx(child))
|
|
98
|
+
out.push(child);
|
|
99
|
+
else
|
|
100
|
+
jsxUnder(child, out);
|
|
101
|
+
});
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function textOf(node, children) {
|
|
105
|
+
if (node.isKind(SyntaxKind.JsxSelfClosingElement))
|
|
106
|
+
return '';
|
|
107
|
+
const own = node.getJsxChildren().map(child => {
|
|
108
|
+
if (child.isKind(SyntaxKind.JsxText))
|
|
109
|
+
return child.getText();
|
|
110
|
+
if (child.isKind(SyntaxKind.JsxExpression) && child.getExpression() && jsxUnder(child).length === 0)
|
|
111
|
+
return DYNAMIC_TEXT;
|
|
112
|
+
return '';
|
|
113
|
+
}).join(' ');
|
|
114
|
+
return stripTags([own, ...children.map(c => c.text)].join(' '));
|
|
115
|
+
}
|
|
116
|
+
/** The sides of the conditions between a JSX element and the node that holds it: which side of a `c ? a : b` it is on, or of a `c && a` (against a `!c && b`). */
|
|
117
|
+
function branchesOf(el, parent) {
|
|
118
|
+
const out = [];
|
|
119
|
+
for (let child = el, up = el.getParent(); up && up !== parent; child = up, up = up.getParent()) {
|
|
120
|
+
if (up.isKind(SyntaxKind.ConditionalExpression)) {
|
|
121
|
+
if (child === up.getWhenTrue())
|
|
122
|
+
out.unshift({ key: `?${up.getStart()}`, side: 'then' });
|
|
123
|
+
else if (child === up.getWhenFalse())
|
|
124
|
+
out.unshift({ key: `?${up.getStart()}`, side: 'else' });
|
|
125
|
+
}
|
|
126
|
+
else if (up.isKind(SyntaxKind.BinaryExpression) && up.getOperatorToken().isKind(SyntaxKind.AmpersandAmpersandToken) && child === up.getRight()) {
|
|
127
|
+
const condition = up.getLeft().getText().replace(/\s+/g, '');
|
|
128
|
+
const negated = /^!(?!=)/.test(condition);
|
|
129
|
+
out.unshift({ key: `&&${negated ? condition.slice(1) : condition}`, side: negated ? 'not' : 'is' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** The nodes, and what is inside them, are under these sides of conditions: the ones they were already under are the inner ones. */
|
|
135
|
+
function markBranches(nodes, branch) {
|
|
136
|
+
if (branch.length === 0)
|
|
137
|
+
return;
|
|
138
|
+
for (const node of nodes) {
|
|
139
|
+
node.branch = [...branch, ...(node.branch ?? [])];
|
|
140
|
+
markBranches(node.children, branch);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function nodesUnder(parent, config, file, options) {
|
|
144
|
+
const nodes = [];
|
|
145
|
+
for (const el of jsxUnder(parent)) {
|
|
146
|
+
const made = fromJsx(el, config, file, options);
|
|
147
|
+
markBranches(made, branchesOf(el, parent));
|
|
148
|
+
nodes.push(...made);
|
|
149
|
+
}
|
|
150
|
+
return nodes;
|
|
151
|
+
}
|
|
152
|
+
/** What a field says in each language: the JSON's (a plain string is the default language's), with the code's value as the default when the JSON has none. */
|
|
153
|
+
function valuesOf(jsonValue, codeDefault) {
|
|
154
|
+
const map = typeof jsonValue === 'string' ? { default: jsonValue }
|
|
155
|
+
: typeof jsonValue === 'object' && jsonValue !== null ? Object.fromEntries(Object.entries(jsonValue).filter((entry) => typeof entry[1] === 'string')) : {};
|
|
156
|
+
if (map.default === undefined && codeDefault !== undefined)
|
|
157
|
+
map.default = codeDefault;
|
|
158
|
+
return Object.keys(map).length > 0 ? map : undefined;
|
|
159
|
+
}
|
|
160
|
+
/** Event props that make a button do something, and the attributes that make the browser do it (a submit, a popover…). */
|
|
161
|
+
const ACTION_PROPS = /^(on[A-Z]\w*|form|formAction|popoverTarget|command|commandFor)$/;
|
|
162
|
+
/** Elements that give a button around which it something to do: a form it belongs to, a link it is inside. */
|
|
163
|
+
const ACTING_ANCESTORS = new Set(['form', 'a', 'WrLink', 'Link', 'NavLink']);
|
|
164
|
+
/**
|
|
165
|
+
* Does pressing this button do anything? Yes when it has a handler (`onClick`…), is a submit/reset button (a `type` the code decides counts),
|
|
166
|
+
* has props spread into it (they may bring a handler: not knowable, not accused), or sits inside a form or a link. A button that is none of
|
|
167
|
+
* these is a drawing of a button.
|
|
168
|
+
*/
|
|
169
|
+
function buttonActs(el) {
|
|
170
|
+
const opening = el.isKind(SyntaxKind.JsxElement) ? el.getOpeningElement() : el;
|
|
171
|
+
for (const attribute of opening.getAttributes()) {
|
|
172
|
+
if (attribute.isKind(SyntaxKind.JsxSpreadAttribute))
|
|
173
|
+
return true;
|
|
174
|
+
const name = attribute.isKind(SyntaxKind.JsxAttribute) ? attribute.getNameNode().getText() : '';
|
|
175
|
+
if (ACTION_PROPS.test(name))
|
|
176
|
+
return true;
|
|
177
|
+
if (name === 'type') {
|
|
178
|
+
const type = str(attrOf(el, 'type'));
|
|
179
|
+
if (type !== 'button')
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return el.getAncestors().some(ancestor => {
|
|
184
|
+
if (!isJsx(ancestor))
|
|
185
|
+
return false;
|
|
186
|
+
const tag = (ancestor.isKind(SyntaxKind.JsxElement) ? ancestor.getOpeningElement() : ancestor).getTagNameNode().getText();
|
|
187
|
+
return ACTING_ANCESTORS.has(tag);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** Attributes of an element only the accessibility rules read. `{spread}` says props are spread into it: what they bring is not knowable. */
|
|
191
|
+
const A11Y_ATTRS = ['title', 'role', 'type', 'name', 'autoComplete', 'className', 'tabIndex', 'aria-labelledby', 'aria-describedby', 'aria-hidden', 'autoPlay', 'controls', 'muted'];
|
|
192
|
+
const KEY_HANDLERS = ['onKeyDown', 'onKeyUp', 'onKeyPress'];
|
|
193
|
+
function accessibilityAttrs(el) {
|
|
194
|
+
const out = {};
|
|
195
|
+
const opening = el.isKind(SyntaxKind.JsxElement) ? el.getOpeningElement() : el;
|
|
196
|
+
for (const name of A11Y_ATTRS) {
|
|
197
|
+
const value = attrOf(el, name);
|
|
198
|
+
if (value !== undefined)
|
|
199
|
+
out[name === 'className' ? 'class' : name.toLowerCase()] = value === true ? 'true' : String(value);
|
|
200
|
+
}
|
|
201
|
+
const names = opening.getAttributes().map(a => (a.isKind(SyntaxKind.JsxAttribute) ? a.getNameNode().getText() : '{spread}'));
|
|
202
|
+
if (names.includes('onClick'))
|
|
203
|
+
out.onclick = 'true';
|
|
204
|
+
if (KEY_HANDLERS.some(handler => names.includes(handler)))
|
|
205
|
+
out.onkeydown = 'true';
|
|
206
|
+
if (names.includes('{spread}'))
|
|
207
|
+
out['{spread}'] = 'true';
|
|
208
|
+
// Every aria-* the code writes (the names: a typo is found by them), and whether it has a value
|
|
209
|
+
const aria = names.filter(name => name.startsWith('aria-'));
|
|
210
|
+
if (aria.length > 0)
|
|
211
|
+
out['{aria}'] = aria.join(' ');
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function fromJsx(el, config, file, options) {
|
|
215
|
+
const opening = el.isKind(SyntaxKind.JsxElement) ? el.getOpeningElement() : el;
|
|
216
|
+
const name = opening.getTagNameNode().getText();
|
|
217
|
+
const wrKind = WR_COMPONENT_TO_KIND[name];
|
|
218
|
+
const id = str(attrOf(el, 'id'));
|
|
219
|
+
const json = (wrKind && id ? config[id] : undefined) ?? {};
|
|
220
|
+
const hidden = json.hidden === true;
|
|
221
|
+
if (hidden && !options.includeHidden)
|
|
222
|
+
return []; // does not render
|
|
223
|
+
const tag = wrKind ? wrTagOf(wrKind, str(attrOf(el, 'tag'))) : name.toLowerCase();
|
|
224
|
+
const kind = wrKind ?? 'element';
|
|
225
|
+
const children = el.isKind(SyntaxKind.JsxElement) ? nodesUnder(el, config, file, options) : [];
|
|
226
|
+
// Plain components we know nothing about (`<Card>`) are only transparent: what they hold is what counts. A shared one is entered: what it renders is part of the page
|
|
227
|
+
if (!wrKind && /^[A-Z]/.test(name))
|
|
228
|
+
return [...sharedNodesOf(el, name, options), ...children];
|
|
229
|
+
const attrs = {};
|
|
230
|
+
const alt = inLang(json.alt, options) ?? str(attrOf(el, 'alt'));
|
|
231
|
+
if (alt !== undefined)
|
|
232
|
+
attrs.alt = alt;
|
|
233
|
+
const aria = wrAriaLabel(json, str(attrOf(el, 'aria-label')), options.lang ?? 'default');
|
|
234
|
+
if (aria !== undefined)
|
|
235
|
+
attrs['aria-label'] = aria;
|
|
236
|
+
const href = str(attrOf(el, 'href'));
|
|
237
|
+
if (href !== undefined)
|
|
238
|
+
attrs.href = href;
|
|
239
|
+
Object.assign(attrs, accessibilityAttrs(el));
|
|
240
|
+
const content = inLang(json.content, options) ?? (wrKind === 'text' ? str(attrOf(el, 'html')) : undefined);
|
|
241
|
+
const text = content !== undefined ? stripTags(content) : textOf(el, children);
|
|
242
|
+
// The fields that are translated (the schema says which), in each language
|
|
243
|
+
const codeDefault = (field) => {
|
|
244
|
+
switch (field) {
|
|
245
|
+
case 'content': return wrKind === 'text' ? str(attrOf(el, 'html')) : textOf(el, children) || undefined;
|
|
246
|
+
case 'alt': return str(attrOf(el, 'alt'));
|
|
247
|
+
case 'ariaLabel': return str(attrOf(el, 'aria-label'));
|
|
248
|
+
case 'label':
|
|
249
|
+
case 'placeholder': return str(attrOf(el, field));
|
|
250
|
+
default: return undefined;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const values = {};
|
|
254
|
+
if (wrKind) {
|
|
255
|
+
for (const field of multilangFieldsOf(wrKind)) {
|
|
256
|
+
const perLanguage = valuesOf(json[field], codeDefault(field));
|
|
257
|
+
if (perLanguage)
|
|
258
|
+
values[field] = perLanguage;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// A rich text holds headings and links the code does not show: they are part of the page (the JSON's string is all there is to read)
|
|
262
|
+
const inside = wrKind === 'text' && content !== undefined
|
|
263
|
+
? htmlNodes(content).map(node => ({ ...node, ...(options.scope ? { scope: options.scope } : {}), where: { file, line: el.getStartLineNumber() } }))
|
|
264
|
+
: [];
|
|
265
|
+
// The entries of a menu are links the JSON holds
|
|
266
|
+
const entries = wrKind === 'nav' && Array.isArray(json.items)
|
|
267
|
+
? json.items.map((item) => ({
|
|
268
|
+
kind: 'element', tag: 'a', attrs: typeof item.href === 'string' ? { href: item.href } : {}, text: inLang(item.label, options) ?? '', children: [],
|
|
269
|
+
...(options.scope ? { scope: options.scope } : {}), where: { file, line: el.getStartLineNumber() },
|
|
270
|
+
}))
|
|
271
|
+
: [];
|
|
272
|
+
return [{
|
|
273
|
+
...(id ? { id } : {}), kind, tag, attrs, text, children: [...children, ...inside, ...entries],
|
|
274
|
+
...(tag === 'button' ? { acts: buttonActs(el) } : {}),
|
|
275
|
+
...(['input', 'select', 'textarea'].includes(tag) ? { insideLabel: el.getAncestors().some(a => isJsx(a) && (a.isKind(SyntaxKind.JsxElement) ? a.getOpeningElement() : a).getTagNameNode().getText() === 'label') } : {}),
|
|
276
|
+
...(options.scope ? { scope: options.scope } : {}),
|
|
277
|
+
...(hidden ? { hidden: true } : {}),
|
|
278
|
+
...(Object.keys(values).length > 0 ? { values } : {}),
|
|
279
|
+
where: { file, line: el.getStartLineNumber() },
|
|
280
|
+
}];
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* One scope of a site as the neutral tree: the elements of its code, with what its JSON says. Read by the checks (a page is its
|
|
284
|
+
* scopes together) and by whatever has to know what is on a page, in each language (the outline an agent reads).
|
|
285
|
+
*/
|
|
286
|
+
export function checkScopeFromCode(ctx, scope, options = {}) {
|
|
287
|
+
const syntax = [];
|
|
288
|
+
options.follow?.seen.add(scope.name);
|
|
289
|
+
const abs = ctx.resolveInSrc(scope.file);
|
|
290
|
+
if (!abs)
|
|
291
|
+
return { name: scope.name, file: scope.file, nodes: [], syntax };
|
|
292
|
+
const project = ctx.project();
|
|
293
|
+
let sourceFile;
|
|
294
|
+
try {
|
|
295
|
+
sourceFile = project.addSourceFileAtPath(abs);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return { name: scope.name, file: scope.file, nodes: [], syntax };
|
|
299
|
+
}
|
|
300
|
+
sourceFile.refreshFromFileSystemSync();
|
|
301
|
+
for (const { message, line } of syntaxErrors(sourceFile.compilerNode))
|
|
302
|
+
syntax.push({ file: scope.file, line, message });
|
|
303
|
+
const rootJsx = findPageRoot(sourceFile);
|
|
304
|
+
if (!rootJsx)
|
|
305
|
+
return { name: scope.name, file: scope.file, nodes: [], syntax };
|
|
306
|
+
const config = readJsonFile(ctx, scope.json) ?? {};
|
|
307
|
+
const read = { ...options, scope: options.scope ?? scope.name };
|
|
308
|
+
const nodes = isJsx(rootJsx) ? fromJsx(rootJsx, config, scope.file, read) : nodesUnder(rootJsx, config, scope.file, read);
|
|
309
|
+
return { name: scope.name, file: scope.file, nodes, syntax };
|
|
310
|
+
}
|
|
311
|
+
/** What a page tells search engines, as the rules read it: every text by language key (a plain string is the default's). */
|
|
312
|
+
function seoOfPage(ctx, name) {
|
|
313
|
+
const seo = wrSeoPageOf(readJsonFile(ctx, wrPageConfigPath(name)) ?? undefined);
|
|
314
|
+
const byLang = (value) => typeof value === 'string' ? { default: value } : { ...(value ?? {}) };
|
|
315
|
+
return { title: byLang(seo.title), description: byLang(seo.description), noindex: seo.noindex === true, family: /\[[^\]]+\]/.test(name), jsonLdProblems: wrJsonLdProblems(seo.jsonLd) };
|
|
316
|
+
}
|
|
317
|
+
/** What a link may lead to: the routes, the languages and the files served as they are. */
|
|
318
|
+
function projectOf(ctx) {
|
|
319
|
+
const langs = readLangs(ctx);
|
|
320
|
+
return {
|
|
321
|
+
routes: Object.keys(readRoutes(ctx)),
|
|
322
|
+
...(langs ? { langs: { default: langs.default, available: langs.available } } : {}),
|
|
323
|
+
publicFiles: listFiles(ctx.publicRoot),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/** Every file under a folder, as paths from it (`/robots.txt`, `/media/a.pdf`); none when there is no such folder. */
|
|
327
|
+
function listFiles(dir, prefix = '') {
|
|
328
|
+
if (!fs.existsSync(dir))
|
|
329
|
+
return [];
|
|
330
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? listFiles(path.join(dir, entry.name), `${prefix}/${entry.name}`) : [`${prefix}/${entry.name}`]);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The pages of a site, as the neutral tree: header + page + footer, the way the visitor of a language sees them (`default` unless asked).
|
|
334
|
+
* The checks run once per language and say, for each problem, in which ones it happens.
|
|
335
|
+
*/
|
|
336
|
+
export function checkPagesFromCode(root, lang = 'default') {
|
|
337
|
+
const ctx = createOperationContext(root, { project: () => new Project({ skipAddingFilesFromTsConfig: true, compilerOptions: { jsx: 4 } }) });
|
|
338
|
+
const langs = readLangs(ctx);
|
|
339
|
+
const multilingual = langs !== null && langs.available.length > 1;
|
|
340
|
+
const project = projectOf(ctx);
|
|
341
|
+
const labelledIds = idsLabelledInCode(ctx);
|
|
342
|
+
return Object.keys(readRoutes(ctx)).map(name => {
|
|
343
|
+
const follow = sharedFollower(ctx);
|
|
344
|
+
const scopes = scopesOfPage(ctx, name).map(scope => checkScopeFromCode(ctx, scope, { lang, follow }));
|
|
345
|
+
const syntax = [...scopes.flatMap(scope => scope.syntax), ...follow.syntax];
|
|
346
|
+
return {
|
|
347
|
+
name, nodes: scopes.flatMap(scope => scope.nodes), ...(syntax.length ? { syntax } : {}),
|
|
348
|
+
...(multilingual ? { lang } : {}), project, labelledIds, seo: seoOfPage(ctx, name),
|
|
349
|
+
};
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The languages a site is checked in: `key` is what the JSON calls it (`default`, or the code), `code` is what people call it (`it`).
|
|
354
|
+
* Just the default one when the site has a single language.
|
|
355
|
+
*/
|
|
356
|
+
export function languagesOf(root) {
|
|
357
|
+
const langs = readLangs(createOperationContext(root));
|
|
358
|
+
if (!langs || langs.available.length < 2)
|
|
359
|
+
return [{ key: 'default', code: langs?.default ?? 'default' }];
|
|
360
|
+
return [{ key: 'default', code: langs.default }, ...langs.available.filter(code => code !== langs.default).map(code => ({ key: code, code }))];
|
|
361
|
+
}
|
|
362
|
+
/** What `index.html` says that matters here: the language of the page and the viewport. */
|
|
363
|
+
function readIndexHtml(root) {
|
|
364
|
+
const file = path.join(root, 'index.html');
|
|
365
|
+
if (!fs.existsSync(file))
|
|
366
|
+
return null;
|
|
367
|
+
const html = fs.readFileSync(file, 'utf-8');
|
|
368
|
+
const lang = /<html\b[^>]*\blang\s*=\s*["']([^"']+)["']/i.exec(html)?.[1] ?? null;
|
|
369
|
+
const viewport = /<meta\b[^>]*\bname\s*=\s*["']viewport["'][^>]*\bcontent\s*=\s*["']([^"']*)["']/i.exec(html)?.[1]
|
|
370
|
+
?? /<meta\b[^>]*\bcontent\s*=\s*["']([^"']*)["'][^>]*\bname\s*=\s*["']viewport["']/i.exec(html)?.[1] ?? null;
|
|
371
|
+
return { lang, viewport };
|
|
372
|
+
}
|
|
373
|
+
/** What the checks know of the site as a whole. */
|
|
374
|
+
export function checkSiteFromCode(root) {
|
|
375
|
+
const ctx = createOperationContext(root);
|
|
376
|
+
const { orphans, dynamicIds } = findOrphanKeys(ctx);
|
|
377
|
+
return { seo: readSeoSite(ctx), index: readIndexHtml(ctx.root), css: readCss(ctx.srcRoot), orphans, dynamicIds };
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=producer-code.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CheckPage } from './types.js';
|
|
2
|
+
/** What the page holds, as plain data: `wrType` is the raw `data-wr-type`, not yet checked against the kinds. */
|
|
3
|
+
export interface DomNode {
|
|
4
|
+
id?: string;
|
|
5
|
+
wrType: string | null;
|
|
6
|
+
tag: string;
|
|
7
|
+
attrs: Record<string, string>;
|
|
8
|
+
text: string;
|
|
9
|
+
/** A form field that has a `<label>`: it sits inside one, or one points at it (`for`). */
|
|
10
|
+
labelled?: true;
|
|
11
|
+
children: DomNode[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reads the children of `root` as plain data. SELF-CONTAINED ON PURPOSE: its source is sent to a browser to be run there
|
|
15
|
+
* (`serializeDom.toString()`), so it must not import or refer to anything outside itself.
|
|
16
|
+
*/
|
|
17
|
+
export declare function serializeDom(root: Element): DomNode[];
|
|
18
|
+
/** The tree a rule sees, from what `serializeDom` read (wherever it ran). */
|
|
19
|
+
export declare function checkPageFromSerialized(name: string, nodes: readonly DomNode[]): CheckPage;
|
|
20
|
+
export declare function checkPageFromDom(name: string, root: Element): CheckPage;
|
|
21
|
+
//# sourceMappingURL=producer-dom.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isWrKind } from '@webround/core/kinds';
|
|
2
|
+
/**
|
|
3
|
+
* Reads the children of `root` as plain data. SELF-CONTAINED ON PURPOSE: its source is sent to a browser to be run there
|
|
4
|
+
* (`serializeDom.toString()`), so it must not import or refer to anything outside itself.
|
|
5
|
+
*/
|
|
6
|
+
export function serializeDom(root) {
|
|
7
|
+
const skip = ['script', 'style', 'noscript', 'template'];
|
|
8
|
+
const attributes = ['alt', 'aria-label', 'aria-labelledby', 'title', 'href'];
|
|
9
|
+
const fields = ['input', 'select', 'textarea'];
|
|
10
|
+
// The fields some `<label for>` points at (`closest` and `ownerDocument` are asked with `?.`: a stand-in for the DOM in a test does not have them)
|
|
11
|
+
const pointedAt = new Set(Array.from(root.ownerDocument?.querySelectorAll('label[for]') ?? []).map(label => label.getAttribute('for')));
|
|
12
|
+
const visible = (parent) => Array.from(parent.children).filter(child => !skip.includes(child.tagName.toLowerCase()));
|
|
13
|
+
const read = (el) => {
|
|
14
|
+
const attrs = {};
|
|
15
|
+
for (const name of attributes) {
|
|
16
|
+
const value = el.getAttribute(name);
|
|
17
|
+
if (value !== null)
|
|
18
|
+
attrs[name] = value;
|
|
19
|
+
}
|
|
20
|
+
const id = el.getAttribute('data-wr-id');
|
|
21
|
+
const tag = el.tagName.toLowerCase();
|
|
22
|
+
const fieldId = el.getAttribute('id');
|
|
23
|
+
const labelled = fields.includes(tag) && (el.closest?.('label') != null || (fieldId !== null && pointedAt.has(fieldId)));
|
|
24
|
+
return {
|
|
25
|
+
...(id ? { id } : {}),
|
|
26
|
+
wrType: el.getAttribute('data-wr-type'),
|
|
27
|
+
tag,
|
|
28
|
+
attrs,
|
|
29
|
+
...(labelled ? { labelled: true } : {}),
|
|
30
|
+
text: (el.textContent ?? '').replace(/\s+/g, ' ').trim(),
|
|
31
|
+
children: visible(el).map(read),
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
return visible(root).map(read);
|
|
35
|
+
}
|
|
36
|
+
function toCheckNode(node) {
|
|
37
|
+
return {
|
|
38
|
+
...(node.id ? { id: node.id } : {}),
|
|
39
|
+
kind: isWrKind(node.wrType) ? node.wrType : 'element',
|
|
40
|
+
tag: node.tag, attrs: node.attrs, text: node.text,
|
|
41
|
+
...(node.labelled ? { insideLabel: true } : {}),
|
|
42
|
+
children: node.children.map(toCheckNode),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** The tree a rule sees, from what `serializeDom` read (wherever it ran). */
|
|
46
|
+
export function checkPageFromSerialized(name, nodes) {
|
|
47
|
+
return { name, source: 'dom', nodes: nodes.map(toCheckNode) };
|
|
48
|
+
}
|
|
49
|
+
export function checkPageFromDom(name, root) {
|
|
50
|
+
return checkPageFromSerialized(name, serializeDom(root));
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=producer-dom.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Issue } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The same problem found in several languages (by their codes) is ONE issue that says in which: a notification per language would be the same
|
|
4
|
+
* one n times. The same goes for a problem in a shared component (the header, the footer, a card): it is in every page that has it, but it is one
|
|
5
|
+
* problem, found in the first. `sharedFiles` are the files of those components.
|
|
6
|
+
*/
|
|
7
|
+
export declare function mergeLanguages(perLanguage: readonly {
|
|
8
|
+
lang: string;
|
|
9
|
+
issues: readonly Issue[];
|
|
10
|
+
}[], sharedFiles: readonly string[]): Issue[];
|
|
11
|
+
/**
|
|
12
|
+
* Everything the checks find in a project: its pages (header and footer included, the way a visitor of each language sees them) and the site
|
|
13
|
+
* as a whole. ONE function: `wr-check`, and with it the container's tools and a Claude Code hook, and the editor's notifications all call this.
|
|
14
|
+
* A problem that happens in some languages says which (`langs`); a site with one language says nothing about languages.
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkProject(root: string): Issue[];
|
|
17
|
+
/** Problems the person chose to leave: `src/config/checks.json`, `{ "muted": [key…] }`. */
|
|
18
|
+
export declare const CHECKS_FILE = "config/checks.json";
|
|
19
|
+
export declare const mutedKeys: (root: string) => string[];
|
|
20
|
+
//# sourceMappingURL=project.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { checkPagesFromCode, checkSiteFromCode, languagesOf } from './producer-code.js';
|
|
2
|
+
import { runRules, runSiteRules } from './rules.js';
|
|
3
|
+
import { createOperationContext } from '../operations/context.js';
|
|
4
|
+
import { readJsonFile, sharedScopesOf } from '../operations/ops/project.js';
|
|
5
|
+
import { issueKey } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* The same problem found in several languages (by their codes) is ONE issue that says in which: a notification per language would be the same
|
|
8
|
+
* one n times. The same goes for a problem in a shared component (the header, the footer, a card): it is in every page that has it, but it is one
|
|
9
|
+
* problem, found in the first. `sharedFiles` are the files of those components.
|
|
10
|
+
*/
|
|
11
|
+
export function mergeLanguages(perLanguage, sharedFiles) {
|
|
12
|
+
const merged = new Map();
|
|
13
|
+
for (const { lang, issues } of perLanguage) {
|
|
14
|
+
for (const issue of issues) {
|
|
15
|
+
const shared = issue.where !== undefined && sharedFiles.includes(issue.where.file);
|
|
16
|
+
const same = JSON.stringify([issue.rule, shared ? null : issue.page, issue.nodeId, issue.where, issue.message]);
|
|
17
|
+
const known = merged.get(same);
|
|
18
|
+
if (known) {
|
|
19
|
+
if (!known.langs?.includes(lang))
|
|
20
|
+
known.langs = [...(known.langs ?? []), lang];
|
|
21
|
+
}
|
|
22
|
+
// A shared component is in every page that has it: its problems are the site's, whichever page found them first
|
|
23
|
+
else
|
|
24
|
+
merged.set(same, { ...issue, langs: [lang], key: shared ? issueKey({ rule: issue.rule, page: '_shared', subject: issue.subject, fix: issue.fix }) : issue.key });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...merged.values()];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Everything the checks find in a project: its pages (header and footer included, the way a visitor of each language sees them) and the site
|
|
31
|
+
* as a whole. ONE function: `wr-check`, and with it the container's tools and a Claude Code hook, and the editor's notifications all call this.
|
|
32
|
+
* A problem that happens in some languages says which (`langs`); a site with one language says nothing about languages.
|
|
33
|
+
*/
|
|
34
|
+
export function checkProject(root) {
|
|
35
|
+
const muted = new Set(mutedKeys(root));
|
|
36
|
+
return withMuted(checkAll(root), muted);
|
|
37
|
+
}
|
|
38
|
+
/** Problems the person chose to leave: `src/config/checks.json`, `{ "muted": [key…] }`. */
|
|
39
|
+
export const CHECKS_FILE = 'config/checks.json';
|
|
40
|
+
export const mutedKeys = (root) => {
|
|
41
|
+
const muted = readJsonFile(createOperationContext(root), CHECKS_FILE)?.muted;
|
|
42
|
+
return Array.isArray(muted) ? muted.filter((key) => typeof key === 'string') : [];
|
|
43
|
+
};
|
|
44
|
+
const withMuted = (issues, muted) => issues.map(issue => (muted.has(issue.key) ? { ...issue, muted: true } : issue));
|
|
45
|
+
function checkAll(root) {
|
|
46
|
+
const languages = languagesOf(root);
|
|
47
|
+
const sharedFiles = sharedScopesOf(createOperationContext(root)).map(scope => scope.file);
|
|
48
|
+
const pages = mergeLanguages(languages.map(({ key, code }) => ({ lang: code, issues: runRules(checkPagesFromCode(root, key)) })), sharedFiles);
|
|
49
|
+
return [...(languages.length > 1 ? pages : pages.map(({ langs: _langs, ...issue }) => issue)), ...runSiteRules(checkSiteFromCode(root))];
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CheckPage, type CheckSite, type Issue, type Rule, type SiteRule } from './types.js';
|
|
2
|
+
export declare const RULES: readonly Rule[];
|
|
3
|
+
export declare const SITE_RULES: readonly SiteRule[];
|
|
4
|
+
export declare function runRules(pages: readonly CheckPage[], rules?: readonly Rule[]): Issue[];
|
|
5
|
+
/** What is wrong with the site as a whole, not with one page. */
|
|
6
|
+
export declare function runSiteRules(site: CheckSite, rules?: readonly SiteRule[]): Issue[];
|
|
7
|
+
//# sourceMappingURL=rules.d.ts.map
|