@deckbuilderkit/editor 0.0.0-stage → 0.1.1

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,74 @@
1
+ import { PanelBanner, t } from '@deckbuilderkit/ui';
2
+ import { useMemo } from 'react';
3
+ import { CodeView } from './CodeView';
4
+ import { MarkdownView } from './MarkdownView';
5
+
6
+ interface Output {
7
+ output_type?: string;
8
+ name?: string;
9
+ text?: string | string[];
10
+ data?: Record<string, string | string[]>;
11
+ ename?: string;
12
+ evalue?: string;
13
+ traceback?: string[];
14
+ }
15
+
16
+ interface Cell {
17
+ cell_type?: string;
18
+ source?: string | string[];
19
+ outputs?: Output[];
20
+ execution_count?: number | null;
21
+ }
22
+
23
+ const join = (v: string | string[] | undefined) => (Array.isArray(v) ? v.join('') : v ?? '');
24
+ // Matches the ESC control character of ANSI color codes.
25
+ const ANSI = /\u001b\[[0-9;]*m/g;
26
+
27
+ function OutputView({ output }: { output: Output }) {
28
+ if (output.output_type === 'error') {
29
+ const trace = (output.traceback ?? []).join('\n').replace(ANSI, '') || `${output.ename}: ${output.evalue}`;
30
+ return <pre className="nb-output nb-error">{trace}</pre>;
31
+ }
32
+ if (output.output_type === 'stream') return <pre className="nb-output">{join(output.text)}</pre>;
33
+ const data = output.data ?? {};
34
+ for (const type of ['image/png', 'image/jpeg', 'image/gif']) {
35
+ if (data[type]) return <div className="nb-output"><img src={`data:${type};base64,${join(data[type]).replace(/\s/g, '')}`} alt="" /></div>;
36
+ }
37
+ if (data['text/plain']) return <pre className="nb-output">{join(data['text/plain'])}</pre>;
38
+ return null;
39
+ }
40
+
41
+ /** A Jupyter notebook: markdown and code cells with their text and image outputs. */
42
+ export function NotebookView({ text }: { text: string }) {
43
+ const parsed = useMemo(() => {
44
+ try {
45
+ const nb = JSON.parse(text) as { cells?: Cell[]; metadata?: { language_info?: { name?: string } } };
46
+ if (!Array.isArray(nb.cells)) return { error: 'cells' };
47
+ return { cells: nb.cells };
48
+ } catch (err) {
49
+ return { error: (err as Error).message };
50
+ }
51
+ }, [text]);
52
+
53
+ if ('error' in parsed) return <PanelBanner tone="err">{t('preview.notebookInvalid', { message: parsed.error ?? '' })}</PanelBanner>;
54
+ return (
55
+ <div className="file-preview">
56
+ <div className="file-preview-body nb">
57
+ {parsed.cells.map((cell, i) => {
58
+ const source = join(cell.source);
59
+ if (cell.cell_type === 'markdown') return <div key={i} className="nb-cell nb-markdown"><MarkdownView markdown={source} /></div>;
60
+ if (cell.cell_type === 'code') {
61
+ return (
62
+ <div key={i} className="nb-cell">
63
+ <div className="nb-prompt">[{cell.execution_count ?? ' '}]</div>
64
+ <div className="nb-code"><CodeView value={source} lang="python" wrap /></div>
65
+ {(cell.outputs ?? []).map((o, j) => <OutputView key={j} output={o} />)}
66
+ </div>
67
+ );
68
+ }
69
+ return source ? <pre key={i} className="nb-cell nb-output">{source}</pre> : null;
70
+ })}
71
+ </div>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,289 @@
1
+ /// <reference path="./pdf-url.d.ts" />
2
+ import { ErrorBanner, errorOf, Icon, PanelLoading, t } from '@deckbuilderkit/ui';
3
+ import * as pdfjs from 'pdfjs-dist';
4
+ import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
5
+ import type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist/types/src/display/api';
6
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
7
+
8
+ pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;
9
+
10
+ interface Layer {
11
+ id: string;
12
+ name: string;
13
+ visible: boolean;
14
+ }
15
+
16
+ type OptionalContent = Awaited<ReturnType<PDFDocumentProxy['getOptionalContentConfig']>>;
17
+
18
+ const ZOOM_STEP = 1.25;
19
+ const WHEEL_STEP = 1.1;
20
+ const MIN_ZOOM = 0.1;
21
+ const MAX_ZOOM = 8;
22
+ /** Padding of the page column and the gap between pages, in px (matches `.pdf-body` and `.pdf-pages`). */
23
+ const PAD = 12;
24
+ const GAP = 12;
25
+
26
+ const clampZoom = (z: number) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z));
27
+
28
+ /**
29
+ * A PDF as one scrolling column of pages (each drawn on a canvas once it comes near the view),
30
+ * with a page box to jump to, zoom (buttons and Ctrl+wheel) and a list of the document's layers
31
+ * (optional content) to toggle.
32
+ */
33
+ export function PdfView({ data }: { data: Uint8Array }) {
34
+ const [doc, setDoc] = useState<PDFDocumentProxy>();
35
+ const [sizes, setSizes] = useState<Array<{ width: number; height: number }>>([]);
36
+ const [error, setError] = useState<ReturnType<typeof errorOf>>();
37
+ const [page, setPage] = useState(1);
38
+ const [pageText, setPageText] = useState('1');
39
+ const [zoom, setZoom] = useState<number | 'fit'>('fit');
40
+ const [layers, setLayers] = useState<Layer[]>([]);
41
+ const [showLayers, setShowLayers] = useState(false);
42
+ const config = useRef<OptionalContent>();
43
+ const [configVersion, setConfigVersion] = useState(0);
44
+ const body = useRef<HTMLDivElement>(null);
45
+ const [bodyWidth, setBodyWidth] = useState(0);
46
+ /** Where the view should be after a wheel zoom re-lays the pages out: the point under the cursor stays put. */
47
+ const anchor = useRef<{ x: number; y: number; k: number }>();
48
+
49
+ useEffect(() => {
50
+ // pdf.js transfers the buffer to its worker, so it gets a copy.
51
+ const task = pdfjs.getDocument({ data: data.slice() });
52
+ let cancelled = false;
53
+ setDoc(undefined);
54
+ setError(undefined);
55
+ setPage(1);
56
+ setPageText('1');
57
+ task.promise
58
+ .then(async (loaded) => {
59
+ if (cancelled) return;
60
+ const optional = await loaded.getOptionalContentConfig();
61
+ const pages = await Promise.all(Array.from({ length: loaded.numPages }, (_, i) => loaded.getPage(i + 1)));
62
+ if (cancelled) return;
63
+ config.current = optional;
64
+ const groups = (optional.getGroups() ?? {}) as Record<string, { name?: string; visible: boolean }>;
65
+ setLayers(Object.entries(groups).map(([id, group]) => ({ id, name: group.name || id, visible: group.visible })));
66
+ setSizes(pages.map((p) => {
67
+ const v = p.getViewport({ scale: 1 });
68
+ return { width: v.width, height: v.height };
69
+ }));
70
+ setDoc(loaded);
71
+ })
72
+ .catch((err) => !cancelled && setError(errorOf(err)));
73
+ return () => {
74
+ cancelled = true;
75
+ void task.destroy();
76
+ };
77
+ }, [data]);
78
+
79
+ useEffect(() => {
80
+ const el = body.current;
81
+ if (!el) return;
82
+ const observer = new ResizeObserver(() => setBodyWidth(el.clientWidth));
83
+ observer.observe(el);
84
+ return () => observer.disconnect();
85
+ }, []);
86
+
87
+ const widest = sizes.reduce((w, s) => Math.max(w, s.width), 0);
88
+ const scale = zoom === 'fit' ? (widest && bodyWidth ? Math.max(0.2, (bodyWidth - 2 * PAD) / widest) : 1) : zoom;
89
+ const heights = sizes.map((s) => Math.floor(s.height * scale));
90
+ const tops: number[] = [];
91
+ heights.reduce((top, h, i) => ((tops[i] = top), top + h + GAP), PAD);
92
+
93
+ // Ctrl+wheel zooms the document instead of scrolling it (or zooming the whole page in a browser tab).
94
+ const scaleRef = useRef(scale);
95
+ scaleRef.current = scale;
96
+ useEffect(() => {
97
+ const el = body.current;
98
+ if (!el) return;
99
+ const onWheel = (e: WheelEvent) => {
100
+ if (!e.ctrlKey) return;
101
+ e.preventDefault();
102
+ const from = scaleRef.current;
103
+ const to = clampZoom(from * (e.deltaY < 0 ? WHEEL_STEP : 1 / WHEEL_STEP));
104
+ if (to === from) return;
105
+ const r = el.getBoundingClientRect();
106
+ // Several events can come before the next render: each builds on the last.
107
+ scaleRef.current = to;
108
+ anchor.current = { x: e.clientX - r.left, y: e.clientY - r.top, k: (anchor.current?.k ?? 1) * (to / from) };
109
+ setZoom(to);
110
+ };
111
+ el.addEventListener('wheel', onWheel, { passive: false });
112
+ return () => el.removeEventListener('wheel', onWheel);
113
+ }, []);
114
+
115
+ useLayoutEffect(() => {
116
+ const el = body.current;
117
+ const a = anchor.current;
118
+ if (!el || !a) return;
119
+ anchor.current = undefined;
120
+ el.scrollLeft = (el.scrollLeft + a.x) * a.k - a.x;
121
+ el.scrollTop = PAD + (el.scrollTop + a.y - PAD) * a.k - a.y;
122
+ }, [scale]);
123
+
124
+ const onScroll = () => {
125
+ const el = body.current;
126
+ if (!el || tops.length === 0) return;
127
+ const line = el.scrollTop + el.clientHeight / 3;
128
+ let i = 0;
129
+ while (i + 1 < tops.length && tops[i + 1]! <= line) i++;
130
+ if (i + 1 !== page) {
131
+ setPage(i + 1);
132
+ setPageText(String(i + 1));
133
+ }
134
+ };
135
+
136
+ const count = doc?.numPages ?? 0;
137
+ const goTo = (n: number) => {
138
+ const target = Math.min(count, Math.max(1, Math.round(n)));
139
+ setPageText(String(target));
140
+ if (!body.current || !Number.isFinite(target) || target < 1) return;
141
+ body.current.scrollTop = tops[target - 1]! - PAD;
142
+ setPage(target);
143
+ };
144
+
145
+ const toggle = (id: string) => {
146
+ const layer = layers.find((l) => l.id === id);
147
+ if (!layer || !config.current) return;
148
+ config.current.setVisibility(id, !layer.visible);
149
+ // Radio-button groups switch each other off, so the state is read back from the config.
150
+ setLayers((all) => all.map((l) => ({ ...l, visible: (config.current!.getGroup(l.id) as { visible: boolean }).visible })));
151
+ setConfigVersion((v) => v + 1);
152
+ };
153
+
154
+ return (
155
+ <div className="file-preview">
156
+ <div className="file-preview-bar pdf-bar">
157
+ <button className="btn btn-ghost btn-sm" disabled={page <= 1} onClick={() => goTo(page - 1)} title={t('pdf.prev')} aria-label={t('pdf.prev')}>
158
+ <span className="pdf-prev"><Icon name="chevron" /></span>
159
+ </button>
160
+ <input
161
+ className="pdf-page-input"
162
+ value={pageText}
163
+ inputMode="numeric"
164
+ aria-label={t('pdf.goTo')}
165
+ title={t('pdf.goTo')}
166
+ onChange={(e) => setPageText(e.target.value.replace(/\D/g, ''))}
167
+ onKeyDown={(e) => e.key === 'Enter' && goTo(Number(pageText))}
168
+ onBlur={() => setPageText(String(page))}
169
+ onFocus={(e) => e.target.select()}
170
+ />
171
+ <span className="pdf-page">{t('pdf.of', { count })}</span>
172
+ <button className="btn btn-ghost btn-sm" disabled={page >= count} onClick={() => goTo(page + 1)} title={t('pdf.next')} aria-label={t('pdf.next')}>
173
+ <Icon name="chevron" />
174
+ </button>
175
+ <button className="btn btn-ghost btn-sm" onClick={() => setZoom(clampZoom(scale / ZOOM_STEP))} title={t('pdf.zoomOut')} aria-label={t('pdf.zoomOut')}>−</button>
176
+ <span className="svg-zoom">{Math.round(scale * 100)}%</span>
177
+ <button className="btn btn-ghost btn-sm btn-icon" onClick={() => setZoom(clampZoom(scale * ZOOM_STEP))} title={t('pdf.zoomIn')} aria-label={t('pdf.zoomIn')}>
178
+ <Icon name="plus" />
179
+ </button>
180
+ <button className="btn btn-ghost btn-sm" onClick={() => setZoom('fit')} aria-pressed={zoom === 'fit'}>{t('pdf.fit')}</button>
181
+ <button className="btn btn-ghost btn-sm" onClick={() => setShowLayers(!showLayers)} aria-pressed={showLayers}>
182
+ {t('pdf.layers')}{layers.length > 0 && ` (${layers.length})`}
183
+ </button>
184
+ </div>
185
+ <div className="pdf-main">
186
+ <div className="file-preview-body pdf-body" ref={body} onScroll={onScroll}>
187
+ {error ? (
188
+ <ErrorBanner error={error} />
189
+ ) : !doc ? (
190
+ <PanelLoading />
191
+ ) : (
192
+ <div className="pdf-pages">
193
+ {sizes.map((s, i) => (
194
+ <PdfPage
195
+ key={i}
196
+ doc={doc}
197
+ number={i + 1}
198
+ width={Math.floor(s.width * scale)}
199
+ height={heights[i]!}
200
+ scale={scale}
201
+ config={config.current!}
202
+ configVersion={configVersion}
203
+ root={body}
204
+ />
205
+ ))}
206
+ </div>
207
+ )}
208
+ </div>
209
+ {showLayers && (
210
+ <div className="pdf-layers">
211
+ {layers.length === 0 && <div className="pdf-layers-empty">{t('pdf.noLayers')}</div>}
212
+ {layers.map((layer) => (
213
+ <label key={layer.id} className="pdf-layer">
214
+ <input type="checkbox" checked={layer.visible} onChange={() => toggle(layer.id)} />
215
+ <span>{layer.name}</span>
216
+ </label>
217
+ ))}
218
+ </div>
219
+ )}
220
+ </div>
221
+ </div>
222
+ );
223
+ }
224
+
225
+ interface PageProps {
226
+ doc: PDFDocumentProxy;
227
+ number: number;
228
+ width: number;
229
+ height: number;
230
+ scale: number;
231
+ config: OptionalContent;
232
+ configVersion: number;
233
+ root: React.RefObject<HTMLDivElement>;
234
+ }
235
+
236
+ /** One page: a box of the page's size, drawn while it is in or near the view and cleared when far away. */
237
+ function PdfPage({ doc, number, width, height, scale, config, configVersion, root }: PageProps) {
238
+ const canvas = useRef<HTMLCanvasElement>(null);
239
+ const [near, setNear] = useState(false);
240
+ const [error, setError] = useState<ReturnType<typeof errorOf>>();
241
+
242
+ useEffect(() => {
243
+ const el = canvas.current;
244
+ if (!el) return;
245
+ const observer = new IntersectionObserver(([entry]) => setNear(entry!.isIntersecting), { root: root.current, rootMargin: '100% 0px' });
246
+ observer.observe(el);
247
+ return () => observer.disconnect();
248
+ }, [root]);
249
+
250
+ useEffect(() => {
251
+ const el = canvas.current;
252
+ if (!el) return;
253
+ if (!near) {
254
+ // Frees the bitmap; the box keeps its CSS size.
255
+ el.width = 0;
256
+ el.height = 0;
257
+ return;
258
+ }
259
+ let cancelled = false;
260
+ let render: RenderTask | undefined;
261
+ void doc.getPage(number).then((p) => {
262
+ if (cancelled) return;
263
+ const viewport = p.getViewport({ scale });
264
+ const ratio = window.devicePixelRatio || 1;
265
+ el.width = Math.floor(viewport.width * ratio);
266
+ el.height = Math.floor(viewport.height * ratio);
267
+ render = p.render({
268
+ canvasContext: el.getContext('2d')!,
269
+ viewport,
270
+ transform: ratio === 1 ? undefined : [ratio, 0, 0, ratio, 0, 0],
271
+ optionalContentConfigPromise: Promise.resolve(config),
272
+ });
273
+ render.promise.catch((err) => {
274
+ if (!cancelled && (err as { name?: string })?.name !== 'RenderingCancelledException') setError(errorOf(err));
275
+ });
276
+ });
277
+ return () => {
278
+ cancelled = true;
279
+ render?.cancel();
280
+ };
281
+ }, [doc, number, scale, near, config, configVersion]);
282
+
283
+ return (
284
+ <div className="pdf-page-box" style={{ width, height }} data-page={number}>
285
+ <canvas ref={canvas} className="pdf-canvas" style={{ width, height }} />
286
+ {error && <div className="pdf-page-error"><ErrorBanner error={error} compact /></div>}
287
+ </div>
288
+ );
289
+ }