markdownr 0.8.0 → 0.8.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.
data/views/browser.erb CHANGED
@@ -4,6 +4,11 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title><%= h(@title) %></title>
7
+ <% if @allow_editor %>
8
+ <script type="importmap">
9
+ <%= editor_import_map_json %>
10
+ </script>
11
+ <% end %>
7
12
  <style>
8
13
  * { box-sizing: border-box; margin: 0; padding: 0; }
9
14
  body {
@@ -89,6 +94,8 @@
89
94
  overflow: auto;
90
95
  -webkit-overflow-scrolling: touch;
91
96
  font-size: 0.85rem;
97
+ scrollbar-width: thin;
98
+ scrollbar-color: rgba(0,0,0,0.35) transparent;
92
99
  }
93
100
  .br-popup-resize {
94
101
  position: absolute;
@@ -101,6 +108,16 @@
101
108
  z-index: 2;
102
109
  }
103
110
 
111
+ /* ── Collapsed popup (double-click header to toggle) ── */
112
+ .br-popup.collapsed {
113
+ height: auto !important;
114
+ min-height: 0 !important;
115
+ max-height: none !important;
116
+ }
117
+ .br-popup.collapsed .br-popup-body { display: none; }
118
+ .br-popup.collapsed .br-popup-resize { display: none; }
119
+ .br-popup.collapsed .br-popup-header { border-bottom: none; }
120
+
104
121
  /* ── View menu popup ── */
105
122
  .br-popup.view-menu {
106
123
  width: auto;
@@ -225,17 +242,48 @@
225
242
  font-family: Georgia, "Times New Roman", serif;
226
243
  }
227
244
  .br-md-content > :first-child { margin-top: 0; }
