@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,133 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { findNode, readJsxLiteral } from '../../vite-plugins/jsx-tree.js';
|
|
4
|
+
import { idsInCode } from '../../check/orphans.js';
|
|
5
|
+
import { defineOperation, invalid, object, OperationError, requiredText, text } from '../define.js';
|
|
6
|
+
import { COMMENT_TEXT_MAX, COMMENTS_MAX, readComments, writeComments } from '../comments-store.js';
|
|
7
|
+
import { readJsonFile, scopeOfInput } from './project.js';
|
|
8
|
+
// What is said about the elements of a site: add a comment to an element (or to a page), list them, mark one as dealt with, delete one. The
|
|
9
|
+
// comments are in `.wr/comments.json` and never reach the published site (see comments-store.ts). An agent starts its work with
|
|
10
|
+
// `comment-list`: what is open is what the people who work on the site asked for.
|
|
11
|
+
const SCOPE_PARAMS = [
|
|
12
|
+
{ name: 'page', type: 'string', help: 'the page (its name, e.g. home) or a shared component (_shared/header, _shared/footer, _shared/<name>)' },
|
|
13
|
+
{ name: 'file', type: 'string', help: 'instead of page: the code file, relative to src/ (what the editor\'s own page sends)' },
|
|
14
|
+
];
|
|
15
|
+
const EXCERPT_MAX = 80;
|
|
16
|
+
/** The kind of the element and the start of what it says, kept with the comment. */
|
|
17
|
+
function contextOf(ctx, scope, id) {
|
|
18
|
+
const file = ctx.resolveInSrc(scope.file);
|
|
19
|
+
if (!fs.existsSync(file))
|
|
20
|
+
throw new OperationError(404, 'file-not-found', `${scope.file} does not exist`);
|
|
21
|
+
const found = findNode(ctx.project(), file, id);
|
|
22
|
+
if (!found)
|
|
23
|
+
throw new OperationError(404, 'node-not-found', `no element with id "${id}" in ${scope.file}`);
|
|
24
|
+
const own = (readJsonFile(ctx, scope.json) ?? {})[id];
|
|
25
|
+
const content = own?.content;
|
|
26
|
+
const fromJson = typeof content === 'string' ? content : typeof content?.default === 'string' ? content.default : undefined;
|
|
27
|
+
const literal = (key) => { const r = readJsxLiteral(found.node, key); return r.present && typeof r.value === 'string' ? r.value : undefined; };
|
|
28
|
+
const raw = fromJson ?? literal('html') ?? literal('alt') ?? literal('src');
|
|
29
|
+
const excerpt = raw?.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, EXCERPT_MAX);
|
|
30
|
+
return { kind: found.kind, ...(excerpt ? { excerpt } : {}) };
|
|
31
|
+
}
|
|
32
|
+
function authorOf(input) {
|
|
33
|
+
const given = object(input, 'author');
|
|
34
|
+
const name = typeof given?.name === 'string' && given.name.trim() ? given.name.trim().slice(0, 80) : undefined;
|
|
35
|
+
const kind = given?.kind;
|
|
36
|
+
if (kind !== undefined && kind !== 'user' && kind !== 'ai')
|
|
37
|
+
throw invalid('author.kind must be user or ai', 'invalid-author');
|
|
38
|
+
// Whoever does not say who they are is an agent: the editor always says (it knows the person)
|
|
39
|
+
return { name: name ?? (kind === 'user' ? 'User' : 'AI'), kind: kind ?? 'ai' };
|
|
40
|
+
}
|
|
41
|
+
export const commentAdd = defineOperation({
|
|
42
|
+
name: 'comment-add',
|
|
43
|
+
summary: 'Leaves a comment on an element of a page (or on the page, or on a shared component, when no element is given): a note for the people who work on the site, or for the agent that works on it. It is plain text, kept apart from the site (it is never published).',
|
|
44
|
+
params: [
|
|
45
|
+
...SCOPE_PARAMS,
|
|
46
|
+
{ name: 'id', type: 'string', help: 'the element the comment is about; without it, about the page (or shared component) itself' },
|
|
47
|
+
{ name: 'text', type: 'string', required: true, help: `what to say (plain text, up to ${COMMENT_TEXT_MAX} characters)` },
|
|
48
|
+
{ name: 'author', type: 'object', help: '{ name, kind }: kind is user (a person) or ai; an agent that does not say who it is, is ai' },
|
|
49
|
+
],
|
|
50
|
+
run: (input, ctx) => {
|
|
51
|
+
const scope = scopeOfInput(ctx, input);
|
|
52
|
+
const body = requiredText(input, 'text').trim();
|
|
53
|
+
if (!body)
|
|
54
|
+
throw invalid('the comment is empty', 'empty-comment');
|
|
55
|
+
if (body.length > COMMENT_TEXT_MAX)
|
|
56
|
+
throw invalid(`a comment has up to ${COMMENT_TEXT_MAX} characters`, 'comment-too-long');
|
|
57
|
+
const elementId = text(input, 'id');
|
|
58
|
+
const all = readComments(ctx);
|
|
59
|
+
if (all.length >= COMMENTS_MAX)
|
|
60
|
+
throw new OperationError(409, 'too-many-comments', `the project has ${COMMENTS_MAX} comments: resolve or delete some first`);
|
|
61
|
+
const comment = {
|
|
62
|
+
id: `c-${crypto.randomBytes(5).toString('hex')}`,
|
|
63
|
+
scope: scope.name,
|
|
64
|
+
...(elementId ? { elementId } : {}),
|
|
65
|
+
text: body,
|
|
66
|
+
author: authorOf(input),
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
status: 'open',
|
|
69
|
+
...(elementId ? { context: contextOf(ctx, scope, elementId) } : {}),
|
|
70
|
+
};
|
|
71
|
+
writeComments(ctx, [...all, comment]);
|
|
72
|
+
return { comment };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
export const commentList = defineOperation({
|
|
76
|
+
name: 'comment-list',
|
|
77
|
+
readOnly: true,
|
|
78
|
+
summary: 'The comments left on the site: the open ones by default (what people asked for). Each has where it is (page and element), who wrote it, what the element was, and `orphan` when the element is no longer in the code. Start here when you are asked to work on the site.',
|
|
79
|
+
params: [
|
|
80
|
+
{ name: 'page', type: 'string', help: 'only this page (or a shared component: _shared/header, _shared/<name>…)' },
|
|
81
|
+
{ name: 'id', type: 'string', help: 'only this element (with page)' },
|
|
82
|
+
{ name: 'status', type: 'string', choices: ['open', 'resolved', 'all'], default: 'open', help: 'open (default), resolved or all' },
|
|
83
|
+
],
|
|
84
|
+
run: (input, ctx) => {
|
|
85
|
+
const page = text(input, 'page');
|
|
86
|
+
const id = text(input, 'id');
|
|
87
|
+
const status = text(input, 'status') ?? 'open';
|
|
88
|
+
if (!['open', 'resolved', 'all'].includes(status))
|
|
89
|
+
throw invalid('status must be open, resolved or all', 'invalid-status');
|
|
90
|
+
const all = readComments(ctx);
|
|
91
|
+
const wanted = all.filter(c => (page === undefined || c.scope === page) && (id === undefined || c.elementId === id) && (status === 'all' || c.status === status));
|
|
92
|
+
// An element that is not in the code any more: said, not guessed when some id is computed (any of them could be it)
|
|
93
|
+
const known = wanted.some(c => c.elementId) ? idsInCode(ctx) : null;
|
|
94
|
+
const comments = wanted.map(c => ({ ...c, orphan: known !== null && c.elementId !== undefined && !known.dynamicIds && !known.ids.has(c.elementId) }));
|
|
95
|
+
return { comments, open: all.filter(c => c.status === 'open').length };
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
export const commentResolve = defineOperation({
|
|
99
|
+
name: 'comment-resolve',
|
|
100
|
+
summary: 'Marks a comment as dealt with (or open again): it stays in the list of resolved ones. Do it after acting on what it asked, so the next run does not do it twice.',
|
|
101
|
+
params: [
|
|
102
|
+
{ name: 'comment', type: 'string', positional: true, required: true, help: 'the comment\'s id (from comment-list)' },
|
|
103
|
+
{ name: 'resolved', type: 'boolean', default: true, help: 'false opens it again' },
|
|
104
|
+
],
|
|
105
|
+
run: (input, ctx) => {
|
|
106
|
+
const id = requiredText(input, 'comment');
|
|
107
|
+
const resolved = input.resolved !== false && input.resolved !== 'false';
|
|
108
|
+
const all = readComments(ctx);
|
|
109
|
+
const found = all.find(c => c.id === id);
|
|
110
|
+
if (!found)
|
|
111
|
+
throw new OperationError(404, 'comment-not-found', `no comment "${id}"`);
|
|
112
|
+
const { resolvedAt: _drop, ...rest } = found;
|
|
113
|
+
const next = resolved ? { ...rest, status: 'resolved', resolvedAt: new Date().toISOString() } : { ...rest, status: 'open' };
|
|
114
|
+
writeComments(ctx, all.map(c => (c.id === id ? next : c)));
|
|
115
|
+
return { comment: next };
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
export const commentDelete = defineOperation({
|
|
119
|
+
name: 'comment-delete',
|
|
120
|
+
destructive: true,
|
|
121
|
+
summary: 'Deletes a comment for good (resolve it to keep it as done).',
|
|
122
|
+
params: [{ name: 'comment', type: 'string', positional: true, required: true, help: 'the comment\'s id (from comment-list)' }],
|
|
123
|
+
run: (input, ctx) => {
|
|
124
|
+
const id = requiredText(input, 'comment');
|
|
125
|
+
const all = readComments(ctx);
|
|
126
|
+
if (!all.some(c => c.id === id))
|
|
127
|
+
throw new OperationError(404, 'comment-not-found', `no comment "${id}"`);
|
|
128
|
+
writeComments(ctx, all.filter(c => c.id !== id));
|
|
129
|
+
return { deleted: id };
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
export const COMMENT_OPERATIONS = [commentAdd, commentList, commentResolve, commentDelete];
|
|
133
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const pageOutline: import("../define.js").Operation;
|
|
2
|
+
export interface ContentItem {
|
|
3
|
+
scope: string;
|
|
4
|
+
id: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
/** The field: content, alt, ariaLabel. */
|
|
7
|
+
field: string;
|
|
8
|
+
/** What it says in each language (the default language is `default`). */
|
|
9
|
+
values: Record<string, string>;
|
|
10
|
+
/** The languages it has nothing in. */
|
|
11
|
+
missing: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const pageContent: import("../define.js").Operation;
|
|
14
|
+
export declare const blocksList: import("../define.js").Operation;
|
|
15
|
+
export declare const CONTENT_OPERATIONS: import("../define.js").Operation[];
|
|
16
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { wrLangKey } from '@webround/core/routing';
|
|
2
|
+
import { WR_BLOCKS } from '../../blocks.js';
|
|
3
|
+
import { checkScopeFromCode, sharedFollower } from '../../check/producer-code.js';
|
|
4
|
+
import { walk } from '../../check/types.js';
|
|
5
|
+
import { defineOperation, requiredText, text } from '../define.js';
|
|
6
|
+
import { readLangs, scopesOfPage } from './project.js';
|
|
7
|
+
// Reading a site: what is on a page, and what it says in each language. The same tree the checks judge (check/producer-code.ts): the
|
|
8
|
+
// code, with the JSON of its values over it. Nothing here is a second reading of the project.
|
|
9
|
+
const PAGE_PARAM = { name: 'page', type: 'string', positional: true, required: true, help: 'the page name (page-list shows them)' };
|
|
10
|
+
export const pageOutline = defineOperation({
|
|
11
|
+
name: 'page-outline',
|
|
12
|
+
readOnly: true,
|
|
13
|
+
summary: 'What is on a page, as a tree: for each element its id, kind, tag, text, the attributes that matter, what it says in each language (values), whether it is hidden, and where in the code it is. Header and footer are listed on their own (_shared/header, _shared/footer), and the shared components the page uses (a card…) are inside it, each element with its `scope` (_shared/product-card): that name is the `page` to use to change them.',
|
|
14
|
+
params: [PAGE_PARAM],
|
|
15
|
+
run: (input, ctx) => {
|
|
16
|
+
const page = requiredText(input, 'page');
|
|
17
|
+
const follow = sharedFollower(ctx);
|
|
18
|
+
const scopes = scopesOfPage(ctx, page).map(scope => checkScopeFromCode(ctx, scope, { includeHidden: true, follow }));
|
|
19
|
+
return {
|
|
20
|
+
page,
|
|
21
|
+
scopes: scopes.map(({ name, file, nodes, syntax }) => ({ name, file, nodes, ...(syntax.length ? { syntax } : {}) })),
|
|
22
|
+
// What is wrong in the file of a shared component the page uses (the scopes above hold only header, page and footer)
|
|
23
|
+
...(follow.syntax.length ? { sharedSyntax: follow.syntax } : {}),
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
export const pageContent = defineOperation({
|
|
28
|
+
name: 'page-content',
|
|
29
|
+
readOnly: true,
|
|
30
|
+
summary: 'Everything on a page that is translated (texts, alt texts, labels), in every language of the site, and which languages are missing for each. The default language is called "default" in the values.',
|
|
31
|
+
params: [
|
|
32
|
+
PAGE_PARAM,
|
|
33
|
+
{ name: 'lang', type: 'string', help: 'only this language (a code of langs-get)' },
|
|
34
|
+
],
|
|
35
|
+
run: (input, ctx) => {
|
|
36
|
+
const page = requiredText(input, 'page');
|
|
37
|
+
const langs = readLangs(ctx) ?? undefined;
|
|
38
|
+
// The languages of the site, each under the key it has in the JSON; a site that is not multilingual has just the default
|
|
39
|
+
const keys = langs ? [...new Set(langs.available.map(code => wrLangKey(code, langs)))] : ['default'];
|
|
40
|
+
const only = text(input, 'lang');
|
|
41
|
+
const wanted = only === undefined ? keys : [wrLangKey(only, langs)];
|
|
42
|
+
const items = [];
|
|
43
|
+
const follow = sharedFollower(ctx);
|
|
44
|
+
for (const scope of scopesOfPage(ctx, page)) {
|
|
45
|
+
const tree = checkScopeFromCode(ctx, scope, { includeHidden: true, follow });
|
|
46
|
+
walk(tree.nodes, (node) => {
|
|
47
|
+
if (!node.id || !node.values)
|
|
48
|
+
return;
|
|
49
|
+
for (const [field, values] of Object.entries(node.values)) {
|
|
50
|
+
items.push({
|
|
51
|
+
scope: node.scope ?? scope.name, id: node.id, kind: node.kind, field,
|
|
52
|
+
values: Object.fromEntries(Object.entries(values).filter(([key]) => only === undefined || wanted.includes(key))),
|
|
53
|
+
missing: wanted.filter(key => values[key] === undefined),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
page,
|
|
60
|
+
languages: langs ? langs.available.map(code => ({ code, key: wrLangKey(code, langs), ...(code === langs.default ? { default: true } : {}) })) : [{ code: 'default', key: 'default', default: true }],
|
|
61
|
+
items,
|
|
62
|
+
missingCount: items.reduce((sum, item) => sum + item.missing.length, 0),
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
export const blocksList = defineOperation({
|
|
67
|
+
name: 'blocks-list',
|
|
68
|
+
readOnly: true,
|
|
69
|
+
summary: 'The catalog of ready-made blocks the editor offers (sections, layouts, elements): each is JSX that node-insert takes by its id.',
|
|
70
|
+
run: () => ({ blocks: WR_BLOCKS.map(({ id, category, jsx }) => ({ id, category, jsx })) }),
|
|
71
|
+
});
|
|
72
|
+
export const CONTENT_OPERATIONS = [pageOutline, pageContent, blocksList];
|
|
73
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineOperation, object, requiredText } from '../define.js';
|
|
2
|
+
import { patchJsonFile, readJsonFile } from './project.js';
|
|
3
|
+
// The raw access the editor's page needs to keep the values of its elements and its own history (an undo writes back exactly what was
|
|
4
|
+
// there). An agent has meaningful operations for this (node-set, page-content…): these are not offered to it.
|
|
5
|
+
export const jsonRead = defineOperation({
|
|
6
|
+
name: 'json-read',
|
|
7
|
+
readOnly: true,
|
|
8
|
+
summary: 'A JSON file of the project (relative to src/), or null if there is none.',
|
|
9
|
+
expose: 'editor',
|
|
10
|
+
params: [{ name: 'file', type: 'string', required: true, help: 'the file, relative to src/' }],
|
|
11
|
+
run: (input, ctx) => ({ value: readJsonFile(ctx, requiredText(input, 'file')) }),
|
|
12
|
+
});
|
|
13
|
+
export const jsonPatch = defineOperation({
|
|
14
|
+
name: 'json-patch',
|
|
15
|
+
summary: 'Merges values into a JSON file of the project (a null value removes the key).',
|
|
16
|
+
expose: 'editor',
|
|
17
|
+
params: [
|
|
18
|
+
{ name: 'file', type: 'string', required: true, help: 'the file, relative to src/' },
|
|
19
|
+
{ name: 'patch', type: 'object', required: true, help: 'the keys to write; null removes one' },
|
|
20
|
+
],
|
|
21
|
+
run: (input, ctx) => {
|
|
22
|
+
patchJsonFile(ctx, requiredText(input, 'file'), object(input, 'patch', { required: true }));
|
|
23
|
+
return {};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
export const JSON_OPERATIONS = [jsonRead, jsonPatch];
|
|
27
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { langCodeValues } from '@webround/core/lang-codes';
|
|
2
|
+
import { defineOperation, invalid, list, requiredText } from '../define.js';
|
|
3
|
+
import { LANGS_FILE, patchJsonFile, readLangs } from './project.js';
|
|
4
|
+
export const langsGet = defineOperation({
|
|
5
|
+
name: 'langs-get',
|
|
6
|
+
readOnly: true,
|
|
7
|
+
summary: 'The languages of the site: the default one and all the available ones. null while the site is not multilingual.',
|
|
8
|
+
run: (_input, ctx) => ({ langs: readLangs(ctx) }),
|
|
9
|
+
});
|
|
10
|
+
export const langsSet = defineOperation({
|
|
11
|
+
name: 'langs-set',
|
|
12
|
+
summary: 'Sets the languages of the site. Texts already written in a language that is taken away stay in the JSON (and come back if it is added again).',
|
|
13
|
+
params: [
|
|
14
|
+
{ name: 'default', type: 'string', required: true, help: 'the default language code (it is not in the URL), e.g. it' },
|
|
15
|
+
{ name: 'available', type: 'array', required: true, help: 'every language code of the site, the default included' },
|
|
16
|
+
],
|
|
17
|
+
run: (input, ctx) => {
|
|
18
|
+
const defaultCode = requiredText(input, 'default');
|
|
19
|
+
const available = list(input, 'available', { required: true });
|
|
20
|
+
const known = new Set(langCodeValues);
|
|
21
|
+
const unknown = [...available, defaultCode].filter(code => !known.has(code));
|
|
22
|
+
if (unknown.length > 0)
|
|
23
|
+
throw invalid(`unknown language code: ${[...new Set(unknown)].join(', ')}`, 'invalid-language');
|
|
24
|
+
if (!available.includes(defaultCode))
|
|
25
|
+
throw invalid('the default language must be one of the available ones', 'invalid-language');
|
|
26
|
+
const langs = { default: defaultCode, available: [...new Set(available)] };
|
|
27
|
+
patchJsonFile(ctx, LANGS_FILE, { default: langs.default, available: langs.available });
|
|
28
|
+
return { langs };
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
export const LANG_OPERATIONS = [langsGet, langsSet];
|
|
32
|
+
//# sourceMappingURL=langs.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const nodeGet: import("../define.js").Operation;
|
|
2
|
+
export declare const nodeSet: import("../define.js").Operation;
|
|
3
|
+
export declare const nodeInsert: import("../define.js").Operation;
|
|
4
|
+
export declare const nodeDelete: import("../define.js").Operation;
|
|
5
|
+
export declare const nodeMove: import("../define.js").Operation;
|
|
6
|
+
export declare const nodeSource: import("../define.js").Operation;
|
|
7
|
+
export declare const NODE_OPERATIONS: import("../define.js").Operation[];
|
|
8
|
+
//# sourceMappingURL=nodes.d.ts.map
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { wrLocalizeItems, wrSetLocalized } from '@webround/core/localized';
|
|
3
|
+
import { getAllFields } from '../../schema.js';
|
|
4
|
+
import { WR_BLOCKS } from '../../blocks.js';
|
|
5
|
+
import { deleteJsx, findNode, insertJsx, moveJsx, readJsxLiteral, writeJsxLiteral } from '../../vite-plugins/jsx-tree.js';
|
|
6
|
+
import { defineOperation, flag, invalid, object, OperationError, requiredText, text } from '../define.js';
|
|
7
|
+
import { dropElementComments } from '../comments-store.js';
|
|
8
|
+
import { langKeyOf, patchJsonFile, readJsonFile, scopeOfInput } from './project.js';
|
|
9
|
+
// The elements of a page: read one, change one of its values, add markup, delete, move. The markup is code (the same JSX a developer writes
|
|
10
|
+
// by hand), so every caller says the same thing; the values that are JSON (translated texts, hidden…) go where the schema says they live.
|
|
11
|
+
const MODES = ['before', 'after', 'first-child', 'last-child'];
|
|
12
|
+
const SCOPE_PARAMS = [
|
|
13
|
+
{ name: 'page', type: 'string', help: 'the page (its name, e.g. home) or a shared component (_shared/header, _shared/footer, _shared/<name> as declared in config/shared.json)' },
|
|
14
|
+
{ name: 'file', type: 'string', help: 'instead of page: the code file, relative to src/ (what the editor\'s own page sends)' },
|
|
15
|
+
];
|
|
16
|
+
const absolute = (ctx, scope) => ctx.resolveInSrc(scope.file);
|
|
17
|
+
function findElement(ctx, scope, id) {
|
|
18
|
+
const file = absolute(ctx, scope);
|
|
19
|
+
if (!fs.existsSync(file))
|
|
20
|
+
throw new OperationError(404, 'file-not-found', `${scope.file} does not exist`);
|
|
21
|
+
const found = findNode(ctx.project(), file, id);
|
|
22
|
+
if (!found)
|
|
23
|
+
throw new OperationError(404, 'node-not-found', `no element with id "${id}" in ${scope.file}`);
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
/** A value for a field, in the type the field has (a toggle is true or false, whatever way it was said). */
|
|
27
|
+
/** The entries of a list field as the caller gave them (a list, or a JSON string of one) written into the JSON, in a language. */
|
|
28
|
+
function localizedEntries(field, current, value, lang) {
|
|
29
|
+
let entries = value;
|
|
30
|
+
if (typeof entries === 'string') {
|
|
31
|
+
try {
|
|
32
|
+
entries = JSON.parse(entries);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw invalid(`${field.key} must be a list`, 'invalid-value');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!Array.isArray(entries) || entries.some(entry => typeof entry !== 'object' || entry === null || Array.isArray(entry)))
|
|
39
|
+
throw invalid(`${field.key} must be a list of entries: ${field.itemFields.map(f => f.key).join(', ')}`, 'invalid-value');
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
for (const item of field.itemFields) {
|
|
42
|
+
const given = entry[item.key];
|
|
43
|
+
const ok = typeof given === 'string' || (item.multilang === true && typeof given === 'object' && given !== null);
|
|
44
|
+
if (!ok)
|
|
45
|
+
throw invalid(`every entry of ${field.key} needs a ${item.key} (text)`, 'invalid-value');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return wrLocalizeItems(current, entries, field.itemFields.filter(f => f.multilang).map(f => f.key), lang);
|
|
49
|
+
}
|
|
50
|
+
function coerce(field, value) {
|
|
51
|
+
const bad = (why) => invalid(`${field.key} ${why}`, 'invalid-value');
|
|
52
|
+
switch (field.type) {
|
|
53
|
+
case 'toggle':
|
|
54
|
+
if (typeof value === 'boolean')
|
|
55
|
+
return value;
|
|
56
|
+
if (value === 'true' || value === 'false')
|
|
57
|
+
return value === 'true';
|
|
58
|
+
throw bad('must be true or false');
|
|
59
|
+
case 'number': {
|
|
60
|
+
const n = typeof value === 'number' ? value : typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN;
|
|
61
|
+
if (!Number.isFinite(n))
|
|
62
|
+
throw bad('must be a number');
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
case 'select':
|
|
66
|
+
if (typeof value !== 'string' || !field.options.includes(value))
|
|
67
|
+
throw bad(`must be one of: ${field.options.join(', ')}`);
|
|
68
|
+
return value;
|
|
69
|
+
default:
|
|
70
|
+
if (typeof value !== 'string')
|
|
71
|
+
throw bad('must be text');
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export const nodeGet = defineOperation({
|
|
76
|
+
name: 'node-get',
|
|
77
|
+
readOnly: true,
|
|
78
|
+
summary: 'One element of a page: what kind it is, the values written in the code (src, href…) and the ones in the JSON (texts per language, hidden…).',
|
|
79
|
+
params: [...SCOPE_PARAMS, { name: 'id', type: 'string', positional: true, required: true, help: 'the element id (its data-wr-id)' }],
|
|
80
|
+
run: (input, ctx) => {
|
|
81
|
+
const scope = scopeOfInput(ctx, input);
|
|
82
|
+
const id = requiredText(input, 'id');
|
|
83
|
+
const found = findElement(ctx, scope, id);
|
|
84
|
+
const fields = getAllFields(found.kind).filter(f => f.backend === 'ast');
|
|
85
|
+
const values = {};
|
|
86
|
+
for (const f of fields) {
|
|
87
|
+
const { present, value } = readJsxLiteral(found.node, f.key);
|
|
88
|
+
if (present && value !== undefined)
|
|
89
|
+
values[f.key] = value;
|
|
90
|
+
}
|
|
91
|
+
const json = (readJsonFile(ctx, scope.json) ?? {})[id] ?? null;
|
|
92
|
+
return { kind: found.kind, fields: fields.map(f => f.key), values, json };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
export const nodeSet = defineOperation({
|
|
96
|
+
name: 'node-set',
|
|
97
|
+
summary: 'Sets one value of an element: where it is stored (the code, or the JSON) is decided by its kind\'s schema. A translated value (a text, an alt, the labels of a menu\'s entries) is written in the language asked for, and the other languages are kept; a list (`items` of a nav) is given whole, as a list of { label, href }. A text\'s content is HTML; plain text is fine.',
|
|
98
|
+
params: [
|
|
99
|
+
...SCOPE_PARAMS,
|
|
100
|
+
{ name: 'id', type: 'string', positional: true, required: true, help: 'the element id' },
|
|
101
|
+
{ name: 'key', type: 'string', required: true, help: 'the value: content, alt, src, href, hidden, ariaLabel… (what the element\'s kind has)' },
|
|
102
|
+
{ name: 'value', type: 'any', required: true, help: 'the new value; null takes a code value (src, href…) away' },
|
|
103
|
+
{ name: 'lang', type: 'string', help: 'the language of a translated value, or of the translated fields of the entries of a list (a menu\'s labels): a code of langs-get; the default language if not said' },
|
|
104
|
+
],
|
|
105
|
+
run: (input, ctx) => {
|
|
106
|
+
const scope = scopeOfInput(ctx, input);
|
|
107
|
+
const id = requiredText(input, 'id');
|
|
108
|
+
const key = requiredText(input, 'key');
|
|
109
|
+
const found = findElement(ctx, scope, id);
|
|
110
|
+
const field = getAllFields(found.kind).find(f => f.key === key);
|
|
111
|
+
if (!field) {
|
|
112
|
+
const known = getAllFields(found.kind).map(f => f.key);
|
|
113
|
+
throw invalid(`a ${found.kind} has no "${key}" (it has: ${known.join(', ') || 'nothing to set'})`, 'unknown-field');
|
|
114
|
+
}
|
|
115
|
+
const value = input.value;
|
|
116
|
+
if (field.backend === 'ast') {
|
|
117
|
+
const previous = readJsxLiteral(found.node, key);
|
|
118
|
+
writeJsxLiteral(found.node, key, value === null ? null : coerce(field, value));
|
|
119
|
+
found.node.getSourceFile().saveSync();
|
|
120
|
+
return { backend: 'ast', previous: previous.present ? previous.value : null };
|
|
121
|
+
}
|
|
122
|
+
// The JSON: the element's own object, one key of it
|
|
123
|
+
const all = readJsonFile(ctx, scope.json) ?? {};
|
|
124
|
+
const current = all[id] ?? {};
|
|
125
|
+
let next;
|
|
126
|
+
let lang;
|
|
127
|
+
if (field.multilang) {
|
|
128
|
+
if (typeof value !== 'string')
|
|
129
|
+
throw invalid(`${key} must be text`, 'invalid-value');
|
|
130
|
+
lang = langKeyOf(ctx, text(input, 'lang'));
|
|
131
|
+
next = wrSetLocalized(current[key], lang, value);
|
|
132
|
+
}
|
|
133
|
+
else if (field.type === 'list') {
|
|
134
|
+
// The entries of a list (a menu): what is translated in them is written in the language asked for, the others are kept
|
|
135
|
+
lang = langKeyOf(ctx, text(input, 'lang'));
|
|
136
|
+
next = localizedEntries(field, current[key], value, lang);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
next = value === null ? null : coerce(field, value);
|
|
140
|
+
}
|
|
141
|
+
const updated = { ...current };
|
|
142
|
+
if (next === null)
|
|
143
|
+
delete updated[key];
|
|
144
|
+
else
|
|
145
|
+
updated[key] = next;
|
|
146
|
+
patchJsonFile(ctx, scope.json, { [id]: updated });
|
|
147
|
+
return { backend: 'json', ...(lang ? { lang } : {}), previous: current[key] ?? null };
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
function anchorOf(input) {
|
|
151
|
+
const given = object(input, 'anchor');
|
|
152
|
+
if (given) {
|
|
153
|
+
if ('root' in given ? given.root === true : typeof given.id === 'string')
|
|
154
|
+
return given;
|
|
155
|
+
throw invalid('anchor must be { id } or { root: true }', 'anchor-not-found');
|
|
156
|
+
}
|
|
157
|
+
const id = text(input, 'anchorId');
|
|
158
|
+
return id ? { id } : null;
|
|
159
|
+
}
|
|
160
|
+
const ANCHOR_PARAMS = [
|
|
161
|
+
{ name: 'anchor', type: 'object', help: 'where: { id: "<element id>" } or { root: true } (the page itself); what the editor sends' },
|
|
162
|
+
{ name: 'anchorId', type: 'string', help: 'where, as just the element id; without it (and without anchor) the page itself' },
|
|
163
|
+
];
|
|
164
|
+
export const nodeInsert = defineOperation({
|
|
165
|
+
name: 'node-insert',
|
|
166
|
+
summary: 'Adds markup to a page, next to an element or inside one (a section). Give the JSX itself, or the id of a block of the catalog (blocks-list). Ids that are taken are renumbered; the imports and the indentation are taken care of. The result has the final ids.',
|
|
167
|
+
params: [
|
|
168
|
+
...SCOPE_PARAMS,
|
|
169
|
+
...ANCHOR_PARAMS,
|
|
170
|
+
{ name: 'mode', type: 'string', choices: [...MODES], help: 'before, after, first-child or last-child of the anchor (default: after an element, last-child of the page)' },
|
|
171
|
+
{ name: 'block', type: 'string', help: 'the id of a block of the catalog (blocks-list)' },
|
|
172
|
+
{ name: 'jsx', type: 'string', help: 'markup to insert: one or more elements, written the way you would write them by hand' },
|
|
173
|
+
{ name: 'uniqueIds', type: 'boolean', default: true, help: 'renumber ids that are already taken (false refuses instead)' },
|
|
174
|
+
{ name: 'jsonSeed', type: 'object', help: 'initial JSON values for the new elements: { "<id>": { … } }' },
|
|
175
|
+
],
|
|
176
|
+
run: (input, ctx) => {
|
|
177
|
+
const scope = scopeOfInput(ctx, input);
|
|
178
|
+
const blockId = text(input, 'block');
|
|
179
|
+
let jsx = text(input, 'jsx');
|
|
180
|
+
if (blockId !== undefined) {
|
|
181
|
+
const block = WR_BLOCKS.find(b => b.id === blockId);
|
|
182
|
+
if (!block)
|
|
183
|
+
throw invalid(`no block "${blockId}" (blocks-list shows them)`, 'unknown-block');
|
|
184
|
+
jsx = block.jsx;
|
|
185
|
+
}
|
|
186
|
+
if (typeof jsx !== 'string' || !jsx.trim())
|
|
187
|
+
throw invalid('jsx (or block) required', 'invalid-jsx');
|
|
188
|
+
const anchor = anchorOf(input) ?? { root: true };
|
|
189
|
+
const requested = text(input, 'mode');
|
|
190
|
+
if (requested !== undefined && !MODES.includes(requested))
|
|
191
|
+
throw invalid(`mode must be one of: ${MODES.join(', ')}`, 'invalid-mode');
|
|
192
|
+
const mode = requested ?? ('root' in anchor ? 'last-child' : 'after');
|
|
193
|
+
const result = insertJsx(ctx.project(), absolute(ctx, scope), { jsx, anchor, mode, uniqueIds: flag(input, 'uniqueIds') });
|
|
194
|
+
if (!result.ok)
|
|
195
|
+
throw new OperationError(result.status, result.code, result.error);
|
|
196
|
+
// The initial values were for the ids asked for: they follow the ones that were changed
|
|
197
|
+
const seed = object(input, 'jsonSeed');
|
|
198
|
+
if (seed)
|
|
199
|
+
patchJsonFile(ctx, scope.json, Object.fromEntries(Object.entries(seed).map(([id, value]) => [result.idMap[id] ?? id, value])));
|
|
200
|
+
return { ids: result.ids, rootIds: result.rootIds, undoable: result.undoable, idMap: result.idMap, jsx: result.jsx, scope: scope.name };
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
export const nodeDelete = defineOperation({
|
|
204
|
+
name: 'node-delete',
|
|
205
|
+
destructive: true,
|
|
206
|
+
summary: 'Deletes an element with everything it contains, and the JSON values (texts, translations…) and the comments of all of them. The root of the header or footer cannot be deleted.',
|
|
207
|
+
params: [
|
|
208
|
+
...SCOPE_PARAMS,
|
|
209
|
+
{ name: 'id', type: 'string', positional: true, required: true, help: 'the element id' },
|
|
210
|
+
{ name: 'pruneImports', type: 'boolean', default: false, help: 'also remove the imports nothing uses any more' },
|
|
211
|
+
],
|
|
212
|
+
run: (input, ctx) => {
|
|
213
|
+
const scope = scopeOfInput(ctx, input);
|
|
214
|
+
const id = requiredText(input, 'id');
|
|
215
|
+
const removed = deleteJsx(ctx.project(), absolute(ctx, scope), id, { pruneImports: flag(input, 'pruneImports') === true });
|
|
216
|
+
if (!removed)
|
|
217
|
+
throw new OperationError(404, 'node-not-found', `no element with id "${id}" in ${scope.file}`);
|
|
218
|
+
if ('refused' in removed)
|
|
219
|
+
throw new OperationError(422, removed.code, removed.error);
|
|
220
|
+
// What was written for them goes too: a key left behind would bring the old text back the day an element takes the id again.
|
|
221
|
+
// What it held comes back, so a caller that can undo can put it back.
|
|
222
|
+
const all = readJsonFile(ctx, scope.json) ?? {};
|
|
223
|
+
const removedJson = Object.fromEntries(removed.removedIds.filter(removedId => removedId in all).map(removedId => [removedId, all[removedId]]));
|
|
224
|
+
if (Object.keys(removedJson).length > 0)
|
|
225
|
+
patchJsonFile(ctx, scope.json, Object.fromEntries(Object.keys(removedJson).map(removedId => [removedId, null])));
|
|
226
|
+
// And what was said about them: not restored by an undo (comments are not in the history)
|
|
227
|
+
const removedComments = dropElementComments(ctx, scope.name, removed.removedIds);
|
|
228
|
+
return { ...removed, removedJson, removedComments, scope: scope.name };
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
export const nodeMove = defineOperation({
|
|
232
|
+
name: 'node-move',
|
|
233
|
+
summary: 'Moves an element, with everything it contains, to another place in the same page (or header, or footer). It answers where it was.',
|
|
234
|
+
params: [
|
|
235
|
+
...SCOPE_PARAMS,
|
|
236
|
+
{ name: 'id', type: 'string', positional: true, required: true, help: 'the element to move' },
|
|
237
|
+
...ANCHOR_PARAMS,
|
|
238
|
+
{ name: 'mode', type: 'string', required: true, choices: [...MODES], help: 'before, after, first-child or last-child of the anchor' },
|
|
239
|
+
],
|
|
240
|
+
run: (input, ctx) => {
|
|
241
|
+
const scope = scopeOfInput(ctx, input);
|
|
242
|
+
const id = requiredText(input, 'id');
|
|
243
|
+
const anchor = anchorOf(input) ?? { root: true };
|
|
244
|
+
const mode = requiredText(input, 'mode');
|
|
245
|
+
if (!MODES.includes(mode))
|
|
246
|
+
throw invalid(`mode must be one of: ${MODES.join(', ')}`, 'invalid-mode');
|
|
247
|
+
const result = moveJsx(ctx.project(), absolute(ctx, scope), { id, anchor, mode: mode });
|
|
248
|
+
if (!result.ok)
|
|
249
|
+
throw new OperationError(result.status, result.code, result.error);
|
|
250
|
+
return { id: result.id, from: result.from, scope: scope.name };
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
export const nodeSource = defineOperation({
|
|
254
|
+
name: 'node-source',
|
|
255
|
+
readOnly: true,
|
|
256
|
+
summary: 'The markup of an element with everything it contains, as written in the code, and the JSON values (texts, translations…) of all of them: what node-insert takes, so a copy is one insert away (this is how an element is duplicated).',
|
|
257
|
+
params: [...SCOPE_PARAMS, { name: 'id', type: 'string', positional: true, required: true, help: 'the element id' }],
|
|
258
|
+
run: (input, ctx) => {
|
|
259
|
+
const scope = scopeOfInput(ctx, input);
|
|
260
|
+
const id = requiredText(input, 'id');
|
|
261
|
+
const found = findElement(ctx, scope, id);
|
|
262
|
+
const jsx = found.full.getText();
|
|
263
|
+
// The ids written in it (the element's own and its children's): their values in the JSON go with it
|
|
264
|
+
const ids = [...jsx.matchAll(/\bid=(?:"([^"]+)"|'([^']+)'|\{["']([^"']+)["']\})/g)].map(m => m[1] ?? m[2] ?? m[3]);
|
|
265
|
+
const all = readJsonFile(ctx, scope.json) ?? {};
|
|
266
|
+
return { jsx, ids, json: Object.fromEntries(ids.filter(i => i in all).map(i => [i, all[i]])), scope: scope.name };
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
export const NODE_OPERATIONS = [nodeGet, nodeSet, nodeInsert, nodeDelete, nodeMove, nodeSource];
|
|
270
|
+
//# sourceMappingURL=nodes.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The file a new page starts as. */
|
|
2
|
+
export declare function wrPageScaffold(component: string): string;
|
|
3
|
+
export declare const pageList: import("../define.js").Operation;
|
|
4
|
+
export declare const pageCreate: import("../define.js").Operation;
|
|
5
|
+
export declare const pageDelete: import("../define.js").Operation;
|
|
6
|
+
export declare const pageUpdate: import("../define.js").Operation;
|
|
7
|
+
export declare const PAGE_OPERATIONS: import("../define.js").Operation[];
|
|
8
|
+
//# sourceMappingURL=pages.d.ts.map
|