@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,304 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import postcss from 'postcss';
|
|
4
|
+
import { WR_THEME_START_PROPERTY } from '@webround/core/theme';
|
|
5
|
+
import { defineOperation, invalid, object, requiredText } from '../define.js';
|
|
6
|
+
// The site's theme: custom properties (`--wr-*`), Google fonts and custom fonts, all in `src/theme.css`. Read and written with postcss, so
|
|
7
|
+
// what the user wrote around them stays as it was.
|
|
8
|
+
const themeFile = (ctx) => path.join(ctx.srcRoot, 'theme.css');
|
|
9
|
+
function readTheme(ctx) {
|
|
10
|
+
const file = themeFile(ctx);
|
|
11
|
+
return postcss.parse(fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '');
|
|
12
|
+
}
|
|
13
|
+
function writeTheme(ctx, root) {
|
|
14
|
+
fs.writeFileSync(themeFile(ctx), root.toString());
|
|
15
|
+
}
|
|
16
|
+
const COLOR_PREFIX = '--wr-color-';
|
|
17
|
+
const isColor = (key) => key.startsWith(COLOR_PREFIX) && key.length > COLOR_PREFIX.length;
|
|
18
|
+
const ruleOf = (root, selector) => root.nodes.find((n) => n.type === 'rule' && n.selector === selector);
|
|
19
|
+
/** The site has a dark theme when its theme has a `.dark` block. */
|
|
20
|
+
const darkRule = (root) => ruleOf(root, '.dark');
|
|
21
|
+
// A color of the theme is a class (`bg-brand`) because the stylesheet maps it: `--color-brand: var(--wr-color-brand)` in an `@theme`. That mapping lives in `theme.css` (index.css only imports
|
|
22
|
+
// it), and is written and taken away with the color, so a color named in the theme is a color of the classes and of the style panel. An older site has its mappings in index.css: they are
|
|
23
|
+
// found there, and taken away from there.
|
|
24
|
+
const indexFile = (ctx) => path.join(ctx.srcRoot, 'index.css');
|
|
25
|
+
function mappingsOf(root, key) {
|
|
26
|
+
const found = [];
|
|
27
|
+
root.walkAtRules('theme', (rule) => rule.walkDecls(/^--color-/, (decl) => { if (decl.value.replace(/\s+/g, '') === `var(${key})`)
|
|
28
|
+
found.push(decl); }));
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
function ensureColorMapping(ctx, root, key) {
|
|
32
|
+
if (!isColor(key))
|
|
33
|
+
return;
|
|
34
|
+
const index = fs.existsSync(indexFile(ctx)) ? postcss.parse(fs.readFileSync(indexFile(ctx), 'utf-8')) : postcss.root();
|
|
35
|
+
if (mappingsOf(root, key).length > 0 || mappingsOf(index, key).length > 0)
|
|
36
|
+
return;
|
|
37
|
+
let block = root.nodes.find((n) => n.type === 'atrule' && n.name === 'theme' && n.params.trim() === 'inline');
|
|
38
|
+
if (!block) {
|
|
39
|
+
block = postcss.atRule({ name: 'theme', params: 'inline' });
|
|
40
|
+
root.append(block);
|
|
41
|
+
}
|
|
42
|
+
block.append({ prop: `--color-${key.slice(COLOR_PREFIX.length)}`, value: `var(${key})` });
|
|
43
|
+
}
|
|
44
|
+
function removeColorMapping(ctx, root, key) {
|
|
45
|
+
if (!isColor(key))
|
|
46
|
+
return;
|
|
47
|
+
mappingsOf(root, key).forEach(decl => decl.remove());
|
|
48
|
+
if (!fs.existsSync(indexFile(ctx)))
|
|
49
|
+
return;
|
|
50
|
+
const index = postcss.parse(fs.readFileSync(indexFile(ctx), 'utf-8'));
|
|
51
|
+
const old = mappingsOf(index, key);
|
|
52
|
+
if (old.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
old.forEach(decl => decl.remove());
|
|
55
|
+
fs.writeFileSync(indexFile(ctx), index.toString());
|
|
56
|
+
}
|
|
57
|
+
function stripQuotes(value) {
|
|
58
|
+
return value.trim().replace(/^['"]|['"]$/g, '');
|
|
59
|
+
}
|
|
60
|
+
function findGoogleImport(root) {
|
|
61
|
+
let found;
|
|
62
|
+
root.walkAtRules('import', (rule) => {
|
|
63
|
+
if (rule.params.includes('fonts.googleapis.com'))
|
|
64
|
+
found = rule;
|
|
65
|
+
});
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
function parseGoogleImport(rule) {
|
|
69
|
+
if (!rule)
|
|
70
|
+
return [];
|
|
71
|
+
const urlMatch = rule.params.match(/url\(['"]?([^'")]+)['"]?\)/);
|
|
72
|
+
if (!urlMatch)
|
|
73
|
+
return [];
|
|
74
|
+
const url = new URL(urlMatch[1]);
|
|
75
|
+
const entries = [];
|
|
76
|
+
for (const family of url.searchParams.getAll('family')) {
|
|
77
|
+
const [name, rest] = family.split(':');
|
|
78
|
+
const weights = rest?.startsWith('wght@') ? rest.slice('wght@'.length) : rest;
|
|
79
|
+
entries.push({ family: name.replace(/\+/g, ' '), weights });
|
|
80
|
+
}
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
function buildGoogleImportRule(entries) {
|
|
84
|
+
const params = entries
|
|
85
|
+
.map(({ family, weights }) => `family=${family.replace(/ /g, '+')}${weights ? `:wght@${weights}` : ''}`)
|
|
86
|
+
.join('&');
|
|
87
|
+
return `url('https://fonts.googleapis.com/css2?${params}&display=swap')`;
|
|
88
|
+
}
|
|
89
|
+
function getThemeItems(root) {
|
|
90
|
+
const items = [];
|
|
91
|
+
const dark = darkRule(root);
|
|
92
|
+
root.walkRules(':root', (rule) => {
|
|
93
|
+
rule.walkDecls(/^--wr-/, (decl) => {
|
|
94
|
+
const inDark = dark?.nodes.find((n) => n.type === 'decl' && n.prop === decl.prop);
|
|
95
|
+
items.push({ kind: 'property', key: decl.prop, value: stripQuotes(decl.value), ...(inDark && inDark.type === 'decl' ? { dark: stripQuotes(inDark.value) } : {}) });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
for (const { family, weights } of parseGoogleImport(findGoogleImport(root))) {
|
|
99
|
+
items.push({ kind: 'google-font', key: family, weights });
|
|
100
|
+
}
|
|
101
|
+
const customByFamily = new Map();
|
|
102
|
+
root.walkAtRules('font-face', (rule) => {
|
|
103
|
+
let family = '';
|
|
104
|
+
const face = { src: '' };
|
|
105
|
+
rule.walkDecls((decl) => {
|
|
106
|
+
const value = stripQuotes(decl.value);
|
|
107
|
+
if (decl.prop === 'font-family')
|
|
108
|
+
family = value;
|
|
109
|
+
else if (decl.prop === 'src')
|
|
110
|
+
face.src = decl.value;
|
|
111
|
+
else if (decl.prop === 'font-weight')
|
|
112
|
+
face.weight = decl.value;
|
|
113
|
+
else if (decl.prop === 'font-style')
|
|
114
|
+
face.style = decl.value;
|
|
115
|
+
else if (decl.prop === 'font-stretch')
|
|
116
|
+
face.stretch = decl.value;
|
|
117
|
+
});
|
|
118
|
+
if (!family)
|
|
119
|
+
return;
|
|
120
|
+
const faces = customByFamily.get(family) ?? [];
|
|
121
|
+
faces.push(face);
|
|
122
|
+
customByFamily.set(family, faces);
|
|
123
|
+
});
|
|
124
|
+
for (const [family, faces] of customByFamily) {
|
|
125
|
+
items.push({ kind: 'custom-font', key: family, faces });
|
|
126
|
+
}
|
|
127
|
+
return items;
|
|
128
|
+
}
|
|
129
|
+
function setIn(rule, key, value) {
|
|
130
|
+
const existing = rule.nodes.find((n) => n.type === 'decl' && n.prop === key);
|
|
131
|
+
if (existing && existing.type === 'decl')
|
|
132
|
+
existing.value = value;
|
|
133
|
+
else
|
|
134
|
+
rule.append({ prop: key, value });
|
|
135
|
+
}
|
|
136
|
+
/** A property in the light theme (`:root`), and in the dark one when it is told (`dark`) or when the site has a dark theme and it is a new color (it starts as the light value). */
|
|
137
|
+
function setProperty(root, key, value, dark) {
|
|
138
|
+
let rootRule = ruleOf(root, ':root');
|
|
139
|
+
if (!rootRule) {
|
|
140
|
+
rootRule = postcss.rule({ selector: ':root' });
|
|
141
|
+
root.append(rootRule);
|
|
142
|
+
}
|
|
143
|
+
const isNew = !rootRule.nodes.some((n) => n.type === 'decl' && n.prop === key);
|
|
144
|
+
setIn(rootRule, key, value);
|
|
145
|
+
const darkBlock = darkRule(root);
|
|
146
|
+
if (dark !== undefined) {
|
|
147
|
+
if (!darkBlock)
|
|
148
|
+
throw invalid('the site has no dark theme: turn it on first (theme-dark)');
|
|
149
|
+
setIn(darkBlock, key, dark);
|
|
150
|
+
}
|
|
151
|
+
else if (darkBlock && isNew && isColor(key))
|
|
152
|
+
setIn(darkBlock, key, value);
|
|
153
|
+
}
|
|
154
|
+
function setGoogleFont(root, family, weights) {
|
|
155
|
+
const importRule = findGoogleImport(root);
|
|
156
|
+
const entries = parseGoogleImport(importRule);
|
|
157
|
+
const idx = entries.findIndex((e) => e.family === family);
|
|
158
|
+
if (idx >= 0)
|
|
159
|
+
entries[idx] = { family, weights };
|
|
160
|
+
else
|
|
161
|
+
entries.push({ family, weights });
|
|
162
|
+
if (importRule) {
|
|
163
|
+
importRule.params = buildGoogleImportRule(entries);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const newRule = postcss.atRule({ name: 'import', params: buildGoogleImportRule(entries) });
|
|
167
|
+
root.prepend(newRule); // @import deve stare in cima al file
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function removeCustomFontBlocks(root, family) {
|
|
171
|
+
root.walkAtRules('font-face', (rule) => {
|
|
172
|
+
let ruleFamily = '';
|
|
173
|
+
rule.walkDecls('font-family', (decl) => { ruleFamily = stripQuotes(decl.value); });
|
|
174
|
+
if (ruleFamily === family)
|
|
175
|
+
rule.remove();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function setCustomFont(root, family, faces) {
|
|
179
|
+
removeCustomFontBlocks(root, family); // edit = replace, niente blocchi duplicati/stale
|
|
180
|
+
for (const face of faces) {
|
|
181
|
+
const rule = postcss.atRule({ name: 'font-face' });
|
|
182
|
+
rule.append({ prop: 'font-family', value: `'${family}'` });
|
|
183
|
+
rule.append({ prop: 'src', value: face.src });
|
|
184
|
+
if (face.weight)
|
|
185
|
+
rule.append({ prop: 'font-weight', value: face.weight });
|
|
186
|
+
if (face.style)
|
|
187
|
+
rule.append({ prop: 'font-style', value: face.style });
|
|
188
|
+
if (face.stretch)
|
|
189
|
+
rule.append({ prop: 'font-stretch', value: face.stretch });
|
|
190
|
+
root.append(rule);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function removeProperty(root, key) {
|
|
194
|
+
for (const selector of [':root', '.dark'])
|
|
195
|
+
ruleOf(root, selector)?.walkDecls(key, (decl) => { decl.remove(); });
|
|
196
|
+
}
|
|
197
|
+
function removeGoogleFont(root, family) {
|
|
198
|
+
const importRule = findGoogleImport(root);
|
|
199
|
+
if (!importRule)
|
|
200
|
+
return;
|
|
201
|
+
const entries = parseGoogleImport(importRule).filter((e) => e.family !== family);
|
|
202
|
+
if (entries.length === 0)
|
|
203
|
+
importRule.remove();
|
|
204
|
+
else
|
|
205
|
+
importRule.params = buildGoogleImportRule(entries);
|
|
206
|
+
}
|
|
207
|
+
const KINDS = ['property', 'google-font', 'custom-font'];
|
|
208
|
+
export const themeGet = defineOperation({
|
|
209
|
+
name: 'theme-get',
|
|
210
|
+
readOnly: true,
|
|
211
|
+
summary: 'The theme of the site: its custom properties (--wr-*, colors, spacing…; a property has `dark` too when the site has a dark theme), Google fonts and custom fonts, and `dark`: whether the site has a dark theme (a `.dark` block).',
|
|
212
|
+
run: (_input, ctx) => { const root = readTheme(ctx); return { items: getThemeItems(root), dark: Boolean(darkRule(root)) }; },
|
|
213
|
+
});
|
|
214
|
+
export const themeSet = defineOperation({
|
|
215
|
+
name: 'theme-set',
|
|
216
|
+
summary: 'Sets one item of the theme: a custom property { kind: "property", key: "--wr-…", value, dark? } (a `--wr-color-<name>` is also a class, `bg-<name>`: it is mapped for you; `dark` is its value in the dark theme, when the site has one), a Google font { kind: "google-font", key: "Inter", weights? } or a custom font { kind: "custom-font", key, faces }. It replaces the item if it is there.',
|
|
217
|
+
params: [{ name: 'item', type: 'object', required: true, help: 'the item: { kind, key, … } as theme-get returns them' }],
|
|
218
|
+
run: (input, ctx) => {
|
|
219
|
+
const item = object(input, 'item', { required: true });
|
|
220
|
+
if (!KINDS.includes(item.kind) || typeof item.key !== 'string' || !item.key)
|
|
221
|
+
throw invalid('item needs a kind and a key');
|
|
222
|
+
const root = readTheme(ctx);
|
|
223
|
+
if (item.kind === 'property') {
|
|
224
|
+
if (typeof item.value !== 'string')
|
|
225
|
+
throw invalid('a property needs a value');
|
|
226
|
+
if (item.dark !== undefined && typeof item.dark !== 'string')
|
|
227
|
+
throw invalid('dark must be a text');
|
|
228
|
+
setProperty(root, item.key, item.value, item.dark);
|
|
229
|
+
ensureColorMapping(ctx, root, item.key);
|
|
230
|
+
}
|
|
231
|
+
else if (item.kind === 'google-font')
|
|
232
|
+
setGoogleFont(root, item.key, item.weights);
|
|
233
|
+
else {
|
|
234
|
+
if (!Array.isArray(item.faces))
|
|
235
|
+
throw invalid('a custom font needs its faces');
|
|
236
|
+
setCustomFont(root, item.key, item.faces);
|
|
237
|
+
}
|
|
238
|
+
writeTheme(ctx, root);
|
|
239
|
+
return { kind: item.kind, key: item.key };
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
export const themeRemove = defineOperation({
|
|
243
|
+
name: 'theme-remove',
|
|
244
|
+
destructive: true,
|
|
245
|
+
summary: 'Removes one item of the theme.',
|
|
246
|
+
params: [
|
|
247
|
+
{ name: 'kind', type: 'string', required: true, choices: KINDS, help: 'what it is' },
|
|
248
|
+
{ name: 'key', type: 'string', required: true, help: 'the property (--wr-…) or the font family' },
|
|
249
|
+
],
|
|
250
|
+
run: (input, ctx) => {
|
|
251
|
+
const kind = requiredText(input, 'kind');
|
|
252
|
+
const key = requiredText(input, 'key');
|
|
253
|
+
if (!KINDS.includes(kind))
|
|
254
|
+
throw invalid(`kind must be one of: ${KINDS.join(', ')}`);
|
|
255
|
+
const root = readTheme(ctx);
|
|
256
|
+
if (kind === 'property') {
|
|
257
|
+
removeProperty(root, key);
|
|
258
|
+
removeColorMapping(ctx, root, key);
|
|
259
|
+
}
|
|
260
|
+
else if (kind === 'google-font')
|
|
261
|
+
removeGoogleFont(root, key);
|
|
262
|
+
else
|
|
263
|
+
removeCustomFontBlocks(root, key);
|
|
264
|
+
writeTheme(ctx, root);
|
|
265
|
+
return { kind, key };
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
/** What the dark theme starts from: every color of the light one (a copy, to be changed). */
|
|
269
|
+
function enableDark(root) {
|
|
270
|
+
if (darkRule(root))
|
|
271
|
+
return;
|
|
272
|
+
const light = ruleOf(root, ':root');
|
|
273
|
+
const dark = postcss.rule({ selector: '.dark' });
|
|
274
|
+
light?.walkDecls(/^--wr-color-/, (decl) => { dark.append({ prop: decl.prop, value: decl.value }); });
|
|
275
|
+
if (light)
|
|
276
|
+
root.insertAfter(light, dark);
|
|
277
|
+
else
|
|
278
|
+
root.append(dark);
|
|
279
|
+
if (!ruleOf(root, 'html.dark'))
|
|
280
|
+
root.append(postcss.rule({ selector: 'html.dark' }).append({ prop: 'color-scheme', value: 'dark' }));
|
|
281
|
+
}
|
|
282
|
+
/** No dark theme: its block goes, with what only made sense with it (`color-scheme`, the choice to start from the system's theme). */
|
|
283
|
+
function disableDark(root) {
|
|
284
|
+
darkRule(root)?.remove();
|
|
285
|
+
ruleOf(root, 'html.dark')?.remove();
|
|
286
|
+
root.walkRules(':root', (rule) => { rule.walkDecls(WR_THEME_START_PROPERTY, (decl) => { decl.remove(); }); });
|
|
287
|
+
}
|
|
288
|
+
export const themeDark = defineOperation({
|
|
289
|
+
name: 'theme-dark',
|
|
290
|
+
summary: 'Turns the dark theme of the site on (a `.dark` block in theme.css that starts as a copy of the light colors, to be changed with theme-set `dark`) or off (the block is taken away). A site has a dark theme only if it has that block.',
|
|
291
|
+
params: [{ name: 'enabled', type: 'boolean', required: true, help: 'true: a dark theme; false: only the light one' }],
|
|
292
|
+
run: (input, ctx) => {
|
|
293
|
+
const enabled = input.enabled === true || input.enabled === 'true';
|
|
294
|
+
const root = readTheme(ctx);
|
|
295
|
+
if (enabled)
|
|
296
|
+
enableDark(root);
|
|
297
|
+
else
|
|
298
|
+
disableDark(root);
|
|
299
|
+
writeTheme(ctx, root);
|
|
300
|
+
return { enabled };
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
export const THEME_OPERATIONS = [themeGet, themeSet, themeRemove, themeDark];
|
|
304
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type OperationRegistry } from './define.js';
|
|
2
|
+
/**
|
|
3
|
+
* Every operation there is. A new one is a file in ops/ and a line here: the dev server's `/wr/op/<name>`, `wr-op`, the container's tools
|
|
4
|
+
* (and the manifest, the reference and the schemas an MCP server offers) all follow, without being touched.
|
|
5
|
+
*/
|
|
6
|
+
export declare const OPERATIONS: import("./define.js").Operation[];
|
|
7
|
+
export declare const createRegistry: () => OperationRegistry;
|
|
8
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createOperationRegistry } from './define.js';
|
|
2
|
+
import { CHECK_OPERATIONS } from './ops/check.js';
|
|
3
|
+
import { COMMENT_OPERATIONS } from './ops/comments.js';
|
|
4
|
+
import { CONTENT_OPERATIONS } from './ops/content.js';
|
|
5
|
+
import { JSON_OPERATIONS } from './ops/json.js';
|
|
6
|
+
import { LANG_OPERATIONS } from './ops/langs.js';
|
|
7
|
+
import { NODE_OPERATIONS } from './ops/nodes.js';
|
|
8
|
+
import { PAGE_OPERATIONS } from './ops/pages.js';
|
|
9
|
+
import { SEO_OPERATIONS } from './ops/seo.js';
|
|
10
|
+
import { STYLE_OPERATIONS } from './ops/style.js';
|
|
11
|
+
import { STYLESET_OPERATIONS } from './ops/stylesets.js';
|
|
12
|
+
import { THEME_OPERATIONS } from './ops/theme.js';
|
|
13
|
+
/**
|
|
14
|
+
* Every operation there is. A new one is a file in ops/ and a line here: the dev server's `/wr/op/<name>`, `wr-op`, the container's tools
|
|
15
|
+
* (and the manifest, the reference and the schemas an MCP server offers) all follow, without being touched.
|
|
16
|
+
*/
|
|
17
|
+
export const OPERATIONS = [...PAGE_OPERATIONS, ...NODE_OPERATIONS, ...CHECK_OPERATIONS, ...CONTENT_OPERATIONS, ...LANG_OPERATIONS, ...SEO_OPERATIONS, ...THEME_OPERATIONS, ...COMMENT_OPERATIONS, ...STYLE_OPERATIONS, ...STYLESET_OPERATIONS, ...JSON_OPERATIONS];
|
|
18
|
+
export const createRegistry = () => createOperationRegistry(OPERATIONS);
|
|
19
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type StringLiteral, type NoSubstitutionTemplateLiteral } from 'ts-morph';
|
|
2
|
+
import { type OperationContext } from './define.js';
|
|
3
|
+
export declare const STYLESET_PREFIX = "s-";
|
|
4
|
+
export declare const STYLESET_FILE = "theme.css";
|
|
5
|
+
export declare const styleSetClass: (name: string) => string;
|
|
6
|
+
export declare function checkStyleSetName(name: string): void;
|
|
7
|
+
export interface StyleSet {
|
|
8
|
+
name: string;
|
|
9
|
+
/** The class an element uses (`s-card`). */
|
|
10
|
+
class: string;
|
|
11
|
+
/** The Tailwind classes it holds, as one text. */
|
|
12
|
+
classes: string;
|
|
13
|
+
}
|
|
14
|
+
/** Every shared style of the site, in the order they are written. A site without `theme.css`, or without any, has none. */
|
|
15
|
+
export declare function readStyleSets(ctx: OperationContext): StyleSet[];
|
|
16
|
+
export declare function readStyleSet(ctx: OperationContext, name: string): StyleSet;
|
|
17
|
+
/** Creates the set, or replaces what it holds (its own hand-written CSS, if someone added any, stays). `classes` are whole Tailwind classes. */
|
|
18
|
+
export declare function writeStyleSet(ctx: OperationContext, name: string, classes: readonly string[]): void;
|
|
19
|
+
export declare function removeStyleSet(ctx: OperationContext, name: string): void;
|
|
20
|
+
export interface StyleSetUsage {
|
|
21
|
+
/** Relative to `src/`. */
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
/** The element's `id`, when the code spells it out. */
|
|
25
|
+
id: string | null;
|
|
26
|
+
}
|
|
27
|
+
type ClassLiteral = StringLiteral | NoSubstitutionTemplateLiteral;
|
|
28
|
+
/** Every place the code uses the class, one per literal that holds it. */
|
|
29
|
+
export declare function findStyleSetUsages(ctx: OperationContext, className: string): {
|
|
30
|
+
usage: StyleSetUsage;
|
|
31
|
+
literal: ClassLiteral;
|
|
32
|
+
}[];
|
|
33
|
+
/**
|
|
34
|
+
* Rewrites every place that uses the class: `edit` gets the classes of that literal (the style's among them) and says what they become. The ONE step that taking a style off, and
|
|
35
|
+
* renaming it, both take. Returns how many places changed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function editStyleSetUsages(ctx: OperationContext, className: string, edit: (classes: string[]) => string): number;
|
|
38
|
+
/** Takes the class out of every place that uses it. `replacement` says what stands in its place given the other classes of that same literal (what the style held, under what the element has of its own). */
|
|
39
|
+
export declare function replaceStyleSetUsages(ctx: OperationContext, className: string, replacement: (others: string) => string): number;
|
|
40
|
+
/** Gives the style another name: its rule in theme.css, and the class in every place that uses it (where it stood among the others). Says how many places changed. */
|
|
41
|
+
export declare function renameStyleSet(ctx: OperationContext, from: string, to: string): {
|
|
42
|
+
places: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The classes among `classes` that `@apply` could not take: Tailwind generates nothing for them (a class of the project's own, a typo, another shared style). `@apply` of one of those
|
|
46
|
+
* breaks the whole stylesheet, so nothing is written until they are out. null when Tailwind cannot be loaded from the project (no way to tell): the caller then trusts only what it knows.
|
|
47
|
+
*/
|
|
48
|
+
export declare function unknownUtilities(ctx: OperationContext, classes: readonly string[], colorTokens: readonly string[]): Promise<string[] | null>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=stylesets-store.d.ts.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
5
|
+
import postcss from 'postcss';
|
|
6
|
+
import { SyntaxKind } from 'ts-morph';
|
|
7
|
+
import { wrParseClassName } from '@webround/core/style';
|
|
8
|
+
import { OperationError } from './define.js';
|
|
9
|
+
import { sourceFiles } from '../check/orphans.js';
|
|
10
|
+
import { loadSourceFile } from '../vite-plugins/jsx-tree.js';
|
|
11
|
+
// Shared styles: a name for a group of Tailwind classes, kept in ONE place so changing it changes every element that uses it. In the code it is a class
|
|
12
|
+
// (`className="s-card"`) and in `src/theme.css` it is a rule that says what it holds (`@apply`), in the components layer so a utility on an element still wins:
|
|
13
|
+
//
|
|
14
|
+
// @layer components {
|
|
15
|
+
// .s-card { @apply px-4 md:px-8 text-lg hover:bg-accent; }
|
|
16
|
+
// }
|
|
17
|
+
//
|
|
18
|
+
// This file is the ONE reader and writer of those rules, and the one that says where a set is used. Nothing else parses or writes them.
|
|
19
|
+
export const STYLESET_PREFIX = 's-';
|
|
20
|
+
export const STYLESET_FILE = 'theme.css';
|
|
21
|
+
/** What a set is called by the person: lowercase words and numbers joined by dashes (`card`, `hero-title`). The class is the name with the prefix. */
|
|
22
|
+
const NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
23
|
+
const RULE = /^\.s-([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/;
|
|
24
|
+
const LAYERS = 'theme, base, components, utilities';
|
|
25
|
+
const INTRO = 'Shared styles: a name for a group of Tailwind classes. Use it as className="s-name"; change it here (or in the editor\'s Styles) and every element that uses it changes. A utility on an element still wins over it.';
|
|
26
|
+
export const styleSetClass = (name) => `${STYLESET_PREFIX}${name}`;
|
|
27
|
+
export function checkStyleSetName(name) {
|
|
28
|
+
if (name.length > 40 || !NAME.test(name)) {
|
|
29
|
+
throw new OperationError(400, 'invalid-styleset-name', `"${name}" cannot be the name of a style: use lowercase words and numbers joined by dashes (card, hero-title), up to 40 characters`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const themeFile = (ctx) => path.join(ctx.srcRoot, STYLESET_FILE);
|
|
33
|
+
function readRoot(ctx) {
|
|
34
|
+
const file = themeFile(ctx);
|
|
35
|
+
if (!fs.existsSync(file))
|
|
36
|
+
throw new OperationError(409, 'no-theme-file', `this site has no src/${STYLESET_FILE}: shared styles are kept there`);
|
|
37
|
+
return postcss.parse(fs.readFileSync(file, 'utf-8'));
|
|
38
|
+
}
|
|
39
|
+
const componentsLayers = (root) => root.nodes.filter((n) => n.type === 'atrule' && n.name === 'layer' && n.params.trim() === 'components');
|
|
40
|
+
const applyOf = (rule) => rule.nodes.filter((n) => n.type === 'atrule' && n.name === 'apply');
|
|
41
|
+
const textOf = (rule) => applyOf(rule).map(a => a.params.trim()).filter(Boolean).join(' ');
|
|
42
|
+
function setRules(root) {
|
|
43
|
+
return componentsLayers(root).flatMap(layer => (layer.nodes ?? []).flatMap((n) => {
|
|
44
|
+
const name = n.type === 'rule' ? RULE.exec(n.selector.trim())?.[1] : undefined;
|
|
45
|
+
return n.type === 'rule' && name ? [{ name, rule: n }] : [];
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/** Every shared style of the site, in the order they are written. A site without `theme.css`, or without any, has none. */
|
|
49
|
+
export function readStyleSets(ctx) {
|
|
50
|
+
if (!fs.existsSync(themeFile(ctx)))
|
|
51
|
+
return [];
|
|
52
|
+
return setRules(readRoot(ctx)).map(({ name, rule }) => ({ name, class: styleSetClass(name), classes: textOf(rule) }));
|
|
53
|
+
}
|
|
54
|
+
export function readStyleSet(ctx, name) {
|
|
55
|
+
const found = readStyleSets(ctx).find(s => s.name === name);
|
|
56
|
+
if (!found)
|
|
57
|
+
throw new OperationError(404, 'styleset-not-found', `there is no style called "${name}" (there is: ${readStyleSets(ctx).map(s => s.name).join(', ') || 'none yet'})`);
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The layers must be said in their order BEFORE the first `@layer components`: index.css imports theme.css ahead of Tailwind, and a layer is ranked where it is first named, so
|
|
62
|
+
* without this line the components would rank under Tailwind's resets and a set's padding would be wiped by them.
|
|
63
|
+
*/
|
|
64
|
+
function ensureLayerOrder(root) {
|
|
65
|
+
if (root.nodes.some(n => n.type === 'atrule' && n.name === 'layer' && !n.nodes && n.params.replace(/\s+/g, '') === LAYERS.replace(/\s+/g, '')))
|
|
66
|
+
return;
|
|
67
|
+
const statement = postcss.atRule({ name: 'layer', params: LAYERS });
|
|
68
|
+
const lastImport = [...root.nodes].reverse().find(n => n.type === 'atrule' && n.name === 'import');
|
|
69
|
+
if (lastImport)
|
|
70
|
+
root.insertAfter(lastImport, statement);
|
|
71
|
+
else
|
|
72
|
+
root.prepend(statement);
|
|
73
|
+
statement.raws.before = lastImport ? '\n\n' : '';
|
|
74
|
+
}
|
|
75
|
+
/** Creates the set, or replaces what it holds (its own hand-written CSS, if someone added any, stays). `classes` are whole Tailwind classes. */
|
|
76
|
+
export function writeStyleSet(ctx, name, classes) {
|
|
77
|
+
checkStyleSetName(name);
|
|
78
|
+
const root = readRoot(ctx);
|
|
79
|
+
ensureLayerOrder(root);
|
|
80
|
+
let layer = componentsLayers(root)[0];
|
|
81
|
+
if (!layer) {
|
|
82
|
+
layer = postcss.atRule({ name: 'layer', params: 'components', nodes: [] });
|
|
83
|
+
layer.raws.before = '\n\n';
|
|
84
|
+
layer.raws.between = ' ';
|
|
85
|
+
layer.prepend(postcss.comment({ text: INTRO }));
|
|
86
|
+
root.append(layer);
|
|
87
|
+
}
|
|
88
|
+
let rule = setRules(root).find(s => s.name === name)?.rule;
|
|
89
|
+
if (!rule) {
|
|
90
|
+
rule = postcss.rule({ selector: `.${styleSetClass(name)}` });
|
|
91
|
+
rule.raws.between = ' ';
|
|
92
|
+
layer.append(rule);
|
|
93
|
+
}
|
|
94
|
+
applyOf(rule).forEach(a => a.remove());
|
|
95
|
+
const text = classes.join(' ').trim();
|
|
96
|
+
if (text)
|
|
97
|
+
rule.prepend(postcss.atRule({ name: 'apply', params: text }));
|
|
98
|
+
fs.writeFileSync(themeFile(ctx), root.toString());
|
|
99
|
+
}
|
|
100
|
+
export function removeStyleSet(ctx, name) {
|
|
101
|
+
const root = readRoot(ctx);
|
|
102
|
+
const found = setRules(root).find(s => s.name === name);
|
|
103
|
+
if (!found)
|
|
104
|
+
throw new OperationError(404, 'styleset-not-found', `there is no style called "${name}"`);
|
|
105
|
+
found.rule.remove();
|
|
106
|
+
fs.writeFileSync(themeFile(ctx), root.toString());
|
|
107
|
+
}
|
|
108
|
+
const hasClass = (text, className) => wrParseClassName(text).some(t => t.raw === className);
|
|
109
|
+
/** The text literals inside the `className` of an element: where the class names are. (`cn("s-card", x && "p-2")`: both literals.) */
|
|
110
|
+
function classLiterals(attribute) {
|
|
111
|
+
return [...attribute.getDescendantsOfKind(SyntaxKind.StringLiteral), ...attribute.getDescendantsOfKind(SyntaxKind.NoSubstitutionTemplateLiteral)];
|
|
112
|
+
}
|
|
113
|
+
/** Every place the code uses the class, one per literal that holds it. */
|
|
114
|
+
export function findStyleSetUsages(ctx, className) {
|
|
115
|
+
const found = [];
|
|
116
|
+
const project = ctx.project();
|
|
117
|
+
for (const absolute of sourceFiles(ctx.srcRoot)) {
|
|
118
|
+
if (!fs.readFileSync(absolute, 'utf-8').includes(className))
|
|
119
|
+
continue;
|
|
120
|
+
const sourceFile = loadSourceFile(project, absolute);
|
|
121
|
+
if (!sourceFile)
|
|
122
|
+
continue;
|
|
123
|
+
for (const attribute of sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
|
|
124
|
+
if (attribute.getNameNode().getText() !== 'className')
|
|
125
|
+
continue;
|
|
126
|
+
for (const literal of classLiterals(attribute)) {
|
|
127
|
+
if (!hasClass(literal.getLiteralText(), className))
|
|
128
|
+
continue;
|
|
129
|
+
const element = attribute.getParent()?.getParent();
|
|
130
|
+
const id = element?.asKind(SyntaxKind.JsxOpeningElement)?.getAttribute('id') ?? element?.asKind(SyntaxKind.JsxSelfClosingElement)?.getAttribute('id');
|
|
131
|
+
const initializer = id?.asKind(SyntaxKind.JsxAttribute)?.getInitializer();
|
|
132
|
+
found.push({
|
|
133
|
+
usage: { file: path.relative(ctx.srcRoot, absolute).split(path.sep).join('/'), line: literal.getStartLineNumber(), id: initializer?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue() ?? null },
|
|
134
|
+
literal,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Rewrites every place that uses the class: `edit` gets the classes of that literal (the style's among them) and says what they become. The ONE step that taking a style off, and
|
|
143
|
+
* renaming it, both take. Returns how many places changed.
|
|
144
|
+
*/
|
|
145
|
+
export function editStyleSetUsages(ctx, className, edit) {
|
|
146
|
+
const touched = new Set();
|
|
147
|
+
let places = 0;
|
|
148
|
+
// The last literal first: a replacement changes the text after it, never before
|
|
149
|
+
for (const { usage, literal } of findStyleSetUsages(ctx, className).reverse()) {
|
|
150
|
+
literal.replaceWithText(JSON.stringify(edit(wrParseClassName(literal.getLiteralText()).map(t => t.raw))));
|
|
151
|
+
touched.add(usage.file);
|
|
152
|
+
places++;
|
|
153
|
+
}
|
|
154
|
+
// (not loadSourceFile: it reads the file again, and the changes are in memory)
|
|
155
|
+
for (const file of touched)
|
|
156
|
+
ctx.project().getSourceFile(path.join(ctx.srcRoot, file))?.saveSync();
|
|
157
|
+
return places;
|
|
158
|
+
}
|
|
159
|
+
/** Takes the class out of every place that uses it. `replacement` says what stands in its place given the other classes of that same literal (what the style held, under what the element has of its own). */
|
|
160
|
+
export function replaceStyleSetUsages(ctx, className, replacement) {
|
|
161
|
+
return editStyleSetUsages(ctx, className, classes => classes.flatMap(c => (c === className ? [replacement(classes.filter(other => other !== className).join(' '))] : [c])).join(' '));
|
|
162
|
+
}
|
|
163
|
+
/** Gives the style another name: its rule in theme.css, and the class in every place that uses it (where it stood among the others). Says how many places changed. */
|
|
164
|
+
export function renameStyleSet(ctx, from, to) {
|
|
165
|
+
checkStyleSetName(to);
|
|
166
|
+
const root = readRoot(ctx);
|
|
167
|
+
const rules = setRules(root);
|
|
168
|
+
const found = rules.find(s => s.name === from);
|
|
169
|
+
if (!found)
|
|
170
|
+
throw new OperationError(404, 'styleset-not-found', `there is no style called "${from}" (there is: ${rules.map(s => s.name).join(', ') || 'none yet'})`);
|
|
171
|
+
if (from !== to && rules.some(s => s.name === to))
|
|
172
|
+
throw new OperationError(409, 'styleset-exists', `there is already a style called "${to}"`);
|
|
173
|
+
if (from === to)
|
|
174
|
+
return { places: 0 };
|
|
175
|
+
found.rule.selector = `.${styleSetClass(to)}`;
|
|
176
|
+
fs.writeFileSync(themeFile(ctx), root.toString());
|
|
177
|
+
return { places: editStyleSetUsages(ctx, styleSetClass(from), classes => classes.map(c => (c === styleSetClass(from) ? styleSetClass(to) : c)).join(' ')) };
|
|
178
|
+
}
|
|
179
|
+
// ── Is it a class Tailwind can apply ─────────────────────────────────────────
|
|
180
|
+
/**
|
|
181
|
+
* The classes among `classes` that `@apply` could not take: Tailwind generates nothing for them (a class of the project's own, a typo, another shared style). `@apply` of one of those
|
|
182
|
+
* breaks the whole stylesheet, so nothing is written until they are out. null when Tailwind cannot be loaded from the project (no way to tell): the caller then trusts only what it knows.
|
|
183
|
+
*/
|
|
184
|
+
export async function unknownUtilities(ctx, classes, colorTokens) {
|
|
185
|
+
let compile;
|
|
186
|
+
let themePath;
|
|
187
|
+
try {
|
|
188
|
+
const require = createRequire(path.join(ctx.root, 'package.json'));
|
|
189
|
+
// The CommonJS build comes with its exports under `default`, the ES one with them at the top
|
|
190
|
+
const loaded = await import(pathToFileURL(require.resolve('tailwindcss')).href);
|
|
191
|
+
const found = loaded.compile ?? loaded.default?.compile;
|
|
192
|
+
if (typeof found !== 'function')
|
|
193
|
+
return null;
|
|
194
|
+
compile = found;
|
|
195
|
+
themePath = require.resolve('tailwindcss/theme.css');
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// The colors are the project's own names: Tailwind only knows them from the stylesheet
|
|
201
|
+
const colors = colorTokens.map(token => `--color-${token}: #000;`).join(' ');
|
|
202
|
+
const compiler = await compile(`@import "tailwindcss/theme.css" layer(theme);\n@theme inline { ${colors} }\n@tailwind utilities;\n`, {
|
|
203
|
+
base: ctx.root,
|
|
204
|
+
loadStylesheet: async (id, base) => { const file = id.startsWith('tailwindcss') ? themePath : id; return { path: file, base, content: fs.readFileSync(file, 'utf-8') }; },
|
|
205
|
+
});
|
|
206
|
+
let css = compiler.build([]);
|
|
207
|
+
const unknown = [];
|
|
208
|
+
for (const candidate of new Set(classes)) {
|
|
209
|
+
const next = compiler.build([candidate]);
|
|
210
|
+
if (next === css)
|
|
211
|
+
unknown.push(candidate);
|
|
212
|
+
css = next;
|
|
213
|
+
}
|
|
214
|
+
return classes.filter(c => unknown.includes(c));
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=stylesets-store.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function setWrValue(id: string, element: Record<string, unknown>, lang: string): void;
|
|
2
|
+
export declare function getWrValue(id: string): unknown;
|
|
3
|
+
export declare function registerWrSchema(id: string, multilangFields: readonly string[]): void;
|
|
4
|
+
export declare function getWrSchema(id: string): readonly string[];
|
|
5
|
+
//# sourceMappingURL=registry.d.ts.map
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// packages/editor/src/registry.ts
|
|
2
|
+
import { wrLocalized } from '@webround/core/localized';
|
|
3
|
+
// What the editor knows of each element, to give to the panel when it is selected. The values are kept as the JSON has them (a text in every language) and put in the language shown when they are READ: an element
|
|
4
|
+
// says which of its fields are texts in every language (`registerWrSchema`) when it is drawn, and one that is drawn later than the values were set (a row that appears when the store has answered, a form that opens, a
|
|
5
|
+
// tab) would otherwise be read before it said so, and the panel would show the whole object (`[object Object]`).
|
|
6
|
+
const values = new Map();
|
|
7
|
+
const schemas = new Map();
|
|
8
|
+
export function setWrValue(id, element, lang) {
|
|
9
|
+
values.set(id, { element, lang });
|
|
10
|
+
}
|
|
11
|
+
export function getWrValue(id) {
|
|
12
|
+
const entry = values.get(id);
|
|
13
|
+
if (!entry)
|
|
14
|
+
return null;
|
|
15
|
+
const multilangFields = getWrSchema(id);
|
|
16
|
+
return Object.fromEntries(Object.entries(entry.element).map(([key, value]) => [key, multilangFields.includes(key) && value && typeof value === 'object' ? wrLocalized(value, entry.lang) : value]));
|
|
17
|
+
}
|
|
18
|
+
export function registerWrSchema(id, multilangFields) {
|
|
19
|
+
schemas.set(id, multilangFields);
|
|
20
|
+
}
|
|
21
|
+
export function getWrSchema(id) {
|
|
22
|
+
return schemas.get(id) ?? [];
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=registry.js.map
|