228
- .br-md-content h1 { font-size: 1.3rem; margin: 1.2rem 0 0.6rem; color: #3a3a3a; }
245
+ .br-md-content h1 { font-size: 1.65rem; margin: 1.4rem 0 0.7rem; color: #2a2a2a; }
229
246
  .br-md-content h2 {
230
- font-size: 1.1rem; margin: 1.4rem 0 0.5rem; color: #3a3a3a;
231
- border-bottom: 1px solid #e0d8c8; padding-bottom: 0.2rem;
247
+ font-size: 1.2rem; margin: 1.4rem 0 0.5rem; color: #3a3a3a;
248
+ border-bottom: 1px solid #b5a78f; padding-bottom: 0.05rem;
232
249
  }
233
- .br-md-content h3 { font-size: 1rem; margin: 1.1rem 0 0.4rem; color: #555; }
250
+ .br-md-content h3 { font-size: 1rem; margin: 1.1rem 0 0.4rem 0.75rem; color: #3a3a3a; }
234
251
  .br-md-content blockquote {
235
252
  border-left: 4px solid #d4b96a; margin: 0.8rem 0; padding: 0.4rem 1rem;
236
253
  background: #fdfcf6; color: #4a4a4a; font-style: italic;
237
254
  }
238
255
  .br-md-content blockquote p { white-space: pre-wrap; margin: 0; }
256
+ .br-md-content .callout {
257
+ border-left: 4px solid var(--callout-color, #888);
258
+ background: color-mix(in srgb, var(--callout-color, #888) 10%, transparent);
259
+ border-radius: 4px;
260
+ margin: 0.8rem 0;
261
+ padding: 0.5rem 0.8rem;
262
+ font-style: normal;
263
+ color: #2a2a2a;
264
+ }
265
+ .br-md-content .callout .callout-title {
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 0.35rem;
269
+ font-weight: 600;
270
+ color: var(--callout-color, #444);
271
+ cursor: default;
272
+ }
273
+ .br-md-content .callout details > summary.callout-title { cursor: pointer; list-style: none; }
274
+ .br-md-content .callout details > summary.callout-title::-webkit-details-marker { display: none; }
275
+ .br-md-content .callout details > summary.callout-title::after {
276
+ content: "▸";
277
+ margin-left: auto;
278
+ font-size: 0.7rem;
279
+ transition: transform 0.15s;
280
+ }
281
+ .br-md-content .callout details[open] > summary.callout-title::after { transform: rotate(90deg); }
282
+ .br-md-content .callout .callout-icon { flex-shrink: 0; }
283
+ .br-md-content .callout .callout-content { margin-top: 0.35rem; }
284
+ .br-md-content .callout .callout-content > :first-child { margin-top: 0; }
285
+ .br-md-content .callout .callout-content > :last-child { margin-bottom: 0; }
286
+ .br-md-content .callout .callout-content p { white-space: normal; margin: 0.4rem 0; }
239
287
  .br-md-content a { color: #8b6914; text-decoration: none; border-bottom: 1px solid #d4b96a; }
240
288
  .br-md-content a:hover { border-bottom-color: #8b6914; }
241
289
  .br-md-content code {
@@ -245,8 +293,34 @@
245
293
  .br-md-content pre {
246
294
  background: #2d2d2d; color: #f0f0f0; padding: 0.8rem 1rem;
247
295
  border-radius: 6px; overflow-x: auto; font-size: 0.82rem; line-height: 1.5;
296
+ position: relative;
248
297
  }
249
298
  .br-md-content pre code { background: none; padding: 0; color: inherit; }
299
+
300
+ /* Copy-to-clipboard button on <pre> blocks */
301
+ .copy-btn {
302
+ position: absolute;
303
+ top: 6px;
304
+ right: 6px;
305
+ background: rgba(255, 255, 255, 0.08);
306
+ color: #ddd;
307
+ border: 1px solid rgba(255, 255, 255, 0.18);
308
+ border-radius: 4px;
309
+ padding: 3px 6px;
310
+ cursor: pointer;
311
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
312
+ line-height: 0;
313
+ opacity: 0;
314
+ transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
315
+ z-index: 1;
316
+ }
317
+ .copy-btn svg { display: block; }
318
+ pre:hover > .copy-btn,
319
+ .copy-btn:focus { opacity: 1; }
320
+ .copy-btn:hover { background: rgba(255, 255, 255, 0.18); color: #fff; }
321
+ .copy-btn.copied {
322
+ background: #2e7d32; color: #fff; border-color: #2e7d32; opacity: 1;
323
+ }
250
324
  .br-md-content hr { border: none; border-top: 1px solid #e0d8c8; margin: 1.5rem 0; }
251
325
  .br-md-content ul, .br-md-content ol { padding-left: 1.5rem; }
252
326
  .br-md-content li { margin-bottom: 0.2rem; }
@@ -297,12 +371,116 @@
297
371
  background: #2d2d2d; color: #f0f0f0; padding: 1rem 1.2rem;
298
372
  overflow: auto; font-family: "SF Mono", Menlo, Consolas, monospace;
299
373
  font-size: 0.8rem; line-height: 1.5; flex: 1; min-height: 0;
374
+ position: relative;
300
375
  }
301
376
 
302
377
  /* Rouge Monokai syntax highlighting (popup + full-screen) */
303
378
  <%= Rouge::Themes::Monokai.render(scope: '.br-code-view').gsub(/^\.br-code-view \{\n.*?background-color:.*?\n\}$/m, '') %>
304
379
  <%= Rouge::Themes::Monokai.render(scope: '.highlight').gsub(/^\.highlight \{\n.*?background-color:.*?\n\}$/m, '') %>
305
380
 
381
+ /* ── In-popup editor (CodeMirror) ── */
382
+ .br-popup-edit-btn {
383
+ background: none; border: none;
384
+ font-size: 0.75rem; line-height: 1;
385
+ color: #8b6914; cursor: pointer;
386
+ padding: 0.15rem 0.45rem;
387
+ border: 1px solid #d4b96a;
388
+ border-radius: 4px;
389
+ flex-shrink: 0;
390
+ font-family: inherit;
391
+ }
392
+ .br-popup-edit-btn:hover { background: #f5f0e0; color: #5b4710; }
393
+ .br-popup.is-editing .br-popup-edit-btn { display: none; }
394
+ .br-editor-toolbar {
395
+ display: flex; gap: 0.4rem; align-items: center;
396
+ padding: 0.35rem 0.6rem;
397
+ background: #f5f1ea; border-bottom: 1px solid #e8e4dc;
398
+ font-size: 0.75rem; color: #555; flex-shrink: 0;
399
+ }
400
+ .br-editor-toolbar .br-editor-lang {
401
+ color: #888; margin-right: auto; font-family: "SF Mono", Menlo, Consolas, monospace;
402
+ }
403
+ .br-editor-toolbar button {
404
+ padding: 0.25rem 0.7rem; font-size: 0.75rem;
405
+ border: 1px solid #d4b96a; border-radius: 4px;
406
+ background: #faf8f4; color: #555; cursor: pointer;
407
+ font-family: inherit;
408
+ }
409
+ .br-editor-toolbar button:hover:not(:disabled) { background: #f0ece4; color: #2c2c2c; }
410
+ .br-editor-toolbar button:disabled { opacity: 0.45; cursor: default; }
411
+ .br-editor-toolbar .br-editor-save { background: #27ae60; color: #fff; border-color: #219a52; }
412
+ .br-editor-toolbar .br-editor-save:hover:not(:disabled) { background: #219a52; color: #fff; }
413
+ .br-editor-toolbar .br-editor-save.dirty {
414
+ background: #e6c029; color: #2c2c2c; border-color: #c5a51d;
415
+ }
416
+ .br-editor-toolbar .br-editor-save.dirty:hover { background: #c5a51d; color: #2c2c2c; }
417
+ .br-editor-toolbar .br-editor-vim.active {
418
+ background: #2a6496; color: #fff; border-color: #1d4970;
419
+ }
420
+ .br-editor-saved-flash {
421
+ color: #27ae60; font-size: 0.75rem; margin-left: 0.4rem;
422
+ opacity: 0; transition: opacity 0.3s;
423
+ }
424
+ .br-editor-saved-flash.show { opacity: 1; }
425
+ .br-editor-pane {
426
+ flex: 1; min-height: 0; min-width: 0;
427
+ overflow: hidden; display: flex; flex-direction: column;
428
+ }
429
+ .br-editor-pane .cm-editor {
430
+ height: 100%; width: 100%; flex: 1; min-height: 0;
431
+ font-size: 0.82rem;
432
+ }
433
+ .br-editor-split {
434
+ flex: 1; min-height: 0; display: flex; flex-direction: row;
435
+ overflow: hidden;
436
+ }
437
+ .br-editor-divider {
438
+ width: 5px; flex-shrink: 0; cursor: col-resize;
439
+ background: #e0d8c8; border-left: 1px solid #c8c0b0; border-right: 1px solid #c8c0b0;
440
+ }
441
+ .br-editor-divider:hover { background: #d4b96a; }
442
+ .br-editor-preview {
443
+ flex: 1 1 50%; min-width: 0; min-height: 0;
444
+ overflow: auto; background: #faf8f4; padding: 0.6rem 0.8rem;
445
+ font-size: 0.85rem;
446
+ }
447
+ /* Live-preview decorations inside the markdown editor */
448
+ .br-editor-pane .cm-editor .cm-md-line-h1 { font-size: 1.55em; font-weight: 700; line-height: 1.3; }
449
+ .br-editor-pane .cm-editor .cm-md-line-h2 { font-size: 1.35em; font-weight: 700; line-height: 1.3; }
450
+ .br-editor-pane .cm-editor .cm-md-line-h3 { font-size: 1.2em; font-weight: 700; }
451
+ .br-editor-pane .cm-editor .cm-md-line-h4 { font-size: 1.08em; font-weight: 700; }
452
+ .br-editor-pane .cm-editor .cm-md-line-h5 { font-size: 1em; font-weight: 700; }
453
+ .br-editor-pane .cm-editor .cm-md-line-h6 { font-size: 0.95em; font-weight: 700; opacity: 0.85; }
454
+ .br-editor-pane .cm-editor .cm-md-strong { font-weight: 700; }
455
+ .br-editor-pane .cm-editor .cm-md-em { font-style: italic; }
456
+ .br-editor-pane .cm-editor .cm-md-code {
457
+ background: rgba(150, 150, 150, 0.18);
458
+ padding: 0 0.25em;
459
+ border-radius: 3px;
460
+ font-family: "SF Mono", Menlo, Consolas, monospace;
461
+ }
462
+ .br-editor-pane .cm-editor .cm-md-link {
463
+ color: #6db1ff;
464
+ text-decoration: underline;
465
+ }
466
+ .br-editor-pane .cm-editor .cm-md-bullet {
467
+ color: #888;
468
+ font-weight: 700;
469
+ }
470
+
471
+ .br-editor-conflict {
472
+ background: #fde8e8; border-bottom: 1px solid #e6b3b3;
473
+ padding: 0.5rem 0.7rem; font-size: 0.78rem; color: #7a1f1f;
474
+ display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;
475
+ flex-shrink: 0;
476
+ }
477
+ .br-editor-conflict button {
478
+ padding: 0.2rem 0.65rem; font-size: 0.75rem; cursor: pointer;
479
+ border: 1px solid #c0392b; border-radius: 3px;
480
+ background: #fff; color: #7a1f1f; font-family: inherit;
481
+ }
482
+ .br-editor-conflict button:hover { background: #f7d4d4; }
483
+
306
484
  /* ── Download / external link in popup ── */
307
485
  .br-link-body {
308
486
  display: flex; flex-direction: column; align-items: center;
@@ -347,7 +525,26 @@
347
525
  padding: 0.25rem 0.4rem; background: #f8f5ef; border-bottom: 1px solid #d4b96a;
348
526
  position: sticky; top: 1.65rem; z-index: 1;
349
527
  }
350
- .br-table-wrap { overflow: auto; flex: 1; min-height: 0; }
528
+ .br-table-wrap {
529
+ overflow: auto; flex: 1; min-height: 0;
530
+ scrollbar-width: thin;
531
+ scrollbar-color: rgba(0,0,0,0.35) transparent;
532
+ }
533
+ .br-popup-body::-webkit-scrollbar,
534
+ .br-table-wrap::-webkit-scrollbar { width: 10px; height: 10px; }
535
+ .br-popup-body::-webkit-scrollbar-track,
536
+ .br-table-wrap::-webkit-scrollbar-track { background: transparent; }
537
+ .br-popup-body::-webkit-scrollbar-thumb,
538
+ .br-table-wrap::-webkit-scrollbar-thumb {
539
+ background: rgba(0,0,0,0.28);
540
+ border-radius: 5px;
541
+ border: 2px solid transparent;
542
+ background-clip: padding-box;
543
+ }
544
+ .br-popup-body::-webkit-scrollbar-thumb:hover,
545
+ .br-table-wrap::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.5); background-clip: padding-box; }
546
+ .br-popup-body::-webkit-scrollbar-corner,
547
+ .br-table-wrap::-webkit-scrollbar-corner { background: transparent; }
351
548
  .br-data-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
352
549
  .br-data-table th {
353
550
  background: #f0ece3; font-weight: 600; text-align: left;
@@ -356,6 +553,8 @@
356
553
  user-select: none; z-index: 1; overflow: hidden; text-overflow: ellipsis;
357
554
  }
358
555
  .br-data-table th:hover { background: #e8e0d0; }
556
+ .br-data-table th.br-corner-th { background: #2d2d2d; cursor: context-menu; }
557
+ .br-data-table th.br-corner-th:hover { background: #4a4a4a; }
359
558
  .br-col-resize {
360
559
  position: absolute; right: -3px; top: 0; bottom: 0; width: 6px;
361
560
  cursor: col-resize; z-index: 2; background: transparent;
@@ -391,6 +590,8 @@
391
590
  .br-data-table tr.br-row-dirty:hover td { background: #fdf3d0; }
392
591
  .br-data-table tr.br-row-invalid td { background: #fde8e8; }
393
592
  .br-data-table tr.br-row-invalid:hover td { background: #fad4d4; }
593
+ .br-data-table tr.br-row-dirty-bypass td { background: #fde8e8; }
594
+ .br-data-table tr.br-row-dirty-bypass:hover td { background: #fad4d4; }
394
595
  .br-data-table td.br-cell-match { background: #e8f5e9 !important; }
395
596
  .br-data-table tr.br-row-match td { background: #e8f5e9 !important; }
396
597
  .br-data-table tr.br-row-match:hover td { background: #c8e6c9 !important; }
@@ -642,6 +843,11 @@
642
843
  var startMode = '<%= @start_mode %>';
643
844
  var rootTitle = '<%= h(@root_title) %>';
644
845
  var csvDatabases = <%= @csv_databases %>;
846
+ var initialPath = <%= (@initial_path || "").to_json %>;
847
+ var isAdmin = <%= @is_admin ? 'true' : 'false' %>;
848
+ var defaultVim = <%= @default_vim ? 'true' : 'false' %>;
849
+ var allowEditor = <%= @allow_editor ? 'true' : 'false' %>;
850
+ var allowCsvEditor = <%= @allow_csv_editor ? 'true' : 'false' %>;
645
851
 
646
852
  // ── Tab management ──
647
853
 
@@ -854,7 +1060,13 @@
854
1060
  closeBtn.className = 'br-popup-btn';
855
1061
  closeBtn.innerHTML = '&times;';
856
1062
  if (opts.color) closeBtn.style.color = 'rgba(255,255,255,0.8)';
857
- closeBtn.onclick = function() { el.remove(); };
1063
+ closeBtn.onclick = function() {
1064
+ if (el._editorIsDirty && el._editorIsDirty()) {
1065
+ if (!confirm('This popup has unsaved edits. Close anyway?')) return;
1066
+ }
1067
+ if (el._editorTeardown) { try { el._editorTeardown(); } catch (e) {} }
1068
+ el.remove();
1069
+ };
858
1070
  header.appendChild(closeBtn);
859
1071
  }
860
1072
 
@@ -874,6 +1086,31 @@
874
1086
 
875
1087
  el.style.zIndex = ++zCounter;
876
1088
  el.addEventListener('mousedown', function() { el.style.zIndex = ++zCounter; });
1089
+ el.addEventListener('touchstart', function() { el.style.zIndex = ++zCounter; }, { passive: true });
1090
+
1091
+ // Double-click / double-tap on header toggles collapsed state.
1092
+ header.addEventListener('dblclick', function(e) {
1093
+ if (e.target.closest('.br-popup-btn')) return;
1094
+ if (e.target.closest('.br-popup-open-tab')) return;
1095
+ toggleCollapsed(el);
1096
+ });
1097
+ var lastTapTime = 0;
1098
+ var tapMoved = false;
1099
+ header.addEventListener('touchstart', function() { tapMoved = false; }, { passive: true });
1100
+ header.addEventListener('touchmove', function() { tapMoved = true; }, { passive: true });
1101
+ header.addEventListener('touchend', function(e) {
1102
+ if (tapMoved) { lastTapTime = 0; return; }
1103
+ if (e.target.closest('.br-popup-btn')) return;
1104
+ if (e.target.closest('.br-popup-open-tab')) return;
1105
+ var now = Date.now();
1106
+ if (now - lastTapTime < 400) {
1107
+ toggleCollapsed(el);
1108
+ e.preventDefault();
1109
+ lastTapTime = 0;
1110
+ } else {
1111
+ lastTapTime = now;
1112
+ }
1113
+ });
877
1114
 
878
1115
  if (opts.x !== undefined && opts.y !== undefined) {
879
1116
  el.style.left = opts.x + 'px';
@@ -1002,6 +1239,18 @@
1002
1239
  return null;
1003
1240
  }
1004
1241
  function bringToFront(el) { el.style.zIndex = ++zCounter; }
1242
+
1243
+ function toggleCollapsed(popup) {
1244
+ if (popup.classList.contains('collapsed')) {
1245
+ popup.classList.remove('collapsed');
1246
+ var saved = popup.getAttribute('data-saved-height');
1247
+ popup.style.height = saved || '';
1248
+ popup.removeAttribute('data-saved-height');
1249
+ } else {
1250
+ popup.setAttribute('data-saved-height', popup.style.height || '');
1251
+ popup.classList.add('collapsed');
1252
+ }
1253
+ }
1005
1254
  function escHtml(s) {
1006
1255
  var d = document.createElement('div');
1007
1256
  d.textContent = s;
@@ -1139,7 +1388,23 @@
1139
1388
  catch(e) { return {}; }
1140
1389
  }
1141
1390
 
1391
+ function countDirtyEditors() {
1392
+ var selector = '.br-popup.is-editing';
1393
+ if (activeTabId) selector += '[data-tab-id="' + activeTabId + '"]';
1394
+ var n = 0;
1395
+ document.querySelectorAll(selector).forEach(function(el) {
1396
+ if (el._editorIsDirty && el._editorIsDirty()) n++;
1397
+ });
1398
+ return n;
1399
+ }
1400
+
1142
1401
  function saveLayout(name) {
1402
+ var dirty = countDirtyEditors();
1403
+ if (dirty > 0) {
1404
+ var msg = dirty + ' popup' + (dirty === 1 ? ' has' : 's have') +
1405
+ ' unsaved edits. Save layout anyway? (Edits will be discarded from the snapshot but remain in the live popups.)';
1406
+ if (!confirm(msg)) return null;
1407
+ }
1143
1408
  var layouts = getLayouts();
1144
1409
  layouts[name] = {
1145
1410
  savedAt: new Date().toISOString(),
@@ -1265,6 +1530,9 @@
1265
1530
  var url = '/browser/api/render/' + encodeURI(path);
1266
1531
  if (opts.queryParams) url += '?' + opts.queryParams;
1267
1532
 
1533
+ popup.setAttribute('data-file-path', path);
1534
+ if (opts.queryParams) popup.setAttribute('data-file-query', opts.queryParams);
1535
+
1268
1536
  fetch(url)
1269
1537
  .then(function(r) { return r.json(); })
1270
1538
  .then(function(data) {
@@ -1277,6 +1545,7 @@
1277
1545
  } else if (data.type === 'external' && data.href) {
1278
1546
  popup.setAttribute('data-browse-href', data.href);
1279
1547
  }
1548
+ popup._editorPriorData = data;
1280
1549
  dispatchRenderer(popup, data);
1281
1550
  })
1282
1551
  .catch(function() {
@@ -1326,8 +1595,13 @@
1326
1595
  }
1327
1596
 
1328
1597
  var items = data.items;
1329
- var sortKey = 'mtime';
1330
- var sortDir = 'desc';
1598
+ var DIR_SORT_KEY = 'markdownr:dirSort';
1599
+ var dirSortPath = data.path || '';
1600
+ var savedSort = {};
1601
+ try { savedSort = JSON.parse(localStorage.getItem(DIR_SORT_KEY)) || {}; } catch (e) {}
1602
+ var entry = savedSort[dirSortPath] || {};
1603
+ var sortKey = entry.key || 'mtime';
1604
+ var sortDir = entry.dir || 'desc';
1331
1605
 
1332
1606
  var sortBar = document.createElement('div');
1333
1607
  sortBar.className = 'br-sort-bar';
@@ -1406,6 +1680,12 @@
1406
1680
  var defaultDir = btn.getAttribute('data-default-dir');
1407
1681
  if (key === sortKey) { sortDir = sortDir === 'asc' ? 'desc' : 'asc'; }
1408
1682
  else { sortKey = key; sortDir = defaultDir; }
1683
+ try {
1684
+ var current = {};
1685
+ try { current = JSON.parse(localStorage.getItem(DIR_SORT_KEY)) || {}; } catch (e2) {}
1686
+ current[dirSortPath] = { key: sortKey, dir: sortDir };
1687
+ localStorage.setItem(DIR_SORT_KEY, JSON.stringify(current));
1688
+ } catch (e3) {}
1409
1689
  render();
1410
1690
  });
1411
1691
 
@@ -1425,12 +1705,480 @@
1425
1705
  render();
1426
1706
  }
1427
1707
 
1708
+ // ── In-popup editor ──
1709
+
1710
+ var editorModulePromise = null;
1711
+ function ensureEditor() {
1712
+ if (!editorModulePromise) {
1713
+ editorModulePromise = import('/__markdownr/editor-loader.js?v=<%= editor_loader_version %>')
1714
+ .catch(function(e) { editorModulePromise = null; throw e; });
1715
+ }
1716
+ return editorModulePromise;
1717
+ }
1718
+
1719
+ function guessLanguageForPath(path) {
1720
+ var lower = (path || '').toLowerCase();
1721
+ var dot = lower.lastIndexOf('.');
1722
+ if (dot < 0) return 'text';
1723
+ var ext = lower.substring(dot + 1);
1724
+ switch (ext) {
1725
+ case 'md': case 'markdown': return 'markdown';
1726
+ case 'js': case 'mjs': return 'javascript';
1727
+ case 'json': return 'json';
1728
+ case 'yaml': case 'yml': return 'yaml';
1729
+ case 'html': case 'erb': case 'htm': return 'html';
1730
+ case 'css': return 'css';
1731
+ case 'py': return 'python';
1732
+ case 'rb': case 'ruby': return 'ruby';
1733
+ case 'sh': case 'bash': case 'zsh': return 'bash';
1734
+ default: return 'text';
1735
+ }
1736
+ }
1737
+
1738
+ // Heuristic for "does this markdown contain anything the live-preview
1739
+ // subset doesn't render?" Used to auto-open the side preview pane on
1740
+ // entering edit mode. We err on the side of opening — false positives
1741
+ // (e.g. a literal `<` in text) are better than false negatives (a table
1742
+ // not rendering and the user not realizing why).
1743
+ function markdownHasUnsupportedElements(text) {
1744
+ if (!text) return false;
1745
+ // Strip frontmatter so its `---` delimiters don't trigger the HR check.
1746
+ var body = text.replace(/^---\s*\n[\s\S]*?\n---\s*(\n|$)/, '');
1747
+ // Strip fenced and inline code so things like `<package>` inside
1748
+ // `https://esm.sh/<package>` don't trigger the HTML-tag check.
1749
+ var stripped = body
1750
+ .replace(/```[\s\S]*?```/g, '')
1751
+ .replace(/`[^`\n]*`/g, '');
1752
+ return /(^|\n)[ \t]*\|.*\|/.test(body) // table row
1753
+ || /!\[[^\]]*\]\([^)]+\)/.test(body) // image
1754
+ || /(^|\n)[ \t]*>[ \t]/.test(body) // blockquote
1755
+ || /(^|\n)(---|\*\*\*|___)\s*(\n|$)/.test(body) // horizontal rule
1756
+ || /<\/?[a-zA-Z][^>]*>/.test(stripped) // raw HTML tag (outside code)
1757
+ || /\[\[[^\]]+\]\]/.test(stripped) // wiki link (outside code)
1758
+ || /\[\^[^\]]+\]/.test(stripped); // footnote (outside code)
1759
+ }
1760
+
1761
+ function getVimPref() {
1762
+ var v = null;
1763
+ try { v = localStorage.getItem('markdownr:vim'); } catch (e) {}
1764
+ if (v === 'true') return true;
1765
+ if (v === 'false') return false;
1766
+ return !!defaultVim;
1767
+ }
1768
+ function setVimPref(on) {
1769
+ try { localStorage.setItem('markdownr:vim', on ? 'true' : 'false'); } catch (e) {}
1770
+ }
1771
+
1772
+ function addEditButton(popup) {
1773
+ if (!isAdmin || !allowEditor) return;
1774
+ var path = popup.getAttribute('data-file-path');
1775
+ if (!path) return;
1776
+ var queryParams = popup.getAttribute('data-file-query');
1777
+ if (queryParams && queryParams.indexOf('raw=1') < 0) {
1778
+ // Other query parameters we don't support yet — skip.
1779
+ return;
1780
+ }
1781
+ var header = popup.querySelector('.br-popup-header');
1782
+ if (!header || header.querySelector('.br-popup-edit-btn')) return;
1783
+
1784
+ var btn = document.createElement('button');
1785
+ btn.type = 'button';
1786
+ btn.className = 'br-popup-edit-btn';
1787
+ btn.textContent = 'Edit';
1788
+ btn.title = 'Edit file (admin)';
1789
+ btn.onclick = function(e) {
1790
+ e.stopPropagation();
1791
+ enterEditMode(popup);
1792
+ };
1793
+ var openTabBtn = header.querySelector('.br-popup-open-tab');
1794
+ if (openTabBtn) header.insertBefore(btn, openTabBtn);
1795
+ else header.appendChild(btn);
1796
+ }
1797
+
1798
+ function flashEditorSaved(toolbar) {
1799
+ var flash = toolbar.querySelector('.br-editor-saved-flash');
1800
+ if (!flash) {
1801
+ flash = document.createElement('span');
1802
+ flash.className = 'br-editor-saved-flash';
1803
+ flash.textContent = 'saved';
1804
+ toolbar.appendChild(flash);
1805
+ }
1806
+ flash.classList.add('show');
1807
+ clearTimeout(flash._t);
1808
+ flash._t = setTimeout(function() { flash.classList.remove('show'); }, 1200);
1809
+ }
1810
+
1811
+ function makeEditorDivider(splitWrap, divider) {
1812
+ var startX, leftStart, totalStart;
1813
+ function onDown(e) {
1814
+ e.preventDefault();
1815
+ var t = e.touches ? e.touches[0] : e;
1816
+ startX = t.clientX;
1817
+ var leftRect = splitWrap.firstElementChild.getBoundingClientRect();
1818
+ leftStart = leftRect.width;
1819
+ totalStart = splitWrap.getBoundingClientRect().width;
1820
+ document.addEventListener('mousemove', onMove);
1821
+ document.addEventListener('mouseup', onUp);
1822
+ document.addEventListener('touchmove', onMove, { passive: false });
1823
+ document.addEventListener('touchend', onUp);
1824
+ }
1825
+ function onMove(e) {
1826
+ e.preventDefault();
1827
+ var t = e.touches ? e.touches[0] : e;
1828
+ var dx = t.clientX - startX;
1829
+ var newLeft = Math.max(120, Math.min(totalStart - 120, leftStart + dx));
1830
+ var pct = (newLeft / totalStart) * 100;
1831
+ splitWrap.firstElementChild.style.flex = '0 0 ' + pct + '%';
1832
+ try { localStorage.setItem('markdownr:editor:split', String(pct)); } catch (e) {}
1833
+ }
1834
+ function onUp() {
1835
+ document.removeEventListener('mousemove', onMove);
1836
+ document.removeEventListener('mouseup', onUp);
1837
+ document.removeEventListener('touchmove', onMove);
1838
+ document.removeEventListener('touchend', onUp);
1839
+ }
1840
+ divider.addEventListener('mousedown', onDown);
1841
+ divider.addEventListener('touchstart', onDown, { passive: false });
1842
+ }
1843
+
1844
+ function enterEditMode(popup) {
1845
+ var path = popup.getAttribute('data-file-path');
1846
+ if (!path) return;
1847
+ if (popup._editorTeardown) return; // already editing
1848
+
1849
+ var priorData = popup._editorPriorData || null;
1850
+ var body = popup.querySelector('.br-popup-body');
1851
+ var origBodyHtml = body.innerHTML;
1852
+ var origBodyDisplay = body.style.display;
1853
+ var origBodyFlexDir = body.style.flexDirection;
1854
+ var origBodyPadding = body.style.padding;
1855
+ body.innerHTML = '<div class="br-empty" style="padding:1rem">Loading editor…</div>';
1856
+ popup.classList.add('is-editing');
1857
+
1858
+ Promise.all([
1859
+ fetch('/__markdownr/api/file/source/' + encodeURI(path), { credentials: 'same-origin' })
1860
+ .then(function(r) {
1861
+ if (r.status === 413) throw new Error('file too large to edit');
1862
+ if (!r.ok) throw new Error('source fetch failed: ' + r.status);
1863
+ var etag = (r.headers.get('ETag') || '').replace(/^"|"$/g, '');
1864
+ return r.text().then(function(text) { return { text: text, etag: etag }; });
1865
+ }),
1866
+ ensureEditor()
1867
+ ]).then(function(arr) {
1868
+ var src = arr[0];
1869
+ var mod = arr[1];
1870
+ buildEditorUI(popup, body, path, src.text, src.etag, priorData, mod, {
1871
+ origBodyHtml: origBodyHtml,
1872
+ origBodyDisplay: origBodyDisplay,
1873
+ origBodyFlexDir: origBodyFlexDir,
1874
+ origBodyPadding: origBodyPadding
1875
+ });
1876
+ }).catch(function(err) {
1877
+ body.innerHTML = '<div class="br-empty" style="padding:1rem;color:#c44">Failed to load editor: ' + escHtml(err && err.message || 'unknown') + '</div>';
1878
+ popup.classList.remove('is-editing');
1879
+ setTimeout(function() {
1880
+ body.style.display = origBodyDisplay;
1881
+ body.style.flexDirection = origBodyFlexDir;
1882
+ body.style.padding = origBodyPadding;
1883
+ if (priorData) dispatchRenderer(popup, priorData);
1884
+ else body.innerHTML = origBodyHtml;
1885
+ }, 1500);
1886
+ });
1887
+ }
1888
+
1889
+ function buildEditorUI(popup, body, path, text, initialEtag, priorData, mod, orig) {
1890
+ body.innerHTML = '';
1891
+ body.style.display = 'flex';
1892
+ body.style.flexDirection = 'column';
1893
+ body.style.padding = '0';
1894
+ popup.classList.add('wide');
1895
+
1896
+ var lang = guessLanguageForPath(path);
1897
+ var isMd = lang === 'markdown';
1898
+
1899
+ var toolbar = document.createElement('div');
1900
+ toolbar.className = 'br-editor-toolbar';
1901
+ body.appendChild(toolbar);
1902
+
1903
+ var langLabel = document.createElement('span');
1904
+ langLabel.className = 'br-editor-lang';
1905
+ langLabel.textContent = lang;
1906
+ toolbar.appendChild(langLabel);
1907
+
1908
+ var saveBtn = document.createElement('button');
1909
+ saveBtn.type = 'button';
1910
+ saveBtn.className = 'br-editor-save';
1911
+ saveBtn.textContent = 'Save';
1912
+ saveBtn.disabled = true;
1913
+ toolbar.appendChild(saveBtn);
1914
+
1915
+ var cancelBtn = document.createElement('button');
1916
+ cancelBtn.type = 'button';
1917
+ cancelBtn.className = 'br-editor-cancel';
1918
+ cancelBtn.textContent = 'Cancel';
1919
+ toolbar.appendChild(cancelBtn);
1920
+
1921
+ var vimBtn = document.createElement('button');
1922
+ vimBtn.type = 'button';
1923
+ vimBtn.className = 'br-editor-vim';
1924
+ var vimOn = getVimPref();
1925
+ function syncVimBtn() {
1926
+ vimBtn.textContent = vimOn ? 'vim: on' : 'vim: off';
1927
+ vimBtn.classList.toggle('active', vimOn);
1928
+ }
1929
+ syncVimBtn();
1930
+ toolbar.appendChild(vimBtn);
1931
+
1932
+ var conflictBar = null;
1933
+ var splitWrap, editorWrap, previewWrap, previewHidden, hidePreviewBtn;
1934
+
1935
+ if (isMd) {
1936
+ hidePreviewBtn = document.createElement('button');
1937
+ hidePreviewBtn.type = 'button';
1938
+ hidePreviewBtn.className = 'br-editor-preview-toggle';
1939
+ // Decision is made once on open: walk the source for elements the
1940
+ // live-preview subset doesn't render. If the doc has any of them, the
1941
+ // side preview opens; otherwise it stays closed. Manual toggle still
1942
+ // works for ad-hoc viewing while typing an unsupported element. We do
1943
+ // NOT persist user preference — re-check happens on each open.
1944
+ previewHidden = !markdownHasUnsupportedElements(text);
1945
+ toolbar.appendChild(hidePreviewBtn);
1946
+
1947
+ splitWrap = document.createElement('div');
1948
+ splitWrap.className = 'br-editor-split';
1949
+ body.appendChild(splitWrap);
1950
+
1951
+ editorWrap = document.createElement('div');
1952
+ editorWrap.className = 'br-editor-pane';
1953
+ splitWrap.appendChild(editorWrap);
1954
+
1955
+ var divider = document.createElement('div');
1956
+ divider.className = 'br-editor-divider';
1957
+ splitWrap.appendChild(divider);
1958
+
1959
+ previewWrap = document.createElement('div');
1960
+ previewWrap.className = 'br-editor-preview';
1961
+ if (priorData && priorData.html) {
1962
+ previewWrap.innerHTML = (priorData.frontmatter_html || '') + '<div class="br-md-content">' + priorData.html + '</div>';
1963
+ addCopyButtons(previewWrap);
1964
+ }
1965
+ splitWrap.appendChild(previewWrap);
1966
+
1967
+ var savedPct = null;
1968
+ try { savedPct = parseFloat(localStorage.getItem('markdownr:editor:split')); } catch (e) {}
1969
+ if (savedPct && savedPct > 10 && savedPct < 90) {
1970
+ editorWrap.style.flex = '0 0 ' + savedPct + '%';
1971
+ } else {
1972
+ editorWrap.style.flex = '0 0 50%';
1973
+ }
1974
+
1975
+ function applyPreviewHide() {
1976
+ previewWrap.style.display = previewHidden ? 'none' : '';
1977
+ divider.style.display = previewHidden ? 'none' : '';
1978
+ editorWrap.style.flex = previewHidden ? '1 1 100%' : (editorWrap.style.flex || '0 0 50%');
1979
+ hidePreviewBtn.textContent = previewHidden ? 'Show preview' : 'Hide preview';
1980
+ }
1981
+ applyPreviewHide();
1982
+ hidePreviewBtn.onclick = function() {
1983
+ previewHidden = !previewHidden;
1984
+ applyPreviewHide();
1985
+ };
1986
+
1987
+ makeEditorDivider(splitWrap, divider);
1988
+ } else {
1989
+ editorWrap = document.createElement('div');
1990
+ editorWrap.className = 'br-editor-pane';
1991
+ body.appendChild(editorWrap);
1992
+ }
1993
+
1994
+ var currentEtag = initialEtag;
1995
+ var dirty = false;
1996
+ var saving = false;
1997
+ var instance = null;
1998
+
1999
+ function setDirty(d) {
2000
+ dirty = d;
2001
+ saveBtn.classList.toggle('dirty', d);
2002
+ saveBtn.disabled = !d || saving;
2003
+ }
2004
+
2005
+ function clearConflict() {
2006
+ if (conflictBar) { conflictBar.remove(); conflictBar = null; }
2007
+ }
2008
+
2009
+ function showConflict(info) {
2010
+ clearConflict();
2011
+ conflictBar = document.createElement('div');
2012
+ conflictBar.className = 'br-editor-conflict';
2013
+ conflictBar.innerHTML = '<span>The file changed on disk since you opened it.</span>';
2014
+ var reload = document.createElement('button');
2015
+ reload.type = 'button';
2016
+ reload.textContent = 'Reload';
2017
+ reload.title = 'Discard your edits and load the new content';
2018
+ var overwrite = document.createElement('button');
2019
+ overwrite.type = 'button';
2020
+ overwrite.textContent = 'Overwrite';
2021
+ overwrite.title = 'Force-save your edits, replacing the new content';
2022
+ conflictBar.appendChild(reload);
2023
+ conflictBar.appendChild(overwrite);
2024
+ body.insertBefore(conflictBar, body.children[1] || null);
2025
+
2026
+ reload.onclick = function() {
2027
+ fetch('/__markdownr/api/file/source/' + encodeURI(path), { credentials: 'same-origin' })
2028
+ .then(function(r) {
2029
+ var etag = (r.headers.get('ETag') || '').replace(/^"|"$/g, '');
2030
+ return r.text().then(function(t) { return { text: t, etag: etag }; });
2031
+ })
2032
+ .then(function(s) {
2033
+ instance.setValue(s.text);
2034
+ currentEtag = s.etag;
2035
+ setDirty(false);
2036
+ clearConflict();
2037
+ });
2038
+ };
2039
+ overwrite.onclick = function() {
2040
+ clearConflict();
2041
+ doSave({ force: true });
2042
+ };
2043
+ }
2044
+
2045
+ function doSave(opts) {
2046
+ opts = opts || {};
2047
+ if (saving) return Promise.resolve(false);
2048
+ var content = instance.getValue();
2049
+ var hdrs = { 'Content-Type': 'text/plain; charset=utf-8' };
2050
+ if (currentEtag && !opts.force) hdrs['If-Match'] = '"' + currentEtag + '"';
2051
+ saving = true;
2052
+ saveBtn.disabled = true;
2053
+ var ok = false;
2054
+ return fetch('/__markdownr/api/file/' + encodeURI(path), {
2055
+ method: 'PUT', credentials: 'same-origin', headers: hdrs, body: content
2056
+ }).then(function(r) {
2057
+ if (r.status === 403) {
2058
+ return r.json().then(function(j) { showAdminRequired(j); throw new Error('forbidden'); });
2059
+ }
2060
+ if (r.status === 409) {
2061
+ return r.json().then(function(j) { showConflict(j); throw new Error('conflict'); });
2062
+ }
2063
+ if (!r.ok) {
2064
+ return r.text().then(function(t) { throw new Error('save failed: ' + r.status + ' ' + t); });
2065
+ }
2066
+ return r.json();
2067
+ }).then(function(j) {
2068
+ currentEtag = j.etag;
2069
+ setDirty(false);
2070
+ flashEditorSaved(toolbar);
2071
+ ok = true;
2072
+ }).catch(function(e) {
2073
+ if (e && e.message !== 'conflict' && e.message !== 'forbidden') {
2074
+ alert('Save failed: ' + (e && e.message || 'unknown'));
2075
+ }
2076
+ }).finally(function() {
2077
+ saving = false;
2078
+ setDirty(dirty);
2079
+ }).then(function() { return ok; });
2080
+ }
2081
+
2082
+ function exitEditing(reload) {
2083
+ if (instance) { try { instance.destroy(); } catch (e) {} }
2084
+ popup._editorTeardown = null;
2085
+ popup.classList.remove('is-editing');
2086
+ body.style.display = orig.origBodyDisplay;
2087
+ body.style.flexDirection = orig.origBodyFlexDir;
2088
+ body.style.padding = orig.origBodyPadding;
2089
+ if (reload) {
2090
+ var url = '/browser/api/render/' + encodeURI(path);
2091
+ var qp = popup.getAttribute('data-file-query');
2092
+ if (qp) url += '?' + qp;
2093
+ fetch(url).then(function(r) { return r.json(); }).then(function(d) {
2094
+ popup._editorPriorData = d;
2095
+ dispatchRenderer(popup, d);
2096
+ }).catch(function() {
2097
+ if (priorData) dispatchRenderer(popup, priorData);
2098
+ });
2099
+ } else if (priorData) {
2100
+ dispatchRenderer(popup, priorData);
2101
+ } else {
2102
+ body.innerHTML = orig.origBodyHtml;
2103
+ }
2104
+ }
2105
+
2106
+ cancelBtn.onclick = function() {
2107
+ if (dirty && !confirm('Discard unsaved changes?')) return;
2108
+ exitEditing(false);
2109
+ };
2110
+ saveBtn.onclick = function() { if (dirty) doSave(); };
2111
+ vimBtn.onclick = function() {
2112
+ vimOn = !vimOn;
2113
+ setVimPref(vimOn);
2114
+ syncVimBtn();
2115
+ if (instance) instance.setVim(vimOn);
2116
+ };
2117
+
2118
+ var debounceTimer = null;
2119
+ var lastPreviewedText = null;
2120
+ function schedulePreview() {
2121
+ if (!isMd) return;
2122
+ clearTimeout(debounceTimer);
2123
+ debounceTimer = setTimeout(function() {
2124
+ var content = instance.getValue();
2125
+ if (content === lastPreviewedText) return;
2126
+ lastPreviewedText = content;
2127
+ var wikiDir = path.indexOf('/') >= 0 ? path.substring(0, path.lastIndexOf('/')) : '';
2128
+ fetch('/__markdownr/api/render/preview', {
2129
+ method: 'POST', credentials: 'same-origin',
2130
+ headers: { 'Content-Type': 'application/json' },
2131
+ body: JSON.stringify({ content: content, current_wiki_dir: wikiDir })
2132
+ }).then(function(r) { return r.ok ? r.json() : null; }).then(function(j) {
2133
+ if (!j || !previewWrap) return;
2134
+ if (lastPreviewedText !== content) return;
2135
+ previewWrap.innerHTML = (j.frontmatter_html || '') + '<div class="br-md-content">' + j.html + '</div>';
2136
+ addCopyButtons(previewWrap);
2137
+ }).catch(function() {});
2138
+ }, 250);
2139
+ }
2140
+
2141
+ instance = mod.createEditor({
2142
+ parent: editorWrap,
2143
+ doc: text,
2144
+ language: lang,
2145
+ vim: vimOn,
2146
+ onSave: function() { if (dirty) doSave(); },
2147
+ onChange: function(newText) {
2148
+ setDirty(newText !== text);
2149
+ if (isMd) schedulePreview();
2150
+ }
2151
+ });
2152
+ if (instance.setSaveHandler) {
2153
+ instance.setSaveHandler(function() {
2154
+ return dirty ? doSave() : Promise.resolve(true);
2155
+ });
2156
+ }
2157
+ if (instance.setQuitHandler) {
2158
+ instance.setQuitHandler(
2159
+ function(opts) { exitEditing(!!(opts && opts.saved)); },
2160
+ function() { return dirty; }
2161
+ );
2162
+ }
2163
+
2164
+ popup._editorTeardown = function() {
2165
+ if (instance) { try { instance.destroy(); } catch (e) {} }
2166
+ popup._editorTeardown = null;
2167
+ };
2168
+ popup._editorIsDirty = function() { return dirty; };
2169
+
2170
+ setTimeout(function() { try { instance.focus(); } catch (e) {} }, 30);
2171
+ }
2172
+
1428
2173
  // ── Markdown renderer ──
1429
2174
 
1430
2175
  function renderMarkdown(popup, data) {
1431
2176
  var body = popup.querySelector('.br-popup-body');
1432
2177
  body.innerHTML = '';
1433
2178
  body.style.padding = '0';
2179
+ body.style.display = '';
2180
+ body.style.flexDirection = '';
2181
+ popup.classList.remove('is-editing');
1434
2182
  popup.classList.add('wide');
1435
2183
 
1436
2184
  if (data.frontmatter_html) {
@@ -1444,6 +2192,9 @@
1444
2192
  content.className = 'br-md-content';
1445
2193
  content.innerHTML = data.html;
1446
2194
  body.appendChild(content);
2195
+ addCopyButtons(content);
2196
+
2197
+ addEditButton(popup);
1447
2198
  }
1448
2199
 
1449
2200
  // ── Code renderer ──
@@ -1454,6 +2205,7 @@
1454
2205
  body.style.display = 'flex';
1455
2206
  body.style.flexDirection = 'column';
1456
2207
  body.style.padding = '0';
2208
+ popup.classList.remove('is-editing');
1457
2209
  popup.classList.add('wide');
1458
2210
 
1459
2211
  var toolbar = document.createElement('div');
@@ -1465,6 +2217,62 @@
1465
2217
  code.className = 'br-code-view';
1466
2218
  code.innerHTML = data.html;
1467
2219
  body.appendChild(code);
2220
+ addCopyButtons(body);
2221
+
2222
+ addEditButton(popup);
2223
+ }
2224
+
2225
+ // ── Copy-to-clipboard buttons on <pre> blocks ──
2226
+
2227
+ var COPY_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';
2228
+ var CHECK_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';
2229
+
2230
+ function legacyCopy(text) {
2231
+ var ta = document.createElement('textarea');
2232
+ ta.value = text;
2233
+ ta.style.position = 'fixed';
2234
+ ta.style.left = '-9999px';
2235
+ document.body.appendChild(ta);
2236
+ ta.select();
2237
+ try { document.execCommand('copy'); } catch (e) {}
2238
+ document.body.removeChild(ta);
2239
+ }
2240
+
2241
+ function addCopyButtons(root) {
2242
+ var pres = root.querySelectorAll('.br-md-content pre, pre.br-code-view');
2243
+ pres.forEach(function(pre) {
2244
+ if (pre.querySelector(':scope > .copy-btn')) return;
2245
+ var btn = document.createElement('button');
2246
+ btn.type = 'button';
2247
+ btn.className = 'copy-btn';
2248
+ btn.title = 'Copy';
2249
+ btn.setAttribute('aria-label', 'Copy code');
2250
+ btn.innerHTML = COPY_ICON_SVG;
2251
+ btn.addEventListener('click', function(e) {
2252
+ e.stopPropagation();
2253
+ e.preventDefault();
2254
+ var clone = pre.cloneNode(true);
2255
+ var existing = clone.querySelector('.copy-btn');
2256
+ if (existing) existing.remove();
2257
+ var codeEl = clone.querySelector('code');
2258
+ var text = (codeEl || clone).textContent;
2259
+ var done = function() {
2260
+ btn.classList.add('copied');
2261
+ btn.innerHTML = CHECK_ICON_SVG;
2262
+ setTimeout(function() {
2263
+ btn.classList.remove('copied');
2264
+ btn.innerHTML = COPY_ICON_SVG;
2265
+ }, 1500);
2266
+ };
2267
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2268
+ navigator.clipboard.writeText(text).then(done, function() { legacyCopy(text); done(); });
2269
+ } else {
2270
+ legacyCopy(text);
2271
+ done();
2272
+ }
2273
+ });
2274
+ pre.appendChild(btn);
2275
+ });
1468
2276
  }
1469
2277
 
1470
2278
  // ── Download renderer ──
@@ -1634,6 +2442,7 @@
1634
2442
  if (openOpts.colFiltersVisible) initOpts.colFiltersVisible = openOpts.colFiltersVisible;
1635
2443
  else if (savedMatches && savedObj.colFiltersVisible) initOpts.colFiltersVisible = savedObj.colFiltersVisible;
1636
2444
  if (openOpts.showRowNumbers) initOpts.showRowNumbers = openOpts.showRowNumbers;
2445
+ if (openOpts.hiddenValues) initOpts.hiddenValues = openOpts.hiddenValues;
1637
2446
  if (openOpts.colWidths) {
1638
2447
  initOpts.colWidths = openOpts.colWidths;
1639
2448
  } else if (savedMatches && savedObj.colWidths) {
@@ -1766,7 +2575,9 @@
1766
2575
  var searchTerms = [];
1767
2576
  var highlightMode = null; // null, 'cells', or 'rows'
1768
2577
  var highlightTerms = [];
2578
+ var hiddenValues = (initOpts.hiddenValues || []).slice();
1769
2579
  var dirtyRows = {};
2580
+ var dirtyBypassIds = {}; // rowIdx → true for rows kept visible only because they're dirty
1770
2581
  var selectedRowIdx = null;
1771
2582
  var editingCell = null;
1772
2583
  var validationErrors = {}; // { rowIndex: [{message, fields}] }
@@ -1780,7 +2591,7 @@
1780
2591
  var showRowNumbers = !!initOpts.showRowNumbers;
1781
2592
  var colWidths = initOpts.colWidths || {};
1782
2593
  var editMode = false;
1783
- var isReadonly = !!data.readonly;
2594
+ var isReadonly = !!data.readonly || !allowCsvEditor;
1784
2595
 
1785
2596
  var searchWrap = document.createElement('div');
1786
2597
  searchWrap.className = 'br-search-wrap';
@@ -1864,8 +2675,12 @@
1864
2675
  });
1865
2676
  }
1866
2677
 
1867
- // In highlight mode, show all rows (only apply col filters); otherwise filter normally
2678
+ // In highlight mode, show all rows (only apply col filters); otherwise filter normally.
2679
+ // Dirty rows always stay visible so an in-progress edit doesn't vanish when its value
2680
+ // no longer matches; those bypass rows get flagged so we can render them in red.
2681
+ dirtyBypassIds = {};
1868
2682
  displayRows = originalRows.filter(function(row) {
2683
+ var matches = true;
1869
2684
  if (!highlightMode && searchTerms.length > 0) {
1870
2685
  var globalMatch = searchTerms.every(function(term) {
1871
2686
  for (var i = 0; i < columns.length; i++) {
@@ -1873,17 +2688,30 @@
1873
2688
  }
1874
2689
  return false;
1875
2690
  });
1876
- if (!globalMatch) return false;
2691
+ if (!globalMatch) matches = false;
1877
2692
  }
1878
- // Per-column filters always apply
1879
- for (var f = 0; f < activeColFilters.length; f++) {
1880
- var af = activeColFilters[f];
1881
- var cellVal = getCellValue(row, af.ci);
1882
- for (var t = 0; t < af.terms.length; t++) {
1883
- if (!termMatchesCell(af.terms[t], cellVal)) return false;
2693
+ if (matches) {
2694
+ for (var f = 0; f < activeColFilters.length; f++) {
2695
+ var af = activeColFilters[f];
2696
+ var cellVal = getCellValue(row, af.ci);
2697
+ for (var t = 0; t < af.terms.length; t++) {
2698
+ if (!termMatchesCell(af.terms[t], cellVal)) { matches = false; break; }
2699
+ }
2700
+ if (!matches) break;
1884
2701
  }
1885
2702
  }
1886
- return true;
2703
+ if (matches && hiddenValues.length > 0) {
2704
+ for (var h = 0; h < hiddenValues.length; h++) {
2705
+ var hv = hiddenValues[h];
2706
+ for (var hi = 0; hi < columns.length; hi++) {
2707
+ if (getCellValue(row, hi) === hv) { matches = false; break; }
2708
+ }
2709
+ if (!matches) break;
2710
+ }
2711
+ }
2712
+ if (matches) return true;
2713
+ if (dirtyRows[row[0]]) { dirtyBypassIds[row[0]] = true; return true; }
2714
+ return false;
1887
2715
  });
1888
2716
 
1889
2717
  if (sortCol >= 0 && sortDirection !== 0) {
@@ -1937,6 +2765,333 @@
1937
2765
 
1938
2766
  var hasValidation = Object.keys(validationErrors).length > 0;
1939
2767
 
2768
+ function showHeaderContextMenu(colIdx, x, y) {
2769
+ var col = columns[colIdx];
2770
+ var menu = document.createElement('div');
2771
+ menu.className = 'br-context-menu';
2772
+ menu.style.left = x + 'px';
2773
+ menu.style.top = y + 'px';
2774
+
2775
+ function addItem(label, onClick) {
2776
+ var btn = document.createElement('button');
2777
+ btn.className = 'br-context-menu-item default';
2778
+ btn.textContent = label;
2779
+ btn.addEventListener('click', function() { dismissContextMenu(); onClick(); });
2780
+ menu.appendChild(btn);
2781
+ }
2782
+ function addSep() {
2783
+ var sep = document.createElement('div');
2784
+ sep.className = 'br-context-menu-sep';
2785
+ menu.appendChild(sep);
2786
+ }
2787
+
2788
+ // Group 1: summaries
2789
+ addItem('Stats', function() { showColumnStats(colIdx, x, y); });
2790
+ addItem('Distinct values', function() { showColumnDistinct(colIdx, x, y); });
2791
+ addSep();
2792
+
2793
+ // Group 2: quick filters
2794
+ addItem('Filter: is blank', function() { setColumnFilter(col.key, '^$'); });
2795
+ addItem('Filter: is not blank', function() { setColumnFilter(col.key, '.'); });
2796
+ addSep();
2797
+
2798
+ // Group 3: export
2799
+ addItem('Copy column', function() { copyColumnValues(colIdx); });
2800
+ addSep();
2801
+
2802
+ // Group 4: width
2803
+ addItem('Auto-width', function() { autoFitColumn(colIdx); });
2804
+ addItem('Reset width', function() { resetColumnWidth(col.key); });
2805
+
2806
+ document.body.appendChild(menu);
2807
+ }
2808
+
2809
+ // Collect raw (non-FK-resolved) numeric values for visible rows.
2810
+ function collectNumericValues(colIdx) {
2811
+ var col = columns[colIdx];
2812
+ var vals = [];
2813
+ displayRows.forEach(function(row) {
2814
+ var v = row[colIdx + 1];
2815
+ if (dirtyRows[row[0]] && dirtyRows[row[0]][col.key] !== undefined) v = dirtyRows[row[0]][col.key];
2816
+ if (v === null || v === undefined || v === '') return;
2817
+ var n = parseFloat(String(v).replace(/[$,]/g, ''));
2818
+ if (!isNaN(n) && isFinite(n)) vals.push(n);
2819
+ });
2820
+ return vals;
2821
+ }
2822
+
2823
+ function formatStatValue(col, n) {
2824
+ if (isCurrencyCol(col)) return formatCurrency(n);
2825
+ if (col.type === 'integer' && Number.isInteger(n)) return n.toLocaleString();
2826
+ return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
2827
+ }
2828
+
2829
+ function showColumnStats(colIdx, x, y) {
2830
+ var col = columns[colIdx];
2831
+ var vals = collectNumericValues(colIdx);
2832
+ var body;
2833
+ if (vals.length === 0) {
2834
+ body = '<div style="padding:0.75rem 1rem;color:#888;">No numeric values in visible rows.</div>';
2835
+ } else {
2836
+ var positives = vals.filter(function(v) { return v > 0; });
2837
+ var negatives = vals.filter(function(v) { return v < 0; });
2838
+ var sections = [{ label: 'All', vals: vals, countSuffix: ' of ' + displayRows.length }];
2839
+ if (positives.length > 0 && negatives.length > 0) {
2840
+ sections.push({ label: 'Positive only', vals: positives, countSuffix: ' of ' + vals.length });
2841
+ sections.push({ label: 'Negative only', vals: negatives, countSuffix: ' of ' + vals.length });
2842
+ }
2843
+ body = '<div style="padding:0.75rem 1rem;">';
2844
+ sections.forEach(function(sec, si) {
2845
+ if (si > 0) body += '<div style="height:0.75rem;"></div>';
2846
+ body += '<div style="font-weight:600;color:#8b6914;margin-bottom:0.25rem;font-size:0.85rem;">' + escHtml(sec.label) + '</div>';
2847
+ body += buildStatsTable(col, sec.vals, sec.countSuffix);
2848
+ });
2849
+ body += '</div>';
2850
+ }
2851
+ createPopup({ title: 'Stats — ' + col.title, html: body, x: x, y: y });
2852
+ }
2853
+
2854
+ function buildStatsTable(col, vals, countSuffix) {
2855
+ var sum = vals.reduce(function(a, b) { return a + b; }, 0);
2856
+ var mean = sum / vals.length;
2857
+ var sorted = vals.slice().sort(function(a, b) { return a - b; });
2858
+ var median = sorted.length % 2 === 1
2859
+ ? sorted[(sorted.length - 1) / 2]
2860
+ : (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2;
2861
+ var rows = [
2862
+ ['Sum', formatStatValue(col, sum)],
2863
+ ['Average', formatStatValue(col, mean)],
2864
+ ['Median', formatStatValue(col, median)],
2865
+ ['Min', formatStatValue(col, sorted[0])],
2866
+ ['Max', formatStatValue(col, sorted[sorted.length - 1])],
2867
+ ['Count', vals.length + (countSuffix || '')]
2868
+ ];
2869
+ var html = '<table style="width:100%;border-collapse:collapse;border:1px solid #e8e4dc;">';
2870
+ rows.forEach(function(r, i) {
2871
+ var bg = i % 2 === 0 ? '#faf8f4' : '#ffffff';
2872
+ html += '<tr style="background:' + bg + ';">' +
2873
+ '<td style="padding:0.3rem 0.75rem;color:#666;">' + escHtml(r[0]) + '</td>' +
2874
+ '<td style="padding:0.3rem 0.75rem;text-align:right;font-weight:600;color:#3a3a3a;">' + escHtml(r[1]) + '</td>' +
2875
+ '</tr>';
2876
+ });
2877
+ html += '</table>';
2878
+ return html;
2879
+ }
2880
+
2881
+ function showColumnDistinct(colIdx, x, y) {
2882
+ var col = columns[colIdx];
2883
+ var counts = {};
2884
+ var order = [];
2885
+ displayRows.forEach(function(row) {
2886
+ var v = getCellValue(row, colIdx);
2887
+ if (!(v in counts)) { counts[v] = 0; order.push(v); }
2888
+ counts[v]++;
2889
+ });
2890
+ order.sort(function(a, b) {
2891
+ if (counts[b] !== counts[a]) return counts[b] - counts[a];
2892
+ return String(a).localeCompare(String(b));
2893
+ });
2894
+ var body;
2895
+ if (order.length === 0) {
2896
+ body = '<div style="padding:0.75rem 1rem;color:#888;">No visible rows.</div>';
2897
+ } else {
2898
+ var maxCount = counts[order[0]];
2899
+ body = '<div style="padding:0.5rem 0.75rem;">' +
2900
+ '<div style="color:#888;margin-bottom:0.5rem;">' + order.length + ' distinct value' +
2901
+ (order.length === 1 ? '' : 's') + ' in ' + displayRows.length + ' visible row' +
2902
+ (displayRows.length === 1 ? '' : 's') + '</div>' +
2903
+ '<table style="width:100%;border-collapse:collapse;">';
2904
+ order.forEach(function(v) {
2905
+ var label = v === '' ? '<span style="color:#bbb;font-style:italic;">(blank)</span>' : escHtml(v);
2906
+ var pct = Math.max(2, Math.round(counts[v] / maxCount * 100));
2907
+ var bar = '<div style="position:relative;height:1.3em;min-width:60px;">' +
2908
+ '<div style="position:absolute;left:0;top:0;bottom:0;width:' + pct + '%;background:rgba(212,185,106,0.45);border-radius:2px;"></div>' +
2909
+ '<span style="position:relative;padding:0 0.4rem;font-weight:600;color:#5a4410;">' + counts[v] + '</span>' +
2910
+ '</div>';
2911
+ body += '<tr>' +
2912
+ '<td style="padding:0.2rem 0.75rem 0.2rem 0;">' + label + '</td>' +
2913
+ '<td style="padding:0.2rem 0;text-align:right;width:40%;">' + bar + '</td>' +
2914
+ '</tr>';
2915
+ });
2916
+ body += '</table></div>';
2917
+ }
2918
+ createPopup({ title: 'Distinct — ' + col.title, html: body, x: x, y: y });
2919
+ }
2920
+
2921
+ function setColumnFilter(colKey, expr) {
2922
+ colFilterValues[colKey] = expr;
2923
+ if (!colFiltersVisible) {
2924
+ colFiltersVisible = true;
2925
+ colFilterToggle.textContent = '▴';
2926
+ }
2927
+ render();
2928
+ var input = tableWrap.querySelector('[data-col-filter="' + colKey + '"]');
2929
+ if (input) { input.focus(); input.selectionStart = input.selectionEnd = input.value.length; }
2930
+ }
2931
+
2932
+ function copyColumnValues(colIdx) {
2933
+ var col = columns[colIdx];
2934
+ var lines = [col.title].concat(displayRows.map(function(row) { return getCellValue(row, colIdx); }));
2935
+ copyToClipboard(lines.join('\n'));
2936
+ }
2937
+
2938
+ function copyToClipboard(text) {
2939
+ function fallback() {
2940
+ var ta = document.createElement('textarea');
2941
+ ta.value = text;
2942
+ ta.style.position = 'fixed';
2943
+ ta.style.top = '0'; ta.style.left = '0';
2944
+ ta.style.opacity = '0';
2945
+ document.body.appendChild(ta);
2946
+ ta.focus(); ta.select();
2947
+ try { document.execCommand('copy'); } catch (e) {}
2948
+ ta.remove();
2949
+ }
2950
+ if (navigator.clipboard && navigator.clipboard.writeText) {
2951
+ navigator.clipboard.writeText(text).catch(fallback);
2952
+ } else {
2953
+ fallback();
2954
+ }
2955
+ }
2956
+
2957
+ function autoFitColumn(colIdx) {
2958
+ var col = columns[colIdx];
2959
+ var sampleCell = tableWrap.querySelector('td[data-col]');
2960
+ var sampleTh = tableWrap.querySelector('th[data-sort-col]');
2961
+ if (!sampleCell || !sampleTh) return;
2962
+ var cellFont = getComputedStyle(sampleCell).font;
2963
+ var headerFont = getComputedStyle(sampleTh).font;
2964
+ var ctx = document.createElement('canvas').getContext('2d');
2965
+ ctx.font = headerFont;
2966
+ var maxW = ctx.measureText(col.title).width + 28; // header padding + sort arrow
2967
+ ctx.font = cellFont;
2968
+ displayRows.forEach(function(row) {
2969
+ var w = ctx.measureText(getCellValue(row, colIdx)).width;
2970
+ if (w > maxW) maxW = w;
2971
+ });
2972
+ colWidths[col.key] = Math.min(Math.ceil(maxW) + 20, 500);
2973
+ render();
2974
+ }
2975
+
2976
+ function resetColumnWidth(colKey) {
2977
+ delete colWidths[colKey];
2978
+ render();
2979
+ }
2980
+
2981
+ function showTableCornerContextMenu(x, y) {
2982
+ var menu = document.createElement('div');
2983
+ menu.className = 'br-context-menu';
2984
+ menu.style.left = x + 'px';
2985
+ menu.style.top = y + 'px';
2986
+
2987
+ function addItem(icon, label, onClick) {
2988
+ var btn = document.createElement('button');
2989
+ btn.className = 'br-context-menu-item default';
2990
+ btn.innerHTML = '<span style="display:inline-block;width:1.6em;text-align:center;">' + icon + '</span>' + escHtml(label);
2991
+ btn.addEventListener('click', function() { dismissContextMenu(); onClick(); });
2992
+ menu.appendChild(btn);
2993
+ }
2994
+ function addSep() {
2995
+ var sep = document.createElement('div');
2996
+ sep.className = 'br-context-menu-sep';
2997
+ menu.appendChild(sep);
2998
+ }
2999
+
3000
+ addItem('📋', 'Copy as CSV', function() { copyTableAsCsv(); });
3001
+ addItem('📋', 'Copy as markdown', function() { copyTableAsMarkdown(); });
3002
+ addItem('🖨', 'Print', function() { printTable(); });
3003
+ addSep();
3004
+
3005
+ var canValidate = dbKey && dbKey !== '_unmapped' && !isReadonly;
3006
+ if (canValidate) {
3007
+ addItem('🔥', 'Validate all rows', function() { runValidateAll(); });
3008
+ }
3009
+ addItem('#', (showRowNumbers ? 'Hide' : 'Show') + ' row numbers', function() { toggleRowNumbers(); });
3010
+ addSep();
3011
+
3012
+ if (dbKey && dbKey !== '_unmapped') {
3013
+ addItem('💾', 'Save as default view', function() { saveCurrentAsDefaultView(); });
3014
+ addItem('💾', 'Show saved default view', function() { showSavedDefaultViewPopup(); });
3015
+ addSep();
3016
+ }
3017
+ addItem('💾', 'Show all markdownr localStorage', function() { showAllLocalStoragePopup(); });
3018
+
3019
+ document.body.appendChild(menu);
3020
+ }
3021
+
3022
+ function csvEscape(s) {
3023
+ s = (s === null || s === undefined) ? '' : String(s);
3024
+ if (s.indexOf(',') !== -1 || s.indexOf('"') !== -1 || s.indexOf('\n') !== -1 || s.indexOf('\r') !== -1) {
3025
+ return '"' + s.replace(/"/g, '""') + '"';
3026
+ }
3027
+ return s;
3028
+ }
3029
+
3030
+ function mdEscape(s) {
3031
+ return ((s === null || s === undefined) ? '' : String(s)).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
3032
+ }
3033
+
3034
+ function copyTableAsCsv() {
3035
+ var lines = [columns.map(function(c) { return csvEscape(c.title); }).join(',')];
3036
+ displayRows.forEach(function(row) {
3037
+ lines.push(columns.map(function(_, ci) { return csvEscape(getCellValue(row, ci)); }).join(','));
3038
+ });
3039
+ copyToClipboard(lines.join('\n'));
3040
+ }
3041
+
3042
+ function copyTableAsMarkdown() {
3043
+ var header = '| ' + columns.map(function(c) { return mdEscape(c.title); }).join(' | ') + ' |';
3044
+ var sep = '| ' + columns.map(function(c) { return isRightAligned(c) ? '---:' : '---'; }).join(' | ') + ' |';
3045
+ var body = displayRows.map(function(row) {
3046
+ return '| ' + columns.map(function(_, ci) { return mdEscape(getCellValue(row, ci)); }).join(' | ') + ' |';
3047
+ });
3048
+ copyToClipboard([header, sep].concat(body).join('\n'));
3049
+ }
3050
+
3051
+ function getDisplayCellValue(row, ci) {
3052
+ var val = getCellValue(row, ci);
3053
+ if (val !== '' && isCurrencyCol(columns[ci])) return formatCurrency(val);
3054
+ return val;
3055
+ }
3056
+
3057
+ function printTable() {
3058
+ var titleEl = popup.querySelector('.br-popup-title');
3059
+ var titleText = titleEl ? titleEl.textContent : 'Table';
3060
+ var w = window.open('', '_blank');
3061
+ if (!w) { alert('Pop-up blocked. Allow pop-ups to print.'); return; }
3062
+ var alignAttr = function(col) { return isRightAligned(col) ? ' style="text-align:right"' : ''; };
3063
+ var headerHtml = columns.map(function(c) {
3064
+ return '<th' + alignAttr(c) + '>' + escHtml(c.title) + '</th>';
3065
+ }).join('');
3066
+ var bodyHtml = displayRows.map(function(row) {
3067
+ return '<tr>' + columns.map(function(_, ci) {
3068
+ var v = getDisplayCellValue(row, ci);
3069
+ return '<td' + alignAttr(columns[ci]) + '>' + (v === '' ? '' : escHtml(v)) + '</td>';
3070
+ }).join('') + '</tr>';
3071
+ }).join('');
3072
+ var doc = '<!doctype html><html><head><meta charset="utf-8"><title>' + escHtml(titleText) + '</title>' +
3073
+ '<style>' +
3074
+ 'body{font-family:-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;color:#000;margin:1rem;}' +
3075
+ 'h1{font-size:1.1rem;margin:0 0 0.6rem 0;}' +
3076
+ '.meta{font-size:0.75rem;color:#666;margin-bottom:0.6rem;}' +
3077
+ 'table{border-collapse:collapse;width:100%;font-size:11px;}' +
3078
+ 'th,td{border:1px solid #999;padding:4px 6px;vertical-align:top;}' +
3079
+ 'th{background:#eee;text-align:left;}' +
3080
+ 'tbody tr:nth-child(even) td{background:#f6f6f6;}' +
3081
+ '@media print{body{margin:0.4in;} h1{font-size:1rem;} thead{display:table-header-group;} tr{page-break-inside:avoid;}}' +
3082
+ '</style></head><body>' +
3083
+ '<h1>' + escHtml(titleText) + '</h1>' +
3084
+ '<div class="meta">' + displayRows.length + ' row' + (displayRows.length === 1 ? '' : 's') +
3085
+ ' · ' + new Date().toLocaleString() + '</div>' +
3086
+ '<table><thead><tr>' + headerHtml + '</tr></thead><tbody>' + bodyHtml + '</tbody></table>' +
3087
+ '<script>window.onload=function(){setTimeout(function(){window.print();},150);};<\/script>' +
3088
+ '</body></html>';
3089
+ w.document.open();
3090
+ w.document.write(doc);
3091
+ w.document.close();
3092
+ }
3093
+ popup._print = printTable;
3094
+
1940
3095
  function isRightAligned(col) {
1941
3096
  if (col.type === 'integer' || col.type === 'number') return true;
1942
3097
  var c = col.constraints || {};
@@ -1962,15 +3117,20 @@
1962
3117
  }
1963
3118
 
1964
3119
  function renderTableHTML() {
3120
+ var DEFAULT_MAX_COL_WIDTH = 500;
1965
3121
  var cellStyles = columns.map(function(col) {
1966
3122
  var parts = [];
1967
- if (colWidths[col.key]) parts.push('white-space:normal');
3123
+ if (colWidths[col.key]) {
3124
+ parts.push('white-space:normal', 'overflow-wrap:anywhere');
3125
+ } else {
3126
+ parts.push('max-width:' + DEFAULT_MAX_COL_WIDTH + 'px', 'overflow:hidden', 'text-overflow:ellipsis');
3127
+ }
1968
3128
  if (isRightAligned(col)) parts.push('text-align:right');
1969
3129
  return parts.length > 0 ? ' style="' + parts.join(';') + '"' : '';
1970
3130
  });
1971
3131
  var html = '<table class="br-data-table' + (editMode ? ' edit-mode' : '') + '"><thead><tr>';
3132
+ html += '<th class="br-corner-th" data-table-corner="1" title="Right-click for table actions" style="width:1px;padding:0.4rem 0.3rem;"></th>';
1972
3133
  if (hasValidation) html += '<th style="width:1px;padding:0.4rem 0.2rem;"></th>';
1973
- html += '<th style="width:1px;padding:0.4rem 0.3rem;"></th>';
1974
3134
  if (showRowNumbers) html += '<th style="width:1px;padding:0.4rem 0.3rem;color:#999;font-size:0.75rem;">#</th>';
1975
3135
  columns.forEach(function(col, ci) {
1976
3136
  var cls = '';
@@ -1979,7 +3139,9 @@
1979
3139
  else if (ci === sortCol && sortDirection === -1) { cls = ' class="sort-desc"'; arrow += '\u25bc'; }
1980
3140
  else { arrow += '\u25b8'; }
1981
3141
  arrow += '</span>';
1982
- var wStyle = colWidths[col.key] ? ' style="width:' + colWidths[col.key] + 'px;min-width:' + colWidths[col.key] + 'px;max-width:' + colWidths[col.key] + 'px"' : '';
3142
+ var wStyle = colWidths[col.key]
3143
+ ? ' style="width:' + colWidths[col.key] + 'px;min-width:' + colWidths[col.key] + 'px;max-width:' + colWidths[col.key] + 'px"'
3144
+ : ' style="max-width:' + DEFAULT_MAX_COL_WIDTH + 'px"';
1983
3145
  html += '<th' + cls + ' data-sort-col="' + ci + '"' + wStyle + '>' + escHtml(col.title) + arrow + '<span class="br-col-resize" data-resize-col="' + ci + '"></span></th>';
1984
3146
  });
1985
3147
  if (reverseRefs.length > 0) {
@@ -1988,8 +3150,8 @@
1988
3150
  html += '</tr>';
1989
3151
  if (colFiltersVisible) {
1990
3152
  html += '<tr class="br-col-filter-row">';
1991
- if (hasValidation) html += '<td></td>';
1992
3153
  html += '<td></td>';
3154
+ if (hasValidation) html += '<td></td>';
1993
3155
  if (showRowNumbers) html += '<td></td>';
1994
3156
  columns.forEach(function(col) {
1995
3157
  var val = colFilterValues[col.key] || '';
@@ -2003,17 +3165,11 @@
2003
3165
  displayRows.forEach(function(row, displayIndex) {
2004
3166
  var rowIdx = row[0];
2005
3167
  var isDirty = !!dirtyRows[rowIdx];
3168
+ var isBypass = !!dirtyBypassIds[rowIdx];
2006
3169
  var rowErrors = validationErrors[rowIdx];
2007
- var rowCls = isDirty ? 'br-row-dirty' : (rowErrors ? 'br-row-invalid' : '');
3170
+ var rowCls = isBypass ? 'br-row-dirty-bypass' : (isDirty ? 'br-row-dirty' : (rowErrors ? 'br-row-invalid' : ''));
2008
3171
  if (selectedRowIdx === rowIdx) rowCls = (rowCls ? rowCls + ' ' : '') + 'br-row-selected';
2009
3172
  html += '<tr data-row-idx="' + rowIdx + '"' + (rowCls ? ' class="' + rowCls + '"' : '') + '>';
2010
- if (hasValidation) {
2011
- if (rowErrors) {
2012
- html += '<td class="br-val-cell"><span class="br-val-icon" data-val-row="' + rowIdx + '" title="Validation errors">\ud83d\udd25</span></td>';
2013
- } else {
2014
- html += '<td class="br-val-cell"></td>';
2015
- }
2016
- }
2017
3173
  var rowCellInvalid = rowHasCellErrors(rowIdx);
2018
3174
  if (isDirty) {
2019
3175
  var saveCls = 'br-row-save' + (rowCellInvalid ? ' has-errors' : '');
@@ -2025,6 +3181,13 @@
2025
3181
  } else {
2026
3182
  html += '<td class="br-row-actions"></td>';
2027
3183
  }
3184
+ if (hasValidation) {
3185
+ if (rowErrors) {
3186
+ html += '<td class="br-val-cell"><span class="br-val-icon" data-val-row="' + rowIdx + '" title="Validation errors">\ud83d\udd25</span></td>';
3187
+ } else {
3188
+ html += '<td class="br-val-cell"></td>';
3189
+ }
3190
+ }
2028
3191
  if (showRowNumbers) html += '<td style="color:#999;font-size:0.75rem;text-align:right;white-space:nowrap;">' + (rowIdx + 1) + '</td>';
2029
3192
  for (var i = 1; i < row.length; i++) {
2030
3193
  var val = row[i];
@@ -2047,7 +3210,11 @@
2047
3210
  var fkClass = (!editMode && colRef) ? ' fk-link' : '';
2048
3211
  var fkParts = [];
2049
3212
  if (colRef && colRef.color) fkParts.push('background:' + escHtml(colRef.color) + '18');
2050
- if (colWidths[colKey]) fkParts.push('white-space:normal');
3213
+ if (colWidths[colKey]) {
3214
+ fkParts.push('white-space:normal', 'overflow-wrap:anywhere');
3215
+ } else {
3216
+ fkParts.push('max-width:' + DEFAULT_MAX_COL_WIDTH + 'px', 'overflow:hidden', 'text-overflow:ellipsis');
3217
+ }
2051
3218
  if (isRightAligned(columns[i - 1])) fkParts.push('text-align:right');
2052
3219
  var fkStyleAttr = fkParts.length > 0 ? ' style="' + fkParts.join(';') + '"' : '';
2053
3220
  var fkTitle = cellHasError ? escHtml(cellValidationErrors[cellErrKey]) : 'ID: ' + escHtml(String(val));
@@ -3066,8 +4233,18 @@
3066
4233
  render();
3067
4234
  });
3068
4235
 
3069
- validateBtn.addEventListener('click', function() {
4236
+ function runValidateAll() {
3070
4237
  if (!dbKey || dbKey === '_unmapped') return;
4238
+ // Toggle off if validation is currently shown
4239
+ if (Object.keys(validationErrors).length > 0) {
4240
+ validationErrors = {};
4241
+ serverCellErrorKeys.forEach(function(k) { delete cellValidationErrors[k]; });
4242
+ serverCellErrorKeys = [];
4243
+ hasValidation = false;
4244
+ updateValidateBtn();
4245
+ render();
4246
+ return;
4247
+ }
3071
4248
  validateBtn.disabled = true;
3072
4249
  validateBtn.title = 'Validating\u2026';
3073
4250
  var url = '/browser/api/csv/databases/' + encodeURIComponent(dbKey) +
@@ -3103,7 +4280,8 @@
3103
4280
  validateBtn.disabled = false;
3104
4281
  validateBtn.title = 'Validation failed';
3105
4282
  });
3106
- });
4283
+ }
4284
+ validateBtn.addEventListener('click', runValidateAll);
3107
4285
 
3108
4286
  tableWrap.addEventListener('click', function(e) {
3109
4287
  var valIcon = e.target.closest('[data-val-row]');
@@ -3118,13 +4296,14 @@
3118
4296
  createPopup({ title: 'Row ' + (rowIdx + 1) + ' \u2014 validation errors', html: errHtml, x: pos.x, y: pos.y });
3119
4297
  });
3120
4298
 
3121
- rowNumToggle.addEventListener('click', function() {
4299
+ function toggleRowNumbers() {
3122
4300
  showRowNumbers = !showRowNumbers;
3123
4301
  rowNumToggle.classList.toggle('active', showRowNumbers);
3124
4302
  renderTableHTML();
3125
- });
4303
+ }
4304
+ rowNumToggle.addEventListener('click', toggleRowNumbers);
3126
4305
 
3127
- saveDefaultBtn.addEventListener('click', function() {
4306
+ function saveCurrentAsDefaultView() {
3128
4307
  var tableKey = data.table || data.table_key;
3129
4308
  var viewKey = data.view || data.view_key || 'all';
3130
4309
  var extra = {};
@@ -3139,11 +4318,10 @@
3139
4318
  saveDefaultViewKey(dbKey, tableKey, viewKey, extra);
3140
4319
  saveDefaultBtn.classList.add('active');
3141
4320
  saveDefaultBtn.title = 'Saved as default view';
3142
- });
4321
+ }
4322
+ saveDefaultBtn.addEventListener('click', saveCurrentAsDefaultView);
3143
4323
 
3144
- saveDefaultBtn.addEventListener('contextmenu', function(e) {
3145
- e.preventDefault();
3146
- e.stopPropagation();
4324
+ function showSavedDefaultViewPopup() {
3147
4325
  var tableKey = data.table || data.table_key;
3148
4326
  var lsKey = 'markdownr:defaultView:' + dbKey + ':' + tableKey;
3149
4327
  var saved = getSavedDefaultView(dbKey, tableKey);
@@ -3166,11 +4344,14 @@
3166
4344
  } else {
3167
4345
  body.innerHTML = '<div style="padding:0.75rem;font-size:0.85rem;color:#888;">No saved default view for this table.</div>';
3168
4346
  }
3169
- });
3170
-
3171
- rowNumToggle.addEventListener('contextmenu', function(e) {
4347
+ }
4348
+ saveDefaultBtn.addEventListener('contextmenu', function(e) {
3172
4349
  e.preventDefault();
3173
4350
  e.stopPropagation();
4351
+ showSavedDefaultViewPopup();
4352
+ });
4353
+
4354
+ function showAllLocalStoragePopup() {
3174
4355
  var all = {};
3175
4356
  for (var i = 0; i < localStorage.length; i++) {
3176
4357
  var k = localStorage.key(i);
@@ -3182,6 +4363,11 @@
3182
4363
  var pos = nextPosition();
3183
4364
  var p = createPopup({ title: 'All markdownr localStorage', x: pos.x, y: pos.y, wide: true });
3184
4365
  p.querySelector('.br-popup-body').innerHTML = '<pre style="margin:0;padding:0.75rem;font-size:0.8rem;white-space:pre-wrap;font-family:monospace;">' + escHtml(yaml) + '</pre>';
4366
+ }
4367
+ rowNumToggle.addEventListener('contextmenu', function(e) {
4368
+ e.preventDefault();
4369
+ e.stopPropagation();
4370
+ showAllLocalStoragePopup();
3185
4371
  });
3186
4372
 
3187
4373
  colFilterToggle.addEventListener('click', function() {
@@ -3314,6 +4500,29 @@
3314
4500
  }
3315
4501
 
3316
4502
  tableWrap.addEventListener('contextmenu', function(e) {
4503
+ // Shift+right-click bypasses our menu and shows the browser's native menu (for DevTools Inspect).
4504
+ if (e.shiftKey) { dismissContextMenu(); return; }
4505
+ if (window.__brInspectNext) {
4506
+ window.__brInspectNext = false;
4507
+ return; // let the native browser context menu show (for DevTools "Inspect")
4508
+ }
4509
+
4510
+ var cornerTh = e.target.closest('th[data-table-corner]');
4511
+ if (cornerTh) {
4512
+ e.preventDefault();
4513
+ dismissContextMenu();
4514
+ showTableCornerContextMenu(e.clientX, e.clientY);
4515
+ return;
4516
+ }
4517
+
4518
+ var th = e.target.closest('th[data-sort-col]');
4519
+ if (th) {
4520
+ e.preventDefault();
4521
+ dismissContextMenu();
4522
+ showHeaderContextMenu(parseInt(th.getAttribute('data-sort-col')), e.clientX, e.clientY);
4523
+ return;
4524
+ }
4525
+
3317
4526
  var tr = e.target.closest('tr[data-row-idx]');
3318
4527
  if (!tr) return;
3319
4528
  var td = e.target.closest('td[data-col]');
@@ -3445,6 +4654,29 @@
3445
4654
  });
3446
4655
  menu.appendChild(matchRowsBtn);
3447
4656
 
4657
+ // Hide matched rows
4658
+ var hideBtn = document.createElement('button');
4659
+ hideBtn.className = 'br-context-menu-item default';
4660
+ hideBtn.textContent = 'Hide matched rows';
4661
+ hideBtn.addEventListener('click', function() {
4662
+ dismissContextMenu();
4663
+ if (hiddenValues.indexOf(cellVal) === -1) hiddenValues.push(cellVal);
4664
+ render();
4665
+ });
4666
+ menu.appendChild(hideBtn);
4667
+
4668
+ if (hiddenValues.length > 0) {
4669
+ var unhideBtn = document.createElement('button');
4670
+ unhideBtn.className = 'br-context-menu-item default';
4671
+ unhideBtn.textContent = 'Unhide all';
4672
+ unhideBtn.addEventListener('click', function() {
4673
+ dismissContextMenu();
4674
+ hiddenValues = [];
4675
+ render();
4676
+ });
4677
+ menu.appendChild(unhideBtn);
4678
+ }
4679
+
3448
4680
  // Clear matches
3449
4681
  var sepClear = document.createElement('div');
3450
4682
  sepClear.className = 'br-context-menu-sep';
@@ -3483,6 +4715,20 @@
3483
4715
  deleteRow(dbKey, data.table, rowIdx);
3484
4716
  });
3485
4717
  menu.appendChild(deleteBtn);
4718
+
4719
+ var sepInspect = document.createElement('div');
4720
+ sepInspect.className = 'br-context-menu-sep';
4721
+ menu.appendChild(sepInspect);
4722
+ var inspectBtn = document.createElement('button');
4723
+ inspectBtn.className = 'br-context-menu-item default';
4724
+ inspectBtn.textContent = 'Inspect (or ⇧ + right-click)';
4725
+ inspectBtn.title = 'Click, then right-click the same spot again to open browser Inspect. Or use Shift + right-click directly.';
4726
+ inspectBtn.addEventListener('click', function() {
4727
+ dismissContextMenu();
4728
+ window.__brInspectNext = true;
4729
+ });
4730
+ menu.appendChild(inspectBtn);
4731
+
3486
4732
  document.body.appendChild(menu);
3487
4733
 
3488
4734
  // Async: fetch add-on actions for this row and append them to the menu.
@@ -3779,6 +5025,7 @@
3779
5025
  showRowNumbers: showRowNumbers
3780
5026
  };
3781
5027
  if (Object.keys(colWidths).length > 0) state.colWidths = Object.assign({}, colWidths);
5028
+ if (hiddenValues.length > 0) state.hiddenValues = hiddenValues.slice();
3782
5029
  return state;
3783
5030
  };
3784
5031
 
@@ -3794,7 +5041,7 @@
3794
5041
  csvDatabases = databases;
3795
5042
  hasCsvDatabases = Array.isArray(csvDatabases) && csvDatabases.length > 0;
3796
5043
  // Refresh any open database popups in the active tab
3797
- var tabId = activeTabId();
5044
+ var tabId = activeTabId;
3798
5045
  var popups = document.querySelectorAll('.br-popup[data-tab-id="' + tabId + '"]');
3799
5046
  popups.forEach(function(popup) {
3800
5047
  if (!popup._getLayoutState) return;
@@ -3818,8 +5065,9 @@
3818
5065
  }
3819
5066
 
3820
5067
  function showDatabaseList(databases, opts) {
5068
+ var dbListTitle = document.title + ' Databases';
3821
5069
  if (databases.length === 0) {
3822
- createPopup({ title: 'Databases', html: '<div class="br-empty">No databases configured</div>', pinned: true, noClose: true, x: 8, y: TAB_BAR_HEIGHT + 8 });
5070
+ createPopup({ title: dbListTitle, html: '<div class="br-empty">No databases configured</div>', pinned: true, noClose: true, x: 8, y: TAB_BAR_HEIGHT + 8 });
3823
5071
  return;
3824
5072
  }
3825
5073
  // Skip straight to single database only when there are no virtual databases
@@ -3836,7 +5084,7 @@
3836
5084
  var dupIcon = '<span class="br-dup-btn" data-dup-db="' + escHtml(db.key) + '">+</span>';
3837
5085
  html += '<button class="' + cls + '" data-db="' + db.key + '">' + dupIcon + label + '</button>';
3838
5086
  });
3839
- var popup = createPopup({ id: 'db-list', title: 'Databases', pinned: true, noClose: true, x: 8, y: TAB_BAR_HEIGHT + 8 });
5087
+ var popup = createPopup({ id: 'db-list', title: dbListTitle, pinned: true, noClose: true, x: 8, y: TAB_BAR_HEIGHT + 8 });
3840
5088
  popup._getLayoutState = function() { return { type: 'db-list' }; };
3841
5089
  popup.querySelector('.br-popup-body').innerHTML = html;
3842
5090
  popup.querySelector('.br-popup-body').style.padding = '0.4rem 0.5rem';
@@ -4352,10 +5600,10 @@
4352
5600
  { label: 'Restore layout', action: function() { showRestoreLayoutList(menu); } },
4353
5601
  { label: 'Manage layouts', action: function() { menu.remove(); showManageLayouts(); } },
4354
5602
  'sep',
4355
- { label: 'Open databases', action: function() { showDatabaseList(csvDatabases, {}); } },
4356
- { label: 'Open root directory', action: function() { loadContent('', { title: rootTitle }); } },
5603
+ { label: 'Open databases', action: function() { menu.remove(); showDatabaseList(csvDatabases, {}); } },
5604
+ { label: 'Open root directory', action: function() { menu.remove(); loadContent('', { title: rootTitle }); } },
4357
5605
  'sep',
4358
- { label: 'Reload databases', action: function() { reloadDatabases(); } }
5606
+ { label: 'Reload databases', action: function() { menu.remove(); reloadDatabases(); } }
4359
5607
  ];
4360
5608
 
4361
5609
  items.forEach(function(item) {
@@ -4368,7 +5616,8 @@
4368
5616
  var btn = document.createElement('button');
4369
5617
  btn.className = 'br-context-menu-item default';
4370
5618
  btn.textContent = item.label;
4371
- btn.addEventListener('click', function() {
5619
+ btn.addEventListener('click', function(ev) {
5620
+ ev.stopPropagation();
4372
5621
  item.action();
4373
5622
  });
4374
5623
  menu.appendChild(btn);
@@ -4385,6 +5634,140 @@
4385
5634
  }, 0);
4386
5635
  });
4387
5636
 
5637
+ // ── Print topmost popup (Cmd+P / Ctrl+P) ──
5638
+
5639
+ var IS_MAC_PLATFORM = /Mac|iPhone|iPad|iPod/i.test(navigator.platform || '');
5640
+
5641
+ function findTopmostPopup() {
5642
+ var selector = '.br-popup:not(.view-menu)';
5643
+ if (activeTabId) selector += '[data-tab-id="' + activeTabId + '"]';
5644
+ var popups = Array.from(document.querySelectorAll(selector)).filter(function(el) {
5645
+ return el.style.display !== 'none';
5646
+ });
5647
+ if (popups.length === 0) return null;
5648
+ return popups.reduce(function(top, p) {
5649
+ var z = parseInt(p.style.zIndex) || 0;
5650
+ var topZ = parseInt(top.style.zIndex) || 0;
5651
+ return z > topZ ? p : top;
5652
+ });
5653
+ }
5654
+
5655
+ function printPopupGeneric(popup) {
5656
+ var titleEl = popup.querySelector('.br-popup-title');
5657
+ var titleText = titleEl ? titleEl.textContent : 'Popup';
5658
+ var bodyEl = popup.querySelector('.br-popup-body');
5659
+ if (!bodyEl) return;
5660
+ var w = window.open('', '_blank');
5661
+ if (!w) { alert('Pop-up blocked. Allow pop-ups to print.'); return; }
5662
+ var stylesHtml = '';
5663
+ document.querySelectorAll('style').forEach(function(s) {
5664
+ stylesHtml += '<style>' + s.textContent + '</style>';
5665
+ });
5666
+ var doc = '<!doctype html><html><head><meta charset="utf-8"><title>' + escHtml(titleText) + '</title>' +
5667
+ stylesHtml +
5668
+ '<style>' +
5669
+ 'body{font-family:-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;color:#000;margin:1rem;background:#fff;}' +
5670
+ 'h1.br-print-title{font-size:1.1rem;margin:0 0 0.6rem 0;}' +
5671
+ '.br-print-body{}' +
5672
+ '@media print{body{margin:0.4in;}}' +
5673
+ '</style></head><body>' +
5674
+ '<h1 class="br-print-title">' + escHtml(titleText) + '</h1>' +
5675
+ '<div class="br-print-body">' + bodyEl.innerHTML + '</div>' +
5676
+ '<script>window.onload=function(){setTimeout(function(){window.print();},150);};<\/script>' +
5677
+ '</body></html>';
5678
+ w.document.open();
5679
+ w.document.write(doc);
5680
+ w.document.close();
5681
+ }
5682
+
5683
+ function printPopup(popup) {
5684
+ if (!popup) return false;
5685
+ if (typeof popup._print === 'function') {
5686
+ popup._print();
5687
+ } else {
5688
+ printPopupGeneric(popup);
5689
+ }
5690
+ return true;
5691
+ }
5692
+
5693
+ document.addEventListener('keydown', function(e) {
5694
+ var key = e.key && e.key.toLowerCase();
5695
+ if (key !== 'p') return;
5696
+ if (e.altKey || e.shiftKey) return;
5697
+ var modifier = IS_MAC_PLATFORM ? e.metaKey : e.ctrlKey;
5698
+ if (!modifier) return;
5699
+ var popup = findTopmostPopup();
5700
+ if (!popup) return;
5701
+ e.preventDefault();
5702
+ e.stopPropagation();
5703
+ printPopup(popup);
5704
+ });
5705
+
5706
+ // ── Refresh topmost popup (Cmd+R / Ctrl+R) ──
5707
+
5708
+ function refreshPopup(popup) {
5709
+ if (!popup || !popup._getLayoutState) return false;
5710
+ var s = popup._getLayoutState();
5711
+
5712
+ if (s.type === 'csv-table') {
5713
+ var hasDirty = popup.querySelector('.br-row-dirty, .br-row-dirty-bypass, .br-row-invalid');
5714
+ if (hasDirty && !confirm('Unsaved edits in this table will be discarded. Refresh anyway?')) return false;
5715
+ }
5716
+
5717
+ if (s.type === 'db-list' || s.type === 'db') {
5718
+ reloadDatabases();
5719
+ return true;
5720
+ }
5721
+
5722
+ var rect = popup.getBoundingClientRect();
5723
+ var geo = {
5724
+ x: parseInt(popup.style.left) || Math.round(rect.left),
5725
+ y: parseInt(popup.style.top) || Math.round(rect.top),
5726
+ width: Math.round(rect.width),
5727
+ height: Math.round(rect.height)
5728
+ };
5729
+ popup.remove();
5730
+
5731
+ switch (s.type) {
5732
+ case 'csv-table':
5733
+ var tableLabel = s.tableLabel || s.tableKey;
5734
+ openCsvTablePopup(s.dbKey, s.tableKey, tableLabel, s.viewKey, s.tableColor, {
5735
+ searchQuery: s.searchQuery,
5736
+ colFilterValues: s.colFilterValues,
5737
+ colFiltersVisible: s.colFiltersVisible,
5738
+ showRowNumbers: s.showRowNumbers,
5739
+ hiddenValues: s.hiddenValues,
5740
+ colWidths: s.colWidths,
5741
+ x: geo.x, y: geo.y, width: geo.width, height: geo.height
5742
+ });
5743
+ return true;
5744
+ case 'db-search':
5745
+ var searchDb = csvDatabases.find(function(d) { return d.key === s.dbKey; });
5746
+ if (searchDb) showDatabaseSearch(searchDb, s.query, { x: geo.x, y: geo.y, width: geo.width, height: geo.height });
5747
+ return true;
5748
+ case 'schema':
5749
+ showTableSchema(s.dbKey, s.tableKey, s.tableColor, geo.x, geo.y, geo.width, geo.height);
5750
+ return true;
5751
+ case 'content':
5752
+ loadContent(s.path, { pos: { x: geo.x, y: geo.y }, width: geo.width, height: geo.height });
5753
+ return true;
5754
+ }
5755
+ return false;
5756
+ }
5757
+
5758
+ document.addEventListener('keydown', function(e) {
5759
+ var key = e.key && e.key.toLowerCase();
5760
+ if (key !== 'r') return;
5761
+ if (e.altKey || e.shiftKey) return;
5762
+ var modifier = IS_MAC_PLATFORM ? e.metaKey : e.ctrlKey;
5763
+ if (!modifier) return;
5764
+ var popup = findTopmostPopup();
5765
+ if (!popup) return;
5766
+ e.preventDefault();
5767
+ e.stopPropagation();
5768
+ refreshPopup(popup);
5769
+ });
5770
+
4388
5771
  // ── Start ──
4389
5772
 
4390
5773
  function init() {
@@ -4395,6 +5778,9 @@
4395
5778
  activeTabId = firstTab.id;
4396
5779
  renderTabBar();
4397
5780
  initNewTab(firstTab);
5781
+ if (initialPath) {
5782
+ loadContent(initialPath, { title: initialPath.split('/').pop() });
5783
+ }
4398
5784
  }
4399
5785
 
4400
5786
  if (document.readyState === 'loading') {