@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,686 @@
|
|
|
1
|
+
// Il TSX di una pagina letto e scritto come albero JSX: trovare un nodo per id e INSERIRE markup.
|
|
2
|
+
//
|
|
3
|
+
// Il contratto dell'inserimento è codice: JSX, lo stesso per l'interfaccia visuale, per chi scrive a mano e per un'AI. Questo modulo
|
|
4
|
+
// non impone una rappresentazione intermedia: valida, sceglie gli id, indenta, importa e scrive, in modo atomico. Se qualunque
|
|
5
|
+
// passo fallisce il file non viene toccato.
|
|
6
|
+
//
|
|
7
|
+
// Solo ts-morph, niente Vite né filesystem diretto: si prova con un Project in memoria (vedi test/insert.test.ts).
|
|
8
|
+
import { SyntaxKind, ts, } from 'ts-morph';
|
|
9
|
+
import { WR_COMPONENT_TO_KIND, wrTakesChildren } from '@webround/core/kinds';
|
|
10
|
+
export function toFullNode(node) {
|
|
11
|
+
if (node.getKind() === SyntaxKind.JsxSelfClosingElement)
|
|
12
|
+
return node;
|
|
13
|
+
const full = node.getParentIfKind(SyntaxKind.JsxElement);
|
|
14
|
+
if (!full)
|
|
15
|
+
throw new Error('JsxOpeningElement senza JsxElement genitore: file corrotto?');
|
|
16
|
+
return full;
|
|
17
|
+
}
|
|
18
|
+
export function loadSourceFile(project, filePath) {
|
|
19
|
+
const existing = project.getSourceFile(filePath);
|
|
20
|
+
if (existing) {
|
|
21
|
+
existing.refreshFromFileSystemSync();
|
|
22
|
+
return existing;
|
|
23
|
+
}
|
|
24
|
+
return project.addSourceFileAtPathIfExists(filePath);
|
|
25
|
+
}
|
|
26
|
+
/** Il valore letterale (stringa) dell'attributo `id` di un tag, se c'è ed è letterale. */
|
|
27
|
+
function literalId(node) {
|
|
28
|
+
const attr = node.getAttribute('id');
|
|
29
|
+
if (!attr || attr.getKind() !== SyntaxKind.JsxAttribute)
|
|
30
|
+
return null;
|
|
31
|
+
const init = attr.getInitializer?.();
|
|
32
|
+
return init?.getKind() === SyntaxKind.StringLiteral ? init.getLiteralValue() : null;
|
|
33
|
+
}
|
|
34
|
+
export function findNode(project, filePath, id) {
|
|
35
|
+
const sourceFile = loadSourceFile(project, filePath);
|
|
36
|
+
if (!sourceFile)
|
|
37
|
+
return null;
|
|
38
|
+
for (const node of tagNodes(sourceFile)) {
|
|
39
|
+
const kind = WR_COMPONENT_TO_KIND[node.getTagNameNode().getText()];
|
|
40
|
+
if (kind && literalId(node) === id)
|
|
41
|
+
return { node, full: toFullNode(node), kind };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function tagNodes(root) {
|
|
46
|
+
return [
|
|
47
|
+
...root.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
|
|
48
|
+
...root.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
/** Gli id letterali di tutti i componenti Wr* di un file. */
|
|
52
|
+
export function collectFileIds(sourceFile) {
|
|
53
|
+
const ids = new Set();
|
|
54
|
+
for (const node of tagNodes(sourceFile)) {
|
|
55
|
+
if (!WR_COMPONENT_TO_KIND[node.getTagNameNode().getText()])
|
|
56
|
+
continue;
|
|
57
|
+
const id = literalId(node);
|
|
58
|
+
if (id !== null)
|
|
59
|
+
ids.add(id);
|
|
60
|
+
}
|
|
61
|
+
return ids;
|
|
62
|
+
}
|
|
63
|
+
export function getIndent(node) {
|
|
64
|
+
const text = node.getSourceFile().getFullText();
|
|
65
|
+
const lineStart = text.lastIndexOf('\n', node.getStart() - 1) + 1;
|
|
66
|
+
return text.slice(lineStart, node.getStart()).match(/^[ \t]*/)?.[0] ?? '';
|
|
67
|
+
}
|
|
68
|
+
/** Il nome locale con cui un identificatore è già importato nel file, da qualunque modulo. */
|
|
69
|
+
function importedNames(sourceFile) {
|
|
70
|
+
const names = new Set();
|
|
71
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
72
|
+
for (const named of decl.getNamedImports())
|
|
73
|
+
names.add(named.getAliasNode()?.getText() ?? named.getName());
|
|
74
|
+
const def = decl.getDefaultImport();
|
|
75
|
+
if (def)
|
|
76
|
+
names.add(def.getText());
|
|
77
|
+
const ns = decl.getNamespaceImport();
|
|
78
|
+
if (ns)
|
|
79
|
+
names.add(ns.getText());
|
|
80
|
+
}
|
|
81
|
+
return names;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Importa da `@webround/core` i componenti che servono, se il file non li importa già: da dove non importa. Un file che prende
|
|
85
|
+
* `WrImage` da `@webround/editor` lo tiene così, e non ne ottiene un secondo (un identificatore duplicato non compila).
|
|
86
|
+
*/
|
|
87
|
+
export function ensureImports(sourceFile, names) {
|
|
88
|
+
const already = importedNames(sourceFile);
|
|
89
|
+
const missing = names.filter(name => !already.has(name));
|
|
90
|
+
if (missing.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
const moduleSpecifier = '@webround/core';
|
|
93
|
+
let importDecl = sourceFile.getImportDeclaration(d => d.getModuleSpecifierValue() === moduleSpecifier);
|
|
94
|
+
if (!importDecl)
|
|
95
|
+
importDecl = sourceFile.addImportDeclaration({ moduleSpecifier, namedImports: [] });
|
|
96
|
+
// Come testo, in coda all'ultimo specificatore: l'import che l'utente ha impaginato (anche su più righe) resta com'è,
|
|
97
|
+
// e togliere il nome (l'annulla) rida' il file identico. ts-morph, qui, rifarebbe l'impaginazione.
|
|
98
|
+
for (const name of missing) {
|
|
99
|
+
const decl = sourceFile.getImportDeclaration(d => d.getModuleSpecifierValue() === moduleSpecifier);
|
|
100
|
+
const specs = decl.getNamedImports();
|
|
101
|
+
const last = specs[specs.length - 1];
|
|
102
|
+
if (last)
|
|
103
|
+
sourceFile.insertText(last.getEnd(), `, ${name}`);
|
|
104
|
+
else
|
|
105
|
+
decl.addNamedImport(name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const unwrap = (node) => {
|
|
109
|
+
let current = node;
|
|
110
|
+
while (current && current.getKind() === SyntaxKind.ParenthesizedExpression)
|
|
111
|
+
current = current.getExpression();
|
|
112
|
+
return current;
|
|
113
|
+
};
|
|
114
|
+
const isJsxRoot = (node) => !!node && (node.getKind() === SyntaxKind.JsxElement || node.getKind() === SyntaxKind.JsxFragment || node.getKind() === SyntaxKind.JsxSelfClosingElement);
|
|
115
|
+
/**
|
|
116
|
+
* `<WrShared name="…">` (the root of a shared component) is not an element of the page: what it holds is. A component that returns just one
|
|
117
|
+
* element inside it has THAT as its root, like a header does: nothing can be put next to it, it cannot be deleted or moved.
|
|
118
|
+
*/
|
|
119
|
+
function insideShared(root) {
|
|
120
|
+
if (!root.isKind(SyntaxKind.JsxElement) || root.getOpeningElement().getTagNameNode().getText() !== 'WrShared')
|
|
121
|
+
return root;
|
|
122
|
+
const children = root.getJsxChildren().filter(child => !(child.isKind(SyntaxKind.JsxText) && child.getText().trim() === ''));
|
|
123
|
+
return children.length === 1 && isJsxRoot(children[0]) ? children[0] : root;
|
|
124
|
+
}
|
|
125
|
+
/** La radice JSX restituita da una funzione componente (corpo a blocco o a espressione). */
|
|
126
|
+
function rootOfComponent(fn) {
|
|
127
|
+
const body = fn?.getBody?.();
|
|
128
|
+
if (!body)
|
|
129
|
+
return null;
|
|
130
|
+
if (body.getKind() !== SyntaxKind.Block) {
|
|
131
|
+
const direct = unwrap(body);
|
|
132
|
+
return isJsxRoot(direct) ? insideShared(direct) : null;
|
|
133
|
+
}
|
|
134
|
+
for (const statement of body.getStatements()) {
|
|
135
|
+
if (statement.getKind() !== SyntaxKind.ReturnStatement)
|
|
136
|
+
continue;
|
|
137
|
+
const returned = unwrap(statement.getExpression());
|
|
138
|
+
if (isJsxRoot(returned))
|
|
139
|
+
return insideShared(returned);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* La radice JSX del componente del file: quello esportato di default (`export default function Home() { return (<>…</>); }`) o, se
|
|
145
|
+
* il file non ha un default, il primo componente esportato per nome che restituisce JSX (`export function Header() { … }`, i file di
|
|
146
|
+
* `src/layout/`).
|
|
147
|
+
*/
|
|
148
|
+
export function findPageRoot(sourceFile) {
|
|
149
|
+
const declarations = sourceFile.getDefaultExportSymbol()?.getDeclarations() ?? [];
|
|
150
|
+
for (const declaration of declarations) {
|
|
151
|
+
let fn = declaration;
|
|
152
|
+
if (declaration.getKind() === SyntaxKind.ExportAssignment) {
|
|
153
|
+
fn = unwrap(declaration.getExpression());
|
|
154
|
+
// `export default Home;` → il componente dichiarato nel file
|
|
155
|
+
if (fn?.getKind() === SyntaxKind.Identifier) {
|
|
156
|
+
const name = fn.getText();
|
|
157
|
+
fn = sourceFile.getFunction(name) ?? sourceFile.getVariableDeclaration(name)?.getInitializer();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const root = rootOfComponent(fn);
|
|
161
|
+
if (root)
|
|
162
|
+
return root;
|
|
163
|
+
}
|
|
164
|
+
if (declarations.length > 0)
|
|
165
|
+
return null;
|
|
166
|
+
for (const [, exported] of sourceFile.getExportedDeclarations()) {
|
|
167
|
+
for (const declaration of exported) {
|
|
168
|
+
const fn = declaration.getKind() === SyntaxKind.VariableDeclaration ? declaration.getInitializer?.() : declaration;
|
|
169
|
+
const root = rootOfComponent(fn);
|
|
170
|
+
if (root)
|
|
171
|
+
return root;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/** Dentro un'espressione (`{cond && <X/>}`, `{items.map(...)}`, una render prop): il codice decide cosa c'è, non si inserisce lì. */
|
|
177
|
+
function isGeneratedByCode(node) {
|
|
178
|
+
for (let current = node.getParent(); current; current = current.getParent()) {
|
|
179
|
+
if (current.getKind() === SyntaxKind.JsxExpression)
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const fail = (status, code, error) => ({ ok: false, status, code, error });
|
|
185
|
+
const SNIPPET_PREFIX = 'const __wr = <>';
|
|
186
|
+
const SNIPPET_SUFFIX = '</>;';
|
|
187
|
+
function parseTsx(text) {
|
|
188
|
+
return ts.createSourceFile('snippet.tsx', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
189
|
+
}
|
|
190
|
+
/** What the parser could not read: the message and the line of each problem (an unclosed tag, a stray token…). */
|
|
191
|
+
export function syntaxErrors(sf) {
|
|
192
|
+
const diagnostics = sf.parseDiagnostics ?? [];
|
|
193
|
+
return diagnostics.map(d => ({
|
|
194
|
+
message: ts.flattenDiagnosticMessageText(d.messageText, '\n'),
|
|
195
|
+
line: sf.getLineAndCharacterOfPosition(d.start ?? 0).line + 1,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
function syntaxError(sf) {
|
|
199
|
+
const first = syntaxErrors(sf)[0];
|
|
200
|
+
return first ? `${first.message} (line ${first.line})` : null;
|
|
201
|
+
}
|
|
202
|
+
const applyEdits = (text, edits) => [...edits].sort((a, b) => b.pos - a.pos).reduce((acc, e) => acc.slice(0, e.pos) + e.text + acc.slice(e.end), text);
|
|
203
|
+
/** `hero-2` → `hero`: the base a new number is added to. */
|
|
204
|
+
const baseOf = (id) => id.replace(/-\d+$/, '');
|
|
205
|
+
function freeId(base, taken, firstNumber) {
|
|
206
|
+
for (let n = firstNumber;; n++) {
|
|
207
|
+
const candidate = `${base}-${n}`;
|
|
208
|
+
if (!taken.has(candidate))
|
|
209
|
+
return candidate;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function prepareSnippet(rawJsx, taken, uniqueIds) {
|
|
213
|
+
if (!rawJsx.trim())
|
|
214
|
+
return fail(400, 'invalid-jsx', 'the markup is empty');
|
|
215
|
+
const wrapped = SNIPPET_PREFIX + rawJsx + SNIPPET_SUFFIX;
|
|
216
|
+
const sf = parseTsx(wrapped);
|
|
217
|
+
const problem = syntaxError(sf);
|
|
218
|
+
if (problem)
|
|
219
|
+
return fail(400, 'invalid-jsx', problem);
|
|
220
|
+
const statement = sf.statements[0];
|
|
221
|
+
const fragment = statement?.declarationList.declarations[0]?.initializer;
|
|
222
|
+
if (!fragment || !ts.isJsxFragment(fragment))
|
|
223
|
+
return fail(400, 'invalid-jsx', 'the markup is not a list of JSX elements');
|
|
224
|
+
// Il primo livello: solo elementi (e spazi), di qualunque tag
|
|
225
|
+
const top = fragment.children.filter(child => !(ts.isJsxText(child) && child.containsOnlyTriviaWhiteSpaces));
|
|
226
|
+
if (top.length === 0)
|
|
227
|
+
return fail(400, 'invalid-jsx', 'the markup has no elements');
|
|
228
|
+
for (const child of top) {
|
|
229
|
+
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
230
|
+
return fail(400, 'invalid-jsx', 'only JSX elements can be inserted at the top level');
|
|
231
|
+
}
|
|
232
|
+
// Tutti i tag, nell'ordine del documento
|
|
233
|
+
const tags = [];
|
|
234
|
+
const visit = (node) => {
|
|
235
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node))
|
|
236
|
+
tags.push(node);
|
|
237
|
+
ts.forEachChild(node, visit);
|
|
238
|
+
};
|
|
239
|
+
visit(fragment);
|
|
240
|
+
const edits = [];
|
|
241
|
+
const ids = [];
|
|
242
|
+
const idMap = {};
|
|
243
|
+
const used = new Set(taken);
|
|
244
|
+
const finalIdOf = new Map();
|
|
245
|
+
const usedTags = new Set();
|
|
246
|
+
for (const tag of tags) {
|
|
247
|
+
const name = tag.tagName.getText(sf);
|
|
248
|
+
const kind = WR_COMPONENT_TO_KIND[name];
|
|
249
|
+
// Anything that is not one of ours (an HTML tag, the developer's own component, even a "Wr..." that is theirs) is left alone
|
|
250
|
+
if (!kind)
|
|
251
|
+
continue;
|
|
252
|
+
usedTags.add(name);
|
|
253
|
+
const attr = tag.attributes.properties.find(p => ts.isJsxAttribute(p) && p.name.getText(sf) === 'id');
|
|
254
|
+
const literal = attr?.initializer && ts.isStringLiteral(attr.initializer) ? attr.initializer : undefined;
|
|
255
|
+
// `id={something}`: the developer's business. The editor cannot find such an element, and does not count it.
|
|
256
|
+
if (attr && !literal)
|
|
257
|
+
continue;
|
|
258
|
+
let finalId;
|
|
259
|
+
if (!literal) {
|
|
260
|
+
finalId = freeId(kind, used, 1);
|
|
261
|
+
const at = tag.tagName.getEnd() - SNIPPET_PREFIX.length;
|
|
262
|
+
edits.push({ pos: at, end: at, text: ` id="${finalId}"` });
|
|
263
|
+
}
|
|
264
|
+
else if (used.has(literal.text)) {
|
|
265
|
+
if (!uniqueIds)
|
|
266
|
+
return fail(409, 'id-exists', `id already exists: ${literal.text}`);
|
|
267
|
+
finalId = freeId(baseOf(literal.text), used, 2);
|
|
268
|
+
idMap[literal.text] = finalId;
|
|
269
|
+
edits.push({ pos: literal.getStart(sf) - SNIPPET_PREFIX.length, end: literal.getEnd() - SNIPPET_PREFIX.length, text: `"${finalId}"` });
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
finalId = literal.text;
|
|
273
|
+
}
|
|
274
|
+
used.add(finalId);
|
|
275
|
+
ids.push(finalId);
|
|
276
|
+
finalIdOf.set(tag, finalId);
|
|
277
|
+
}
|
|
278
|
+
const jsx = applyEdits(rawJsx, edits);
|
|
279
|
+
const rootIds = top.map(child => finalIdOf.get(ts.isJsxElement(child) ? child.openingElement : child)).filter((id) => id !== undefined);
|
|
280
|
+
return { jsx, ids, rootIds, undoable: rootIds.length === top.length, idMap, tags: [...usedTags] };
|
|
281
|
+
}
|
|
282
|
+
// ── Dove, e con quale indentazione ───────────────────────────────────────────────────────
|
|
283
|
+
/** L'unità di indentazione del file: tab, o il rientro più piccolo che vi compare (2 o 4 spazi di solito). */
|
|
284
|
+
export function detectIndentUnit(text) {
|
|
285
|
+
let smallest = Infinity;
|
|
286
|
+
for (const line of text.split('\n')) {
|
|
287
|
+
if (!line.trim())
|
|
288
|
+
continue;
|
|
289
|
+
if (line.startsWith('\t'))
|
|
290
|
+
return '\t';
|
|
291
|
+
const spaces = line.match(/^ +/)?.[0].length ?? 0;
|
|
292
|
+
if (spaces > 0 && spaces < smallest)
|
|
293
|
+
smallest = spaces;
|
|
294
|
+
}
|
|
295
|
+
return Number.isFinite(smallest) ? ' '.repeat(Math.min(smallest, 8)) : ' ';
|
|
296
|
+
}
|
|
297
|
+
/** Toglie il rientro comune a tutte le righe non vuote. */
|
|
298
|
+
function dedent(text) {
|
|
299
|
+
const lines = text.replace(/^\s*\n/, '').trimEnd().split('\n');
|
|
300
|
+
const indents = lines.filter(l => l.trim()).map(l => l.match(/^[ \t]*/)[0].length);
|
|
301
|
+
const common = indents.length ? Math.min(...indents) : 0;
|
|
302
|
+
return lines.map(l => l.slice(common)).join('\n');
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Rifà i rientri interni del markup con l'unità del file: un blocco scritto a 2 spazi in un file a 4 (o a tab) non deve
|
|
306
|
+
* lasciare un solo livello "storto". Il rientro comune è già tolto (`dedent`); qui si convertono i livelli.
|
|
307
|
+
*/
|
|
308
|
+
export function reindent(text, unit) {
|
|
309
|
+
const lead = (l) => l.match(/^[ \t]*/)[0];
|
|
310
|
+
const lines = text.split('\n');
|
|
311
|
+
const own = lines.filter(l => l.trim() && lead(l)).map(l => lead(l));
|
|
312
|
+
if (!own.length)
|
|
313
|
+
return text;
|
|
314
|
+
const ownUnit = own.some(l => l.includes('\t')) ? '\t' : ' '.repeat(Math.min(...own.map(l => l.length)));
|
|
315
|
+
if (ownUnit === unit)
|
|
316
|
+
return text;
|
|
317
|
+
return lines.map(l => {
|
|
318
|
+
if (!l.trim())
|
|
319
|
+
return l;
|
|
320
|
+
const ws = lead(l);
|
|
321
|
+
const levels = ownUnit === '\t' ? ws.split('\t').length - 1 : Math.round(ws.length / ownUnit.length);
|
|
322
|
+
return unit.repeat(levels) + l.slice(ws.length);
|
|
323
|
+
}).join('\n');
|
|
324
|
+
}
|
|
325
|
+
const indentLines = (text, prefix) => text.split('\n').map(l => (l ? prefix + l : l)).join('\n');
|
|
326
|
+
/** Il rientro dei figli di un contenitore: quello del suo primo figlio elemento, se ne ha; altrimenti il suo più un'unità. */
|
|
327
|
+
function childIndent(container, unit) {
|
|
328
|
+
if (container.getKind() !== SyntaxKind.JsxSelfClosingElement) {
|
|
329
|
+
const first = container.getJsxChildren().find(c => c.getKind() === SyntaxKind.JsxElement || c.getKind() === SyntaxKind.JsxSelfClosingElement);
|
|
330
|
+
if (first)
|
|
331
|
+
return getIndent(first);
|
|
332
|
+
}
|
|
333
|
+
return getIndent(container) + unit;
|
|
334
|
+
}
|
|
335
|
+
/** Il nuovo testo del file, con il markup inserito dove dice `mode` rispetto a `target`. */
|
|
336
|
+
function insertionEdit(text, target, mode, markup, unit) {
|
|
337
|
+
const own = getIndent(target);
|
|
338
|
+
const kind = target.getKind();
|
|
339
|
+
if (mode === 'before') {
|
|
340
|
+
const pos = target.getStart();
|
|
341
|
+
return { pos, end: pos, text: indentLines(markup, own).slice(own.length) + '\n' + own };
|
|
342
|
+
}
|
|
343
|
+
if (mode === 'after') {
|
|
344
|
+
const pos = target.getEnd();
|
|
345
|
+
return { pos, end: pos, text: '\n' + indentLines(markup, own) };
|
|
346
|
+
}
|
|
347
|
+
const inner = childIndent(target, unit);
|
|
348
|
+
if (kind === SyntaxKind.JsxSelfClosingElement) {
|
|
349
|
+
// `<WrSection id="s" />` → `<WrSection id="s">` + figlio + `</WrSection>`
|
|
350
|
+
const node = target;
|
|
351
|
+
const open = node.getText().replace(/\s*\/>$/, '>');
|
|
352
|
+
const tag = node.getTagNameNode().getText();
|
|
353
|
+
return { pos: node.getStart(), end: node.getEnd(), text: `${open}\n${indentLines(markup, inner)}\n${own}</${tag}>` };
|
|
354
|
+
}
|
|
355
|
+
const container = target;
|
|
356
|
+
if (mode === 'first-child') {
|
|
357
|
+
const pos = kind === SyntaxKind.JsxFragment ? container.getOpeningFragment().getEnd() : container.getOpeningElement().getEnd();
|
|
358
|
+
return { pos, end: pos, text: '\n' + indentLines(markup, inner) };
|
|
359
|
+
}
|
|
360
|
+
// last-child: subito prima del tag di chiusura, che sta già su una riga sua con il rientro del contenitore
|
|
361
|
+
const closing = kind === SyntaxKind.JsxFragment ? container.getClosingFragment() : container.getClosingElement();
|
|
362
|
+
const closingLineStart = text.lastIndexOf('\n', closing.getStart() - 1) + 1;
|
|
363
|
+
const beforeClosing = text.slice(closingLineStart, closing.getStart());
|
|
364
|
+
if (/^[ \t]*$/.test(beforeClosing)) {
|
|
365
|
+
return { pos: closingLineStart, end: closingLineStart, text: indentLines(markup, inner) + '\n' };
|
|
366
|
+
}
|
|
367
|
+
// il tag di chiusura sta in fondo a una riga con altro contenuto: va a capo
|
|
368
|
+
return { pos: closing.getStart(), end: closing.getStart(), text: '\n' + indentLines(markup, inner) + '\n' + own };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Inserisce markup JSX in un file: valida, sceglie gli id, indenta, aggiunge gli import e scrive. Atomico: o il file è cambiato come
|
|
372
|
+
* richiesto, o non è cambiato.
|
|
373
|
+
*/
|
|
374
|
+
export function insertJsx(project, filePath, request) {
|
|
375
|
+
const { anchor, mode } = request;
|
|
376
|
+
const sourceFile = loadSourceFile(project, filePath);
|
|
377
|
+
if (!sourceFile)
|
|
378
|
+
return fail(404, 'anchor-not-found', 'file not found');
|
|
379
|
+
const original = sourceFile.getFullText();
|
|
380
|
+
// 1. Il bersaglio
|
|
381
|
+
let target;
|
|
382
|
+
if ('root' in anchor) {
|
|
383
|
+
if (mode !== 'first-child' && mode !== 'last-child')
|
|
384
|
+
return fail(400, 'invalid-mode', 'the page root takes children only: use "first-child" or "last-child"');
|
|
385
|
+
const root = findPageRoot(sourceFile);
|
|
386
|
+
if (!root)
|
|
387
|
+
return fail(422, 'no-page-root', 'no JSX root found: the default export of the page must return JSX');
|
|
388
|
+
target = root;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const found = findNode(project, filePath, anchor.id);
|
|
392
|
+
if (!found)
|
|
393
|
+
return fail(404, 'anchor-not-found', `no element with id "${anchor.id}"`);
|
|
394
|
+
if (isGeneratedByCode(found.full))
|
|
395
|
+
return fail(422, 'generated-by-code', `"${anchor.id}" is generated by code (inside {...}): nothing can be added around or inside it from the editor`);
|
|
396
|
+
if ((mode === 'before' || mode === 'after') && findPageRoot(sourceFile) === found.full) {
|
|
397
|
+
return fail(422, 'component-root', `"${anchor.id}" is the only element its component returns (the root of a header or a footer): put the block inside it, not next to it`);
|
|
398
|
+
}
|
|
399
|
+
if ((mode === 'before' || mode === 'after')) {
|
|
400
|
+
const parent = found.full.getParent();
|
|
401
|
+
if (!parent || (parent.getKind() !== SyntaxKind.JsxElement && parent.getKind() !== SyntaxKind.JsxFragment)) {
|
|
402
|
+
return fail(422, 'generated-by-code', `"${anchor.id}" is not a direct child in JSX: nothing can be added next to it`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if ((mode === 'first-child' || mode === 'last-child') && !wrTakesChildren(found.kind)) {
|
|
406
|
+
return fail(422, 'not-a-container', `<${found.node.getTagNameNode().getText()}> does not take children: put the block next to it (before or after)`);
|
|
407
|
+
}
|
|
408
|
+
target = found.full;
|
|
409
|
+
}
|
|
410
|
+
// 2. Il markup
|
|
411
|
+
const unit = detectIndentUnit(original);
|
|
412
|
+
const prepared = prepareSnippet(reindent(dedent(request.jsx), unit), collectFileIds(sourceFile), request.uniqueIds !== false);
|
|
413
|
+
if ('ok' in prepared)
|
|
414
|
+
return prepared;
|
|
415
|
+
// 3. Il nuovo testo, controllato prima di scrivere
|
|
416
|
+
const edit = insertionEdit(original, target, mode, prepared.jsx, unit);
|
|
417
|
+
const next = applyEdits(original, [edit]);
|
|
418
|
+
const problem = syntaxError(parseTsx(next));
|
|
419
|
+
if (problem)
|
|
420
|
+
return fail(500, 'invalid-result', `the insertion would leave the file invalid, nothing was written: ${problem}`);
|
|
421
|
+
// 4. Scrittura e import; se qualcosa fallisce il file torna com'era
|
|
422
|
+
try {
|
|
423
|
+
sourceFile.replaceWithText(next);
|
|
424
|
+
ensureImports(sourceFile, prepared.tags);
|
|
425
|
+
sourceFile.saveSync();
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
sourceFile.replaceWithText(original);
|
|
429
|
+
sourceFile.saveSync();
|
|
430
|
+
return fail(500, 'invalid-result', String(err));
|
|
431
|
+
}
|
|
432
|
+
return { ok: true, ids: prepared.ids, rootIds: prepared.rootIds, undoable: prepared.undoable, idMap: prepared.idMap, jsx: prepared.jsx };
|
|
433
|
+
}
|
|
434
|
+
// ── Cancellare un sottoalbero (e sapere dove stava, per l'undo) ──────────────────────────
|
|
435
|
+
// Tutti i tag JSX (self-closing e di apertura) dentro `root`, root incluso se è un self-closing.
|
|
436
|
+
function getJsxTagNodes(root) {
|
|
437
|
+
return [
|
|
438
|
+
...(root.getKind() === SyntaxKind.JsxSelfClosingElement ? [root] : []),
|
|
439
|
+
...root.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement),
|
|
440
|
+
...root.getDescendantsOfKind(SyntaxKind.JsxOpeningElement),
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
// Id letterali dei componenti Wr* dentro `full` (se stesso incluso), senza duplicati.
|
|
444
|
+
function collectSubtreeIds(full) {
|
|
445
|
+
const ids = new Set();
|
|
446
|
+
for (const node of getJsxTagNodes(full)) {
|
|
447
|
+
if (!WR_COMPONENT_TO_KIND[node.getTagNameNode().getText()])
|
|
448
|
+
continue;
|
|
449
|
+
const idAttr = node.getAttribute('id');
|
|
450
|
+
if (!idAttr || idAttr.getKind() !== SyntaxKind.JsxAttribute)
|
|
451
|
+
continue;
|
|
452
|
+
const init = idAttr.getInitializer?.();
|
|
453
|
+
if (init?.getKind() === SyntaxKind.StringLiteral)
|
|
454
|
+
ids.add(init.getLiteralValue());
|
|
455
|
+
}
|
|
456
|
+
return [...ids];
|
|
457
|
+
}
|
|
458
|
+
// Identificatori "radice" dei tag usati nel sottoalbero (`Foo` per `<Foo.Bar>`),
|
|
459
|
+
// esclusi i tag HTML intrinseci (minuscoli). Da calcolare PRIMA di rimuovere il nodo.
|
|
460
|
+
function collectSubtreeTagHeads(full) {
|
|
461
|
+
const heads = new Set();
|
|
462
|
+
for (const node of getJsxTagNodes(full)) {
|
|
463
|
+
const head = node.getTagNameNode().getText().split('.')[0];
|
|
464
|
+
if (/^[A-Z]/.test(head))
|
|
465
|
+
heads.add(head);
|
|
466
|
+
}
|
|
467
|
+
return heads;
|
|
468
|
+
}
|
|
469
|
+
// Dove stava il nodo prima di essere cancellato, per poterlo reinserire esattamente
|
|
470
|
+
// lì con l'undo: il fratello JSX precedente con un id noto, o — se non c'è, perché
|
|
471
|
+
// era il primo figlio — il genitore (inserimento come primo figlio), o la radice della pagina.
|
|
472
|
+
function getAnchor(full) {
|
|
473
|
+
const idOf = (opening) => {
|
|
474
|
+
const idAttr = opening.getAttribute('id');
|
|
475
|
+
const init = idAttr?.getKind() === SyntaxKind.JsxAttribute ? idAttr.getInitializer?.() : null;
|
|
476
|
+
return init?.getKind() === SyntaxKind.StringLiteral ? init.getLiteralValue() : null;
|
|
477
|
+
};
|
|
478
|
+
let sibling = full.getPreviousSibling();
|
|
479
|
+
while (sibling) {
|
|
480
|
+
if (sibling.getKind() === SyntaxKind.JsxElement || sibling.getKind() === SyntaxKind.JsxSelfClosingElement) {
|
|
481
|
+
const openingTag = sibling.getKind() === SyntaxKind.JsxSelfClosingElement
|
|
482
|
+
? sibling
|
|
483
|
+
: sibling.getOpeningElement();
|
|
484
|
+
const id = WR_COMPONENT_TO_KIND[openingTag.getTagNameNode().getText()] ? idOf(openingTag) : null;
|
|
485
|
+
if (id !== null)
|
|
486
|
+
return { anchor: { id }, anchorMode: 'after' };
|
|
487
|
+
}
|
|
488
|
+
sibling = sibling.getPreviousSibling();
|
|
489
|
+
}
|
|
490
|
+
const parent = full.getParentIfKind(SyntaxKind.JsxElement);
|
|
491
|
+
if (parent) {
|
|
492
|
+
const id = idOf(parent.getOpeningElement());
|
|
493
|
+
if (id !== null)
|
|
494
|
+
return { anchor: { id }, anchorMode: 'first-child' };
|
|
495
|
+
}
|
|
496
|
+
// Il primo figlio diretto della radice della pagina (che non ha id): si torna a inserirlo in cima alla pagina
|
|
497
|
+
const root = findPageRoot(full.getSourceFile());
|
|
498
|
+
const container = full.getParent();
|
|
499
|
+
if (root && container && container === root)
|
|
500
|
+
return { anchor: { root: true }, anchorMode: 'first-child' };
|
|
501
|
+
return { anchor: null, anchorMode: 'first-child' };
|
|
502
|
+
}
|
|
503
|
+
/** L'id letterale di un tag, se ce l'ha. */
|
|
504
|
+
function idOfJsx(opening) {
|
|
505
|
+
const idAttr = opening?.getAttribute('id');
|
|
506
|
+
const init = idAttr?.getKind() === SyntaxKind.JsxAttribute ? idAttr.getInitializer?.() : null;
|
|
507
|
+
return init?.getKind() === SyntaxKind.StringLiteral ? init.getLiteralValue() : null;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Un elemento che resta senza figli torna `<X … />`: è l'inverso di un inserimento dentro un elemento che si apre per accogliere un
|
|
511
|
+
* figlio, e così annullare riporta il file com'era. (Solo se dentro non c'è altro che spazi.)
|
|
512
|
+
*/
|
|
513
|
+
function closeIfEmpty(sourceFile, id) {
|
|
514
|
+
// Sul file com'è adesso in memoria (la cancellazione non è ancora salvata): findNode lo rileggerebbe dal disco
|
|
515
|
+
const element = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).find(e => idOfJsx(e.getOpeningElement()) === id);
|
|
516
|
+
if (!element)
|
|
517
|
+
return;
|
|
518
|
+
if (!element.getJsxChildren().every(child => child.getKind() === SyntaxKind.JsxText && child.getText().trim() === ''))
|
|
519
|
+
return;
|
|
520
|
+
const openingText = element.getOpeningElement().getText();
|
|
521
|
+
element.getSourceFile().replaceText([element.getStart(), element.getEnd()], `${openingText.slice(0, -1).trimEnd()} />`);
|
|
522
|
+
}
|
|
523
|
+
function removeFullNode(full) {
|
|
524
|
+
const sourceFile = full.getSourceFile();
|
|
525
|
+
const fullText = sourceFile.getFullText();
|
|
526
|
+
let start = full.getStart();
|
|
527
|
+
while (start > 0 && (fullText[start - 1] === ' ' || fullText[start - 1] === '\t'))
|
|
528
|
+
start--;
|
|
529
|
+
let end = full.getEnd();
|
|
530
|
+
while (end < fullText.length && (fullText[end] === '\n' || fullText[end] === '\r'))
|
|
531
|
+
end++;
|
|
532
|
+
sourceFile.removeText(start, end);
|
|
533
|
+
}
|
|
534
|
+
// Rimuove gli import dei soli identificatori usati dal ramo appena tagliato (`removedHeads`)
|
|
535
|
+
// che nel file non sono più referenziati da nessuna parte. Gli altri import non si toccano.
|
|
536
|
+
function pruneImportsOf(sourceFile, removedHeads) {
|
|
537
|
+
if (removedHeads.size === 0)
|
|
538
|
+
return;
|
|
539
|
+
const stillUsed = (name, decl) => sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)
|
|
540
|
+
.some(id => id.getText() === name && id.getFirstAncestorByKind(SyntaxKind.ImportDeclaration) !== decl);
|
|
541
|
+
const localOf = (named) => named.getAliasNode()?.getText() ?? named.getName();
|
|
542
|
+
const count = sourceFile.getImportDeclarations().length;
|
|
543
|
+
for (let i = count - 1; i >= 0; i--) { // dall'ultimo: togliere un import intero non sposta gli indici di quelli prima
|
|
544
|
+
let decl = sourceFile.getImportDeclarations()[i];
|
|
545
|
+
let touched = false;
|
|
546
|
+
const drop = decl.getNamedImports().map(localOf).filter(local => removedHeads.has(local) && !stillUsed(local, decl));
|
|
547
|
+
for (const local of drop) {
|
|
548
|
+
// Come testo (vedi `ensureImports`): niente impaginazione rifatta dagli altri specificatori.
|
|
549
|
+
decl = sourceFile.getImportDeclarations()[i];
|
|
550
|
+
const specs = decl.getNamedImports();
|
|
551
|
+
const k = specs.findIndex(n => localOf(n) === local);
|
|
552
|
+
if (specs.length === 1)
|
|
553
|
+
specs[0].remove();
|
|
554
|
+
else if (k > 0)
|
|
555
|
+
sourceFile.removeText(specs[k - 1].getEnd(), specs[k].getEnd());
|
|
556
|
+
else
|
|
557
|
+
sourceFile.removeText(specs[0].getStart(), specs[1].getStart());
|
|
558
|
+
touched = true;
|
|
559
|
+
}
|
|
560
|
+
decl = sourceFile.getImportDeclarations()[i];
|
|
561
|
+
const defaultImport = decl.getDefaultImport();
|
|
562
|
+
if (defaultImport && removedHeads.has(defaultImport.getText()) && !stillUsed(defaultImport.getText(), decl)) {
|
|
563
|
+
decl.removeDefaultImport();
|
|
564
|
+
touched = true;
|
|
565
|
+
}
|
|
566
|
+
const nsImport = decl.getNamespaceImport();
|
|
567
|
+
if (nsImport && removedHeads.has(nsImport.getText()) && !stillUsed(nsImport.getText(), decl)) {
|
|
568
|
+
decl.removeNamespaceImport();
|
|
569
|
+
touched = true;
|
|
570
|
+
}
|
|
571
|
+
const empty = !decl.getDefaultImport() && !decl.getNamespaceImport() && decl.getNamedImports().length === 0;
|
|
572
|
+
if (touched && empty)
|
|
573
|
+
decl.remove();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
export function deleteJsx(project, filePath, id, options = {}) {
|
|
577
|
+
const found = findNode(project, filePath, id);
|
|
578
|
+
if (!found)
|
|
579
|
+
return null;
|
|
580
|
+
// L'unico elemento che un componente restituisce (la radice di un header o di un footer): senza lascerebbe `return ( )`, codice non valido
|
|
581
|
+
if (findPageRoot(found.full.getSourceFile()) === found.full) {
|
|
582
|
+
return { refused: true, code: 'component-root', error: `"${id}" is the only element its component returns: remove the header or the footer of a page with the page's setting, or edit the code` };
|
|
583
|
+
}
|
|
584
|
+
// catturare TUTTO prima di mutare l'albero: testo esatto + ancora per l'undo
|
|
585
|
+
const removedIds = collectSubtreeIds(found.full);
|
|
586
|
+
const removedTagHeads = collectSubtreeTagHeads(found.full);
|
|
587
|
+
// senza il rientro del nodo sulle righe di continuazione: reinserito altrove prende quello del posto nuovo
|
|
588
|
+
const own = getIndent(found.full);
|
|
589
|
+
const removedJsx = found.full.getText().split('\n').map((line, i) => (i > 0 && line.startsWith(own) ? line.slice(own.length) : line)).join('\n');
|
|
590
|
+
const { anchor, anchorMode } = getAnchor(found.full);
|
|
591
|
+
const sourceFile = found.full.getSourceFile();
|
|
592
|
+
const parentId = idOfJsx(found.full.getParentIfKind(SyntaxKind.JsxElement)?.getOpeningElement());
|
|
593
|
+
removeFullNode(found.full);
|
|
594
|
+
if (parentId)
|
|
595
|
+
closeIfEmpty(sourceFile, parentId);
|
|
596
|
+
if (options.pruneImports !== false)
|
|
597
|
+
pruneImportsOf(sourceFile, removedTagHeads);
|
|
598
|
+
sourceFile.saveSync();
|
|
599
|
+
return { removedIds, removedJsx, anchor, anchorMode };
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Sposta un elemento (e i suoi figli) in un altro punto dello stesso file. Gli id restano quelli: il JSON non si tocca. È una
|
|
603
|
+
* cancellazione e un inserimento dello stesso testo; se l'inserimento non riesce il file torna com'era, e la scrittura finale è una.
|
|
604
|
+
*/
|
|
605
|
+
export function moveJsx(project, filePath, request) {
|
|
606
|
+
const { id, anchor, mode } = request;
|
|
607
|
+
const found = findNode(project, filePath, id);
|
|
608
|
+
if (!found)
|
|
609
|
+
return fail(404, 'anchor-not-found', `no element with id "${id}"`);
|
|
610
|
+
const sourceFile = found.full.getSourceFile();
|
|
611
|
+
const original = sourceFile.getFullText();
|
|
612
|
+
if (isGeneratedByCode(found.full))
|
|
613
|
+
return fail(422, 'generated-by-code', `"${id}" is generated by code (inside {...}): it cannot be moved from the editor`);
|
|
614
|
+
if (findPageRoot(sourceFile) === found.full)
|
|
615
|
+
return fail(422, 'component-root', `"${id}" is the only element its component returns: it has nowhere to move`);
|
|
616
|
+
if ('id' in anchor) {
|
|
617
|
+
if (anchor.id === id)
|
|
618
|
+
return fail(422, 'invalid-move', 'an element cannot be moved next to or into itself');
|
|
619
|
+
const target = findNode(project, filePath, anchor.id);
|
|
620
|
+
if (target && target.full.getStart() >= found.full.getStart() && target.full.getEnd() <= found.full.getEnd()) {
|
|
621
|
+
return fail(422, 'invalid-move', `"${anchor.id}" is inside "${id}": an element cannot be moved into what it contains`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const from = getAnchor(found.full);
|
|
625
|
+
if (!from.anchor)
|
|
626
|
+
return fail(422, 'generated-by-code', `"${id}" has no fixed place in the JSX to be moved back to`);
|
|
627
|
+
const removed = deleteJsx(project, filePath, id, { pruneImports: false });
|
|
628
|
+
if (!removed || 'refused' in removed)
|
|
629
|
+
return fail(422, 'component-root', removed && 'error' in removed ? removed.error : 'not found');
|
|
630
|
+
const inserted = insertJsx(project, filePath, { jsx: removed.removedJsx, anchor, mode, uniqueIds: false });
|
|
631
|
+
if (!inserted.ok) {
|
|
632
|
+
sourceFile.replaceWithText(original);
|
|
633
|
+
sourceFile.saveSync();
|
|
634
|
+
return inserted;
|
|
635
|
+
}
|
|
636
|
+
return { ok: true, id, from: { anchor: from.anchor, mode: from.anchorMode } };
|
|
637
|
+
}
|
|
638
|
+
// ── Attributes: the literal values the editor's panel reads and writes ────────────────
|
|
639
|
+
/** The literal value of a JSX attribute: a string, a boolean, a number. `present: false` for anything the code decides (an expression). */
|
|
640
|
+
export function readJsxLiteral(node, key) {
|
|
641
|
+
const attr = node.getAttribute(key);
|
|
642
|
+
if (!attr || attr.getKind() !== SyntaxKind.JsxAttribute)
|
|
643
|
+
return { present: false };
|
|
644
|
+
const init = attr.getInitializer?.();
|
|
645
|
+
if (!init)
|
|
646
|
+
return { present: true, value: true };
|
|
647
|
+
if (init.getKind() === SyntaxKind.StringLiteral)
|
|
648
|
+
return { present: true, value: init.getLiteralValue() };
|
|
649
|
+
if (init.getKind() === SyntaxKind.JsxExpression) {
|
|
650
|
+
const expr = init.getExpression();
|
|
651
|
+
if (!expr)
|
|
652
|
+
return { present: false };
|
|
653
|
+
if (expr.getKind() === SyntaxKind.TrueKeyword)
|
|
654
|
+
return { present: true, value: true };
|
|
655
|
+
if (expr.getKind() === SyntaxKind.FalseKeyword)
|
|
656
|
+
return { present: true, value: false };
|
|
657
|
+
if (expr.getKind() === SyntaxKind.NumericLiteral)
|
|
658
|
+
return { present: true, value: Number(expr.getText()) };
|
|
659
|
+
if (expr.getKind() === SyntaxKind.StringLiteral)
|
|
660
|
+
return { present: true, value: expr.getLiteralValue() };
|
|
661
|
+
}
|
|
662
|
+
return { present: false };
|
|
663
|
+
}
|
|
664
|
+
/** Writes (or, with null, removes) a literal attribute. */
|
|
665
|
+
export function writeJsxLiteral(node, key, value) {
|
|
666
|
+
const existing = node.getAttribute(key);
|
|
667
|
+
if (value === null) {
|
|
668
|
+
if (existing && existing.getKind() === SyntaxKind.JsxAttribute)
|
|
669
|
+
existing.remove();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
// JSX non ha escape con backslash dentro le stringhe-attributo: se il valore contiene `"`
|
|
673
|
+
// l'unico modo sicuro è un'espressione `{"..."}` (JSON.stringify produce un literal TS valido).
|
|
674
|
+
let text;
|
|
675
|
+
if (typeof value === 'string')
|
|
676
|
+
text = value.includes('"') ? `{${JSON.stringify(value)}}` : `"${value}"`;
|
|
677
|
+
else
|
|
678
|
+
text = `{${value}}`;
|
|
679
|
+
if (existing && existing.getKind() === SyntaxKind.JsxAttribute) {
|
|
680
|
+
existing.setInitializer(text);
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
node.addAttribute({ name: key, initializer: text });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
//# sourceMappingURL=jsx-tree.js.map
|