sqlite-hub 0.16.0 → 0.17.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.
@@ -0,0 +1,248 @@
1
+ import { escapeHtml } from './format.js';
2
+
3
+ const FENCED_CODE_PATTERN = /^\s*(```|~~~)/;
4
+ const TASK_LINE_PATTERN = /^(\s*)([-*+])\s+\[([ xX])\]\s+(.*?)(\r?)$/;
5
+
6
+ function decodeBasicHtmlEntities(value = '') {
7
+ return String(value)
8
+ .replace(/:/gi, ':')
9
+ .replace(/:/g, ':')
10
+ .replace(/:/gi, ':')
11
+ .replace(/&/gi, '&')
12
+ .replace(/"/gi, '"')
13
+ .replace(/'/gi, "'");
14
+ }
15
+
16
+ function sanitizeRenderedMarkdown(html = '') {
17
+ return String(html).replace(/\s(href|src)=("([^"]*)"|'([^']*)'|([^\s>]+))/gi, (match, attribute, raw, doubleQuoted, singleQuoted, bare) => {
18
+ const value = doubleQuoted ?? singleQuoted ?? bare ?? '';
19
+ const decodedValue = decodeBasicHtmlEntities(value).trim();
20
+
21
+ if (/^(javascript|data|vbscript):/i.test(decodedValue)) {
22
+ return ` ${attribute}="#"`;
23
+ }
24
+
25
+ return match;
26
+ });
27
+ }
28
+
29
+ function renderFallbackMarkdown(markdown = '') {
30
+ const lines = String(markdown).split(/\r?\n/);
31
+ const html = [];
32
+ let listItems = [];
33
+ let listTag = 'ul';
34
+
35
+ const flushList = () => {
36
+ if (!listItems.length) {
37
+ return;
38
+ }
39
+
40
+ html.push(`<${listTag}>${listItems.map(item => `<li>${item}</li>`).join('')}</${listTag}>`);
41
+ listItems = [];
42
+ listTag = 'ul';
43
+ };
44
+
45
+ for (const line of lines) {
46
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
47
+ const unorderedMatch = line.match(/^\s*[-*+]\s+(.+)$/);
48
+ const orderedMatch = line.match(/^\s*\d+[.)]\s+(.+)$/);
49
+
50
+ if (headingMatch) {
51
+ flushList();
52
+ const level = Math.min(6, headingMatch[1].length);
53
+ html.push(`<h${level}>${escapeHtml(headingMatch[2])}</h${level}>`);
54
+ continue;
55
+ }
56
+
57
+ if (unorderedMatch) {
58
+ if (listItems.length && listTag !== 'ul') {
59
+ flushList();
60
+ }
61
+ listTag = 'ul';
62
+ listItems.push(escapeHtml(unorderedMatch[1]));
63
+ continue;
64
+ }
65
+
66
+ if (orderedMatch) {
67
+ if (listItems.length && listTag !== 'ol') {
68
+ flushList();
69
+ }
70
+ listTag = 'ol';
71
+ listItems.push(escapeHtml(orderedMatch[1]));
72
+ continue;
73
+ }
74
+
75
+ flushList();
76
+
77
+ if (line.trim()) {
78
+ html.push(`<p>${escapeHtml(line)}</p>`);
79
+ }
80
+ }
81
+
82
+ flushList();
83
+ return html.join('\n');
84
+ }
85
+
86
+ function renderMarkdownBlock(markdown = '') {
87
+ const source = escapeHtml(markdown);
88
+ const markedRuntime = globalThis.marked;
89
+ const parser = typeof markedRuntime?.parse === 'function' ? markedRuntime.parse.bind(markedRuntime) : null;
90
+
91
+ if (!parser) {
92
+ return renderFallbackMarkdown(markdown);
93
+ }
94
+
95
+ return sanitizeRenderedMarkdown(
96
+ parser(source, {
97
+ async: false,
98
+ breaks: false,
99
+ gfm: true,
100
+ }),
101
+ );
102
+ }
103
+
104
+ function renderMarkdownInline(markdown = '') {
105
+ const source = escapeHtml(markdown);
106
+ const markedRuntime = globalThis.marked;
107
+ const parser =
108
+ typeof markedRuntime?.parseInline === 'function' ? markedRuntime.parseInline.bind(markedRuntime) : null;
109
+
110
+ if (!parser) {
111
+ return source;
112
+ }
113
+
114
+ return sanitizeRenderedMarkdown(
115
+ parser(source, {
116
+ async: false,
117
+ breaks: false,
118
+ gfm: true,
119
+ }),
120
+ );
121
+ }
122
+
123
+ function renderTaskList(items = []) {
124
+ if (!items.length) {
125
+ return '';
126
+ }
127
+
128
+ return `
129
+ <ul class="document-markdown-task-list">
130
+ ${items
131
+ .map(item => {
132
+ const checked = item.checked;
133
+
134
+ return `
135
+ <li class="document-markdown-task-item ${checked ? 'is-checked' : ''}">
136
+ <label class="document-markdown-task-label">
137
+ <input
138
+ ${checked ? 'checked' : ''}
139
+ class="document-markdown-task-checkbox"
140
+ data-action="toggle-document-todo"
141
+ data-line-index="${escapeHtml(String(item.lineIndex))}"
142
+ type="checkbox"
143
+ />
144
+ <span class="document-markdown-task-text">${renderMarkdownInline(item.text)}</span>
145
+ </label>
146
+ </li>
147
+ `;
148
+ })
149
+ .join('')}
150
+ </ul>
151
+ `;
152
+ }
153
+
154
+ export function renderMarkdownPreview(markdown = '') {
155
+ const lines = String(markdown ?? '').split('\n');
156
+ const blocks = [];
157
+ let markdownLines = [];
158
+ let taskItems = [];
159
+ let inFence = false;
160
+
161
+ const flushMarkdown = () => {
162
+ if (!markdownLines.length) {
163
+ return;
164
+ }
165
+
166
+ blocks.push(renderMarkdownBlock(markdownLines.join('\n')));
167
+ markdownLines = [];
168
+ };
169
+
170
+ const flushTasks = () => {
171
+ if (!taskItems.length) {
172
+ return;
173
+ }
174
+
175
+ blocks.push(renderTaskList(taskItems));
176
+ taskItems = [];
177
+ };
178
+
179
+ lines.forEach((line, lineIndex) => {
180
+ const fenceLine = FENCED_CODE_PATTERN.test(line);
181
+
182
+ if (fenceLine) {
183
+ flushTasks();
184
+ markdownLines.push(line);
185
+ inFence = !inFence;
186
+ return;
187
+ }
188
+
189
+ if (!inFence) {
190
+ const taskMatch = line.match(TASK_LINE_PATTERN);
191
+
192
+ if (taskMatch) {
193
+ flushMarkdown();
194
+ taskItems.push({
195
+ checked: taskMatch[3].toLowerCase() === 'x',
196
+ lineIndex,
197
+ text: taskMatch[4],
198
+ });
199
+ return;
200
+ }
201
+ }
202
+
203
+ flushTasks();
204
+ markdownLines.push(line);
205
+ });
206
+
207
+ flushTasks();
208
+ flushMarkdown();
209
+
210
+ return blocks.join('\n').trim() || '<p class="document-markdown-empty">Empty document</p>';
211
+ }
212
+
213
+ function isLineInsideFencedCode(lines, lineIndex) {
214
+ let inFence = false;
215
+
216
+ for (let index = 0; index < lineIndex; index += 1) {
217
+ if (FENCED_CODE_PATTERN.test(lines[index] ?? '')) {
218
+ inFence = !inFence;
219
+ }
220
+ }
221
+
222
+ return inFence;
223
+ }
224
+
225
+ export function toggleMarkdownTodoLine(markdown = '', lineIndex) {
226
+ const lines = String(markdown ?? '').split('\n');
227
+ const numericLineIndex = Number(lineIndex);
228
+
229
+ if (!Number.isInteger(numericLineIndex) || numericLineIndex < 0 || numericLineIndex >= lines.length) {
230
+ return String(markdown ?? '');
231
+ }
232
+
233
+ if (isLineInsideFencedCode(lines, numericLineIndex)) {
234
+ return String(markdown ?? '');
235
+ }
236
+
237
+ const line = lines[numericLineIndex];
238
+ const match = line.match(TASK_LINE_PATTERN);
239
+
240
+ if (!match) {
241
+ return String(markdown ?? '');
242
+ }
243
+
244
+ const nextMarker = match[3].toLowerCase() === 'x' ? ' ' : 'x';
245
+ lines[numericLineIndex] = `${match[1]}${match[2]} [${nextMarker}] ${match[4]}${match[5]}`;
246
+
247
+ return lines.join('\n');
248
+ }
@@ -0,0 +1,300 @@
1
+ import { escapeHtml, formatCompactDateTime, formatNumber } from '../utils/format.js';
2
+ import { renderMarkdownPreview } from '../utils/markdownDocuments.js';
3
+
4
+ function renderMissingDatabase() {
5
+ return `
6
+ <section class="view-surface">
7
+ <div class="view-frame flex min-h-full items-center justify-center">
8
+ <div class="text-center">
9
+ <span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
10
+ <p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
11
+ No Active SQLite Database
12
+ </p>
13
+ <p class="mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
14
+ Select a database before opening its document folder.
15
+ </p>
16
+ </div>
17
+ </div>
18
+ </section>
19
+ `;
20
+ }
21
+
22
+ function renderDocumentListItem(item, selectedId) {
23
+ const isSelected = String(item.id) === String(selectedId);
24
+
25
+ return `
26
+ <button
27
+ class="documents-list-item ${isSelected ? 'is-selected' : ''}"
28
+ data-action="select-document"
29
+ data-document-id="${escapeHtml(item.id)}"
30
+ type="button"
31
+ >
32
+ <span class="material-symbols-outlined documents-list-item__icon">description</span>
33
+ <span class="documents-list-item__body">
34
+ <span class="documents-list-item__title" title="${escapeHtml(item.filename)}">
35
+ ${escapeHtml(item.filename)}
36
+ </span>
37
+ <span class="documents-list-item__meta">
38
+ ${escapeHtml(formatCompactDateTime(item.updatedAt))} // ${formatNumber(item.contentLength ?? 0)} chars
39
+ </span>
40
+ </span>
41
+ </button>
42
+ `;
43
+ }
44
+
45
+ function renderDocumentsSidebar(documents) {
46
+ const items = documents.items ?? [];
47
+
48
+ return `
49
+ <aside class="documents-view__sidebar">
50
+ <div class="documents-view__sidebar-header">
51
+ <div>
52
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/50">
53
+ Documents
54
+ </div>
55
+ <div class="mt-1 font-mono text-xs text-primary-container">${formatNumber(items.length)} files</div>
56
+ </div>
57
+ <button
58
+ class="icon-button"
59
+ data-action="create-document"
60
+ title="New document"
61
+ type="button"
62
+ ${documents.saving ? 'disabled aria-disabled="true"' : ''}
63
+ >
64
+ <span class="material-symbols-outlined">add</span>
65
+ </button>
66
+ </div>
67
+ <div class="documents-view__sidebar-body custom-scrollbar">
68
+ ${
69
+ items.length
70
+ ? items.map(item => renderDocumentListItem(item, documents.selectedId)).join('')
71
+ : `<div class="documents-list-empty">No documents yet</div>`
72
+ }
73
+ </div>
74
+ </aside>
75
+ `;
76
+ }
77
+
78
+ function renderDocumentToolbar(documents) {
79
+ const disabled = documents.saving || documents.detailLoading || !documents.selectedId;
80
+
81
+ return ` <label class="documents-filename-field">
82
+ <input
83
+ class="control-input"
84
+ data-bind="document-field"
85
+ data-field="filename"
86
+ name="filename"
87
+ spellcheck="false"
88
+ type="text"
89
+ value="${escapeHtml(documents.draftFilename)}"
90
+ ${!documents.selectedId ? 'disabled' : ''}
91
+ />
92
+ </label>
93
+ <div class="documents-toolbar">
94
+
95
+
96
+
97
+ <div class="documents-toolbar__actions">
98
+ <button
99
+ class="standard-button"
100
+ data-action="toggle-document-pane"
101
+ data-pane="editor"
102
+ type="button"
103
+ >
104
+ <span class="material-symbols-outlined">${documents.editorVisible ? 'visibility_off' : 'edit_note'}</span>
105
+ ${documents.editorVisible ? 'Hide Editor' : 'Show Editor'}
106
+ </button>
107
+ <button
108
+ class="standard-button"
109
+ data-action="toggle-document-pane"
110
+ data-pane="preview"
111
+ type="button"
112
+ >
113
+ <span class="material-symbols-outlined">${documents.previewVisible ? 'visibility_off' : 'visibility'}</span>
114
+ ${documents.previewVisible ? 'Hide Preview' : 'Show Preview'}
115
+ </button>
116
+ <input
117
+ accept=".md,.markdown,text/markdown,text/plain"
118
+ data-bind="document-import-file"
119
+ hidden
120
+ type="file"
121
+ />
122
+ </div>
123
+ <div class="documents-toolbar__actions">
124
+ <button
125
+ class="standard-button"
126
+ data-action="open-document-insert-table-modal"
127
+ type="button"
128
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
129
+ >
130
+ <span class="material-symbols-outlined">table_chart</span>
131
+ Insert Table
132
+ </button>
133
+ <button
134
+ class="standard-button"
135
+ data-action="open-document-insert-note-modal"
136
+ type="button"
137
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
138
+ >
139
+ <span class="material-symbols-outlined">note_add</span>
140
+ Insert Note
141
+ </button>
142
+ <button
143
+ class="standard-button"
144
+ data-action="export-document-markdown"
145
+ type="button"
146
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
147
+ >
148
+ <span class="material-symbols-outlined">download</span>
149
+ Export .md
150
+ </button>
151
+ <button
152
+ class="standard-button"
153
+ data-action="import-document-markdown"
154
+ type="button"
155
+ ${documents.saving ? 'disabled aria-disabled="true"' : ''}
156
+ >
157
+ <span class="material-symbols-outlined">upload_file</span>
158
+ Import .md
159
+ </button>
160
+ </div>
161
+ <div class="documents-toolbar__actions">
162
+ <button
163
+ class="standard-button"
164
+ data-action="delete-document"
165
+ type="button"
166
+ ${disabled || documents.deleting ? 'disabled aria-disabled="true"' : ''}
167
+ >
168
+ <span class="material-symbols-outlined">delete</span>
169
+ Delete
170
+ </button>
171
+ <button
172
+ class="signature-button"
173
+ data-action="save-document"
174
+ type="button"
175
+ ${disabled || !documents.dirty ? 'disabled aria-disabled="true"' : ''}
176
+ >
177
+ <span class="material-symbols-outlined">save</span>
178
+ ${documents.saving ? 'Saving...' : 'Save'}
179
+ </button>
180
+ </div>
181
+ </div>
182
+ `;
183
+ }
184
+
185
+ function renderDocumentEditor(documents) {
186
+ if (!documents.editorVisible) {
187
+ return '';
188
+ }
189
+
190
+ return `
191
+ <section class="documents-pane documents-pane--editor">
192
+ <div class="documents-pane__header">
193
+ <span>Editor</span>
194
+ <span>${formatNumber(documents.draftContent.length)} chars</span>
195
+ </div>
196
+ <textarea
197
+ class="documents-editor-input custom-scrollbar"
198
+ data-bind="document-field"
199
+ data-field="content"
200
+ name="content"
201
+ spellcheck="true"
202
+ ${!documents.selectedId ? 'disabled' : ''}
203
+ >${escapeHtml(documents.draftContent)}</textarea>
204
+ </section>
205
+ `;
206
+ }
207
+
208
+ function renderDocumentPreview(documents) {
209
+ if (!documents.previewVisible) {
210
+ return '';
211
+ }
212
+
213
+ return `
214
+ <section class="documents-pane documents-pane--preview">
215
+ <div class="documents-pane__header">
216
+ <span>Preview</span>
217
+ <span>${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
218
+ </div>
219
+ <div class="document-markdown-preview custom-scrollbar" data-document-preview>
220
+ ${renderMarkdownPreview(documents.draftContent)}
221
+ </div>
222
+ </section>
223
+ `;
224
+ }
225
+
226
+ function renderEmptyDocumentsState(documents) {
227
+ return `
228
+ <div class="documents-empty-state">
229
+ <span class="material-symbols-outlined">description</span>
230
+ <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
231
+ No Documents
232
+ </p>
233
+ <button
234
+ class="signature-button mt-4"
235
+ data-action="create-document"
236
+ type="button"
237
+ ${documents.saving ? 'disabled aria-disabled="true"' : ''}
238
+ >
239
+ <span class="material-symbols-outlined">add</span>
240
+ New Document
241
+ </button>
242
+ </div>
243
+ `;
244
+ }
245
+
246
+ function renderDocumentDetail(documents) {
247
+ if (documents.loading && !documents.items.length) {
248
+ return `
249
+ <main class="documents-view__detail">
250
+ <div class="documents-empty-state">
251
+ <span class="material-symbols-outlined">sync</span>
252
+ <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
253
+ Loading Documents
254
+ </p>
255
+ </div>
256
+ </main>
257
+ `;
258
+ }
259
+
260
+ if (!documents.selectedId) {
261
+ return `
262
+ <main class="documents-view__detail">
263
+ ${renderEmptyDocumentsState(documents)}
264
+ </main>
265
+ `;
266
+ }
267
+
268
+ const paneCount = Number(documents.editorVisible) + Number(documents.previewVisible);
269
+
270
+ return `
271
+ <main class="documents-view__detail">
272
+ ${renderDocumentToolbar(documents)}
273
+ ${documents.saveError ? `<div class="documents-error">${escapeHtml(documents.saveError.message)}</div>` : ''}
274
+ <div class="documents-workspace ${paneCount > 1 ? 'documents-workspace--split' : ''}">
275
+ ${renderDocumentEditor(documents)}
276
+ ${renderDocumentPreview(documents)}
277
+ </div>
278
+ </main>
279
+ `;
280
+ }
281
+
282
+ export function renderDocumentsView(state) {
283
+ if (!state.connections.active) {
284
+ return {
285
+ main: renderMissingDatabase(),
286
+ panel: '',
287
+ };
288
+ }
289
+
290
+ const documents = state.documents;
291
+
292
+ return {
293
+ main: `<div class="documents-view">
294
+ ${renderDocumentsSidebar(documents)}
295
+ ${renderDocumentDetail(documents)}
296
+ </div>
297
+ </div>`,
298
+ panel: '',
299
+ };
300
+ }
@@ -24,6 +24,9 @@ function renderSettingsContent(state) {
24
24
  `;
25
25
  }
26
26
 
27
+ const appVersion = escapeHtml(state.settings.appVersion ?? '0.0.0');
28
+ const sqliteVersion = escapeHtml(state.settings.sqliteVersion ?? 'unknown');
29
+
27
30
  return `
28
31
  <div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
29
32
  <section class="shell-section overflow-hidden">
@@ -32,14 +35,47 @@ function renderSettingsContent(state) {
32
35
  <span class="material-symbols-outlined text-xs text-primary-container">deployed_code</span>
33
36
  </div>
34
37
  <div class="space-y-5 p-6">
38
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-2">
39
+ <div>
40
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
41
+ Current_Version
42
+ </div>
43
+ <div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
44
+ v${appVersion}
45
+ </div>
46
+ </div>
47
+ <div>
48
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
49
+ SQLite_Runtime
50
+ </div>
51
+ <div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
52
+ ${sqliteVersion}
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div class="border-t border-outline-variant/10 py-4 text-sm leading-6 text-on-surface-variant">
57
+ SQL functions and syntax are evaluated with this SQLite runtime.
58
+ </div>
59
+ </div>
60
+ </section>
61
+
62
+ <section class="shell-section overflow-hidden">
63
+ <div class="flex items-center justify-between border-b border-outline-variant/10 bg-surface-container-highest px-4 py-2">
64
+ <span class="text-[10px] font-bold uppercase tracking-[0.25em]">CLI</span>
65
+ <span class="material-symbols-outlined text-xs text-primary-container">terminal</span>
66
+ </div>
67
+ <div class="space-y-4 p-6">
35
68
  <div>
36
69
  <div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
37
- Current_Version
70
+ Custom_Port
38
71
  </div>
39
- <div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
40
- v${escapeHtml(state.settings.appVersion ?? '0.0.0')}
72
+ <div class="mt-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 font-mono text-sm text-primary-container">
73
+ sqlite-hub --port:PORT
41
74
  </div>
42
75
  </div>
76
+ <div class="text-sm leading-6 text-on-surface-variant">
77
+ Start SQLite Hub with a different local port when the default port is already in use.
78
+ </div>
43
79
  </div>
44
80
  </section>
45
81
 
@@ -588,6 +588,19 @@
588
588
  white-space: pre-wrap;
589
589
  }
590
590
 
591
+ .copy-column-preview--editable {
592
+ display: block;
593
+ min-height: 10rem;
594
+ outline: none;
595
+ resize: vertical;
596
+ width: 100%;
597
+ }
598
+
599
+ .copy-column-preview--editable:focus {
600
+ border-color: rgb(var(--rgb-primary) / 0.55);
601
+ color: rgb(var(--rgb-on-surface) / 0.9);
602
+ }
603
+
591
604
  .metric-card {
592
605
  background: var(--color-surface-low);
593
606
  display: flex;