@deckbuilderkit/editor 0.0.0-stage → 0.1.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.
Files changed (56) hide show
  1. package/package.json +53 -4
  2. package/src/ArchiveView.tsx +111 -0
  3. package/src/CodeEditor.tsx +265 -0
  4. package/src/CodeView.tsx +67 -0
  5. package/src/DiffView.tsx +139 -0
  6. package/src/EnvView.tsx +42 -0
  7. package/src/FilePreview.tsx +108 -0
  8. package/src/FontView.tsx +48 -0
  9. package/src/HarView.tsx +70 -0
  10. package/src/HexView.tsx +28 -0
  11. package/src/HtmlPreview.tsx +23 -0
  12. package/src/MarkdownEditor.tsx +90 -0
  13. package/src/MarkdownToc.tsx +135 -0
  14. package/src/MarkdownView.tsx +84 -0
  15. package/src/MediaView.tsx +17 -0
  16. package/src/NotebookView.tsx +74 -0
  17. package/src/PdfView.tsx +289 -0
  18. package/src/RequestEditor.tsx +419 -0
  19. package/src/ResponseView.tsx +89 -0
  20. package/src/SqlGraph.tsx +322 -0
  21. package/src/SqlSteps.tsx +487 -0
  22. package/src/SqliteView.tsx +68 -0
  23. package/src/SvgView.tsx +231 -0
  24. package/src/TableView.tsx +85 -0
  25. package/src/VarSuggest.tsx +125 -0
  26. package/src/curl-file.ts +70 -0
  27. package/src/editorOptions.ts +83 -0
  28. package/src/editorOptionsContext.ts +4 -0
  29. package/src/fileKinds.ts +69 -0
  30. package/src/http-client.ts +9 -0
  31. package/src/http-import.ts +594 -0
  32. package/src/http.ts +251 -0
  33. package/src/import-warning.ts +8 -0
  34. package/src/index.ts +56 -0
  35. package/src/languages.ts +107 -0
  36. package/src/markdown.ts +123 -0
  37. package/src/markdownLive.ts +289 -0
  38. package/src/options.ts +3 -0
  39. package/src/pdf-url.d.ts +4 -0
  40. package/src/sql/format.ts +637 -0
  41. package/src/sql/formatOptions.ts +71 -0
  42. package/src/sql/lineage.ts +365 -0
  43. package/src/sql/lint.ts +951 -0
  44. package/src/sql/markerHighlight.ts +50 -0
  45. package/src/sql/markers.ts +138 -0
  46. package/src/sql/outline.ts +662 -0
  47. package/src/sql/statements.ts +83 -0
  48. package/src/sql/tokens.ts +156 -0
  49. package/src/sql/xmlQuery.ts +249 -0
  50. package/src/sqlProblems.ts +40 -0
  51. package/src/table.ts +89 -0
  52. package/src/text-save.ts +28 -0
  53. package/src/theme.ts +86 -0
  54. package/src/toc.ts +11 -0
  55. package/src/variables.ts +123 -0
  56. package/README.md +0 -3
