@developer_tribe/react-builder 1.2.32 → 1.2.34
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/assets/prompt-scheme-onboard.generated.d.ts +1 -0
- package/dist/assets/prompt-scheme-paywall.generated.d.ts +1 -0
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +3 -3
- package/dist/build-components/patterns.generated.d.ts +52 -52
- package/dist/components/BuilderProvider.d.ts +2 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +6 -6
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +6 -6
- package/dist/index.web.esm.js.map +1 -1
- package/dist/modals/PromptManagerModal.d.ts +9 -0
- package/dist/modals/index.d.ts +1 -0
- package/dist/styles.css +1 -1
- package/dist/utils/nodeXml.d.ts +11 -0
- package/package.json +5 -1
- package/scripts/prebuild/assets/prompt_scheme.md +77 -0
- package/scripts/prebuild/generate-prompt-schemes.js +464 -0
- package/scripts/prebuild/prebuild.js +4 -0
- package/src/RenderPage.tsx +6 -6
- package/src/assets/meta.json +1 -1
- package/src/assets/prompt-scheme-onboard.generated.ts +4 -0
- package/src/assets/prompt-scheme-paywall.generated.ts +4 -0
- package/src/attribute-analyser/style/native/useExtractImageStyle.ts +1 -1
- package/src/attribute-analyser/style/native/useExtractTextStyle.ts +1 -1
- package/src/attribute-analyser/style/native/useExtractViewStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractImageStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractTextStyle.ts +1 -1
- package/src/attribute-analyser/style/web/useExtractViewStyle.ts +1 -1
- package/src/build-components/BIcon/pattern.json +1 -3
- package/src/build-components/BackgroundImage/pattern.json +2 -10
- package/src/build-components/Button/pattern.json +1 -3
- package/src/build-components/Carousel/pattern.json +2 -8
- package/src/build-components/CarouselDots/CarouselDots.tsx +1 -1
- package/src/build-components/CarouselProvider/pattern.json +1 -4
- package/src/build-components/CountDown/pattern.json +1 -3
- package/src/build-components/Image/pattern.json +2 -9
- package/src/build-components/Main/pattern.json +1 -3
- package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +1 -1
- package/src/build-components/NavigationBarColor/pattern.json +1 -3
- package/src/build-components/Onboard/pattern.json +2 -6
- package/src/build-components/OnboardButton/OnboardButton.tsx +1 -1
- package/src/build-components/OnboardButton/pattern.json +3 -14
- package/src/build-components/OnboardButtons/pattern.json +4 -15
- package/src/build-components/OnboardDot/OnboardDot.tsx +1 -1
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +3 -3
- package/src/build-components/OnboardDot/pattern.json +15 -16
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +3 -3
- package/src/build-components/OnboardFooter/pattern.json +15 -19
- package/src/build-components/OnboardItem/pattern.json +3 -11
- package/src/build-components/OnboardProvider/pattern.json +2 -8
- package/src/build-components/OnboardSubtitle/pattern.json +1 -4
- package/src/build-components/OnboardTitle/pattern.json +1 -4
- package/src/build-components/PaywallBackground/pattern.json +1 -3
- package/src/build-components/PaywallCloseButton/pattern.json +1 -3
- package/src/build-components/PaywallOptions/pattern.json +1 -3
- package/src/build-components/PaywallProvider/pattern.json +1 -3
- package/src/build-components/PaywallSubscribeButton/pattern.json +1 -3
- package/src/build-components/PriceTag/pattern.json +2 -8
- package/src/build-components/Pricing/pattern.json +1 -3
- package/src/build-components/Promo/pattern.json +1 -3
- package/src/build-components/Separator/pattern.json +1 -3
- package/src/build-components/StatusBarColor/StatusBarColor.tsx +1 -1
- package/src/build-components/StatusBarColor/pattern.json +1 -3
- package/src/build-components/Text/pattern.json +1 -3
- package/src/build-components/View/pattern.json +4 -16
- package/src/build-components/patterns.generated.ts +52 -52
- package/src/components/BottomBar.tsx +28 -1
- package/src/components/BuilderProvider.tsx +5 -14
- package/src/hooks/useLocalize.ts +1 -1
- package/src/modals/MockableFeatureModal.tsx +552 -5
- package/src/modals/Modal.tsx +7 -1
- package/src/modals/PromptManagerModal.tsx +270 -0
- package/src/modals/index.ts +1 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/modals/_prompt-manager-modal.scss +95 -0
- package/src/utils/nodeXml.ts +196 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import Modal from './Modal';
|
|
3
|
+
import { useRenderStore } from '../store';
|
|
4
|
+
import type { Node } from '../types/Node';
|
|
5
|
+
import { nodeToXml, xmlToNode } from '../utils/nodeXml';
|
|
6
|
+
import { analyseAndProccess } from '../utils/analyseNode';
|
|
7
|
+
import { onboardPromptScheme } from '../assets/prompt-scheme-onboard.generated';
|
|
8
|
+
import { paywallPromptScheme } from '../assets/prompt-scheme-paywall.generated';
|
|
9
|
+
|
|
10
|
+
type PromptManagerTab = 'xml-editor' | 'prompt-schema';
|
|
11
|
+
|
|
12
|
+
type PromptManagerModalProps = {
|
|
13
|
+
data: Node;
|
|
14
|
+
setData: React.Dispatch<React.SetStateAction<Node>>;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function PromptManagerModal({
|
|
19
|
+
data,
|
|
20
|
+
setData,
|
|
21
|
+
onClose,
|
|
22
|
+
}: PromptManagerModalProps) {
|
|
23
|
+
const [activeTab, setActiveTab] = useState<PromptManagerTab>('xml-editor');
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Modal
|
|
27
|
+
onClose={onClose}
|
|
28
|
+
ariaLabelledBy="prompt-manager-modal-title"
|
|
29
|
+
className="modal--large modal--scrollable"
|
|
30
|
+
contentClassName="prompt-manager-modal__content"
|
|
31
|
+
>
|
|
32
|
+
<div className="modal__header prompt-manager-modal__header">
|
|
33
|
+
<h3 id="prompt-manager-modal-title" className="modal__title">
|
|
34
|
+
Prompt Manager
|
|
35
|
+
</h3>
|
|
36
|
+
<button type="button" className="editor-button" onClick={onClose}>
|
|
37
|
+
Close
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="prompt-manager-modal__tabs">
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
className={`prompt-manager-modal__tab${
|
|
45
|
+
activeTab === 'xml-editor' ? ' is-active' : ''
|
|
46
|
+
}`}
|
|
47
|
+
onClick={() => setActiveTab('xml-editor')}
|
|
48
|
+
>
|
|
49
|
+
XML Editor
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className={`prompt-manager-modal__tab${
|
|
54
|
+
activeTab === 'prompt-schema' ? ' is-active' : ''
|
|
55
|
+
}`}
|
|
56
|
+
onClick={() => setActiveTab('prompt-schema')}
|
|
57
|
+
>
|
|
58
|
+
Prompt Şeması
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="prompt-manager-modal__body">
|
|
63
|
+
{activeTab === 'xml-editor' && (
|
|
64
|
+
<XmlEditorTab data={data} setData={setData} />
|
|
65
|
+
)}
|
|
66
|
+
{activeTab === 'prompt-schema' && <PromptSchemaTab />}
|
|
67
|
+
</div>
|
|
68
|
+
</Modal>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------------------------ */
|
|
73
|
+
/* Tab 1 – XML Editor */
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
|
|
76
|
+
function XmlEditorTab({
|
|
77
|
+
data,
|
|
78
|
+
setData,
|
|
79
|
+
}: {
|
|
80
|
+
data: Node;
|
|
81
|
+
setData: React.Dispatch<React.SetStateAction<Node>>;
|
|
82
|
+
}) {
|
|
83
|
+
const setCurrent = useRenderStore((s) => s.setCurrent);
|
|
84
|
+
|
|
85
|
+
const initialXml = useMemo(() => {
|
|
86
|
+
try {
|
|
87
|
+
return nodeToXml(data);
|
|
88
|
+
} catch {
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
}, [data]);
|
|
92
|
+
|
|
93
|
+
const [text, setText] = useState(initialXml);
|
|
94
|
+
const [parseError, setParseError] = useState<string | null>(null);
|
|
95
|
+
const [applyError, setApplyError] = useState<string | null>(null);
|
|
96
|
+
const [applySuccess, setApplySuccess] = useState(false);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setText(initialXml);
|
|
100
|
+
setParseError(null);
|
|
101
|
+
setApplyError(null);
|
|
102
|
+
setApplySuccess(false);
|
|
103
|
+
}, [initialXml]);
|
|
104
|
+
|
|
105
|
+
const handleChange = useCallback((value: string) => {
|
|
106
|
+
setText(value);
|
|
107
|
+
setApplyError(null);
|
|
108
|
+
setApplySuccess(false);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const handleApply = () => {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = xmlToNode(text);
|
|
114
|
+
setParseError(null);
|
|
115
|
+
setApplyError(null);
|
|
116
|
+
setApplySuccess(false);
|
|
117
|
+
try {
|
|
118
|
+
const processed = analyseAndProccess(parsed as Node) as Node;
|
|
119
|
+
setData(processed);
|
|
120
|
+
// Keep selection in sync with the new root.
|
|
121
|
+
setCurrent(processed);
|
|
122
|
+
setApplySuccess(true);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
setApplyError(e instanceof Error ? e.message : 'Failed to apply XML');
|
|
125
|
+
}
|
|
126
|
+
} catch (e) {
|
|
127
|
+
setParseError(e instanceof Error ? e.message : 'Invalid XML');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleCopy = async () => {
|
|
132
|
+
try {
|
|
133
|
+
await navigator.clipboard.writeText(text);
|
|
134
|
+
} catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="prompt-manager-modal__editor-wrap">
|
|
141
|
+
<div className="prompt-manager-modal__toolbar">
|
|
142
|
+
<div style={{ fontSize: 12, opacity: 0.75 }}>node.xml</div>
|
|
143
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
144
|
+
<button type="button" className="editor-button" onClick={handleCopy}>
|
|
145
|
+
Copy
|
|
146
|
+
</button>
|
|
147
|
+
<button type="button" className="editor-button" onClick={handleApply}>
|
|
148
|
+
Apply
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<textarea
|
|
154
|
+
className="prompt-manager-modal__editor"
|
|
155
|
+
value={text}
|
|
156
|
+
onChange={(e) => {
|
|
157
|
+
setText(e.target.value);
|
|
158
|
+
setApplyError(null);
|
|
159
|
+
setApplySuccess(false);
|
|
160
|
+
}}
|
|
161
|
+
spellCheck={false}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
{parseError ? (
|
|
165
|
+
<div style={{ fontSize: 12, color: '#d12f2f' }}>
|
|
166
|
+
Invalid XML: {parseError}
|
|
167
|
+
</div>
|
|
168
|
+
) : applyError ? (
|
|
169
|
+
<div style={{ fontSize: 12, color: '#d12f2f' }}>
|
|
170
|
+
Could not apply: {applyError}
|
|
171
|
+
</div>
|
|
172
|
+
) : applySuccess ? (
|
|
173
|
+
<div style={{ fontSize: 12, color: '#2e7d32' }}>
|
|
174
|
+
✓ Applied successfully
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<div style={{ fontSize: 12, opacity: 0.7 }}>
|
|
178
|
+
Edit XML and press Apply to update the node tree.
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ------------------------------------------------------------------ */
|
|
186
|
+
/* Tab 2 – Prompt Şeması */
|
|
187
|
+
/* ------------------------------------------------------------------ */
|
|
188
|
+
|
|
189
|
+
type PromptSchemaSubTab = 'onboard' | 'paywall';
|
|
190
|
+
|
|
191
|
+
function PromptSchemaTab() {
|
|
192
|
+
const [subTab, setSubTab] = useState<PromptSchemaSubTab>('onboard');
|
|
193
|
+
const [copied, setCopied] = useState(false);
|
|
194
|
+
|
|
195
|
+
const activeScheme =
|
|
196
|
+
subTab === 'onboard' ? onboardPromptScheme : paywallPromptScheme;
|
|
197
|
+
|
|
198
|
+
const handleCopy = async () => {
|
|
199
|
+
try {
|
|
200
|
+
await navigator.clipboard.writeText(activeScheme);
|
|
201
|
+
setCopied(true);
|
|
202
|
+
setTimeout(() => setCopied(false), 1500);
|
|
203
|
+
} catch {
|
|
204
|
+
// ignore
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="prompt-manager-modal__editor-wrap">
|
|
210
|
+
{/* Sub-tab selector + copy */}
|
|
211
|
+
<div className="prompt-manager-modal__toolbar">
|
|
212
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
className="editor-button"
|
|
216
|
+
onClick={() => {
|
|
217
|
+
setSubTab('onboard');
|
|
218
|
+
setCopied(false);
|
|
219
|
+
}}
|
|
220
|
+
style={
|
|
221
|
+
subTab === 'onboard'
|
|
222
|
+
? {
|
|
223
|
+
background: '#1565c0',
|
|
224
|
+
color: '#fff',
|
|
225
|
+
borderColor: '#1565c0',
|
|
226
|
+
}
|
|
227
|
+
: undefined
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
Onboard
|
|
231
|
+
</button>
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
className="editor-button"
|
|
235
|
+
onClick={() => {
|
|
236
|
+
setSubTab('paywall');
|
|
237
|
+
setCopied(false);
|
|
238
|
+
}}
|
|
239
|
+
style={
|
|
240
|
+
subTab === 'paywall'
|
|
241
|
+
? {
|
|
242
|
+
background: '#6a1b9a',
|
|
243
|
+
color: '#fff',
|
|
244
|
+
borderColor: '#6a1b9a',
|
|
245
|
+
}
|
|
246
|
+
: undefined
|
|
247
|
+
}
|
|
248
|
+
>
|
|
249
|
+
Paywall
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
<button type="button" className="editor-button" onClick={handleCopy}>
|
|
253
|
+
{copied ? '✓ Copied' : 'Copy'}
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<textarea
|
|
258
|
+
className="prompt-manager-modal__editor"
|
|
259
|
+
value={activeScheme}
|
|
260
|
+
readOnly
|
|
261
|
+
spellCheck={false}
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
<div style={{ fontSize: 12, opacity: 0.7 }}>
|
|
265
|
+
{activeScheme.length.toLocaleString()} chars · read-only
|
|
266
|
+
(auto-generated)
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
package/src/modals/index.ts
CHANGED
|
@@ -10,3 +10,4 @@ export { ProductPresetsModal } from './ProductPresetsModal';
|
|
|
10
10
|
export { BenefitEditModal } from './BenefitEditModal';
|
|
11
11
|
export { BenefitPresetsModal } from './BenefitPresetsModal';
|
|
12
12
|
export { InspectModal } from './InspectModal';
|
|
13
|
+
export { PromptManagerModal } from './PromptManagerModal';
|
package/src/styles/index.scss
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@use '../foundation/colors' as colors;
|
|
2
|
+
@use '../foundation/sizes' as sizes;
|
|
3
|
+
@use '../foundation/typography' as typography;
|
|
4
|
+
|
|
5
|
+
.prompt-manager-modal__content {
|
|
6
|
+
width: calc(100vw - 32px);
|
|
7
|
+
height: calc(100vh - 32px);
|
|
8
|
+
max-width: 1400px;
|
|
9
|
+
max-height: calc(100vh - 32px);
|
|
10
|
+
padding: 0;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.prompt-manager-modal__header {
|
|
16
|
+
padding: sizes.$spaceComfy sizes.$spaceRoomy;
|
|
17
|
+
border-bottom: 1px solid colors.$borderColor;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: space-between;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.prompt-manager-modal__tabs {
|
|
24
|
+
display: flex;
|
|
25
|
+
padding: 0 sizes.$spaceRoomy;
|
|
26
|
+
border-bottom: 1px solid colors.$borderColor;
|
|
27
|
+
background: colors.$canvasColor;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.prompt-manager-modal__tab {
|
|
31
|
+
padding: sizes.$spaceComfy sizes.$spaceRoomy;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
color: colors.$mutedTextColor;
|
|
35
|
+
background: none;
|
|
36
|
+
border: none;
|
|
37
|
+
border-bottom: 2px solid transparent;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
transition: all 0.2s ease;
|
|
40
|
+
|
|
41
|
+
&:hover {
|
|
42
|
+
color: colors.$textColor;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&.is-active {
|
|
46
|
+
color: colors.$primary;
|
|
47
|
+
border-bottom-color: colors.$primary;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.prompt-manager-modal__body {
|
|
52
|
+
flex: 1;
|
|
53
|
+
min-height: 0;
|
|
54
|
+
padding: sizes.$spaceRoomy;
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.prompt-manager-modal__editor-wrap {
|
|
61
|
+
flex: 1;
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
gap: sizes.$spaceComfy;
|
|
65
|
+
min-height: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.prompt-manager-modal__toolbar {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.prompt-manager-modal__editor {
|
|
75
|
+
flex: 1;
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: 100%;
|
|
78
|
+
resize: none;
|
|
79
|
+
border: 1px solid colors.$borderColor;
|
|
80
|
+
border-radius: sizes.$radiusRounded;
|
|
81
|
+
padding: sizes.$spaceComfy;
|
|
82
|
+
font-family:
|
|
83
|
+
'ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas',
|
|
84
|
+
'Liberation Mono', 'Courier New', monospace;
|
|
85
|
+
font-size: 13px;
|
|
86
|
+
line-height: 1.5;
|
|
87
|
+
background: colors.$muted;
|
|
88
|
+
color: colors.$textColor;
|
|
89
|
+
outline: none;
|
|
90
|
+
|
|
91
|
+
&:focus {
|
|
92
|
+
border-color: colors.$primary;
|
|
93
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, colors.$primary, transparent 90%);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Node, NodeData } from '../types/Node';
|
|
2
|
+
|
|
3
|
+
/* ------------------------------------------------------------------ */
|
|
4
|
+
/* Node → XML */
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
|
|
7
|
+
function escapeXmlAttr(str: string, quote: '"' | "'"): string {
|
|
8
|
+
let result = str
|
|
9
|
+
.replace(/&/g, '&')
|
|
10
|
+
.replace(/</g, '<')
|
|
11
|
+
.replace(/>/g, '>');
|
|
12
|
+
if (quote === '"') {
|
|
13
|
+
result = result.replace(/"/g, '"');
|
|
14
|
+
} else {
|
|
15
|
+
result = result.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function attrValueToString(value: unknown): string {
|
|
21
|
+
if (value === null || value === undefined) return '';
|
|
22
|
+
if (typeof value === 'string') return value;
|
|
23
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
24
|
+
return String(value);
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nodeToXmlInner(node: Node, indent: number): string {
|
|
29
|
+
const pad = ' '.repeat(indent);
|
|
30
|
+
|
|
31
|
+
if (node === null || node === undefined) return '';
|
|
32
|
+
if (typeof node === 'string')
|
|
33
|
+
return `${pad}${escapeXmlAttr(node, '"').replace(/"/g, '"')}`; // Text content doesn't need to escape quotes
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(node)) {
|
|
36
|
+
return node.map((child) => nodeToXmlInner(child, indent)).join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof node !== 'object') return '';
|
|
40
|
+
|
|
41
|
+
const record = node as NodeData;
|
|
42
|
+
const type = record.type;
|
|
43
|
+
if (!type) return '';
|
|
44
|
+
|
|
45
|
+
// Build attribute string
|
|
46
|
+
const attrs: string[] = [];
|
|
47
|
+
|
|
48
|
+
const addAttr = (key: string, val: unknown) => {
|
|
49
|
+
const strValue = attrValueToString(val);
|
|
50
|
+
// Smart quote selection:
|
|
51
|
+
// If value contains double quotes but NOT single quotes, use single quotes.
|
|
52
|
+
// Otherwise default to double quotes (and escape internal double quotes).
|
|
53
|
+
const useSingle = strValue.includes('"') && !strValue.includes("'");
|
|
54
|
+
const quote = useSingle ? "'" : '"';
|
|
55
|
+
const escaped = escapeXmlAttr(strValue, quote);
|
|
56
|
+
attrs.push(`${key}=${quote}${escaped}${quote}`);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (record.key) addAttr('key', record.key);
|
|
60
|
+
if (record.sourceType) addAttr('sourceType', record.sourceType);
|
|
61
|
+
if (record.isMain) addAttr('isMain', 'true');
|
|
62
|
+
|
|
63
|
+
if (record.attributes && typeof record.attributes === 'object') {
|
|
64
|
+
for (const [k, v] of Object.entries(record.attributes)) {
|
|
65
|
+
addAttr(k, v);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
|
|
70
|
+
|
|
71
|
+
// Children
|
|
72
|
+
const children = record.children;
|
|
73
|
+
if (
|
|
74
|
+
children === null ||
|
|
75
|
+
children === undefined ||
|
|
76
|
+
(Array.isArray(children) && children.length === 0)
|
|
77
|
+
) {
|
|
78
|
+
return `${pad}<${type}${attrStr} />`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const childXml = nodeToXmlInner(children, indent + 1);
|
|
82
|
+
return `${pad}<${type}${attrStr}>\n${childXml}\n${pad}</${type}>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Serialize a Node tree to an XML string.
|
|
87
|
+
*/
|
|
88
|
+
export function nodeToXml(node: Node): string {
|
|
89
|
+
if (node === null || node === undefined) return '';
|
|
90
|
+
return nodeToXmlInner(node, 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ------------------------------------------------------------------ */
|
|
94
|
+
/* XML → Node */
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
96
|
+
|
|
97
|
+
function unescapeXml(str: string): string {
|
|
98
|
+
return str
|
|
99
|
+
.replace(/"/g, '"')
|
|
100
|
+
.replace(/>/g, '>')
|
|
101
|
+
.replace(/</g, '<')
|
|
102
|
+
.replace(/&/g, '&');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseAttrValue(raw: string): unknown {
|
|
106
|
+
const str = unescapeXml(raw);
|
|
107
|
+
|
|
108
|
+
// Boolean
|
|
109
|
+
if (str === 'true') return true;
|
|
110
|
+
if (str === 'false') return false;
|
|
111
|
+
|
|
112
|
+
// Number
|
|
113
|
+
if (str !== '' && !Number.isNaN(Number(str))) return Number(str);
|
|
114
|
+
|
|
115
|
+
// JSON object / array
|
|
116
|
+
if (
|
|
117
|
+
(str.startsWith('{') && str.endsWith('}')) ||
|
|
118
|
+
(str.startsWith('[') && str.endsWith(']'))
|
|
119
|
+
) {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(str);
|
|
122
|
+
} catch {
|
|
123
|
+
// fall through to string
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return str;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function domElementToNode(el: Element): NodeData {
|
|
131
|
+
const type = el.tagName;
|
|
132
|
+
const nodeData: NodeData = { type, children: null };
|
|
133
|
+
|
|
134
|
+
// Reserved XML attributes → NodeData fields
|
|
135
|
+
const reserved = new Set(['key', 'sourceType', 'isMain']);
|
|
136
|
+
const attributes: Record<string, unknown> = {};
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
139
|
+
const attr = el.attributes[i];
|
|
140
|
+
if (reserved.has(attr.name)) {
|
|
141
|
+
if (attr.name === 'key') nodeData.key = attr.value;
|
|
142
|
+
else if (attr.name === 'sourceType') nodeData.sourceType = attr.value;
|
|
143
|
+
else if (attr.name === 'isMain') nodeData.isMain = attr.value === 'true';
|
|
144
|
+
} else {
|
|
145
|
+
attributes[attr.name] = parseAttrValue(attr.value);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (Object.keys(attributes).length > 0) {
|
|
150
|
+
nodeData.attributes = attributes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Children
|
|
154
|
+
const childNodes: Node[] = [];
|
|
155
|
+
for (let i = 0; i < el.childNodes.length; i++) {
|
|
156
|
+
const child = el.childNodes[i];
|
|
157
|
+
if (child.nodeType === 1) {
|
|
158
|
+
// Element
|
|
159
|
+
childNodes.push(domElementToNode(child as Element));
|
|
160
|
+
} else if (child.nodeType === 3) {
|
|
161
|
+
// Text
|
|
162
|
+
const text = (child.textContent ?? '').trim();
|
|
163
|
+
if (text) childNodes.push(text);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (childNodes.length === 0) {
|
|
168
|
+
nodeData.children = null;
|
|
169
|
+
} else if (childNodes.length === 1) {
|
|
170
|
+
nodeData.children = childNodes[0];
|
|
171
|
+
} else {
|
|
172
|
+
nodeData.children = childNodes;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return nodeData;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse an XML string back into a Node tree.
|
|
180
|
+
*
|
|
181
|
+
* Uses the browser's built-in DOMParser.
|
|
182
|
+
*/
|
|
183
|
+
export function xmlToNode(xml: string): Node {
|
|
184
|
+
if (!xml.trim()) return null;
|
|
185
|
+
|
|
186
|
+
const parser = new DOMParser();
|
|
187
|
+
const doc = parser.parseFromString(xml, 'text/xml');
|
|
188
|
+
|
|
189
|
+
const parseError = doc.querySelector('parsererror');
|
|
190
|
+
if (parseError) {
|
|
191
|
+
throw new Error(parseError.textContent ?? 'XML parse error');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const root = doc.documentElement;
|
|
195
|
+
return domElementToNode(root);
|
|
196
|
+
}
|