@@ -0,0 +1,322 @@
1
+ import { Icon, PanelNotice, t } from '@deckbuilderkit/ui';
2
+ import { type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import type { SqlProblem } from './sql/lint';
4
+ import { type LayoutPoint, type LineageEdge, type LineageNode, layoutGraph, lineageGraph, lineageOf, sqlNameKey } from './sql/lineage';
5
+ import type { SqlOutline, SqlStatementOutline, SqlStep } from './sql/outline';
6
+
7
+ export interface SqlGraphProps {
8
+ outline: SqlOutline;
9
+ problems: readonly SqlProblem[];
10
+ cursor?: number;
11
+ onJump(from: number, to?: number): void;
12
+ }
13
+
14
+ interface View {
15
+ x: number;
16
+ y: number;
17
+ k: number;
18
+ }
19
+
20
+ const MIN_ZOOM = 0.2;
21
+ const MAX_ZOOM = 3;
22
+ const PAD = 16;
23
+ const PAN_STEP = 40;
24
+ const CHAR_W = 7;
25
+ const TAG_CHAR_W = 6;
26
+ const OUTER = new Set(['left', 'right', 'full']);
27
+
28
+ const clampZoom = (k: number) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, k));
29
+
30
+ function statementTitle(s: SqlStatementOutline): string {
31
+ const kind = t(`sqlSteps.kind.${s.kind}`);
32
+ return s.target ? `${kind} ${s.target}` : kind;
33
+ }
34
+
35
+ function nodeLabel(n: LineageNode): string {
36
+ return n.kind === 'main' ? t('sqlSteps.main') : n.label;
37
+ }
38
+
39
+ function fitText(text: string, width: number, charW: number): string {
40
+ const max = Math.max(1, Math.floor(width / charW));
41
+ return text.length <= max ? text : `${text.slice(0, Math.max(1, max - 1))}…`;
42
+ }
43
+
44
+ /** A smooth path through the points: horizontal tangents at each point, as the layout runs left to right. */
45
+ function edgePath(points: readonly LayoutPoint[]): string {
46
+ if (points.length === 0) return '';
47
+ let d = `M ${points[0]!.x} ${points[0]!.y}`;
48
+ for (let i = 1; i < points.length; i++) {
49
+ const a = points[i - 1]!;
50
+ const b = points[i]!;
51
+ const dx = (b.x - a.x) / 2;
52
+ d += ` C ${a.x + dx} ${a.y}, ${b.x - dx} ${b.y}, ${b.x} ${b.y}`;
53
+ }
54
+ return d;
55
+ }
56
+
57
+ function stepOf(statement: SqlStatementOutline, node: LineageNode): SqlStep | undefined {
58
+ if (node.kind === 'main') return statement.steps.find((s) => s.kind === 'main');
59
+ if (node.kind === 'cte') return statement.steps.find((s) => s.kind === 'cte' && `cte:${sqlNameKey(s.name)}` === node.id);
60
+ return undefined;
61
+ }
62
+
63
+ /** The lineage of one statement as a left-to-right graph: tables and CTEs feeding the main query. */
64
+ export function SqlGraph({ outline, problems, cursor, onJump }: SqlGraphProps) {
65
+ const markerId = `sqlgraph-arrow-${useId().replace(/:/g, '')}`;
66
+ const candidates = useMemo(() => outline.statements.map((s, i) => ({ s, i })).filter((x) => x.s.steps.length > 0), [outline]);
67
+ const [picked, setPicked] = useState<number>();
68
+ const atCursor = useMemo(() => {
69
+ if (cursor === undefined) return candidates[0]?.i;
70
+ return (candidates.find(({ s }) => cursor >= s.from && cursor <= s.to) ?? candidates[0])?.i;
71
+ }, [candidates, cursor]);
72
+ const current = picked !== undefined && candidates.some((c) => c.i === picked) ? picked : atCursor;
73
+ const statement = current === undefined ? undefined : outline.statements[current];
74
+
75
+ const graph = useMemo(() => (statement ? lineageGraph(statement) : { nodes: [], edges: [] }), [statement]);
76
+ const layout = useMemo(() => layoutGraph(graph, { heightOf: (n) => (n.tags.length > 0 ? 52 : 34), rankGap: 120, nodeGap: 26 }), [graph]);
77
+ const edgeInfo = useMemo(() => {
78
+ const map = new Map<string, LineageEdge>();
79
+ for (const e of graph.edges) if (!map.has(`${e.from}>${e.to}`)) map.set(`${e.from}>${e.to}`, e);
80
+ return map;
81
+ }, [graph]);
82
+ const flagged = useMemo(() => {
83
+ const set = new Set<string>();
84
+ for (const p of problems) {
85
+ if (p.statement !== current || p.step === undefined) continue;
86
+ set.add(p.step === '' ? 'main' : `cte:${sqlNameKey(p.step)}`);
87
+ }
88
+ return set;
89
+ }, [problems, current]);
90
+
91
+ const [focus, setFocus] = useState<Set<string>>();
92
+ const [hover, setHover] = useState<{ node: LineageNode; x: number; y: number }>();
93
+ const box = useRef<HTMLDivElement>(null);
94
+ const [view, setViewState] = useState<View>({ x: 0, y: 0, k: 1 });
95
+ const viewRef = useRef(view);
96
+ const setView = useCallback((next: View) => {
97
+ viewRef.current = next;
98
+ setViewState(next);
99
+ }, []);
100
+
101
+ const fit = useCallback(() => {
102
+ const el = box.current;
103
+ if (!el || layout.width === 0) return;
104
+ const cw = el.clientWidth;
105
+ const ch = el.clientHeight;
106
+ const k = clampZoom(Math.min((cw - PAD * 2) / layout.width, (ch - PAD * 2) / layout.height, 1.25));
107
+ setView({ x: (cw - layout.width * k) / 2, y: (ch - layout.height * k) / 2, k });
108
+ }, [layout, setView]);
109
+
110
+ useLayoutEffect(() => {
111
+ setFocus(undefined);
112
+ fit();
113
+ // Refit when the statement (or its shape) changes, not on every edit.
114
+ }, [current, graph.nodes.length]);
115
+
116
+ const zoomAt = useCallback((factor: number, px?: number, py?: number) => {
117
+ const el = box.current;
118
+ const v = viewRef.current;
119
+ const x0 = px ?? (el ? el.clientWidth / 2 : 0);
120
+ const y0 = py ?? (el ? el.clientHeight / 2 : 0);
121
+ const k = clampZoom(v.k * factor);
122
+ setView({ x: x0 - ((x0 - v.x) * k) / v.k, y: y0 - ((y0 - v.y) * k) / v.k, k });
123
+ }, [setView]);
124
+
125
+ useEffect(() => {
126
+ const el = box.current;
127
+ if (!el) return;
128
+ const onWheel = (e: WheelEvent) => {
129
+ e.preventDefault();
130
+ const r = el.getBoundingClientRect();
131
+ zoomAt(Math.exp(-e.deltaY * (e.ctrlKey ? 0.004 : 0.0015)), e.clientX - r.left, e.clientY - r.top);
132
+ };
133
+ el.addEventListener('wheel', onWheel, { passive: false });
134
+ return () => el.removeEventListener('wheel', onWheel);
135
+ }, [zoomAt, statement]);
136
+
137
+ const drag = useRef<{ x: number; y: number; vx: number; vy: number; moved: boolean } | null>(null);
138
+ const moved = useRef(false);
139
+ const onPointerDown = (e: ReactPointerEvent) => {
140
+ if (e.button !== 0) return;
141
+ const v = viewRef.current;
142
+ drag.current = { x: e.clientX, y: e.clientY, vx: v.x, vy: v.y, moved: false };
143
+ moved.current = false;
144
+ };
145
+ const onPointerMove = (e: ReactPointerEvent) => {
146
+ const d = drag.current;
147
+ if (!d) return;
148
+ const dx = e.clientX - d.x;
149
+ const dy = e.clientY - d.y;
150
+ if (!d.moved && Math.abs(dx) + Math.abs(dy) < 4) return;
151
+ if (!d.moved) {
152
+ d.moved = true;
153
+ moved.current = true;
154
+ (e.currentTarget as Element).setPointerCapture(e.pointerId);
155
+ setHover(undefined);
156
+ }
157
+ setView({ ...viewRef.current, x: d.vx + dx, y: d.vy + dy });
158
+ };
159
+ const onPointerUp = (e: ReactPointerEvent) => {
160
+ if (drag.current?.moved) (e.currentTarget as Element).releasePointerCapture(e.pointerId);
161
+ drag.current = null;
162
+ };
163
+
164
+ const onKeyDown = (e: ReactKeyboardEvent) => {
165
+ const v = viewRef.current;
166
+ const pan: Record<string, [number, number]> = { ArrowLeft: [PAN_STEP, 0], ArrowRight: [-PAN_STEP, 0], ArrowUp: [0, PAN_STEP], ArrowDown: [0, -PAN_STEP] };
167
+ const step = pan[e.key];
168
+ if (step) setView({ ...v, x: v.x + step[0], y: v.y + step[1] });
169
+ else if (e.key === '+' || e.key === '=') zoomAt(1.2);
170
+ else if (e.key === '-') zoomAt(1 / 1.2);
171
+ else if (e.key === '0') fit();
172
+ else if (e.key === 'Escape' && focus) setFocus(undefined);
173
+ else return;
174
+ e.preventDefault();
175
+ e.stopPropagation();
176
+ };
177
+
178
+ const showTip = (node: LineageNode, e: ReactMouseEvent) => {
179
+ if (drag.current?.moved) return;
180
+ const r = box.current?.getBoundingClientRect();
181
+ if (r) setHover({ node, x: e.clientX - r.left, y: e.clientY - r.top });
182
+ };
183
+ const lit = focus ?? (hover ? lineageOf(graph, hover.node.id) : undefined);
184
+ const tipStep = hover && statement ? stepOf(statement, hover.node) : undefined;
185
+
186
+ if (candidates.length === 0 || !statement) {
187
+ return <PanelNotice icon={<Icon name="list-tree" size={20} />} title={t('sqlSteps.graph.empty')} message={t('sqlSteps.graph.emptyHint')} />;
188
+ }
189
+
190
+ return (
191
+ <div className="sqlgraph">
192
+ <div className="sqlgraph-bar">
193
+ <select
194
+ className="sqlgraph-select"
195
+ value={current}
196
+ onChange={(e) => setPicked(Number(e.target.value))}
197
+ aria-label={t('sqlSteps.graph.statement')}
198
+ title={t('sqlSteps.graph.statement')}
199
+ >
200
+ {candidates.map(({ s, i }) => (
201
+ <option key={i} value={i}>{t('sqlSteps.graph.option', { n: i + 1, title: statementTitle(s) })}</option>
202
+ ))}
203
+ </select>
204
+ <button className="btn btn-ghost btn-sm btn-icon" onClick={fit} title={t('sqlSteps.graph.fit')} aria-label={t('sqlSteps.graph.fit')}>
205
+ <Icon name="maximize" />
206
+ </button>
207
+ <button className="btn btn-ghost btn-sm btn-icon" onClick={() => zoomAt(1.2)} title={t('sqlSteps.graph.zoomIn')} aria-label={t('sqlSteps.graph.zoomIn')}>
208
+ <Icon name="plus" />
209
+ </button>
210
+ <button className="btn btn-ghost btn-sm btn-icon" onClick={() => zoomAt(1 / 1.2)} title={t('sqlSteps.graph.zoomOut')} aria-label={t('sqlSteps.graph.zoomOut')}>
211
+ <span className="sqlgraph-minus" aria-hidden="true">−</span>
212
+ </button>
213
+ </div>
214
+ <div
215
+ ref={box}
216
+ className="sqlgraph-canvas"
217
+ tabIndex={0}
218
+ role="img"
219
+ aria-label={t('sqlSteps.graph.label')}
220
+ onKeyDown={onKeyDown}
221
+ onPointerDown={onPointerDown}
222
+ onPointerMove={onPointerMove}
223
+ onPointerUp={onPointerUp}
224
+ onPointerCancel={onPointerUp}
225
+ onDoubleClick={() => setFocus(undefined)}
226
+ >
227
+ <svg className="sqlgraph-svg">
228
+ <defs>
229
+ <marker id={markerId} viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
230
+ <path className="sqlgraph-arrow" d="M 0 0 L 10 5 L 0 10 z" />
231
+ </marker>
232
+ </defs>
233
+ <g transform={`translate(${view.x} ${view.y}) scale(${view.k})`}>
234
+ {layout.edges.map((e, i) => {
235
+ const info = edgeInfo.get(`${e.from}>${e.to}`);
236
+ const hot = !!lit && lit.has(e.from) && lit.has(e.to);
237
+ const dim = !!lit && !hot;
238
+ const end = e.points[e.points.length - 1];
239
+ const join = info?.join && info.join !== 'from' ? info.join : undefined;
240
+ return (
241
+ <g key={i} className={`sqlgraph-edge${dim ? ' is-dim' : ''}${hot ? ' is-hot' : ''}${join && OUTER.has(join) ? ' is-outer' : ''}`}>
242
+ <path d={edgePath(e.points)} markerEnd={`url(#${markerId})`}>
243
+ {info?.on && <title>{info.on}</title>}
244
+ </path>
245
+ {join && end && <text className="sqlgraph-join" x={end.x - 6} y={end.y - 5} textAnchor="end">{join}</text>}
246
+ </g>
247
+ );
248
+ })}
249
+ {graph.nodes.map((n) => {
250
+ const b = layout.nodes.get(n.id);
251
+ if (!b) return null;
252
+ const dim = !!lit && !lit.has(n.id);
253
+ const label = nodeLabel(n);
254
+ let tagX = 10;
255
+ return (
256
+ <g
257
+ key={n.id}
258
+ className={`sqlgraph-node is-${n.kind}${dim ? ' is-dim' : ''}${n.from !== undefined ? ' is-link' : ''}`}
259
+ transform={`translate(${b.x} ${b.y})`}
260
+ onClick={() => {
261
+ if (!moved.current && n.from !== undefined) onJump(n.from, n.to);
262
+ }}
263
+ onDoubleClick={(e) => {
264
+ e.stopPropagation();
265
+ setFocus(lineageOf(graph, n.id));
266
+ }}
267
+ onMouseEnter={(e) => showTip(n, e)}
268
+ onMouseMove={(e) => showTip(n, e)}
269
+ onMouseLeave={() => setHover(undefined)}
270
+ >
271
+ <rect className="sqlgraph-base" width={b.w} height={b.h} rx={6} />
272
+ <rect className="sqlgraph-box" width={b.w} height={b.h} rx={6} />
273
+ <text className="sqlgraph-label" x={10} y={21}>{fitText(label, b.w - 24, CHAR_W)}</text>
274
+ {n.tags.slice(0, 2).map((tag) => {
275
+ const room = b.w - 8 - tagX;
276
+ if (room < 24) return null;
277
+ const text = fitText(tag, room - 12, TAG_CHAR_W);
278
+ const w = Math.min(room, text.length * TAG_CHAR_W + 12);
279
+ const x = tagX;
280
+ tagX += w + 4;
281
+ return (
282
+ <g key={tag} transform={`translate(${x} 30)`}>
283
+ <rect className="sqlgraph-tag" width={w} height={15} rx={7.5} />
284
+ <text className="sqlgraph-tag-text" x={w / 2} y={11} textAnchor="middle">{text}</text>
285
+ </g>
286
+ );
287
+ })}
288
+ {flagged.has(n.id) && (
289
+ <circle className="sqlgraph-dot" cx={b.w - 9} cy={9} r={4}>
290
+ <title>{t('sqlSteps.graph.hasProblems')}</title>
291
+ </circle>
292
+ )}
293
+ </g>
294
+ );
295
+ })}
296
+ </g>
297
+ </svg>
298
+ {hover && (
299
+ <div className="sqlgraph-tip" style={{ left: hover.x + 12, top: hover.y + 12 }}>
300
+ <div className="sqlgraph-tip-head">
301
+ <strong>{nodeLabel(hover.node)}</strong>
302
+ <span className="muted">{t(`sqlSteps.graph.kind.${hover.node.kind}`)}</span>
303
+ </div>
304
+ {tipStep && tipStep.joins.length > 0 && (
305
+ <div className="sqlgraph-tip-row">
306
+ <span className="muted">{t('sqlSteps.joins')}</span>
307
+ {tipStep.joins.slice(0, 4).map((j, i) => <code key={i}>{j.type} {j.target}{j.on ? ` ${t('sqlSteps.on')} ${j.on}` : ''}</code>)}
308
+ </div>
309
+ )}
310
+ {tipStep && tipStep.filters.length > 0 && (
311
+ <div className="sqlgraph-tip-row">
312
+ <span className="muted">{t('sqlSteps.filters')}</span>
313
+ {tipStep.filters.slice(0, 4).map((f, i) => <code key={i}>{f}</code>)}
314
+ </div>
315
+ )}
316
+ {hover.node.tags.length > 0 && <div className="muted">{hover.node.tags.join(', ')}</div>}
317
+ </div>
318
+ )}
319
+ </div>
320
+ </div>
321
+ );
322
+ }