pygpt-net 2.6.46__py3-none-any.whl → 2.6.48__py3-none-any.whl
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.
- pygpt_net/CHANGELOG.txt +9 -0
- pygpt_net/__init__.py +1 -1
- pygpt_net/app.py +5 -0
- pygpt_net/app_core.py +39 -39
- pygpt_net/controller/__init__.py +72 -64
- pygpt_net/controller/audio/audio.py +2 -0
- pygpt_net/controller/chat/text.py +2 -1
- pygpt_net/controller/ctx/common.py +0 -7
- pygpt_net/controller/ctx/ctx.py +172 -6
- pygpt_net/controller/ctx/extra.py +3 -3
- pygpt_net/controller/notepad/notepad.py +0 -2
- pygpt_net/controller/settings/editor.py +3 -1
- pygpt_net/controller/theme/common.py +8 -2
- pygpt_net/controller/theme/theme.py +5 -5
- pygpt_net/controller/ui/tabs.py +9 -2
- pygpt_net/core/ctx/ctx.py +79 -26
- pygpt_net/core/render/web/renderer.py +2 -0
- pygpt_net/core/tabs/tab.py +2 -2
- pygpt_net/core/tabs/tabs.py +57 -10
- pygpt_net/data/config/config.json +2 -2
- pygpt_net/data/config/models.json +2 -2
- pygpt_net/data/css/web-blocks.css +256 -270
- pygpt_net/data/css/web-chatgpt.css +276 -301
- pygpt_net/data/css/web-chatgpt_wide.css +286 -294
- pygpt_net/data/js/app.js +1218 -1186
- pygpt_net/js_rc.py +14192 -14641
- pygpt_net/provider/core/config/patch.py +9 -0
- pygpt_net/provider/core/ctx/db_sqlite/storage.py +19 -5
- pygpt_net/ui/__init__.py +9 -14
- pygpt_net/ui/layout/chat/chat.py +2 -2
- pygpt_net/ui/layout/ctx/ctx_list.py +71 -1
- pygpt_net/ui/widget/lists/base.py +32 -1
- pygpt_net/ui/widget/lists/context.py +45 -2
- pygpt_net/ui/widget/tabs/body.py +11 -3
- pygpt_net/ui/widget/textarea/notepad.py +0 -4
- {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/METADATA +11 -2
- {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/RECORD +40 -40
- {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.46.dist-info → pygpt_net-2.6.48.dist-info}/entry_points.txt +0 -0
pygpt_net/data/js/app.js
CHANGED
|
@@ -461,147 +461,151 @@
|
|
|
461
461
|
// ==========================================================================
|
|
462
462
|
|
|
463
463
|
class Config {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
464
|
+
constructor() {
|
|
465
|
+
// Process identifier (passed from host).
|
|
466
|
+
this.PID = Utils.g('PID', 0);
|
|
467
|
+
|
|
468
|
+
// UI: scroll behavior and busy/zoom signalling thresholds (milliseconds / pixels).
|
|
469
|
+
this.UI = {
|
|
470
|
+
AUTO_FOLLOW_REENABLE_PX: Utils.g('AUTO_FOLLOW_REENABLE_PX', 8),
|
|
471
|
+
SCROLL_NEAR_MARGIN_PX: Utils.g('SCROLL_NEAR_MARGIN_PX', 450),
|
|
472
|
+
INTERACTION_BUSY_MS: Utils.g('UI_INTERACTION_BUSY_MS', 140),
|
|
473
|
+
ZOOM_BUSY_MS: Utils.g('UI_ZOOM_BUSY_MS', 300)
|
|
474
|
+
};
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
476
|
+
// FAB (floating action button) visibility and debounce.
|
|
477
|
+
this.FAB = {
|
|
478
|
+
SHOW_DOWN_THRESHOLD_PX: Utils.g('SHOW_DOWN_THRESHOLD_PX', 0),
|
|
479
|
+
TOGGLE_DEBOUNCE_MS: Utils.g('FAB_TOGGLE_DEBOUNCE_MS', 100)
|
|
480
|
+
};
|
|
481
481
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
482
|
+
// Highlighting controls and per-frame budget.
|
|
483
|
+
this.HL = {
|
|
484
|
+
PER_FRAME: Utils.g('HL_PER_FRAME', 2),
|
|
485
|
+
DISABLE_ALL: Utils.g('DISABLE_SYNTAX_HIGHLIGHT', false)
|
|
486
|
+
};
|
|
487
487
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
488
|
+
// Intersection-like margins (we do our own scan, but these guide budgets).
|
|
489
|
+
this.OBSERVER = {
|
|
490
|
+
CODE_ROOT_MARGIN: Utils.g('CODE_ROOT_MARGIN', '1000px 0px 1000px 0px'),
|
|
491
|
+
BOX_ROOT_MARGIN: Utils.g('BOX_ROOT_MARGIN', '1500px 0px 1500px 0px'),
|
|
492
|
+
CODE_THRESHOLD: [0, 0.001], BOX_THRESHOLD: 0
|
|
493
|
+
};
|
|
494
494
|
|
|
495
|
-
|
|
496
|
-
|
|
495
|
+
// Viewport scan preload distance (in pixels).
|
|
496
|
+
this.SCAN = { PRELOAD_PX: Utils.g('SCAN_PRELOAD_PX', 1000) };
|
|
497
497
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
498
|
+
// Code scroll behavior (auto-follow re-enable margin and near-bottom margin).
|
|
499
|
+
this.CODE_SCROLL = {
|
|
500
|
+
AUTO_FOLLOW_REENABLE_PX: Utils.g('CODE_AUTO_FOLLOW_REENABLE_PX', 8),
|
|
501
|
+
NEAR_MARGIN_PX: Utils.g('CODE_SCROLL_NEAR_MARGIN_PX', 48)
|
|
502
|
+
};
|
|
503
503
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
};
|
|
504
|
+
// Stream (snapshot) budgets and queue limits.
|
|
505
|
+
this.STREAM = {
|
|
506
|
+
MAX_PER_FRAME: Utils.g('STREAM_MAX_PER_FRAME', 8),
|
|
507
|
+
EMERGENCY_COALESCE_LEN: Utils.g('STREAM_EMERGENCY_COALESCE_LEN', 300),
|
|
508
|
+
COALESCE_MODE: Utils.g('STREAM_COALESCE_MODE', 'fixed'),
|
|
509
|
+
SNAPSHOT_MAX_STEP: Utils.g('STREAM_SNAPSHOT_MAX_STEP', 8000),
|
|
510
|
+
QUEUE_MAX_ITEMS: Utils.g('STREAM_QUEUE_MAX_ITEMS', 1200),
|
|
511
|
+
PRESERVE_CODES_MAX: Utils.g('STREAM_PRESERVE_CODES_MAX', 120)
|
|
512
|
+
};
|
|
514
513
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
514
|
+
// Math (KaTeX) idle batching and per-batch hint.
|
|
515
|
+
this.MATH = {
|
|
516
|
+
IDLE_TIMEOUT_MS: Utils.g('MATH_IDLE_TIMEOUT_MS', 800),
|
|
517
|
+
BATCH_HINT: Utils.g('MATH_BATCH_HINT', 24)
|
|
518
|
+
};
|
|
520
519
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
520
|
+
// Icon URLs (provided by host app).
|
|
521
|
+
this.ICONS = {
|
|
522
|
+
EXPAND: Utils.g('ICON_EXPAND', ''), COLLAPSE: Utils.g('ICON_COLLAPSE', ''),
|
|
523
|
+
CODE_MENU: Utils.g('ICON_CODE_MENU', ''), CODE_COPY: Utils.g('ICON_CODE_COPY', ''),
|
|
524
|
+
CODE_RUN: Utils.g('ICON_CODE_RUN', ''), CODE_PREVIEW: Utils.g('ICON_CODE_PREVIEW', '')
|
|
525
|
+
};
|
|
527
526
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
527
|
+
// Localized UI strings.
|
|
528
|
+
this.LOCALE = {
|
|
529
|
+
PREVIEW: Utils.g('LOCALE_PREVIEW', 'Preview'),
|
|
530
|
+
RUN: Utils.g('LOCALE_RUN', 'Run'),
|
|
531
|
+
COLLAPSE: Utils.g('LOCALE_COLLAPSE', 'Collapse'),
|
|
532
|
+
EXPAND: Utils.g('LOCALE_EXPAND', 'Expand'),
|
|
533
|
+
COPY: Utils.g('LOCALE_COPY', 'Copy'),
|
|
534
|
+
COPIED: Utils.g('LOCALE_COPIED', 'Copied')
|
|
535
|
+
};
|
|
537
536
|
|
|
538
|
-
|
|
539
|
-
|
|
537
|
+
// Code block styling theme (hljs theme key or custom).
|
|
538
|
+
this.CODE_STYLE = Utils.g('CODE_SYNTAX_STYLE', 'default');
|
|
540
539
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
540
|
+
// Adaptive snapshot profile for plain text.
|
|
541
|
+
this.PROFILE_TEXT = {
|
|
542
|
+
base: Utils.g('PROFILE_TEXT_BASE', 4),
|
|
543
|
+
growth: Utils.g('PROFILE_TEXT_GROWTH', 1.28),
|
|
544
|
+
minInterval: Utils.g('PROFILE_TEXT_MIN_INTERVAL', 4),
|
|
545
|
+
softLatency: Utils.g('PROFILE_TEXT_SOFT_LATENCY', 60),
|
|
546
|
+
adaptiveStep: Utils.g('PROFILE_TEXT_ADAPTIVE_STEP', false)
|
|
547
|
+
};
|
|
549
548
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
};
|
|
549
|
+
// Adaptive snapshot profile for code (line-aware).
|
|
550
|
+
this.PROFILE_CODE = {
|
|
551
|
+
base: 2048,
|
|
552
|
+
growth: 2.6,
|
|
553
|
+
minInterval: 500,
|
|
554
|
+
softLatency: 1200,
|
|
555
|
+
minLinesForHL: Utils.g('PROFILE_CODE_HL_N_LINE', 25),
|
|
556
|
+
minCharsForHL: Utils.g('PROFILE_CODE_HL_N_CHARS', 5000),
|
|
557
|
+
promoteMinInterval: 300, promoteMaxLatency: 800, promoteMinLines: Utils.g('PROFILE_CODE_HL_N_LINE', 25),
|
|
558
|
+
adaptiveStep: Utils.g('PROFILE_CODE_ADAPTIVE_STEP', true),
|
|
559
|
+
stopAfterLines: Utils.g('PROFILE_CODE_STOP_HL_AFTER_LINES', 300),
|
|
560
|
+
streamPlainAfterLines: 0,
|
|
561
|
+
streamPlainAfterChars: 0,
|
|
562
|
+
maxFrozenChars: 32000,
|
|
563
|
+
finalHighlightMaxLines: Utils.g('PROFILE_CODE_FINAL_HL_MAX_LINES', 1500),
|
|
564
|
+
finalHighlightMaxChars: Utils.g('PROFILE_CODE_FINAL_HL_MAX_CHARS', 350000)
|
|
565
|
+
};
|
|
568
566
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
567
|
+
// Debounce for heavy resets (ms).
|
|
568
|
+
this.RESET = {
|
|
569
|
+
HEAVY_DEBOUNCE_MS: Utils.g('RESET_HEAVY_DEBOUNCE_MS', 24)
|
|
570
|
+
};
|
|
573
571
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
572
|
+
// Logging caps (used by Logger).
|
|
573
|
+
this.LOG = {
|
|
574
|
+
MAX_QUEUE: Utils.g('LOG_MAX_QUEUE', 400),
|
|
575
|
+
MAX_BYTES: Utils.g('LOG_MAX_BYTES', 256 * 1024),
|
|
576
|
+
BATCH_MAX: Utils.g('LOG_BATCH_MAX', 64),
|
|
577
|
+
RATE_LIMIT_PER_SEC: Utils.g('LOG_RATE_LIMIT_PER_SEC', 0)
|
|
578
|
+
};
|
|
581
579
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
580
|
+
// Async tuning for background work.
|
|
581
|
+
this.ASYNC = {
|
|
582
|
+
SLICE_MS: Utils.g('ASYNC_SLICE_MS', 12),
|
|
583
|
+
MIN_YIELD_MS: Utils.g('ASYNC_MIN_YIELD_MS', 0),
|
|
584
|
+
MD_NODES_PER_SLICE: Utils.g('ASYNC_MD_NODES_PER_SLICE', 12)
|
|
585
|
+
};
|
|
588
586
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
587
|
+
// RAF pump tuning (budget per frame).
|
|
588
|
+
this.RAF = {
|
|
589
|
+
FLUSH_BUDGET_MS: Utils.g('RAF_FLUSH_BUDGET_MS', 7),
|
|
590
|
+
MAX_TASKS_PER_FLUSH: Utils.g('RAF_MAX_TASKS_PER_FLUSH', 120)
|
|
591
|
+
};
|
|
594
592
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
593
|
+
// Markdown tuning – allow/disallow indented code blocks.
|
|
594
|
+
this.MD = {
|
|
595
|
+
ALLOW_INDENTED_CODE: Utils.g('MD_ALLOW_INDENTED_CODE', false)
|
|
596
|
+
};
|
|
599
597
|
|
|
600
|
-
|
|
601
|
-
|
|
598
|
+
// Custom markup rules for simple tags in text.
|
|
599
|
+
// NOTE: added nl2br + allowBr for think rules; rest unchanged.
|
|
600
|
+
this.CUSTOM_MARKUP_RULES = Utils.g('CUSTOM_MARKUP_RULES', [
|
|
602
601
|
{ name: 'cmd', open: '[!cmd]', close: '[/!cmd]', tag: 'div', className: 'cmd', innerMode: 'text' },
|
|
603
|
-
|
|
604
|
-
|
|
602
|
+
|
|
603
|
+
// Think (Markdown-style) – convert newlines to <br>, allow real <br> tokens; safe-escape everything else.
|
|
604
|
+
{ name: 'think_md', open: '[!think]', close: '[/!think]', tag: 'think', className: '', innerMode: 'text', nl2br: true, allowBr: true },
|
|
605
|
+
|
|
606
|
+
// Think (HTML-style, streaming-friendly)
|
|
607
|
+
{ name: 'think_html', open: '<think>', close: '</think>', tag: 'think', className: '', innerMode: 'text', stream: true, nl2br: true, allowBr: true },
|
|
608
|
+
|
|
605
609
|
{ name: 'tool', open: '<tool>', close: '</tool>', tag: 'div', className: 'cmd', innerMode: 'text', stream: true },
|
|
606
610
|
|
|
607
611
|
// Streams+final: convert [!exec]... into fenced python code BEFORE markdown-it
|
|
@@ -612,8 +616,8 @@
|
|
|
612
616
|
{ name: 'exec_html', open: '<execute>', close: '</execute>', innerMode: 'text', stream: true,
|
|
613
617
|
openReplace: '```python\n', closeReplace: '\n```', phase: 'source' }
|
|
614
618
|
]);
|
|
619
|
+
}
|
|
615
620
|
}
|
|
616
|
-
}
|
|
617
621
|
|
|
618
622
|
// ==========================================================================
|
|
619
623
|
// 1) DOM references
|
|
@@ -1220,8 +1224,6 @@
|
|
|
1220
1224
|
_d(line, ctx) { try { this.logger.debug('CM', line, ctx); } catch (_) {} }
|
|
1221
1225
|
|
|
1222
1226
|
// Decode HTML entities once (safe)
|
|
1223
|
-
// This addresses cases when linkify/full markdown path leaves literal """ etc. in text nodes.
|
|
1224
|
-
// We decode only for rules that explicitly opt-in (see compile()) to avoid changing semantics globally.
|
|
1225
1227
|
decodeEntitiesOnce(s) {
|
|
1226
1228
|
if (!s || s.indexOf('&') === -1) return String(s || '');
|
|
1227
1229
|
const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
|
|
@@ -1229,14 +1231,22 @@
|
|
|
1229
1231
|
return ta.value;
|
|
1230
1232
|
}
|
|
1231
1233
|
|
|
1232
|
-
//
|
|
1234
|
+
// Escape helpers
|
|
1233
1235
|
_escHtml(s) {
|
|
1234
1236
|
try { return Utils.escapeHtml(s); } catch (_) {
|
|
1235
1237
|
return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
1236
1238
|
}
|
|
1237
1239
|
}
|
|
1240
|
+
_escapeHtmlAllowBr(text, { convertNewlines = true } = {}) {
|
|
1241
|
+
const PLACEHOLDER = '\u0001__BR__\u0001';
|
|
1242
|
+
let s = String(text || '');
|
|
1243
|
+
s = s.replace(/<br\s*\/?>/gi, PLACEHOLDER);
|
|
1244
|
+
s = this._escHtml(s);
|
|
1245
|
+
if (convertNewlines) s = s.replace(/\r\n|\r|\n/g, '<br>');
|
|
1246
|
+
s = s.replace(new RegExp(PLACEHOLDER, 'g'), '<br>');
|
|
1247
|
+
return s;
|
|
1248
|
+
}
|
|
1238
1249
|
|
|
1239
|
-
// quick check if any rule's open token is present in text (used to skip expensive work early)
|
|
1240
1250
|
hasAnyOpenToken(text, rules) {
|
|
1241
1251
|
if (!text || !rules || !rules.length) return false;
|
|
1242
1252
|
for (let i = 0; i < rules.length; i++) {
|
|
@@ -1247,19 +1257,25 @@
|
|
|
1247
1257
|
return false;
|
|
1248
1258
|
}
|
|
1249
1259
|
|
|
1250
|
-
// Build inner HTML from text according to rule's mode (markdown-inline | text) with optional entity decode.
|
|
1251
1260
|
_materializeInnerHTML(rule, text, MD) {
|
|
1252
1261
|
let payload = String(text || '');
|
|
1262
|
+
const wantsBr = !!(rule && (rule.nl2br || rule.allowBr));
|
|
1263
|
+
|
|
1253
1264
|
if (rule && rule.decodeEntities && payload && payload.indexOf('&') !== -1) {
|
|
1254
1265
|
try { payload = this.decodeEntitiesOnce(payload); } catch (_) { /* keep original */ }
|
|
1255
1266
|
}
|
|
1267
|
+
|
|
1268
|
+
if (wantsBr) {
|
|
1269
|
+
try { return this._escapeHtmlAllowBr(payload, { convertNewlines: !!rule.nl2br }); }
|
|
1270
|
+
catch (_) { return this._escHtml(payload); }
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1256
1273
|
if (rule && rule.innerMode === 'markdown-inline' && MD && typeof MD.renderInline === 'function') {
|
|
1257
1274
|
try { return MD.renderInline(payload); } catch (_) { return this._escHtml(payload); }
|
|
1258
1275
|
}
|
|
1259
1276
|
return this._escHtml(payload);
|
|
1260
1277
|
}
|
|
1261
1278
|
|
|
1262
|
-
// Make a DOM Fragment from HTML string (robust across contexts).
|
|
1263
1279
|
_fragmentFromHTML(html, ctxNode) {
|
|
1264
1280
|
let frag = null;
|
|
1265
1281
|
try {
|
|
@@ -1276,18 +1292,14 @@
|
|
|
1276
1292
|
return frag;
|
|
1277
1293
|
}
|
|
1278
1294
|
}
|
|
1279
|
-
|
|
1280
|
-
// Replace one element in DOM with HTML string (keeps siblings intact).
|
|
1281
1295
|
_replaceElementWithHTML(el, html) {
|
|
1282
1296
|
if (!el || !el.parentNode) return;
|
|
1283
1297
|
const parent = el.parentNode;
|
|
1284
1298
|
const frag = this._fragmentFromHTML(html, el);
|
|
1285
1299
|
try {
|
|
1286
|
-
// Insert new nodes before the old element, then remove the old element (widely supported).
|
|
1287
1300
|
parent.insertBefore(frag, el);
|
|
1288
1301
|
parent.removeChild(el);
|
|
1289
1302
|
} catch (_) {
|
|
1290
|
-
// Conservative fallback: wrap in a span if direct fragment insertion failed for some reason.
|
|
1291
1303
|
const tmp = document.createElement('span');
|
|
1292
1304
|
tmp.innerHTML = String(html || '');
|
|
1293
1305
|
while (tmp.firstChild) parent.insertBefore(tmp.firstChild, el);
|
|
@@ -1295,7 +1307,6 @@
|
|
|
1295
1307
|
}
|
|
1296
1308
|
}
|
|
1297
1309
|
|
|
1298
|
-
// Compile rules once; also precompile strict and whitespace-tolerant "full match" regexes.
|
|
1299
1310
|
compile(rules) {
|
|
1300
1311
|
const src = Array.isArray(rules) ? rules : (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
|
|
1301
1312
|
const compiled = [];
|
|
@@ -1312,18 +1323,12 @@
|
|
|
1312
1323
|
const openReplace = String((r.openReplace != null ? r.openReplace : (r.openReplace || '')) || '');
|
|
1313
1324
|
const closeReplace = String((r.closeReplace != null ? r.closeReplace : (r.closeReplace || '')) || '');
|
|
1314
1325
|
|
|
1315
|
-
// Back-compat: decode entities default true for cmd-like
|
|
1316
1326
|
const decodeEntities = (typeof r.decodeEntities === 'boolean')
|
|
1317
1327
|
? r.decodeEntities
|
|
1318
1328
|
: ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
|
|
1319
1329
|
|
|
1320
|
-
// Optional application phase (where replacement should happen)
|
|
1321
|
-
// - 'source' => before markdown-it
|
|
1322
|
-
// - 'html' => after markdown-it (DOM fragment)
|
|
1323
|
-
// - 'both'
|
|
1324
1330
|
let phaseRaw = (typeof r.phase === 'string') ? r.phase.toLowerCase() : '';
|
|
1325
1331
|
if (phaseRaw !== 'source' && phaseRaw !== 'html' && phaseRaw !== 'both') phaseRaw = '';
|
|
1326
|
-
// Heuristic: if replacement contains fenced code backticks, default to 'source'
|
|
1327
1332
|
const looksLikeFence = (openReplace.indexOf('```') !== -1) || (closeReplace.indexOf('```') !== -1);
|
|
1328
1333
|
const phase = phaseRaw || (looksLikeFence ? 'source' : 'html');
|
|
1329
1334
|
|
|
@@ -1331,6 +1336,9 @@
|
|
|
1331
1336
|
const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
|
|
1332
1337
|
const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
|
|
1333
1338
|
|
|
1339
|
+
const nl2br = !!r.nl2br;
|
|
1340
|
+
const allowBr = !!r.allowBr;
|
|
1341
|
+
|
|
1334
1342
|
const item = {
|
|
1335
1343
|
name: r.name || tag,
|
|
1336
1344
|
tag, className, innerMode,
|
|
@@ -1339,12 +1347,13 @@
|
|
|
1339
1347
|
re, reFull, reFullTrim,
|
|
1340
1348
|
stream,
|
|
1341
1349
|
openReplace, closeReplace,
|
|
1342
|
-
phase,
|
|
1343
|
-
isSourceFence: looksLikeFence
|
|
1350
|
+
phase,
|
|
1351
|
+
isSourceFence: looksLikeFence,
|
|
1352
|
+
nl2br, allowBr
|
|
1344
1353
|
};
|
|
1345
1354
|
compiled.push(item);
|
|
1346
1355
|
if (stream) hasStream = true;
|
|
1347
|
-
this._d('COMPILE_RULE', { name: item.name, phase: item.phase, stream: item.stream });
|
|
1356
|
+
this._d('COMPILE_RULE', { name: item.name, phase: item.phase, stream: item.stream, nl2br: item.nl2br, allowBr: item.allowBr });
|
|
1348
1357
|
}
|
|
1349
1358
|
|
|
1350
1359
|
if (compiled.length === 0) {
|
|
@@ -1357,7 +1366,8 @@
|
|
|
1357
1366
|
reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$'),
|
|
1358
1367
|
stream: false,
|
|
1359
1368
|
openReplace: '', closeReplace: '',
|
|
1360
|
-
phase: 'html', isSourceFence: false
|
|
1369
|
+
phase: 'html', isSourceFence: false,
|
|
1370
|
+
nl2br: false, allowBr: false
|
|
1361
1371
|
};
|
|
1362
1372
|
compiled.push(item);
|
|
1363
1373
|
this._d('COMPILE_RULE_FALLBACK', { name: item.name });
|
|
@@ -1367,16 +1377,12 @@
|
|
|
1367
1377
|
return compiled;
|
|
1368
1378
|
}
|
|
1369
1379
|
|
|
1370
|
-
// pre-markdown source transformer – applies only rules for 'source'/'both' with replacements
|
|
1371
|
-
// - Skips replacements inside fenced code blocks (``` / ~~~).
|
|
1372
|
-
// - Applies only when the rule opener is at top-level of the line (no list markers/blockquote).
|
|
1373
1380
|
transformSource(src, opts) {
|
|
1374
1381
|
let s = String(src || '');
|
|
1375
1382
|
this.ensureCompiled();
|
|
1376
1383
|
const rules = this.__compiled;
|
|
1377
1384
|
if (!rules || !rules.length) return s;
|
|
1378
1385
|
|
|
1379
|
-
// Pick only source-phase rules with explicit replacements
|
|
1380
1386
|
const candidates = [];
|
|
1381
1387
|
for (let i = 0; i < rules.length; i++) {
|
|
1382
1388
|
const r = rules[i];
|
|
@@ -1385,14 +1391,11 @@
|
|
|
1385
1391
|
}
|
|
1386
1392
|
if (!candidates.length) return s;
|
|
1387
1393
|
|
|
1388
|
-
// Compute fenced-code ranges once to exclude them from replacements (production-safe).
|
|
1389
1394
|
const fences = this._findFenceRanges(s);
|
|
1390
1395
|
if (!fences.length) {
|
|
1391
|
-
// No code fences in source; apply top-level guarded replacements globally.
|
|
1392
1396
|
return this._applySourceReplacementsInChunk(s, s, 0, candidates);
|
|
1393
1397
|
}
|
|
1394
1398
|
|
|
1395
|
-
// Apply replacements only in segments outside fenced code.
|
|
1396
1399
|
let out = '';
|
|
1397
1400
|
let last = 0;
|
|
1398
1401
|
for (let k = 0; k < fences.length; k++) {
|
|
@@ -1401,7 +1404,7 @@
|
|
|
1401
1404
|
const chunk = s.slice(last, a);
|
|
1402
1405
|
out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
|
|
1403
1406
|
}
|
|
1404
|
-
out += s.slice(a, b);
|
|
1407
|
+
out += s.slice(a, b);
|
|
1405
1408
|
last = b;
|
|
1406
1409
|
}
|
|
1407
1410
|
if (last < s.length) {
|
|
@@ -1411,7 +1414,6 @@
|
|
|
1411
1414
|
return out;
|
|
1412
1415
|
}
|
|
1413
1416
|
|
|
1414
|
-
// expose custom fence specs (to StreamEngine)
|
|
1415
1417
|
getSourceFenceSpecs() {
|
|
1416
1418
|
this.ensureCompiled();
|
|
1417
1419
|
const rules = this.__compiled || [];
|
|
@@ -1419,14 +1421,12 @@
|
|
|
1419
1421
|
for (let i = 0; i < rules.length; i++) {
|
|
1420
1422
|
const r = rules[i];
|
|
1421
1423
|
if (!r || !r.isSourceFence) continue;
|
|
1422
|
-
// Only expose when they actually look like fences in source phase
|
|
1423
1424
|
if (r.phase !== 'source' && r.phase !== 'both') continue;
|
|
1424
1425
|
out.push({ open: r.open, close: r.close });
|
|
1425
1426
|
}
|
|
1426
1427
|
return out;
|
|
1427
1428
|
}
|
|
1428
1429
|
|
|
1429
|
-
// Ensure rules are compiled and cached.
|
|
1430
1430
|
ensureCompiled() {
|
|
1431
1431
|
if (!this.__compiled) {
|
|
1432
1432
|
this.__compiled = this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
|
|
@@ -1435,14 +1435,11 @@
|
|
|
1435
1435
|
return this.__compiled;
|
|
1436
1436
|
}
|
|
1437
1437
|
|
|
1438
|
-
// Replace rules set (also exposes rules on window).
|
|
1439
1438
|
setRules(rules) {
|
|
1440
1439
|
this.__compiled = this.compile(rules);
|
|
1441
1440
|
window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() : (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
|
|
1442
1441
|
this._d('SET_RULES', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
|
|
1443
1442
|
}
|
|
1444
|
-
|
|
1445
|
-
// Return current rules as array.
|
|
1446
1443
|
getRules() {
|
|
1447
1444
|
const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice()
|
|
1448
1445
|
: (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
|
|
@@ -1450,25 +1447,34 @@
|
|
|
1450
1447
|
return list;
|
|
1451
1448
|
}
|
|
1452
1449
|
|
|
1453
|
-
// Fast switch: do we have any rules that want streaming parsing?
|
|
1454
1450
|
hasStreamRules() {
|
|
1455
1451
|
this.ensureCompiled();
|
|
1456
1452
|
return !!this.__hasStreamRules;
|
|
1457
1453
|
}
|
|
1458
1454
|
|
|
1459
|
-
|
|
1455
|
+
hasStreamOpenerAtStart(text) {
|
|
1456
|
+
if (!text) return false;
|
|
1457
|
+
this.ensureCompiled();
|
|
1458
|
+
const rules = (this.__compiled || []).filter(r => !!r.stream);
|
|
1459
|
+
if (!rules.length) return false;
|
|
1460
|
+
const t = String(text).trimStart();
|
|
1461
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1462
|
+
const r = rules[i];
|
|
1463
|
+
if (!r || !r.open) continue;
|
|
1464
|
+
if (t.startsWith(r.open)) return true;
|
|
1465
|
+
}
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1460
1469
|
isInsideForbiddenContext(node) {
|
|
1461
1470
|
const p = node.parentElement; if (!p) return true;
|
|
1462
|
-
// IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
|
|
1463
1471
|
return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
|
|
1464
1472
|
}
|
|
1465
1473
|
isInsideForbiddenElement(el) {
|
|
1466
1474
|
if (!el) return true;
|
|
1467
|
-
// IMPORTANT: exclude code/math/hljs/wrappers AND list contexts (ul/ol/li/dl/dt/dd)
|
|
1468
1475
|
return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
|
|
1469
1476
|
}
|
|
1470
1477
|
|
|
1471
|
-
// Global finder on a single text blob (original per-text-node logic).
|
|
1472
1478
|
findNextMatch(text, from, rules) {
|
|
1473
1479
|
let best = null;
|
|
1474
1480
|
for (const rule of rules) {
|
|
@@ -1481,8 +1487,6 @@
|
|
|
1481
1487
|
}
|
|
1482
1488
|
return best;
|
|
1483
1489
|
}
|
|
1484
|
-
|
|
1485
|
-
// Strict full match of a pure text node (legacy path).
|
|
1486
1490
|
findFullMatch(text, rules) {
|
|
1487
1491
|
for (const rule of rules) {
|
|
1488
1492
|
if (rule.reFull) {
|
|
@@ -1500,13 +1504,19 @@
|
|
|
1500
1504
|
return null;
|
|
1501
1505
|
}
|
|
1502
1506
|
|
|
1503
|
-
|
|
1504
|
-
setInnerByMode(el, mode, text, MD, decodeEntities = false) {
|
|
1507
|
+
setInnerByMode(el, mode, text, MD, decodeEntities = false, rule = null) {
|
|
1505
1508
|
let payload = String(text || '');
|
|
1509
|
+
const wantsBr = !!(rule && (rule.nl2br || rule.allowBr));
|
|
1510
|
+
|
|
1506
1511
|
if (decodeEntities && payload && payload.indexOf('&') !== -1) {
|
|
1507
1512
|
try { payload = this.decodeEntitiesOnce(payload); } catch (_) {}
|
|
1508
1513
|
}
|
|
1509
1514
|
|
|
1515
|
+
if (wantsBr) {
|
|
1516
|
+
el.innerHTML = this._escapeHtmlAllowBr(payload, { convertNewlines: !!(rule && rule.nl2br) });
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1510
1520
|
if (mode === 'markdown-inline' && typeof window.markdownit !== 'undefined') {
|
|
1511
1521
|
try {
|
|
1512
1522
|
if (MD && typeof MD.renderInline === 'function') { el.innerHTML = MD.renderInline(payload); return; }
|
|
@@ -1517,7 +1527,6 @@
|
|
|
1517
1527
|
el.textContent = payload;
|
|
1518
1528
|
}
|
|
1519
1529
|
|
|
1520
|
-
// Try to replace an entire <p> that is a full custom markup match.
|
|
1521
1530
|
_tryReplaceFullParagraph(el, rules, MD) {
|
|
1522
1531
|
if (!el || el.tagName !== 'P') return false;
|
|
1523
1532
|
if (this.isInsideForbiddenElement(el)) {
|
|
@@ -1533,8 +1542,7 @@
|
|
|
1533
1542
|
if (!m) continue;
|
|
1534
1543
|
|
|
1535
1544
|
const innerText = m[1] || '';
|
|
1536
|
-
|
|
1537
|
-
if (rule.phase !== 'html' && rule.phase !== 'both') continue; // element materialization is html-phase only
|
|
1545
|
+
if (rule.phase !== 'html' && rule.phase !== 'both') continue;
|
|
1538
1546
|
|
|
1539
1547
|
if (rule.openReplace || rule.closeReplace) {
|
|
1540
1548
|
const innerHTML = this._materializeInnerHTML(rule, innerText, MD);
|
|
@@ -1548,7 +1556,7 @@
|
|
|
1548
1556
|
const out = document.createElement(outTag === 'p' ? 'p' : outTag);
|
|
1549
1557
|
if (rule.className) out.className = rule.className;
|
|
1550
1558
|
out.setAttribute('data-cm', rule.name);
|
|
1551
|
-
this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities);
|
|
1559
|
+
this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities, rule);
|
|
1552
1560
|
|
|
1553
1561
|
try { el.replaceWith(out); } catch (_) {
|
|
1554
1562
|
const par = el.parentNode; if (par) par.replaceChild(out, el);
|
|
@@ -1560,7 +1568,6 @@
|
|
|
1560
1568
|
return false;
|
|
1561
1569
|
}
|
|
1562
1570
|
|
|
1563
|
-
// Core implementation shared by static and streaming passes.
|
|
1564
1571
|
applyRules(root, MD, rules) {
|
|
1565
1572
|
if (!root || !rules || !rules.length) return;
|
|
1566
1573
|
|
|
@@ -1577,7 +1584,6 @@
|
|
|
1577
1584
|
if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
|
|
1578
1585
|
const tc = p && (p.textContent || '');
|
|
1579
1586
|
if (!tc || !this.hasAnyOpenToken(tc, rules)) continue;
|
|
1580
|
-
// Skip paragraphs inside forbidden contexts (includes lists now)
|
|
1581
1587
|
if (this.isInsideForbiddenElement(p)) continue;
|
|
1582
1588
|
this._tryReplaceFullParagraph(p, rules, MD);
|
|
1583
1589
|
}
|
|
@@ -1586,7 +1592,7 @@
|
|
|
1586
1592
|
this._d('P_TOLERANT_SCAN_ERR', String(e));
|
|
1587
1593
|
}
|
|
1588
1594
|
|
|
1589
|
-
// Phase 2:
|
|
1595
|
+
// Phase 2: per-text-node inline replacements
|
|
1590
1596
|
const self = this;
|
|
1591
1597
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
1592
1598
|
acceptNode: (node) => {
|
|
@@ -1600,14 +1606,12 @@
|
|
|
1600
1606
|
let node;
|
|
1601
1607
|
while ((node = walker.nextNode())) {
|
|
1602
1608
|
const text = node.nodeValue;
|
|
1603
|
-
if (!text || !this.hasAnyOpenToken(text, rules)) continue;
|
|
1609
|
+
if (!text || !this.hasAnyOpenToken(text, rules)) continue;
|
|
1604
1610
|
const parent = node.parentElement;
|
|
1605
1611
|
|
|
1606
|
-
// Entire text node equals one full match and parent is <p>.
|
|
1607
1612
|
if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
|
|
1608
1613
|
const fm = this.findFullMatch(text, rules);
|
|
1609
1614
|
if (fm) {
|
|
1610
|
-
// If explicit HTML replacements are provided, swap <p> for exact HTML (only for html/both phase).
|
|
1611
1615
|
if ((fm.rule.phase === 'html' || fm.rule.phase === 'both') && (fm.rule.openReplace || fm.rule.closeReplace)) {
|
|
1612
1616
|
const innerHTML = this._materializeInnerHTML(fm.rule, fm.inner, MD);
|
|
1613
1617
|
const html = String(fm.rule.openReplace || '') + innerHTML + String(fm.rule.closeReplace || '');
|
|
@@ -1615,13 +1619,11 @@
|
|
|
1615
1619
|
this._d('WALKER_FULL_REPLACE_HTML', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1616
1620
|
continue;
|
|
1617
1621
|
}
|
|
1618
|
-
|
|
1619
|
-
// Backward-compatible: only replace as <p> when rule tag is 'p'
|
|
1620
1622
|
if (fm.rule.tag === 'p') {
|
|
1621
1623
|
const out = document.createElement('p');
|
|
1622
1624
|
if (fm.rule.className) out.className = fm.rule.className;
|
|
1623
1625
|
out.setAttribute('data-cm', fm.rule.name);
|
|
1624
|
-
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities);
|
|
1626
|
+
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities, fm.rule);
|
|
1625
1627
|
try { parent.replaceWith(out); } catch (_) {
|
|
1626
1628
|
const par = parent.parentNode; if (par) par.replaceChild(out, parent);
|
|
1627
1629
|
}
|
|
@@ -1631,7 +1633,6 @@
|
|
|
1631
1633
|
}
|
|
1632
1634
|
}
|
|
1633
1635
|
|
|
1634
|
-
// General inline replacement inside the text node (span-like or HTML-replace).
|
|
1635
1636
|
let i = 0;
|
|
1636
1637
|
let didReplace = false;
|
|
1637
1638
|
const frag = document.createDocumentFragment();
|
|
@@ -1644,7 +1645,6 @@
|
|
|
1644
1645
|
frag.appendChild(document.createTextNode(text.slice(i, m.start)));
|
|
1645
1646
|
}
|
|
1646
1647
|
|
|
1647
|
-
// If HTML replacements are provided, build exact HTML around processed inner – only for html/both phase.
|
|
1648
1648
|
if ((m.rule.openReplace || m.rule.closeReplace) && (m.rule.phase === 'html' || m.rule.phase === 'both')) {
|
|
1649
1649
|
const innerHTML = this._materializeInnerHTML(m.rule, m.inner, MD);
|
|
1650
1650
|
const html = String(m.rule.openReplace || '') + innerHTML + String(m.rule.closeReplace || '');
|
|
@@ -1654,20 +1654,17 @@
|
|
|
1654
1654
|
i = m.end; didReplace = true; continue;
|
|
1655
1655
|
}
|
|
1656
1656
|
|
|
1657
|
-
// If rule is not html-phase, do NOT inject open/close replacements here (source-only rules are handled pre-md).
|
|
1658
1657
|
if (m.rule.openReplace || m.rule.closeReplace) {
|
|
1659
|
-
// Source-only replacement met in DOM pass – keep original text verbatim for this match.
|
|
1660
1658
|
frag.appendChild(document.createTextNode(text.slice(m.start, m.end)));
|
|
1661
1659
|
this._d('WALKER_INLINE_SKIP_SOURCE_PHASE_HTML', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1662
1660
|
i = m.end; didReplace = true; continue;
|
|
1663
1661
|
}
|
|
1664
1662
|
|
|
1665
|
-
// Element-based inline replacement (original behavior).
|
|
1666
1663
|
const tag = (m.rule.tag === 'p') ? 'span' : m.rule.tag;
|
|
1667
1664
|
const el = document.createElement(tag);
|
|
1668
1665
|
if (m.rule.className) el.className = m.rule.className;
|
|
1669
1666
|
el.setAttribute('data-cm', m.rule.name);
|
|
1670
|
-
this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities);
|
|
1667
|
+
this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities, m.rule);
|
|
1671
1668
|
frag.appendChild(el);
|
|
1672
1669
|
this._d('WALKER_INLINE_MATCH', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1673
1670
|
|
|
@@ -1676,10 +1673,7 @@
|
|
|
1676
1673
|
}
|
|
1677
1674
|
|
|
1678
1675
|
if (!didReplace) continue;
|
|
1679
|
-
|
|
1680
|
-
if (i < text.length) {
|
|
1681
|
-
frag.appendChild(document.createTextNode(text.slice(i)));
|
|
1682
|
-
}
|
|
1676
|
+
if (i < text.length) frag.appendChild(document.createTextNode(text.slice(i)));
|
|
1683
1677
|
|
|
1684
1678
|
const parentNode = node.parentNode;
|
|
1685
1679
|
if (parentNode) {
|
|
@@ -1689,42 +1683,35 @@
|
|
|
1689
1683
|
}
|
|
1690
1684
|
}
|
|
1691
1685
|
|
|
1692
|
-
// Public API:
|
|
1686
|
+
// Public API: full pass (static)
|
|
1693
1687
|
apply(root, MD) {
|
|
1694
1688
|
this.ensureCompiled();
|
|
1695
1689
|
this.applyRules(root, MD, this.__compiled);
|
|
1696
1690
|
}
|
|
1697
1691
|
|
|
1698
|
-
// Public API:
|
|
1692
|
+
// Public API: stream pass (stream-enabled rules only)
|
|
1699
1693
|
applyStream(root, MD) {
|
|
1700
1694
|
this.ensureCompiled();
|
|
1701
1695
|
if (!this.__hasStreamRules) return;
|
|
1702
1696
|
const rules = this.__compiled.filter(r => !!r.stream);
|
|
1703
1697
|
if (!rules.length) return;
|
|
1704
1698
|
|
|
1705
|
-
//
|
|
1699
|
+
// 1) Normal html-phase replacements where both tokens are within one node/paragraph
|
|
1706
1700
|
this.applyRules(root, MD, rules);
|
|
1707
1701
|
|
|
1708
|
-
//
|
|
1702
|
+
// 2) If only opener is present, start pending block and wrap the remainder of the snapshot
|
|
1709
1703
|
try { this.applyStreamPartialOpeners(root, MD, rules); } catch (_) {}
|
|
1710
|
-
}
|
|
1711
1704
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1705
|
+
// 3) If a pending block exists and we can see a closer now – finalize immediately
|
|
1706
|
+
try { this.applyStreamFinalizeClosers(root, rules); } catch (_) {}
|
|
1707
|
+
}
|
|
1715
1708
|
|
|
1716
|
-
// Streaming
|
|
1717
|
-
// in the SAME text node. We wrap the tail after the opener into the target element and mark
|
|
1718
|
-
// it as pending (data-cm-pending="1"). On subsequent snapshots (when a close arrives),
|
|
1719
|
-
// the standard full-pass (applyRules) will supersede this.
|
|
1709
|
+
// Streaming: begin pending wrapper when opener is unmatched in the same text node.
|
|
1720
1710
|
applyStreamPartialOpeners(root, MD, rulesAll) {
|
|
1721
1711
|
if (!root) return;
|
|
1722
1712
|
|
|
1723
|
-
// Consider only DOM-phase element rules (html/both) without explicit HTML replacements.
|
|
1724
|
-
// Source-phase rules (e.g. exec fences) are intentionally excluded here to avoid changing
|
|
1725
|
-
// code streaming semantics (handled by transformSource/StreamEngine).
|
|
1726
1713
|
const rules = (rulesAll || []).filter(r =>
|
|
1727
|
-
(r && (r.phase === 'html' || r.phase === 'both') && !(r.openReplace || r.closeReplace))
|
|
1714
|
+
(r && (r.phase === 'html' || r.phase === 'both') && !(r.openReplace || r.closeReplace) && r.open && r.close)
|
|
1728
1715
|
);
|
|
1729
1716
|
if (!rules.length) return;
|
|
1730
1717
|
|
|
@@ -1745,18 +1732,14 @@
|
|
|
1745
1732
|
const text = node.nodeValue || '';
|
|
1746
1733
|
if (!text) continue;
|
|
1747
1734
|
|
|
1748
|
-
// Find the last unmatched opener among eligible rules in this node.
|
|
1749
1735
|
let best = null; // { rule, start }
|
|
1750
1736
|
for (let i = 0; i < rules.length; i++) {
|
|
1751
1737
|
const r = rules[i];
|
|
1752
1738
|
if (!r || !r.open || !r.close) continue;
|
|
1753
1739
|
|
|
1754
|
-
// Find last occurrence for better UX on multiple openers; keeps earlier content intact.
|
|
1755
1740
|
const idx = text.lastIndexOf(r.open);
|
|
1756
1741
|
if (idx === -1) continue;
|
|
1757
1742
|
|
|
1758
|
-
// If a closing token for this rule exists after this opener within the same node,
|
|
1759
|
-
// let the standard pass handle it (we only want truly unmatched opens).
|
|
1760
1743
|
const after = text.indexOf(r.close, idx + r.open.length);
|
|
1761
1744
|
if (after !== -1) continue;
|
|
1762
1745
|
|
|
@@ -1765,37 +1748,144 @@
|
|
|
1765
1748
|
|
|
1766
1749
|
if (!best) continue;
|
|
1767
1750
|
|
|
1768
|
-
// Build fragment: keep prefix as text, wrap the tail into the rule element and mark as pending.
|
|
1769
1751
|
const r = best.rule;
|
|
1770
1752
|
const start = best.start;
|
|
1753
|
+
const openLen = r.open.length;
|
|
1754
|
+
const prefixText = text.slice(0, start);
|
|
1755
|
+
const fromOffset = start + openLen;
|
|
1771
1756
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1757
|
+
try {
|
|
1758
|
+
const range = document.createRange();
|
|
1759
|
+
range.setStart(node, Math.min(fromOffset, node.nodeValue.length));
|
|
1760
|
+
|
|
1761
|
+
// end to the end of scope
|
|
1762
|
+
let endNode = root;
|
|
1763
|
+
try {
|
|
1764
|
+
endNode = (root.nodeType === 11 || root.nodeType === 1) ? root : node.parentNode;
|
|
1765
|
+
while (endNode && endNode.lastChild) endNode = endNode.lastChild;
|
|
1766
|
+
} catch (_) {}
|
|
1767
|
+
if (endNode && endNode.nodeType === 3) range.setEnd(endNode, endNode.nodeValue.length);
|
|
1768
|
+
else if (endNode) range.setEndAfter(endNode);
|
|
1769
|
+
else range.setEndAfter(node);
|
|
1775
1770
|
|
|
1776
|
-
|
|
1771
|
+
const remainder = range.extractContents();
|
|
1772
|
+
|
|
1773
|
+
const outTag = (r.tag && typeof r.tag === 'string') ? r.tag.toLowerCase() : 'span';
|
|
1774
|
+
const hostTag = (outTag === 'p') ? 'span' : outTag;
|
|
1775
|
+
const el = document.createElement(hostTag);
|
|
1776
|
+
if (r.className) el.className = r.className;
|
|
1777
|
+
el.setAttribute('data-cm', r.name);
|
|
1778
|
+
el.setAttribute('data-cm-pending', '1');
|
|
1779
|
+
|
|
1780
|
+
el.appendChild(remainder);
|
|
1781
|
+
range.insertNode(el);
|
|
1782
|
+
range.detach();
|
|
1783
|
+
|
|
1784
|
+
try { node.nodeValue = prefixText; } catch (_) {}
|
|
1785
|
+
this._d('STREAM_PENDING_OPEN_WRAP_WHOLE', { rule: r.name, open: r.open, preview: this.logger.pv(text, 160) });
|
|
1786
|
+
|
|
1787
|
+
return; // wrap-to-end done; exit to avoid scanning mutated subtree
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
// Fallback: wrap only tail of current node
|
|
1790
|
+
try {
|
|
1791
|
+
const tail = text.slice(start + r.open.length);
|
|
1792
|
+
const frag = document.createDocumentFragment();
|
|
1793
|
+
|
|
1794
|
+
if (prefixText) frag.appendChild(document.createTextNode(prefixText));
|
|
1795
|
+
|
|
1796
|
+
const el = document.createElement((r.tag === 'p') ? 'span' : r.tag);
|
|
1797
|
+
if (r.className) el.className = r.className;
|
|
1798
|
+
el.setAttribute('data-cm', r.name);
|
|
1799
|
+
el.setAttribute('data-cm-pending', '1');
|
|
1800
|
+
|
|
1801
|
+
this.setInnerByMode(el, r.innerMode, tail, MD, !!r.decodeEntities, r);
|
|
1802
|
+
frag.appendChild(el);
|
|
1803
|
+
|
|
1804
|
+
node.parentNode.replaceChild(frag, node);
|
|
1805
|
+
this._d('STREAM_PENDING_OPEN_WRAP_FALLBACK', { rule: r.name, err: String(err) });
|
|
1806
|
+
return;
|
|
1807
|
+
} catch (_) { /* worst-case: keep node as-is */ }
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// NEW: streaming close finalization — if a pending wrapper contains a closing token,
|
|
1813
|
+
// remove the token and move the tail outside the wrapper, then clear pending flag.
|
|
1814
|
+
applyStreamFinalizeClosers(root, rulesAll) {
|
|
1815
|
+
if (!root) return;
|
|
1777
1816
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1817
|
+
const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
|
|
1818
|
+
const pending = scope.querySelectorAll('[data-cm][data-cm-pending="1"]');
|
|
1819
|
+
if (!pending || !pending.length) return;
|
|
1820
|
+
|
|
1821
|
+
const rulesByName = new Map();
|
|
1822
|
+
(rulesAll || []).forEach(r => { if (r && r.name) rulesByName.set(r.name, r); });
|
|
1823
|
+
|
|
1824
|
+
for (let i = 0; i < pending.length; i++) {
|
|
1825
|
+
const el = pending[i];
|
|
1826
|
+
const name = el.getAttribute('data-cm') || '';
|
|
1827
|
+
const rule = rulesByName.get(name);
|
|
1828
|
+
if (!rule || !rule.close) continue;
|
|
1829
|
+
|
|
1830
|
+
// Look for the first text node inside 'el' that contains the closing token
|
|
1831
|
+
const self = this;
|
|
1832
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
|
1833
|
+
acceptNode(node) {
|
|
1834
|
+
const val = node && node.nodeValue ? node.nodeValue : '';
|
|
1835
|
+
if (!val || val.indexOf(rule.close) === -1) return NodeFilter.FILTER_SKIP;
|
|
1836
|
+
if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
|
|
1837
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1783
1840
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1841
|
+
let nodeWithClose = null;
|
|
1842
|
+
let idxInNode = -1;
|
|
1843
|
+
let tn;
|
|
1844
|
+
while ((tn = walker.nextNode())) {
|
|
1845
|
+
const idx = tn.nodeValue.indexOf(rule.close);
|
|
1846
|
+
if (idx !== -1) { nodeWithClose = tn; idxInNode = idx; break; }
|
|
1847
|
+
}
|
|
1848
|
+
if (!nodeWithClose) continue; // still pending; no closer yet
|
|
1787
1849
|
|
|
1788
1850
|
try {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1851
|
+
const tokenLen = rule.close.length;
|
|
1852
|
+
|
|
1853
|
+
// Range for content AFTER the closing token -> will be moved outside
|
|
1854
|
+
const afterRange = document.createRange();
|
|
1855
|
+
afterRange.setStart(nodeWithClose, idxInNode + tokenLen);
|
|
1856
|
+
// End at end of wrapper
|
|
1857
|
+
let endNode = el;
|
|
1858
|
+
while (endNode && endNode.lastChild) endNode = endNode.lastChild;
|
|
1859
|
+
if (endNode && endNode.nodeType === 3) afterRange.setEnd(endNode, endNode.nodeValue.length);
|
|
1860
|
+
else afterRange.setEndAfter(el.lastChild || el);
|
|
1861
|
+
|
|
1862
|
+
const tail = afterRange.extractContents();
|
|
1863
|
+
afterRange.detach();
|
|
1864
|
+
|
|
1865
|
+
// Delete the closing token itself
|
|
1866
|
+
const tok = document.createRange();
|
|
1867
|
+
tok.setStart(nodeWithClose, idxInNode);
|
|
1868
|
+
tok.setEnd(nodeWithClose, idxInNode + tokenLen);
|
|
1869
|
+
tok.deleteContents();
|
|
1870
|
+
tok.detach();
|
|
1871
|
+
|
|
1872
|
+
// Move tail right after the wrapper (preserve DOM order)
|
|
1873
|
+
if (el.parentNode && tail && tail.childNodes.length) {
|
|
1874
|
+
el.parentNode.insertBefore(tail, el.nextSibling);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Finalize the wrapper
|
|
1878
|
+
el.removeAttribute('data-cm-pending');
|
|
1879
|
+
this._d('STREAM_PENDING_CLOSE_FINALIZED', { rule: rule.name });
|
|
1880
|
+
|
|
1881
|
+
} catch (err) {
|
|
1882
|
+
this._d('STREAM_PENDING_CLOSE_ERR', { rule: rule.name, err: String(err) });
|
|
1883
|
+
// If anything goes wrong, keep the block pending (safer for next snapshot).
|
|
1793
1884
|
}
|
|
1794
1885
|
}
|
|
1795
1886
|
}
|
|
1796
1887
|
|
|
1797
|
-
//
|
|
1798
|
-
// Matches Markdown fences at line-start with up to 3 spaces/tabs indentation.
|
|
1888
|
+
// Source fence scan (unchanged)
|
|
1799
1889
|
_findFenceRanges(s) {
|
|
1800
1890
|
const ranges = [];
|
|
1801
1891
|
const n = s.length;
|
|
@@ -1807,7 +1897,6 @@
|
|
|
1807
1897
|
|
|
1808
1898
|
while (i < n) {
|
|
1809
1899
|
const lineStart = i;
|
|
1810
|
-
// Find line end and newline length
|
|
1811
1900
|
let j = lineStart;
|
|
1812
1901
|
while (j < n && s.charCodeAt(j) !== 10 && s.charCodeAt(j) !== 13) j++;
|
|
1813
1902
|
const lineEnd = j;
|
|
@@ -1817,30 +1906,24 @@
|
|
|
1817
1906
|
else nl = 1;
|
|
1818
1907
|
}
|
|
1819
1908
|
|
|
1820
|
-
// Compute indentation up to 3 "spaces" (tabs count as 1 here – safe heuristic)
|
|
1821
1909
|
let k = lineStart;
|
|
1822
1910
|
let indent = 0;
|
|
1823
1911
|
while (k < lineEnd) {
|
|
1824
1912
|
const c = s.charCodeAt(k);
|
|
1825
|
-
if (c === 32
|
|
1826
|
-
else if (c === 9
|
|
1913
|
+
if (c === 32) { indent++; if (indent > 3) break; k++; }
|
|
1914
|
+
else if (c === 9) { indent++; if (indent > 3) break; k++; }
|
|
1827
1915
|
else break;
|
|
1828
1916
|
}
|
|
1829
1917
|
|
|
1830
1918
|
if (!inFence) {
|
|
1831
1919
|
if (indent <= 3 && k < lineEnd) {
|
|
1832
1920
|
const ch = s.charCodeAt(k);
|
|
1833
|
-
if (ch === 0x60
|
|
1921
|
+
if (ch === 0x60 || ch === 0x7E) {
|
|
1834
1922
|
const mark = String.fromCharCode(ch);
|
|
1835
1923
|
let m = k;
|
|
1836
1924
|
while (m < lineEnd && s.charCodeAt(m) === ch) m++;
|
|
1837
1925
|
const run = m - k;
|
|
1838
|
-
if (run >= 3) {
|
|
1839
|
-
inFence = true;
|
|
1840
|
-
fenceMark = mark;
|
|
1841
|
-
fenceLen = run;
|
|
1842
|
-
startLineStart = lineStart;
|
|
1843
|
-
}
|
|
1926
|
+
if (run >= 3) { inFence = true; fenceMark = mark; fenceLen = run; startLineStart = lineStart; }
|
|
1844
1927
|
}
|
|
1845
1928
|
}
|
|
1846
1929
|
} else {
|
|
@@ -1849,14 +1932,13 @@
|
|
|
1849
1932
|
while (m < lineEnd && s.charCodeAt(m) === fenceMark.charCodeAt(0)) m++;
|
|
1850
1933
|
const run = m - k;
|
|
1851
1934
|
if (run >= fenceLen) {
|
|
1852
|
-
// Only whitespace is allowed after closing fence on the same line
|
|
1853
1935
|
let onlyWS = true;
|
|
1854
1936
|
for (let t = m; t < lineEnd; t++) {
|
|
1855
1937
|
const cc = s.charCodeAt(t);
|
|
1856
1938
|
if (cc !== 32 && cc !== 9) { onlyWS = false; break; }
|
|
1857
1939
|
}
|
|
1858
1940
|
if (onlyWS) {
|
|
1859
|
-
const endIdx = lineEnd + nl;
|
|
1941
|
+
const endIdx = lineEnd + nl;
|
|
1860
1942
|
ranges.push([startLineStart, endIdx]);
|
|
1861
1943
|
inFence = false; fenceMark = ''; fenceLen = 0; startLineStart = 0;
|
|
1862
1944
|
}
|
|
@@ -1865,26 +1947,19 @@
|
|
|
1865
1947
|
}
|
|
1866
1948
|
i = lineEnd + nl;
|
|
1867
1949
|
}
|
|
1868
|
-
|
|
1869
|
-
// If EOF while still in fence, mark until end of string.
|
|
1870
1950
|
if (inFence) ranges.push([startLineStart, n]);
|
|
1871
1951
|
return ranges;
|
|
1872
1952
|
}
|
|
1873
1953
|
|
|
1874
|
-
// Check if match starts at "top-level" of a line:
|
|
1875
|
-
// - up to 3 leading spaces/tabs allowed
|
|
1876
|
-
// - not a list item marker ("- ", "+ ", "* ", "1. ", "1) ") and not a blockquote ("> ")
|
|
1877
|
-
// - nothing else precedes the token on the same line
|
|
1878
1954
|
_isTopLevelLineInSource(s, absIdx) {
|
|
1879
1955
|
let ls = absIdx;
|
|
1880
1956
|
while (ls > 0) {
|
|
1881
1957
|
const ch = s.charCodeAt(ls - 1);
|
|
1882
|
-
if (ch === 10
|
|
1958
|
+
if (ch === 10 || ch === 13) break;
|
|
1883
1959
|
ls--;
|
|
1884
1960
|
}
|
|
1885
1961
|
const prefix = s.slice(ls, absIdx);
|
|
1886
1962
|
|
|
1887
|
-
// Strip up to 3 leading "spaces" (tabs treated as 1 – acceptable heuristic)
|
|
1888
1963
|
let i = 0, indent = 0;
|
|
1889
1964
|
while (i < prefix.length) {
|
|
1890
1965
|
const c = prefix.charCodeAt(i);
|
|
@@ -1895,18 +1970,14 @@
|
|
|
1895
1970
|
if (indent > 3) return false;
|
|
1896
1971
|
const rest = prefix.slice(i);
|
|
1897
1972
|
|
|
1898
|
-
// Reject lists/blockquote
|
|
1899
1973
|
if (/^>\s?/.test(rest)) return false;
|
|
1900
1974
|
if (/^[-+*]\s/.test(rest)) return false;
|
|
1901
1975
|
if (/^\d+[.)]\s/.test(rest)) return false;
|
|
1902
1976
|
|
|
1903
|
-
// If any other non-whitespace text precedes the token on this line – not top-level
|
|
1904
1977
|
if (rest.trim().length > 0) return false;
|
|
1905
|
-
|
|
1906
1978
|
return true;
|
|
1907
1979
|
}
|
|
1908
1980
|
|
|
1909
|
-
// Apply source-phase replacements to one outside-of-fence chunk with top-level guard.
|
|
1910
1981
|
_applySourceReplacementsInChunk(full, chunk, baseOffset, rules) {
|
|
1911
1982
|
let t = chunk;
|
|
1912
1983
|
for (let i = 0; i < rules.length; i++) {
|
|
@@ -1914,15 +1985,14 @@
|
|
|
1914
1985
|
if (!r || !(r.openReplace || r.closeReplace)) continue;
|
|
1915
1986
|
try {
|
|
1916
1987
|
r.re.lastIndex = 0;
|
|
1917
|
-
t = t.replace(r.re, (match, inner, offset
|
|
1988
|
+
t = t.replace(r.re, (match, inner, offset) => {
|
|
1918
1989
|
const abs = baseOffset + (offset | 0);
|
|
1919
|
-
// Only apply when opener is at top-level on that line (not in lists/blockquote)
|
|
1920
1990
|
if (!this._isTopLevelLineInSource(full, abs)) return match;
|
|
1921
1991
|
const open = r.openReplace || '';
|
|
1922
1992
|
const close = r.closeReplace || '';
|
|
1923
1993
|
return open + (inner || '') + close;
|
|
1924
1994
|
});
|
|
1925
|
-
} catch (_) {
|
|
1995
|
+
} catch (_) {}
|
|
1926
1996
|
}
|
|
1927
1997
|
return t;
|
|
1928
1998
|
}
|
|
@@ -3976,1072 +4046,1034 @@
|
|
|
3976
4046
|
// ==========================================================================
|
|
3977
4047
|
|
|
3978
4048
|
class StreamEngine {
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
// Streaming buffer (rope-like) – avoids O(n^2) string concatenation when many small chunks arrive.
|
|
3986
|
-
// streamBuf holds the already materialized prefix; _sbParts keeps recent tail parts; _sbLen tracks their length.
|
|
3987
|
-
this.streamBuf = ''; // materialized prefix (string used by render)
|
|
3988
|
-
this._sbParts = []; // pending string chunks (array) not yet joined
|
|
3989
|
-
this._sbLen = 0; // length of pending chunks
|
|
4049
|
+
constructor(cfg, dom, renderer, math, highlighter, codeScroll, scrollMgr, raf, asyncer, logger) {
|
|
4050
|
+
this.cfg = cfg; this.dom = dom; this.renderer = renderer; this.math = math;
|
|
4051
|
+
this.highlighter = highlighter; this.codeScroll = codeScroll; this.scrollMgr = scrollMgr; this.raf = raf;
|
|
4052
|
+
this.asyncer = asyncer;
|
|
4053
|
+
this.logger = logger || new Logger(cfg);
|
|
3990
4054
|
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
4055
|
+
// Streaming buffer (rope-like) – avoids O(n^2) string concatenation when many small chunks arrive.
|
|
4056
|
+
// streamBuf holds the already materialized prefix; _sbParts keeps recent tail parts; _sbLen tracks their length.
|
|
4057
|
+
this.streamBuf = ''; // materialized prefix (string used by render)
|
|
4058
|
+
this._sbParts = []; // pending string chunks (array) not yet joined
|
|
4059
|
+
this._sbLen = 0; // length of pending chunks
|
|
3995
4060
|
|
|
3996
|
-
|
|
3997
|
-
|
|
4061
|
+
this.fenceOpen = false; this.fenceMark = '`'; this.fenceLen = 3;
|
|
4062
|
+
this.fenceTail = ''; this.fenceBuf = '';
|
|
4063
|
+
this.lastSnapshotTs = 0; this.nextSnapshotStep = cfg.PROFILE_TEXT.base;
|
|
4064
|
+
this.snapshotScheduled = false; this.snapshotRAF = 0;
|
|
3998
4065
|
|
|
3999
|
-
|
|
4066
|
+
this.codeStream = { open: false, lines: 0, chars: 0 };
|
|
4067
|
+
this.activeCode = null;
|
|
4000
4068
|
|
|
4001
|
-
|
|
4069
|
+
this.suppressPostFinalizePass = false;
|
|
4002
4070
|
|
|
4003
|
-
|
|
4004
|
-
this._firstCodeOpenSnapDone = false;
|
|
4071
|
+
this._promoteScheduled = false;
|
|
4005
4072
|
|
|
4006
|
-
|
|
4007
|
-
|
|
4073
|
+
// Guard to ensure first fence-open is materialized immediately when stream starts with code.
|
|
4074
|
+
this._firstCodeOpenSnapDone = false;
|
|
4008
4075
|
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
this._lastInjectedEOL = false;
|
|
4076
|
+
// Streaming mode flag – controls reduced rendering (no linkify etc.) on hot path.
|
|
4077
|
+
this.isStreaming = false;
|
|
4012
4078
|
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
_d(tag, data) { this.logger.debug('STREAM', tag, data); }
|
|
4079
|
+
// Tracks whether renderSnapshot injected a one-off synthetic EOL for parsing an open fence
|
|
4080
|
+
// (used to strip it from the initial streaming tail to avoid "#\n foo" on first line).
|
|
4081
|
+
this._lastInjectedEOL = false;
|
|
4017
4082
|
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4083
|
+
this._customFenceSpecs = []; // [{ open, close }, ...]
|
|
4084
|
+
this._fenceCustom = null; // currently active custom fence spec or null
|
|
4085
|
+
}
|
|
4086
|
+
_d(tag, data) { this.logger.debug('STREAM', tag, data); }
|
|
4021
4087
|
|
|
4022
|
-
|
|
4088
|
+
setCustomFenceSpecs(specs) {
|
|
4089
|
+
this._customFenceSpecs = Array.isArray(specs) ? specs.slice() : [];
|
|
4090
|
+
}
|
|
4023
4091
|
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
// Single-part fast path avoids a temporary array join.
|
|
4039
|
-
this.streamBuf += (this._sbParts.length === 1 ? this._sbParts[0] : this._sbParts.join(''));
|
|
4040
|
-
this._sbParts.length = 0;
|
|
4041
|
-
this._sbLen = 0;
|
|
4042
|
-
}
|
|
4043
|
-
return this.streamBuf;
|
|
4044
|
-
}
|
|
4045
|
-
// Reset the rope to an empty state.
|
|
4046
|
-
_clearStreamBuffer() {
|
|
4047
|
-
this.streamBuf = '';
|
|
4092
|
+
// --- Rope buffer helpers (internal) ---
|
|
4093
|
+
_appendChunk(s) {
|
|
4094
|
+
if (!s) return;
|
|
4095
|
+
this._sbParts.push(s);
|
|
4096
|
+
this._sbLen += s.length;
|
|
4097
|
+
}
|
|
4098
|
+
getStreamLength() {
|
|
4099
|
+
return (this.streamBuf.length + this._sbLen);
|
|
4100
|
+
}
|
|
4101
|
+
getStreamText() {
|
|
4102
|
+
if (this._sbLen > 0) {
|
|
4103
|
+
// Join pending parts into the materialized prefix and clear the tail.
|
|
4104
|
+
// Single-part fast path avoids a temporary array join.
|
|
4105
|
+
this.streamBuf += (this._sbParts.length === 1 ? this._sbParts[0] : this._sbParts.join(''));
|
|
4048
4106
|
this._sbParts.length = 0;
|
|
4049
4107
|
this._sbLen = 0;
|
|
4050
4108
|
}
|
|
4109
|
+
return this.streamBuf;
|
|
4110
|
+
}
|
|
4111
|
+
_clearStreamBuffer() {
|
|
4112
|
+
this.streamBuf = '';
|
|
4113
|
+
this._sbParts.length = 0;
|
|
4114
|
+
this._sbLen = 0;
|
|
4115
|
+
}
|
|
4051
4116
|
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4117
|
+
// Reset all streaming state and counters.
|
|
4118
|
+
reset() {
|
|
4119
|
+
this._clearStreamBuffer();
|
|
4120
|
+
this.fenceOpen = false; this.fenceMark = '`'; this.fenceLen = 3;
|
|
4121
|
+
this.fenceTail = ''; this.fenceBuf = '';
|
|
4122
|
+
this.lastSnapshotTs = 0; this.nextSnapshotStep = this.profile().base;
|
|
4123
|
+
this.snapshotScheduled = false; this.snapshotRAF = 0;
|
|
4124
|
+
this.codeStream = { open: false, lines: 0, chars: 0 };
|
|
4125
|
+
this.activeCode = null; this.suppressPostFinalizePass = false;
|
|
4126
|
+
this._promoteScheduled = false;
|
|
4127
|
+
this._firstCodeOpenSnapDone = false;
|
|
4128
|
+
|
|
4129
|
+
// Clear any previous synthetic EOL marker.
|
|
4130
|
+
this._lastInjectedEOL = false;
|
|
4131
|
+
this._fenceCustom = null;
|
|
4132
|
+
|
|
4133
|
+
this._d('RESET', { });
|
|
4134
|
+
}
|
|
4135
|
+
defuseActiveToPlain() {
|
|
4136
|
+
if (!this.activeCode || !this.activeCode.codeEl || !this.activeCode.codeEl.isConnected) return;
|
|
4137
|
+
const codeEl = this.activeCode.codeEl;
|
|
4138
|
+
const fullText = (this.activeCode.frozenEl?.textContent || '') + (this.activeCode.tailEl?.textContent || '');
|
|
4139
|
+
try {
|
|
4140
|
+
codeEl.textContent = fullText;
|
|
4141
|
+
codeEl.removeAttribute('data-highlighted');
|
|
4142
|
+
codeEl.classList.remove('hljs');
|
|
4143
|
+
codeEl.dataset._active_stream = '0';
|
|
4144
|
+
const st = this.codeScroll.state(codeEl); st.autoFollow = false;
|
|
4145
|
+
} catch (_) {}
|
|
4146
|
+
this._d('DEFUSE_ACTIVE_TO_PLAIN', { len: fullText.length });
|
|
4147
|
+
this.activeCode = null;
|
|
4148
|
+
}
|
|
4149
|
+
defuseOrphanActiveBlocks(root) {
|
|
4150
|
+
try {
|
|
4151
|
+
const scope = root || document;
|
|
4152
|
+
const nodes = scope.querySelectorAll('pre code[data-_active_stream="1"]');
|
|
4153
|
+
let n = 0;
|
|
4154
|
+
nodes.forEach(codeEl => {
|
|
4155
|
+
if (!codeEl.isConnected) return;
|
|
4156
|
+
let text = '';
|
|
4157
|
+
const frozen = codeEl.querySelector('.hl-frozen');
|
|
4158
|
+
const tail = codeEl.querySelector('.hl-tail');
|
|
4159
|
+
if (frozen || tail) text = (frozen?.textContent || '') + (tail?.textContent || '');
|
|
4160
|
+
else text = codeEl.textContent || '';
|
|
4161
|
+
codeEl.textContent = text;
|
|
4077
4162
|
codeEl.removeAttribute('data-highlighted');
|
|
4078
4163
|
codeEl.classList.remove('hljs');
|
|
4079
4164
|
codeEl.dataset._active_stream = '0';
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
this.
|
|
4084
|
-
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
const tail = codeEl.querySelector('.hl-tail');
|
|
4096
|
-
if (frozen || tail) text = (frozen?.textContent || '') + (tail?.textContent || '');
|
|
4097
|
-
else text = codeEl.textContent || '';
|
|
4098
|
-
codeEl.textContent = text;
|
|
4099
|
-
codeEl.removeAttribute('data-highlighted');
|
|
4100
|
-
codeEl.classList.remove('hljs');
|
|
4101
|
-
codeEl.dataset._active_stream = '0';
|
|
4102
|
-
try { this.codeScroll.attachHandlers(codeEl); } catch (_) {}
|
|
4103
|
-
n++;
|
|
4104
|
-
});
|
|
4105
|
-
if (n) this._d('DEFUSE_ORPHAN_ACTIVE_BLOCKS', { count: n });
|
|
4106
|
-
} catch (e) { this._d('DEFUSE_ORPHAN_ACTIVE_ERR', String(e)); }
|
|
4107
|
-
}
|
|
4108
|
-
// Abort streaming and clear state with options.
|
|
4109
|
-
abortAndReset(opts) {
|
|
4110
|
-
const o = Object.assign({
|
|
4111
|
-
finalizeActive: true,
|
|
4112
|
-
clearBuffer: true,
|
|
4113
|
-
clearMsg: false,
|
|
4114
|
-
defuseOrphans: true,
|
|
4115
|
-
reason: '',
|
|
4116
|
-
suppressLog: false
|
|
4117
|
-
}, (opts || {}));
|
|
4165
|
+
try { this.codeScroll.attachHandlers(codeEl); } catch (_) {}
|
|
4166
|
+
n++;
|
|
4167
|
+
});
|
|
4168
|
+
if (n) this._d('DEFUSE_ORPHAN_ACTIVE_BLOCKS', { count: n });
|
|
4169
|
+
} catch (e) { this._d('DEFUSE_ORPHAN_ACTIVE_ERR', String(e)); }
|
|
4170
|
+
}
|
|
4171
|
+
abortAndReset(opts) {
|
|
4172
|
+
const o = Object.assign({
|
|
4173
|
+
finalizeActive: true,
|
|
4174
|
+
clearBuffer: true,
|
|
4175
|
+
clearMsg: false,
|
|
4176
|
+
defuseOrphans: true,
|
|
4177
|
+
reason: '',
|
|
4178
|
+
suppressLog: false
|
|
4179
|
+
}, (opts || {}));
|
|
4118
4180
|
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4181
|
+
try { this.raf.cancelGroup('StreamEngine'); } catch (_) {}
|
|
4182
|
+
try { this.raf.cancel('SE:snapshot'); } catch (_) {}
|
|
4183
|
+
this.snapshotScheduled = false; this.snapshotRAF = 0;
|
|
4122
4184
|
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
}
|
|
4129
|
-
} catch (e) {
|
|
4130
|
-
this._d('ABORT_FINALIZE_ERR', String(e));
|
|
4185
|
+
const hadActive = !!this.activeCode;
|
|
4186
|
+
try {
|
|
4187
|
+
if (this.activeCode) {
|
|
4188
|
+
if (o.finalizeActive === true) this.finalizeActiveCode();
|
|
4189
|
+
else this.defuseActiveToPlain();
|
|
4131
4190
|
}
|
|
4191
|
+
} catch (e) {
|
|
4192
|
+
this._d('ABORT_FINALIZE_ERR', String(e));
|
|
4193
|
+
}
|
|
4132
4194
|
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4195
|
+
if (o.defuseOrphans) {
|
|
4196
|
+
try { this.defuseOrphanActiveBlocks(); }
|
|
4197
|
+
catch (e) { this._d('ABORT_DEFUSE_ORPHANS_ERR', String(e)); }
|
|
4198
|
+
}
|
|
4137
4199
|
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
}
|
|
4145
|
-
if (o.clearMsg === true) {
|
|
4146
|
-
try { this.dom.resetEphemeral(); } catch (_) {}
|
|
4147
|
-
}
|
|
4148
|
-
if (!o.suppressLog) this._d('ABORT_AND_RESET', { hadActive, ...o });
|
|
4200
|
+
if (o.clearBuffer) {
|
|
4201
|
+
this._clearStreamBuffer();
|
|
4202
|
+
this.fenceOpen = false; this.fenceMark = '`'; this.fenceLen = 3;
|
|
4203
|
+
this.fenceTail = ''; this.fenceBuf = '';
|
|
4204
|
+
this.codeStream.open = false; this.codeStream.lines = 0; this.codeStream.chars = 0;
|
|
4205
|
+
window.__lastSnapshotLen = 0;
|
|
4149
4206
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
// Reset adaptive snapshot budget to base.
|
|
4153
|
-
resetBudget() { this.nextSnapshotStep = this.profile().base; }
|
|
4154
|
-
// Check whether [from, end) contains only spaces/tabs.
|
|
4155
|
-
onlyTrailingWhitespace(s, from, end) {
|
|
4156
|
-
for (let i = from; i < end; i++) { const c = s.charCodeAt(i); if (c !== 0x20 && c !== 0x09) return false; }
|
|
4157
|
-
return true;
|
|
4207
|
+
if (o.clearMsg === true) {
|
|
4208
|
+
try { this.dom.resetEphemeral(); } catch (_) {}
|
|
4158
4209
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
}
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4210
|
+
if (!o.suppressLog) this._d('ABORT_AND_RESET', { hadActive, ...o });
|
|
4211
|
+
}
|
|
4212
|
+
profile() { return this.fenceOpen ? this.cfg.PROFILE_CODE : this.cfg.PROFILE_TEXT; }
|
|
4213
|
+
resetBudget() { this.nextSnapshotStep = this.profile().base; }
|
|
4214
|
+
onlyTrailingWhitespace(s, from, end) {
|
|
4215
|
+
for (let i = from; i < end; i++) { const c = s.charCodeAt(i); if (c !== 0x20 && c !== 0x09) return false; }
|
|
4216
|
+
return true;
|
|
4217
|
+
}
|
|
4218
|
+
updateFenceHeuristic(chunk) {
|
|
4219
|
+
const prev = (this.fenceBuf || '');
|
|
4220
|
+
const s = prev + (chunk || '');
|
|
4221
|
+
const preLen = prev.length;
|
|
4222
|
+
const n = s.length; let i = 0;
|
|
4223
|
+
let opened = false; let closed = false; let splitAt = -1;
|
|
4224
|
+
let atLineStart = (preLen === 0) ? true : /[\n\r]$/.test(prev);
|
|
4225
|
+
|
|
4226
|
+
const inNewOrCrosses = (j, k) => (j >= preLen) || (k > preLen);
|
|
4227
|
+
|
|
4228
|
+
while (i < n) {
|
|
4229
|
+
const ch = s[i];
|
|
4230
|
+
if (ch === '\r' || ch === '\n') { atLineStart = true; i++; continue; }
|
|
4231
|
+
if (!atLineStart) { i++; continue; }
|
|
4232
|
+
atLineStart = false;
|
|
4233
|
+
|
|
4234
|
+
// Skip list/blockquote/indent normalization (existing logic)
|
|
4235
|
+
let j = i;
|
|
4236
|
+
while (j < n) {
|
|
4237
|
+
let localSpaces = 0;
|
|
4238
|
+
while (j < n && (s[j] === ' ' || s[j] === '\t')) { localSpaces += (s[j] === '\t') ? 4 : 1; j++; if (localSpaces > 3) break; }
|
|
4239
|
+
if (j < n && s[j] === '>') { j++; if (j < n && s[j] === ' ') j++; continue; }
|
|
4240
|
+
|
|
4241
|
+
let saved = j;
|
|
4242
|
+
if (j < n && (s[j] === '-' || s[j] === '*' || s[j] === '+')) {
|
|
4243
|
+
let jj = j + 1; if (jj < n && s[jj] === ' ') { j = jj + 1; } else { j = saved; }
|
|
4244
|
+
} else {
|
|
4245
|
+
let k2 = j; let hasDigit = false;
|
|
4246
|
+
while (k2 < n && s[k2] >= '0' && s[k2] <= '9') { hasDigit = true; k2++; }
|
|
4247
|
+
if (hasDigit && k2 < n && (s[k2] === '.' || s[k2] === ')')) {
|
|
4248
|
+
k2++; if (k2 < n && s[k2] === ' ') { j = k2 + 1; } else { j = saved; }
|
|
4249
|
+
} else { j = saved; }
|
|
4199
4250
|
}
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4251
|
+
break;
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
let indent = 0;
|
|
4255
|
+
while (j < n && (s[j] === ' ' || s[j] === '\t')) {
|
|
4256
|
+
indent += (s[j] === '\t') ? 4 : 1; j++; if (indent > 3) break;
|
|
4257
|
+
}
|
|
4258
|
+
if (indent > 3) { i = j; continue; }
|
|
4259
|
+
|
|
4260
|
+
// 1) Custom fences first (e.g. [!exec] ... [/!exec], <execute>...</execute>)
|
|
4261
|
+
if (!this.fenceOpen && this._customFenceSpecs && this._customFenceSpecs.length) {
|
|
4262
|
+
for (let ci = 0; ci < this._customFenceSpecs.length; ci++) {
|
|
4263
|
+
const spec = this._customFenceSpecs[ci];
|
|
4264
|
+
const open = spec && spec.open ? spec.open : '';
|
|
4265
|
+
if (!open) continue;
|
|
4266
|
+
const k = j + open.length;
|
|
4267
|
+
if (k <= n && s.slice(j, k) === open) {
|
|
4268
|
+
if (!inNewOrCrosses(j, k)) { /* seen fully in previous prefix */ }
|
|
4269
|
+
else {
|
|
4270
|
+
this.fenceOpen = true; this._fenceCustom = spec; opened = true; i = k;
|
|
4271
|
+
this._d('FENCE_OPEN_DETECTED_CUSTOM', { open, idxStart: j, idxEnd: k, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4272
|
+
continue; // main while
|
|
4216
4273
|
}
|
|
4217
4274
|
}
|
|
4218
|
-
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
} else {
|
|
4237
|
-
this._d('FENCE_CLOSE_REJECTED_CUSTOM_NON_WS_AFTER', { close, idxStart: j, idxEnd: k });
|
|
4275
|
+
}
|
|
4276
|
+
} else if (this.fenceOpen && this._fenceCustom && this._fenceCustom.close) {
|
|
4277
|
+
const close = this._fenceCustom.close;
|
|
4278
|
+
const k = j + close.length;
|
|
4279
|
+
if (k <= n && s.slice(j, k) === close) {
|
|
4280
|
+
// Require only trailing whitespace on the line (consistent with ``` logic)
|
|
4281
|
+
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
4282
|
+
const onlyWS = this.onlyTrailingWhitespace(s, k, eol);
|
|
4283
|
+
if (onlyWS) {
|
|
4284
|
+
if (!inNewOrCrosses(j, k)) { /* seen in previous prefix */ }
|
|
4285
|
+
else {
|
|
4286
|
+
this.fenceOpen = false; this._fenceCustom = null; closed = true;
|
|
4287
|
+
const endInS = k;
|
|
4288
|
+
const rel = endInS - preLen;
|
|
4289
|
+
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4290
|
+
i = k;
|
|
4291
|
+
this._d('FENCE_CLOSE_DETECTED_CUSTOM', { close, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4292
|
+
continue; // main while
|
|
4238
4293
|
}
|
|
4294
|
+
} else {
|
|
4295
|
+
this._d('FENCE_CLOSE_REJECTED_CUSTOM_NON_WS_AFTER', { close, idxStart: j, idxEnd: k });
|
|
4239
4296
|
}
|
|
4240
4297
|
}
|
|
4298
|
+
}
|
|
4241
4299
|
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4300
|
+
// 2) Standard markdown-it fences (``` or ~~~) – leave your original logic intact
|
|
4301
|
+
if (j < n && (s[j] === '`' || s[j] === '~')) {
|
|
4302
|
+
const mark = s[j]; let k = j; while (k < n && s[k] === mark) k++; const run = k - j;
|
|
4245
4303
|
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4304
|
+
if (!this.fenceOpen) {
|
|
4305
|
+
if (run >= 3) {
|
|
4306
|
+
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
4307
|
+
this.fenceOpen = true; this.fenceMark = mark; this.fenceLen = run; opened = true; i = k;
|
|
4308
|
+
this._d('FENCE_OPEN_DETECTED', { mark, run, idxStart: j, idxEnd: k, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4309
|
+
continue;
|
|
4310
|
+
}
|
|
4311
|
+
} else if (!this._fenceCustom) {
|
|
4312
|
+
if (mark === this.fenceMark && run >= this.fenceLen) {
|
|
4313
|
+
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
4314
|
+
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
4315
|
+
if (this.onlyTrailingWhitespace(s, k, eol)) {
|
|
4316
|
+
this.fenceOpen = false; closed = true;
|
|
4317
|
+
const endInS = k;
|
|
4318
|
+
const rel = endInS - preLen;
|
|
4319
|
+
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4320
|
+
i = k;
|
|
4321
|
+
this._d('FENCE_CLOSE_DETECTED', { mark, run, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4251
4322
|
continue;
|
|
4252
|
-
}
|
|
4253
|
-
|
|
4254
|
-
if (mark === this.fenceMark && run >= this.fenceLen) {
|
|
4255
|
-
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
4256
|
-
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
4257
|
-
if (this.onlyTrailingWhitespace(s, k, eol)) {
|
|
4258
|
-
this.fenceOpen = false; closed = true;
|
|
4259
|
-
const endInS = k;
|
|
4260
|
-
const rel = endInS - preLen;
|
|
4261
|
-
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4262
|
-
i = k;
|
|
4263
|
-
this._d('FENCE_CLOSE_DETECTED', { mark, run, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4264
|
-
continue;
|
|
4265
|
-
} else {
|
|
4266
|
-
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k });
|
|
4267
|
-
}
|
|
4323
|
+
} else {
|
|
4324
|
+
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k });
|
|
4268
4325
|
}
|
|
4269
4326
|
}
|
|
4270
4327
|
}
|
|
4271
|
-
|
|
4272
|
-
i = j + 1;
|
|
4273
4328
|
}
|
|
4274
4329
|
|
|
4275
|
-
|
|
4276
|
-
this.fenceBuf = s.slice(-MAX_TAIL);
|
|
4277
|
-
this.fenceTail = s.slice(-3);
|
|
4278
|
-
return { opened, closed, splitAt };
|
|
4330
|
+
i = j + 1;
|
|
4279
4331
|
}
|
|
4280
4332
|
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
return snap;
|
|
4287
|
-
}
|
|
4288
|
-
// Detect structural boundaries in a chunk (for snapshot decisions).
|
|
4289
|
-
hasStructuralBoundary(chunk) { if (!chunk) return false; return /\n(\n|[-*]\s|\d+\.\s|#{1,6}\s|>\s)/.test(chunk); }
|
|
4290
|
-
// Decide whether we should snapshot on this chunk.
|
|
4291
|
-
shouldSnapshotOnChunk(chunk, chunkHasNL, hasBoundary) {
|
|
4292
|
-
const prof = this.profile(); const now = Utils.now();
|
|
4293
|
-
if (this.activeCode && this.fenceOpen) return false;
|
|
4294
|
-
if ((now - this.lastSnapshotTs) < prof.minInterval) return false;
|
|
4295
|
-
if (hasBoundary) return true;
|
|
4333
|
+
const MAX_TAIL = 512;
|
|
4334
|
+
this.fenceBuf = s.slice(-MAX_TAIL);
|
|
4335
|
+
this.fenceTail = s.slice(-3);
|
|
4336
|
+
return { opened, closed, splitAt };
|
|
4337
|
+
}
|
|
4296
4338
|
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
}
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4339
|
+
// Ensure message snapshot container exists.
|
|
4340
|
+
getMsgSnapshotRoot(msg) {
|
|
4341
|
+
if (!msg) return null;
|
|
4342
|
+
let snap = msg.querySelector('.md-snapshot-root');
|
|
4343
|
+
if (!snap) { snap = document.createElement('div'); snap.className = 'md-snapshot-root'; msg.appendChild(snap); }
|
|
4344
|
+
return snap;
|
|
4345
|
+
}
|
|
4346
|
+
hasStructuralBoundary(chunk) { if (!chunk) return false; return /\n(\n|[-*]\s|\d+\.\s|#{1,6}\s|>\s)/.test(chunk); }
|
|
4347
|
+
shouldSnapshotOnChunk(chunk, chunkHasNL, hasBoundary) {
|
|
4348
|
+
const prof = this.profile(); const now = Utils.now();
|
|
4349
|
+
if (this.activeCode && this.fenceOpen) return false;
|
|
4350
|
+
if ((now - this.lastSnapshotTs) < prof.minInterval) return false;
|
|
4351
|
+
if (hasBoundary) return true;
|
|
4352
|
+
|
|
4353
|
+
const delta = Math.max(0, this.getStreamLength() - (window.__lastSnapshotLen || 0));
|
|
4354
|
+
if (this.fenceOpen) { if (chunkHasNL && delta >= this.nextSnapshotStep) return true; return false; }
|
|
4355
|
+
if (delta >= this.nextSnapshotStep) return true;
|
|
4356
|
+
return false;
|
|
4357
|
+
}
|
|
4358
|
+
maybeScheduleSoftSnapshot(msg, chunkHasNL) {
|
|
4359
|
+
const prof = this.profile(); const now = Utils.now();
|
|
4360
|
+
if (this.activeCode && this.fenceOpen) return;
|
|
4361
|
+
if (this.fenceOpen && this.codeStream.lines < 1 && !chunkHasNL) return;
|
|
4362
|
+
if ((now - this.lastSnapshotTs) >= prof.softLatency) this.scheduleSnapshot(msg);
|
|
4363
|
+
}
|
|
4364
|
+
scheduleSnapshot(msg, force = false) {
|
|
4365
|
+
if (this.snapshotScheduled && !this.raf.isScheduled('SE:snapshot')) this.snapshotScheduled = false;
|
|
4366
|
+
if (!force) {
|
|
4367
|
+
if (this.snapshotScheduled) return;
|
|
4305
4368
|
if (this.activeCode && this.fenceOpen) return;
|
|
4306
|
-
|
|
4307
|
-
if (
|
|
4308
|
-
}
|
|
4309
|
-
// Schedule snapshot rendering (coalesced via rAF).
|
|
4310
|
-
scheduleSnapshot(msg, force = false) {
|
|
4311
|
-
if (this.snapshotScheduled && !this.raf.isScheduled('SE:snapshot')) this.snapshotScheduled = false;
|
|
4312
|
-
if (!force) {
|
|
4313
|
-
if (this.snapshotScheduled) return;
|
|
4314
|
-
if (this.activeCode && this.fenceOpen) return;
|
|
4315
|
-
} else {
|
|
4316
|
-
if (this.snapshotScheduled && this.raf.isScheduled('SE:snapshot')) return;
|
|
4317
|
-
}
|
|
4318
|
-
this.snapshotScheduled = true;
|
|
4319
|
-
this.raf.schedule('SE:snapshot', () => { this.snapshotScheduled = false; this.renderSnapshot(msg); }, 'StreamEngine', 0);
|
|
4320
|
-
}
|
|
4321
|
-
// Split code element into frozen and tail spans if needed.
|
|
4322
|
-
ensureSplitCodeEl(codeEl) {
|
|
4323
|
-
if (!codeEl) return null;
|
|
4324
|
-
let frozen = codeEl.querySelector('.hl-frozen'); let tail = codeEl.querySelector('.hl-tail');
|
|
4325
|
-
if (frozen && tail) return { codeEl, frozenEl: frozen, tailEl: tail };
|
|
4326
|
-
const text = codeEl.textContent || ''; codeEl.innerHTML = '';
|
|
4327
|
-
frozen = document.createElement('span'); frozen.className = 'hl-frozen';
|
|
4328
|
-
tail = document.createElement('span'); tail.className = 'hl-tail';
|
|
4329
|
-
codeEl.appendChild(frozen); codeEl.appendChild(tail);
|
|
4330
|
-
if (text) tail.textContent = text; return { codeEl, frozenEl: frozen, tailEl: tail };
|
|
4369
|
+
} else {
|
|
4370
|
+
if (this.snapshotScheduled && this.raf.isScheduled('SE:snapshot')) return;
|
|
4331
4371
|
}
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
const last = codes[codes.length - 1];
|
|
4336
|
-
const cls = Array.from(last.classList).find(c => c.startsWith('language-')) || 'language-plaintext';
|
|
4337
|
-
const lang = (cls.replace('language-', '') || 'plaintext');
|
|
4338
|
-
const parts = this.ensureSplitCodeEl(last); if (!parts) return null;
|
|
4339
|
-
|
|
4340
|
-
// If we injected a synthetic EOL for parsing an open fence, remove it from the streaming tail now.
|
|
4341
|
-
// This prevents breaking the very first code line into "#\n foo" when the next chunk starts with " foo".
|
|
4342
|
-
if (this._lastInjectedEOL && parts.tailEl && parts.tailEl.textContent && parts.tailEl.textContent.endsWith('\n')) {
|
|
4343
|
-
parts.tailEl.textContent = parts.tailEl.textContent.slice(0, -1);
|
|
4344
|
-
// Reset the marker so we don't accidentally trim again in this snapshot lifecycle.
|
|
4345
|
-
this._lastInjectedEOL = false;
|
|
4346
|
-
}
|
|
4347
|
-
|
|
4348
|
-
const st = this.codeScroll.state(parts.codeEl); st.autoFollow = true; st.userInteracted = false;
|
|
4349
|
-
parts.codeEl.dataset._active_stream = '1';
|
|
4350
|
-
const baseFrozenNL = Utils.countNewlines(parts.frozenEl.textContent || ''); const baseTailNL = Utils.countNewlines(parts.tailEl.textContent || '');
|
|
4351
|
-
const ac = { codeEl: parts.codeEl, frozenEl: parts.frozenEl, tailEl: parts.tailEl, lang, frozenLen: parts.frozenEl.textContent.length, lastPromoteTs: 0,
|
|
4352
|
-
lines: 0, tailLines: baseTailNL, linesSincePromote: 0, initialLines: baseFrozenNL + baseTailNL, haltHL: false, plainStream: false };
|
|
4353
|
-
this._d('ACTIVE_CODE_SETUP', { lang, frozenLen: ac.frozenLen, tailLines: ac.tailLines, initialLines: ac.initialLines });
|
|
4354
|
-
return ac;
|
|
4355
|
-
}
|
|
4356
|
-
// Copy previous active code state into the new one (after snapshot).
|
|
4357
|
-
rehydrateActiveCode(oldAC, newAC) {
|
|
4358
|
-
if (!oldAC || !newAC) return;
|
|
4359
|
-
newAC.frozenEl.innerHTML = oldAC.frozenEl ? oldAC.frozenEl.innerHTML : '';
|
|
4360
|
-
const fullText = newAC.codeEl.textContent || ''; const remainder = fullText.slice(oldAC.frozenLen);
|
|
4361
|
-
newAC.tailEl.textContent = remainder;
|
|
4362
|
-
newAC.frozenLen = oldAC.frozenLen; newAC.lang = oldAC.lang;
|
|
4363
|
-
newAC.lines = oldAC.lines; newAC.tailLines = Utils.countNewlines(remainder);
|
|
4364
|
-
newAC.lastPromoteTs = oldAC.lastPromoteTs; newAC.linesSincePromote = oldAC.linesSincePromote || 0;
|
|
4365
|
-
newAC.initialLines = oldAC.initialLines || 0; newAC.haltHL = !!oldAC.haltHL;
|
|
4366
|
-
newAC.plainStream = !!oldAC.plainStream;
|
|
4367
|
-
this._d('ACTIVE_CODE_REHYDRATE', { lang: newAC.lang, frozenLen: newAC.frozenLen, tailLines: newAC.tailLines, initialLines: newAC.initialLines, halted: newAC.haltHL, plainStream: newAC.plainStream });
|
|
4368
|
-
}
|
|
4369
|
-
// Append text to active tail span and update counters.
|
|
4370
|
-
appendToActiveTail(text) {
|
|
4371
|
-
if (!this.activeCode || !this.activeCode.tailEl || !text) return;
|
|
4372
|
-
this.activeCode.tailEl.insertAdjacentText('beforeend', text);
|
|
4373
|
-
const nl = Utils.countNewlines(text);
|
|
4374
|
-
this.activeCode.tailLines += nl; this.activeCode.linesSincePromote += nl;
|
|
4375
|
-
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4376
|
-
if (this.logger.isEnabled('STREAM') && (nl > 0 || text.length >= 64)) {
|
|
4377
|
-
this._d('TAIL_APPEND', { addLen: text.length, addNL: nl, totalTailNL: this.activeCode.tailLines });
|
|
4378
|
-
}
|
|
4379
|
-
}
|
|
4380
|
-
// Enforce budgets: stop incremental hljs and switch to plain streaming if needed.
|
|
4381
|
-
enforceHLStopBudget() {
|
|
4382
|
-
if (!this.activeCode) return;
|
|
4383
|
-
// If global disable was requested, halt early and switch to plain streaming.
|
|
4384
|
-
if (this.cfg.HL.DISABLE_ALL) { this.activeCode.haltHL = true; this.activeCode.plainStream = true; return; }
|
|
4385
|
-
const stop = (this.cfg.PROFILE_CODE.stopAfterLines | 0);
|
|
4386
|
-
const streamPlainLines = (this.cfg.PROFILE_CODE.streamPlainAfterLines | 0);
|
|
4387
|
-
const streamPlainChars = (this.cfg.PROFILE_CODE.streamPlainAfterChars | 0);
|
|
4388
|
-
const maxFrozenChars = (this.cfg.PROFILE_CODE.maxFrozenChars | 0);
|
|
4389
|
-
|
|
4390
|
-
const totalLines = (this.activeCode.initialLines || 0) + (this.activeCode.lines || 0);
|
|
4391
|
-
const frozenChars = this.activeCode.frozenLen | 0;
|
|
4392
|
-
const tailChars = (this.activeCode.tailEl?.textContent || '').length | 0;
|
|
4393
|
-
const totalStreamedChars = frozenChars + tailChars;
|
|
4394
|
-
|
|
4395
|
-
// Switch to plain streaming after budgets – no incremental hljs
|
|
4396
|
-
if ((streamPlainLines > 0 && totalLines >= streamPlainLines) ||
|
|
4397
|
-
(streamPlainChars > 0 && totalStreamedChars >= streamPlainChars) ||
|
|
4398
|
-
(maxFrozenChars > 0 && frozenChars >= maxFrozenChars)) {
|
|
4399
|
-
this.activeCode.haltHL = true;
|
|
4400
|
-
this.activeCode.plainStream = true;
|
|
4401
|
-
try { this.activeCode.codeEl.dataset.hlStreamSuspended = '1'; } catch (_) {}
|
|
4402
|
-
this._d('STREAM_HL_SUSPENDED', { totalLines, totalStreamedChars, frozenChars, reason: 'budget' });
|
|
4403
|
-
return;
|
|
4404
|
-
}
|
|
4372
|
+
this.snapshotScheduled = true;
|
|
4373
|
+
this.raf.schedule('SE:snapshot', () => { this.snapshotScheduled = false; this.renderSnapshot(msg); }, 'StreamEngine', 0);
|
|
4374
|
+
}
|
|
4405
4375
|
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
}
|
|
4428
|
-
_isHLJSSupported(lang) {
|
|
4429
|
-
try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; }
|
|
4430
|
-
}
|
|
4431
|
-
// Try to detect language from a "language: X" style first line directive.
|
|
4432
|
-
_detectDirectiveLangFromText(text) {
|
|
4433
|
-
if (!text) return null;
|
|
4434
|
-
let s = String(text);
|
|
4435
|
-
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
|
|
4436
|
-
const lines = s.split(/\r?\n/);
|
|
4437
|
-
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
4438
|
-
if (i >= lines.length) return null;
|
|
4439
|
-
let first = lines[i].trim();
|
|
4440
|
-
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
4441
|
-
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
4442
|
-
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) return null;
|
|
4443
|
-
|
|
4444
|
-
let cand = this._aliasLang(token);
|
|
4445
|
-
const rest = lines.slice(i + 1).join('\n');
|
|
4446
|
-
if (!rest.trim()) return null;
|
|
4447
|
-
|
|
4448
|
-
let pos = 0, seen = 0;
|
|
4449
|
-
while (seen < i && pos < s.length) { const nl = s.indexOf('\n', pos); if (nl === -1) return null; pos = nl + 1; seen++; }
|
|
4450
|
-
let end = s.indexOf('\n', pos);
|
|
4451
|
-
if (end === -1) end = s.length; else end = end + 1;
|
|
4452
|
-
return { lang: cand, deleteUpto: end };
|
|
4453
|
-
}
|
|
4454
|
-
// Update code element class to reflect new lang (language-xxx).
|
|
4455
|
-
_updateCodeLangClass(codeEl, newLang) {
|
|
4456
|
-
try {
|
|
4457
|
-
Array.from(codeEl.classList).forEach(c => { if (c.startsWith('language-')) codeEl.classList.remove(c); });
|
|
4458
|
-
codeEl.classList.add('language-' + (newLang || 'plaintext'));
|
|
4459
|
-
} catch (_) {}
|
|
4376
|
+
ensureSplitCodeEl(codeEl) {
|
|
4377
|
+
if (!codeEl) return null;
|
|
4378
|
+
let frozen = codeEl.querySelector('.hl-frozen'); let tail = codeEl.querySelector('.hl-tail');
|
|
4379
|
+
if (frozen && tail) return { codeEl, frozenEl: frozen, tailEl: tail };
|
|
4380
|
+
const text = codeEl.textContent || ''; codeEl.innerHTML = '';
|
|
4381
|
+
frozen = document.createElement('span'); frozen.className = 'hl-frozen';
|
|
4382
|
+
tail = document.createElement('span'); tail.className = 'hl-tail';
|
|
4383
|
+
codeEl.appendChild(frozen); codeEl.appendChild(tail);
|
|
4384
|
+
if (text) tail.textContent = text; return { codeEl, frozenEl: frozen, tailEl: tail };
|
|
4385
|
+
}
|
|
4386
|
+
setupActiveCodeFromSnapshot(snap) {
|
|
4387
|
+
const codes = snap.querySelectorAll('pre code'); if (!codes.length) return null;
|
|
4388
|
+
const last = codes[codes.length - 1];
|
|
4389
|
+
const cls = Array.from(last.classList).find(c => c.startsWith('language-')) || 'language-plaintext';
|
|
4390
|
+
const lang = (cls.replace('language-', '') || 'plaintext');
|
|
4391
|
+
const parts = this.ensureSplitCodeEl(last); if (!parts) return null;
|
|
4392
|
+
|
|
4393
|
+
// If we injected a synthetic EOL for parsing an open fence, remove it from the streaming tail now.
|
|
4394
|
+
if (this._lastInjectedEOL && parts.tailEl && parts.tailEl.textContent && parts.tailEl.textContent.endsWith('\n')) {
|
|
4395
|
+
parts.tailEl.textContent = parts.tailEl.textContent.slice(0, -1);
|
|
4396
|
+
this._lastInjectedEOL = false;
|
|
4460
4397
|
}
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4398
|
+
|
|
4399
|
+
const st = this.codeScroll.state(parts.codeEl); st.autoFollow = true; st.userInteracted = false;
|
|
4400
|
+
parts.codeEl.dataset._active_stream = '1';
|
|
4401
|
+
const baseFrozenNL = Utils.countNewlines(parts.frozenEl.textContent || ''); const baseTailNL = Utils.countNewlines(parts.tailEl.textContent || '');
|
|
4402
|
+
const ac = { codeEl: parts.codeEl, frozenEl: parts.frozenEl, tailEl: parts.tailEl, lang, frozenLen: parts.frozenEl.textContent.length, lastPromoteTs: 0,
|
|
4403
|
+
lines: 0, tailLines: baseTailNL, linesSincePromote: 0, initialLines: baseFrozenNL + baseTailNL, haltHL: false, plainStream: false };
|
|
4404
|
+
this._d('ACTIVE_CODE_SETUP', { lang, frozenLen: ac.frozenLen, tailLines: ac.tailLines, initialLines: ac.initialLines });
|
|
4405
|
+
return ac;
|
|
4406
|
+
}
|
|
4407
|
+
rehydrateActiveCode(oldAC, newAC) {
|
|
4408
|
+
if (!oldAC || !newAC) return;
|
|
4409
|
+
newAC.frozenEl.innerHTML = oldAC.frozenEl ? oldAC.frozenEl.innerHTML : '';
|
|
4410
|
+
const fullText = newAC.codeEl.textContent || ''; const remainder = fullText.slice(oldAC.frozenLen);
|
|
4411
|
+
newAC.tailEl.textContent = remainder;
|
|
4412
|
+
newAC.frozenLen = oldAC.frozenLen; newAC.lang = oldAC.lang;
|
|
4413
|
+
newAC.lines = oldAC.lines; newAC.tailLines = Utils.countNewlines(remainder);
|
|
4414
|
+
newAC.lastPromoteTs = oldAC.lastPromoteTs; newAC.linesSincePromote = oldAC.linesSincePromote || 0;
|
|
4415
|
+
newAC.initialLines = oldAC.initialLines || 0; newAC.haltHL = !!oldAC.haltHL;
|
|
4416
|
+
newAC.plainStream = !!oldAC.plainStream;
|
|
4417
|
+
this._d('ACTIVE_CODE_REHYDRATE', { lang: newAC.lang, frozenLen: newAC.frozenLen, tailLines: newAC.tailLines, initialLines: newAC.initialLines, halted: newAC.haltHL, plainStream: newAC.plainStream });
|
|
4418
|
+
}
|
|
4419
|
+
appendToActiveTail(text) {
|
|
4420
|
+
if (!this.activeCode || !this.activeCode.tailEl || !text) return;
|
|
4421
|
+
this.activeCode.tailEl.insertAdjacentText('beforeend', text);
|
|
4422
|
+
const nl = Utils.countNewlines(text);
|
|
4423
|
+
this.activeCode.tailLines += nl; this.activeCode.linesSincePromote += nl;
|
|
4424
|
+
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4425
|
+
if (this.logger.isEnabled('STREAM') && (nl > 0 || text.length >= 64)) {
|
|
4426
|
+
this._d('TAIL_APPEND', { addLen: text.length, addNL: nl, totalTailNL: this.activeCode.tailLines });
|
|
4470
4427
|
}
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4428
|
+
}
|
|
4429
|
+
enforceHLStopBudget() {
|
|
4430
|
+
if (!this.activeCode) return;
|
|
4431
|
+
if (this.cfg.HL.DISABLE_ALL) { this.activeCode.haltHL = true; this.activeCode.plainStream = true; return; }
|
|
4432
|
+
const stop = (this.cfg.PROFILE_CODE.stopAfterLines | 0);
|
|
4433
|
+
const streamPlainLines = (this.cfg.PROFILE_CODE.streamPlainAfterLines | 0);
|
|
4434
|
+
const streamPlainChars = (this.cfg.PROFILE_CODE.streamPlainAfterChars | 0);
|
|
4435
|
+
const maxFrozenChars = (this.cfg.PROFILE_CODE.maxFrozenChars | 0);
|
|
4436
|
+
|
|
4437
|
+
const totalLines = (this.activeCode.initialLines || 0) + (this.activeCode.lines || 0);
|
|
4438
|
+
const frozenChars = this.activeCode.frozenLen | 0;
|
|
4439
|
+
const tailChars = (this.activeCode.tailEl?.textContent || '').length | 0;
|
|
4440
|
+
const totalStreamedChars = frozenChars + tailChars;
|
|
4441
|
+
|
|
4442
|
+
if ((streamPlainLines > 0 && totalLines >= streamPlainLines) ||
|
|
4443
|
+
(streamPlainChars > 0 && totalStreamedChars >= streamPlainChars) ||
|
|
4444
|
+
(maxFrozenChars > 0 && frozenChars >= maxFrozenChars)) {
|
|
4445
|
+
this.activeCode.haltHL = true;
|
|
4446
|
+
this.activeCode.plainStream = true;
|
|
4447
|
+
try { this.activeCode.codeEl.dataset.hlStreamSuspended = '1'; } catch (_) {}
|
|
4448
|
+
this._d('STREAM_HL_SUSPENDED', { totalLines, totalStreamedChars, frozenChars, reason: 'budget' });
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
if (stop > 0 && totalLines >= stop) {
|
|
4453
|
+
this.activeCode.haltHL = true;
|
|
4454
|
+
this.activeCode.plainStream = true;
|
|
4455
|
+
try { this.activeCode.codeEl.dataset.hlStreamSuspended = '1'; } catch (_) {}
|
|
4456
|
+
this._d('STREAM_HL_SUSPENDED', { totalLines, stopAfter: stop, reason: 'stopAfterLines' });
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
_aliasLang(token) {
|
|
4460
|
+
const ALIAS = {
|
|
4461
|
+
txt: 'plaintext', text: 'plaintext', plaintext: 'plaintext',
|
|
4462
|
+
sh: 'bash', shell: 'bash', zsh: 'bash', 'shell-session': 'bash',
|
|
4463
|
+
py: 'python', python3: 'python', py3: 'python',
|
|
4464
|
+
js: 'javascript', node: 'javascript', nodejs: 'javascript',
|
|
4465
|
+
ts: 'typescript', 'ts-node': 'typescript',
|
|
4466
|
+
yml: 'yaml', kt: 'kotlin', rs: 'rust',
|
|
4467
|
+
csharp: 'csharp', 'c#': 'csharp', 'c++': 'cpp',
|
|
4468
|
+
ps: 'powershell', ps1: 'powershell', pwsh: 'powershell', powershell7: 'powershell',
|
|
4469
|
+
docker: 'dockerfile'
|
|
4470
|
+
};
|
|
4471
|
+
const v = String(token || '').trim().toLowerCase();
|
|
4472
|
+
return ALIAS[v] || v;
|
|
4473
|
+
}
|
|
4474
|
+
_isHLJSSupported(lang) {
|
|
4475
|
+
try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; }
|
|
4476
|
+
}
|
|
4477
|
+
_detectDirectiveLangFromText(text) {
|
|
4478
|
+
if (!text) return null;
|
|
4479
|
+
let s = String(text);
|
|
4480
|
+
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
|
|
4481
|
+
const lines = s.split(/\r?\n/);
|
|
4482
|
+
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
4483
|
+
if (i >= lines.length) return null;
|
|
4484
|
+
let first = lines[i].trim();
|
|
4485
|
+
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
4486
|
+
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
4487
|
+
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) return null;
|
|
4488
|
+
|
|
4489
|
+
let cand = this._aliasLang(token);
|
|
4490
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
4491
|
+
if (!rest.trim()) return null;
|
|
4492
|
+
|
|
4493
|
+
let pos = 0, seen = 0;
|
|
4494
|
+
while (seen < i && pos < s.length) { const nl = s.indexOf('\n', pos); if (nl === -1) return null; pos = nl + 1; seen++; }
|
|
4495
|
+
let end = s.indexOf('\n', pos);
|
|
4496
|
+
if (end === -1) end = s.length; else end = end + 1;
|
|
4497
|
+
return { lang: cand, deleteUpto: end };
|
|
4498
|
+
}
|
|
4499
|
+
_updateCodeLangClass(codeEl, newLang) {
|
|
4500
|
+
try {
|
|
4501
|
+
Array.from(codeEl.classList).forEach(c => { if (c.startsWith('language-')) codeEl.classList.remove(c); });
|
|
4502
|
+
codeEl.classList.add('language-' + (newLang || 'plaintext'));
|
|
4503
|
+
} catch (_) {}
|
|
4504
|
+
}
|
|
4505
|
+
_updateCodeHeaderLabel(codeEl, newLabel, newLangToken) {
|
|
4506
|
+
try {
|
|
4507
|
+
const wrap = codeEl.closest('.code-wrapper');
|
|
4508
|
+
if (!wrap) return;
|
|
4509
|
+
const span = wrap.querySelector('.code-header-lang');
|
|
4510
|
+
if (span) span.textContent = newLabel || (newLangToken || 'code');
|
|
4511
|
+
wrap.setAttribute('data-code-lang', newLangToken || '');
|
|
4512
|
+
} catch (_) {}
|
|
4513
|
+
}
|
|
4514
|
+
maybePromoteLanguageFromDirective() {
|
|
4515
|
+
if (!this.activeCode || !this.activeCode.codeEl) return;
|
|
4516
|
+
if (this.activeCode.lang && this.activeCode.lang !== 'plaintext') return;
|
|
4475
4517
|
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4518
|
+
const frozenTxt = this.activeCode.frozenEl ? this.activeCode.frozenEl.textContent : '';
|
|
4519
|
+
const tailTxt = this.activeCode.tailEl ? this.activeCode.tailEl.textContent : '';
|
|
4520
|
+
const combined = frozenTxt + tailTxt;
|
|
4521
|
+
if (!combined) return;
|
|
4480
4522
|
|
|
4481
|
-
|
|
4482
|
-
|
|
4523
|
+
const det = this._detectDirectiveLangFromText(combined);
|
|
4524
|
+
if (!det || !det.lang) return;
|
|
4483
4525
|
|
|
4484
|
-
|
|
4485
|
-
|
|
4526
|
+
const newLang = det.lang;
|
|
4527
|
+
const newCombined = combined.slice(det.deleteUpto);
|
|
4486
4528
|
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
this.activeCode.lang = newLang;
|
|
4500
|
-
this._updateCodeLangClass(codeEl, newLang);
|
|
4501
|
-
this._updateCodeHeaderLabel(codeEl, newLang, newLang);
|
|
4502
|
-
|
|
4503
|
-
this._d('LANG_PROMOTE', { to: newLang, removedChars: det.deleteUpto, tailLines: this.activeCode.tailLines });
|
|
4504
|
-
this.schedulePromoteTail(true);
|
|
4505
|
-
} catch (e) {
|
|
4506
|
-
this._d('LANG_PROMOTE_ERR', String(e));
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
// Highlight a small piece of text based on language (safe fallback to escapeHtml).
|
|
4510
|
-
highlightDeltaText(lang, text) {
|
|
4511
|
-
if (this.cfg.HL.DISABLE_ALL) return Utils.escapeHtml(text);
|
|
4512
|
-
if (window.hljs && lang && hljs.getLanguage && hljs.getLanguage(lang)) {
|
|
4513
|
-
try { return hljs.highlight(text, { language: lang, ignoreIllegals: true }).value; }
|
|
4514
|
-
catch (_) { return Utils.escapeHtml(text); }
|
|
4515
|
-
}
|
|
4516
|
-
return Utils.escapeHtml(text);
|
|
4517
|
-
}
|
|
4518
|
-
// Schedule cooperative tail promotion (async) to avoid blocking UI on each chunk.
|
|
4519
|
-
schedulePromoteTail(force = false) {
|
|
4520
|
-
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4521
|
-
if (this._promoteScheduled) return;
|
|
4522
|
-
this._promoteScheduled = true;
|
|
4523
|
-
this.raf.schedule('SE:promoteTail', () => {
|
|
4524
|
-
this._promoteScheduled = false;
|
|
4525
|
-
this._promoteTailWork(force);
|
|
4526
|
-
}, 'StreamEngine', 1);
|
|
4527
|
-
}
|
|
4528
|
-
// Move a full-line part of tail into frozen region (with highlight if budgets allow).
|
|
4529
|
-
async _promoteTailWork(force = false) {
|
|
4530
|
-
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4531
|
-
|
|
4532
|
-
// If plain streaming mode is on, or incremental hljs is disabled, promote as plain text only.
|
|
4533
|
-
const now = Utils.now(); const prof = this.cfg.PROFILE_CODE;
|
|
4534
|
-
const tailText0 = this.activeCode.tailEl.textContent || ''; if (!tailText0) return;
|
|
4535
|
-
|
|
4536
|
-
if (!force) {
|
|
4537
|
-
if ((now - this.activeCode.lastPromoteTs) < prof.promoteMinInterval) return;
|
|
4538
|
-
const enoughLines = (this.activeCode.linesSincePromote || 0) >= (prof.promoteMinLines || 10);
|
|
4539
|
-
const enoughChars = tailText0.length >= prof.minCharsForHL;
|
|
4540
|
-
if (!enoughLines && !enoughChars) return;
|
|
4541
|
-
}
|
|
4542
|
-
|
|
4543
|
-
// Cut at last full line to avoid moving partial tokens
|
|
4544
|
-
const idx = tailText0.lastIndexOf('\n');
|
|
4545
|
-
if (idx <= -1 && !force) return;
|
|
4546
|
-
const cut = (idx >= 0) ? (idx + 1) : tailText0.length;
|
|
4547
|
-
const delta = tailText0.slice(0, cut); if (!delta) return;
|
|
4548
|
-
|
|
4549
|
-
// Re-evaluate budgets before performing any heavy work
|
|
4550
|
-
this.enforceHLStopBudget();
|
|
4551
|
-
const usePlain = this.activeCode.haltHL || this.activeCode.plainStream || !this._isHLJSSupported(this.activeCode.lang);
|
|
4552
|
-
|
|
4553
|
-
// Cooperative rAF yield before heavy highlight
|
|
4554
|
-
if (!usePlain) await this.asyncer.yield();
|
|
4555
|
-
|
|
4556
|
-
// If tail changed since we captured it, validate prefix to avoid duplication
|
|
4557
|
-
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4558
|
-
const tailNow = this.activeCode.tailEl.textContent || '';
|
|
4559
|
-
if (!tailNow.startsWith(delta)) {
|
|
4560
|
-
// New data arrived; reschedule for next frame without touching DOM
|
|
4561
|
-
this.schedulePromoteTail(false);
|
|
4562
|
-
return;
|
|
4563
|
-
}
|
|
4529
|
+
try {
|
|
4530
|
+
const codeEl = this.activeCode.codeEl;
|
|
4531
|
+
codeEl.innerHTML = '';
|
|
4532
|
+
const frozen = document.createElement('span'); frozen.className = 'hl-frozen';
|
|
4533
|
+
const tail = document.createElement('span'); tail.className = 'hl-tail';
|
|
4534
|
+
tail.textContent = newCombined;
|
|
4535
|
+
codeEl.appendChild(frozen); codeEl.appendChild(tail);
|
|
4536
|
+
this.activeCode.frozenEl = frozen; this.activeCode.tailEl = tail;
|
|
4537
|
+
this.activeCode.frozenLen = 0;
|
|
4538
|
+
this.activeCode.tailLines = Utils.countNewlines(newCombined);
|
|
4539
|
+
this.activeCode.linesSincePromote = 0;
|
|
4564
4540
|
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
this.activeCode.frozenEl.insertAdjacentHTML('beforeend', html);
|
|
4574
|
-
}
|
|
4575
|
-
|
|
4576
|
-
// Update tail and counters
|
|
4577
|
-
this.activeCode.tailEl.textContent = tailNow.slice(delta.length);
|
|
4578
|
-
this.activeCode.frozenLen += delta.length;
|
|
4579
|
-
const promotedLines = Utils.countNewlines(delta);
|
|
4580
|
-
this.activeCode.tailLines = Math.max(0, (this.activeCode.tailLines || 0) - promotedLines);
|
|
4581
|
-
this.activeCode.linesSincePromote = Math.max(0, (this.activeCode.linesSincePromote || 0) - promotedLines);
|
|
4582
|
-
this.activeCode.lastPromoteTs = Utils.now();
|
|
4583
|
-
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4584
|
-
this._d(usePlain ? 'TAIL_PROMOTE_PLAIN' : 'TAIL_PROMOTE_ASYNC', { cut, promotedLines, lang: this.activeCode.lang, plain: usePlain });
|
|
4541
|
+
this.activeCode.lang = newLang;
|
|
4542
|
+
this._updateCodeLangClass(codeEl, newLang);
|
|
4543
|
+
this._updateCodeHeaderLabel(codeEl, newLang, newLang);
|
|
4544
|
+
|
|
4545
|
+
this._d('LANG_PROMOTE', { to: newLang, removedChars: det.deleteUpto, tailLines: this.activeCode.tailLines });
|
|
4546
|
+
this.schedulePromoteTail(true);
|
|
4547
|
+
} catch (e) {
|
|
4548
|
+
this._d('LANG_PROMOTE_ERR', String(e));
|
|
4585
4549
|
}
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4550
|
+
}
|
|
4551
|
+
highlightDeltaText(lang, text) {
|
|
4552
|
+
if (this.cfg.HL.DISABLE_ALL) return Utils.escapeHtml(text);
|
|
4553
|
+
if (window.hljs && lang && hljs.getLanguage && hljs.getLanguage(lang)) {
|
|
4554
|
+
try { return hljs.highlight(text, { language: lang, ignoreIllegals: true }).value; }
|
|
4555
|
+
catch (_) { return Utils.escapeHtml(text); }
|
|
4556
|
+
}
|
|
4557
|
+
return Utils.escapeHtml(text);
|
|
4558
|
+
}
|
|
4559
|
+
schedulePromoteTail(force = false) {
|
|
4560
|
+
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4561
|
+
if (this._promoteScheduled) return;
|
|
4562
|
+
this._promoteScheduled = true;
|
|
4563
|
+
this.raf.schedule('SE:promoteTail', () => {
|
|
4564
|
+
this._promoteScheduled = false;
|
|
4565
|
+
this._promoteTailWork(force);
|
|
4566
|
+
}, 'StreamEngine', 1);
|
|
4567
|
+
}
|
|
4568
|
+
async _promoteTailWork(force = false) {
|
|
4569
|
+
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4593
4570
|
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
codeEl.innerHTML = '';
|
|
4597
|
-
codeEl.textContent = fullText;
|
|
4598
|
-
codeEl.classList.add('hljs'); // keep visual parity until highlight applies
|
|
4599
|
-
codeEl.removeAttribute('data-highlighted');
|
|
4600
|
-
} catch (_) {}
|
|
4571
|
+
const now = Utils.now(); const prof = this.cfg.PROFILE_CODE;
|
|
4572
|
+
const tailText0 = this.activeCode.tailEl.textContent || ''; if (!tailText0) return;
|
|
4601
4573
|
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
const
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4574
|
+
if (!force) {
|
|
4575
|
+
if ((now - this.activeCode.lastPromoteTs) < prof.promoteMinInterval) return;
|
|
4576
|
+
const enoughLines = (this.activeCode.linesSincePromote || 0) >= (prof.promoteMinLines || 10);
|
|
4577
|
+
const enoughChars = tailText0.length >= prof.minCharsForHL;
|
|
4578
|
+
if (!enoughLines && !enoughChars) return;
|
|
4579
|
+
}
|
|
4608
4580
|
|
|
4609
|
-
|
|
4610
|
-
|
|
4581
|
+
const idx = tailText0.lastIndexOf('\n');
|
|
4582
|
+
if (idx <= -1 && !force) return;
|
|
4583
|
+
const cut = (idx >= 0) ? (idx + 1) : tailText0.length;
|
|
4584
|
+
const delta = tailText0.slice(0, cut); if (!delta) return;
|
|
4611
4585
|
|
|
4612
|
-
|
|
4613
|
-
|
|
4586
|
+
this.enforceHLStopBudget();
|
|
4587
|
+
const usePlain = this.activeCode.haltHL || this.activeCode.plainStream || !this._isHLJSSupported(this.activeCode.lang);
|
|
4614
4588
|
|
|
4615
|
-
|
|
4589
|
+
if (!usePlain) await this.asyncer.yield();
|
|
4616
4590
|
|
|
4617
|
-
|
|
4618
|
-
|
|
4591
|
+
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4592
|
+
const tailNow = this.activeCode.tailEl.textContent || '';
|
|
4593
|
+
if (!tailNow.startsWith(delta)) {
|
|
4594
|
+
this.schedulePromoteTail(false);
|
|
4595
|
+
return;
|
|
4619
4596
|
}
|
|
4620
|
-
|
|
4621
|
-
|
|
4597
|
+
|
|
4598
|
+
if (usePlain) {
|
|
4599
|
+
this.activeCode.frozenEl.insertAdjacentText('beforeend', delta);
|
|
4600
|
+
} else {
|
|
4601
|
+
let html = Utils.escapeHtml(delta);
|
|
4602
|
+
try { html = this.highlightDeltaText(this.activeCode.lang, delta); } catch (_) { html = Utils.escapeHtml(delta); }
|
|
4603
|
+
this.activeCode.frozenEl.insertAdjacentHTML('beforeend', html);
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
this.activeCode.tailEl.textContent = tailNow.slice(delta.length);
|
|
4607
|
+
this.activeCode.frozenLen += delta.length;
|
|
4608
|
+
const promotedLines = Utils.countNewlines(delta);
|
|
4609
|
+
this.activeCode.tailLines = Math.max(0, (this.activeCode.tailLines || 0) - promotedLines);
|
|
4610
|
+
this.activeCode.linesSincePromote = Math.max(0, (this.activeCode.linesSincePromote || 0) - promotedLines);
|
|
4611
|
+
this.activeCode.lastPromoteTs = Utils.now();
|
|
4612
|
+
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4613
|
+
this._d(usePlain ? 'TAIL_PROMOTE_PLAIN' : 'TAIL_PROMOTE_ASYNC', { cut, promotedLines, lang: this.activeCode.lang, plain: usePlain });
|
|
4614
|
+
}
|
|
4615
|
+
finalizeActiveCode() {
|
|
4616
|
+
if (!this.activeCode) return;
|
|
4617
|
+
const codeEl = this.activeCode.codeEl;
|
|
4618
|
+
const fromBottomBefore = Math.max(0, codeEl.scrollHeight - codeEl.clientHeight - codeEl.scrollTop);
|
|
4619
|
+
const wasNearBottom = this.codeScroll.isNearBottomEl(codeEl, this.cfg.CODE_SCROLL.NEAR_MARGIN_PX);
|
|
4620
|
+
const fullText = (this.activeCode.frozenEl.textContent || '') + (this.activeCode.tailEl.textContent || '');
|
|
4621
|
+
|
|
4622
|
+
try {
|
|
4623
|
+
codeEl.innerHTML = '';
|
|
4624
|
+
codeEl.textContent = fullText;
|
|
4625
|
+
codeEl.classList.add('hljs');
|
|
4626
|
+
codeEl.removeAttribute('data-highlighted');
|
|
4627
|
+
} catch (_) {}
|
|
4628
|
+
|
|
4629
|
+
const st = this.codeScroll.state(codeEl); st.autoFollow = false;
|
|
4630
|
+
const maxScrollTop = Math.max(0, codeEl.scrollHeight - codeEl.clientHeight);
|
|
4631
|
+
const target = wasNearBottom ? maxScrollTop : Math.max(0, maxScrollTop - fromBottomBefore);
|
|
4632
|
+
try { codeEl.scrollTop = target; } catch (_) {}
|
|
4633
|
+
st.lastScrollTop = codeEl.scrollTop;
|
|
4634
|
+
codeEl.dataset._active_stream = '0';
|
|
4635
|
+
|
|
4636
|
+
try { codeEl.dataset.justFinalized = '1'; } catch (_) {}
|
|
4637
|
+
this.codeScroll.scheduleScroll(codeEl, false, true);
|
|
4638
|
+
|
|
4639
|
+
try { if (!this.cfg.HL.DISABLE_ALL) this.highlighter.queue(codeEl, null); } catch (_) {}
|
|
4640
|
+
|
|
4641
|
+
this.suppressPostFinalizePass = true;
|
|
4642
|
+
|
|
4643
|
+
this._d('FINALIZE_CODE_NONBLOCK', { lang: this.activeCode.lang, len: fullText.length, highlighted: false });
|
|
4644
|
+
this.activeCode = null;
|
|
4645
|
+
}
|
|
4646
|
+
codeFingerprint(codeEl) {
|
|
4647
|
+
const cls = Array.from(codeEl.classList).find(c => c.startsWith('language-')) || 'language-plaintext';
|
|
4648
|
+
const lang = cls.replace('language-', '') || 'plaintext';
|
|
4649
|
+
const t = codeEl.textContent || ''; const len = t.length; const head = t.slice(0, 64); const tail = t.slice(-64);
|
|
4650
|
+
return `${lang}|${len}|${head}|${tail}`;
|
|
4651
|
+
}
|
|
4652
|
+
codeFingerprintFromWrapper(codeEl) {
|
|
4653
|
+
try {
|
|
4654
|
+
const wrap = codeEl.closest('.code-wrapper'); if (!wrap) return null;
|
|
4622
4655
|
const cls = Array.from(codeEl.classList).find(c => c.startsWith('language-')) || 'language-plaintext';
|
|
4623
|
-
const lang = cls.replace('language-', '') || 'plaintext';
|
|
4624
|
-
const
|
|
4656
|
+
const lang = (cls.replace('language-', '') || 'plaintext');
|
|
4657
|
+
const len = wrap.getAttribute('data-code-len') || '';
|
|
4658
|
+
const head = wrap.getAttribute('data-code-head') || '';
|
|
4659
|
+
const tail = wrap.getAttribute('data-code-tail') || '';
|
|
4660
|
+
if (!len) return null;
|
|
4625
4661
|
return `${lang}|${len}|${head}|${tail}`;
|
|
4662
|
+
} catch (_) {
|
|
4663
|
+
return null;
|
|
4626
4664
|
}
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
if (
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
const
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
if (
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
let reuseCount = 0;
|
|
4662
|
-
for (let i = 0; i < end; i++) {
|
|
4663
|
-
const nc = newCodes[i];
|
|
4664
|
-
if (nc.getAttribute('data-highlighted') === 'yes') continue;
|
|
4665
|
-
// Fingerprint new code: prefer wrapper meta (no .textContent read)
|
|
4666
|
-
let fp = this.codeFingerprintFromWrapper(nc);
|
|
4667
|
-
if (!fp) fp = this.codeFingerprint(nc);
|
|
4668
|
-
const arr = map.get(fp);
|
|
4669
|
-
if (arr && arr.length) {
|
|
4670
|
-
const oldEl = arr.shift();
|
|
4671
|
-
if (oldEl && oldEl.isConnected) {
|
|
4672
|
-
try {
|
|
4673
|
-
nc.replaceWith(oldEl);
|
|
4674
|
-
this.codeScroll.attachHandlers(oldEl);
|
|
4675
|
-
// Preserve whatever final state the old element had
|
|
4676
|
-
if (!oldEl.getAttribute('data-highlighted')) oldEl.setAttribute('data-highlighted', 'yes');
|
|
4677
|
-
const st = this.codeScroll.state(oldEl); st.autoFollow = false;
|
|
4678
|
-
reuseCount++;
|
|
4679
|
-
} catch (_) {}
|
|
4680
|
-
}
|
|
4681
|
-
if (!arr.length) map.delete(fp);
|
|
4665
|
+
}
|
|
4666
|
+
preserveStableClosedCodes(oldSnap, newRoot, skipLastIfStreaming) {
|
|
4667
|
+
try {
|
|
4668
|
+
const oldCodes = Array.from(oldSnap.querySelectorAll('pre code')); if (!oldCodes.length) return;
|
|
4669
|
+
const newCodesPre = Array.from(newRoot.querySelectorAll('pre code'));
|
|
4670
|
+
if (newCodesPre.length > this.cfg.STREAM.PRESERVE_CODES_MAX || oldCodes.length > this.cfg.STREAM.PRESERVE_CODES_MAX) return;
|
|
4671
|
+
|
|
4672
|
+
const map = new Map();
|
|
4673
|
+
for (const el of oldCodes) {
|
|
4674
|
+
if (el.querySelector('.hl-frozen')) continue;
|
|
4675
|
+
if (this.activeCode && el === this.activeCode.codeEl) continue;
|
|
4676
|
+
let fp = this.codeFingerprintFromWrapper(el);
|
|
4677
|
+
if (!fp) fp = el.dataset.fp || (el.dataset.fp = this.codeFingerprint(el));
|
|
4678
|
+
const arr = map.get(fp) || []; arr.push(el); map.set(fp, arr);
|
|
4679
|
+
}
|
|
4680
|
+
const newCodes = newCodesPre;
|
|
4681
|
+
const end = (skipLastIfStreaming && newCodes.length > 0) ? (newCodes.length - 1) : newCodes.length;
|
|
4682
|
+
let reuseCount = 0;
|
|
4683
|
+
for (let i = 0; i < end; i++) {
|
|
4684
|
+
const nc = newCodes[i];
|
|
4685
|
+
if (nc.getAttribute('data-highlighted') === 'yes') continue;
|
|
4686
|
+
let fp = this.codeFingerprintFromWrapper(nc);
|
|
4687
|
+
if (!fp) fp = this.codeFingerprint(nc);
|
|
4688
|
+
const arr = map.get(fp);
|
|
4689
|
+
if (arr && arr.length) {
|
|
4690
|
+
const oldEl = arr.shift();
|
|
4691
|
+
if (oldEl && oldEl.isConnected) {
|
|
4692
|
+
try {
|
|
4693
|
+
nc.replaceWith(oldEl);
|
|
4694
|
+
this.codeScroll.attachHandlers(oldEl);
|
|
4695
|
+
if (!oldEl.getAttribute('data-highlighted')) oldEl.setAttribute('data-highlighted', 'yes');
|
|
4696
|
+
const st = this.codeScroll.state(oldEl); st.autoFollow = false;
|
|
4697
|
+
reuseCount++;
|
|
4698
|
+
} catch (_) {}
|
|
4682
4699
|
}
|
|
4700
|
+
if (!arr.length) map.delete(fp);
|
|
4683
4701
|
}
|
|
4684
|
-
if (reuseCount) this._d('PRESERVE_CODES_REUSED', { reuseCount, skipLastIfStreaming });
|
|
4685
|
-
} catch (e) {
|
|
4686
|
-
this._d('PRESERVE_CODES_ERROR', String(e));
|
|
4687
4702
|
}
|
|
4703
|
+
if (reuseCount) this._d('PRESERVE_CODES_REUSED', { reuseCount, skipLastIfStreaming });
|
|
4704
|
+
} catch (e) {
|
|
4705
|
+
this._d('PRESERVE_CODES_ERROR', String(e));
|
|
4688
4706
|
}
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
this.schedulePromoteTail(true);
|
|
4718
|
-
}
|
|
4707
|
+
}
|
|
4708
|
+
_ensureBottomForJustFinalized(root) {
|
|
4709
|
+
try {
|
|
4710
|
+
const scope = root || document;
|
|
4711
|
+
const nodes = scope.querySelectorAll('pre code[data-just-finalized="1"]');
|
|
4712
|
+
if (!nodes || !nodes.length) return;
|
|
4713
|
+
nodes.forEach((codeEl) => {
|
|
4714
|
+
this.codeScroll.scheduleScroll(codeEl, false, true);
|
|
4715
|
+
const key = { t: 'JF:forceBottom', el: codeEl, n: Math.random() };
|
|
4716
|
+
this.raf.schedule(key, () => {
|
|
4717
|
+
this.codeScroll.scrollToBottom(codeEl, false, true);
|
|
4718
|
+
try { codeEl.dataset.justFinalized = '0'; } catch (_) {}
|
|
4719
|
+
}, 'CodeScroll', 2);
|
|
4720
|
+
});
|
|
4721
|
+
} catch (_) {}
|
|
4722
|
+
}
|
|
4723
|
+
kickVisibility() {
|
|
4724
|
+
const msg = this.getMsg(false, '');
|
|
4725
|
+
if (!msg) return;
|
|
4726
|
+
if (this.codeStream.open && !this.activeCode) {
|
|
4727
|
+
this.scheduleSnapshot(msg, true);
|
|
4728
|
+
return;
|
|
4729
|
+
}
|
|
4730
|
+
const needSnap = (this.getStreamLength() !== (window.__lastSnapshotLen || 0));
|
|
4731
|
+
if (needSnap) this.scheduleSnapshot(msg, true);
|
|
4732
|
+
if (this.activeCode && this.activeCode.codeEl) {
|
|
4733
|
+
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4734
|
+
this.schedulePromoteTail(true);
|
|
4719
4735
|
}
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
try {
|
|
4725
|
-
if (!newAC || !newAC.codeEl || !newAC.codeEl.isConnected) return;
|
|
4736
|
+
}
|
|
4737
|
+
stabilizeHeaderLabel(prevAC, newAC) {
|
|
4738
|
+
try {
|
|
4739
|
+
if (!newAC || !newAC.codeEl || !newAC.codeEl.isConnected) return;
|
|
4726
4740
|
|
|
4727
|
-
|
|
4728
|
-
|
|
4741
|
+
const wrap = newAC.codeEl.closest('.code-wrapper');
|
|
4742
|
+
if (!wrap) return;
|
|
4729
4743
|
|
|
4730
|
-
|
|
4731
|
-
|
|
4744
|
+
const span = wrap.querySelector('.code-header-lang');
|
|
4745
|
+
const curLabel = (span && span.textContent ? span.textContent.trim() : '').toLowerCase();
|
|
4732
4746
|
|
|
4733
|
-
|
|
4734
|
-
if (curLabel === 'output') return;
|
|
4747
|
+
if (curLabel === 'output') return;
|
|
4735
4748
|
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4749
|
+
const tokNow = (wrap.getAttribute('data-code-lang') || '').trim().toLowerCase();
|
|
4750
|
+
const sticky = (wrap.getAttribute('data-lang-sticky') || '').trim().toLowerCase();
|
|
4751
|
+
const prev = (prevAC && prevAC.lang && prevAC.lang !== 'plaintext') ? prevAC.lang.toLowerCase() : '';
|
|
4739
4752
|
|
|
4740
|
-
|
|
4753
|
+
const valid = (t) => !!t && t !== 'plaintext' && this._isHLJSSupported(t);
|
|
4741
4754
|
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4755
|
+
let finalTok = '';
|
|
4756
|
+
if (valid(tokNow)) finalTok = tokNow;
|
|
4757
|
+
else if (valid(prev)) finalTok = prev;
|
|
4758
|
+
else if (valid(sticky)) finalTok = sticky;
|
|
4746
4759
|
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
span.textContent = 'code';
|
|
4758
|
-
}
|
|
4759
|
-
}
|
|
4760
|
-
} catch (_) { /* defensive: never break streaming path */ }
|
|
4761
|
-
}
|
|
4762
|
-
// Render a snapshot of current stream buffer into the DOM.
|
|
4763
|
-
renderSnapshot(msg) {
|
|
4764
|
-
const streaming = !!this.isStreaming;
|
|
4765
|
-
const snap = this.getMsgSnapshotRoot(msg); if (!snap) return;
|
|
4766
|
-
|
|
4767
|
-
// No-op if nothing changed and no active code
|
|
4768
|
-
const prevLen = (window.__lastSnapshotLen || 0);
|
|
4769
|
-
const curLen = this.getStreamLength();
|
|
4770
|
-
if (!this.fenceOpen && !this.activeCode && curLen === prevLen) {
|
|
4771
|
-
this.lastSnapshotTs = Utils.now();
|
|
4772
|
-
this._d('SNAPSHOT_SKIPPED_NO_DELTA', { bufLen: curLen });
|
|
4773
|
-
return;
|
|
4760
|
+
if (finalTok) {
|
|
4761
|
+
this._updateCodeLangClass(newAC.codeEl, finalTok);
|
|
4762
|
+
this._updateCodeHeaderLabel(newAC.codeEl, finalTok, finalTok);
|
|
4763
|
+
try { wrap.setAttribute('data-code-lang', finalTok); } catch (_) {}
|
|
4764
|
+
try { wrap.setAttribute('data-lang-sticky', finalTok); } catch (_) {}
|
|
4765
|
+
newAC.lang = finalTok;
|
|
4766
|
+
} else {
|
|
4767
|
+
if (span && curLabel && curLabel.length < 3) {
|
|
4768
|
+
span.textContent = 'code';
|
|
4769
|
+
}
|
|
4774
4770
|
}
|
|
4771
|
+
} catch (_) { /* defensive: never break streaming path */ }
|
|
4772
|
+
}
|
|
4775
4773
|
|
|
4776
|
-
|
|
4774
|
+
renderSnapshot(msg) {
|
|
4775
|
+
const streaming = !!this.isStreaming;
|
|
4776
|
+
const snap = this.getMsgSnapshotRoot(msg); if (!snap) return;
|
|
4777
4777
|
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4778
|
+
const prevLen = (window.__lastSnapshotLen || 0);
|
|
4779
|
+
const curLen = this.getStreamLength();
|
|
4780
|
+
if (!this.fenceOpen && !this.activeCode && curLen === prevLen) {
|
|
4781
|
+
this.lastSnapshotTs = Utils.now();
|
|
4782
|
+
this._d('SNAPSHOT_SKIPPED_NO_DELTA', { bufLen: curLen });
|
|
4783
|
+
return;
|
|
4784
|
+
}
|
|
4785
4785
|
|
|
4786
|
-
|
|
4787
|
-
const html = streaming ? this.renderer.renderStreamingSnapshot(src) : this.renderer.renderFinalSnapshot(src);
|
|
4786
|
+
const t0 = Utils.now();
|
|
4788
4787
|
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
const tmp = document.createElement('div');
|
|
4797
|
-
tmp.innerHTML = html;
|
|
4798
|
-
frag = document.createDocumentFragment();
|
|
4799
|
-
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
4800
|
-
}
|
|
4788
|
+
// When an open fence is present, append a synthetic EOL only if the current buffer
|
|
4789
|
+
// does not already end with EOL. This stabilizes markdown-it parsing without polluting
|
|
4790
|
+
// the real code tail (we will strip this EOL from the active tail right after snapshot).
|
|
4791
|
+
const allText = this.getStreamText();
|
|
4792
|
+
const needSyntheticEOL = (this.fenceOpen && !/[\r\n]$/.test(allText));
|
|
4793
|
+
this._lastInjectedEOL = !!needSyntheticEOL;
|
|
4794
|
+
const src = needSyntheticEOL ? (allText + '\n') : allText;
|
|
4801
4795
|
|
|
4802
|
-
|
|
4803
|
-
// Apply Custom Markup on the fragment only if at least one rule opted-in for stream.
|
|
4804
|
-
try {
|
|
4805
|
-
if (this.renderer && this.renderer.customMarkup && this.renderer.customMarkup.hasStreamRules()) {
|
|
4806
|
-
const MDinline = this.renderer.MD_STREAM || this.renderer.MD || null;
|
|
4807
|
-
this.renderer.customMarkup.applyStream(frag, MDinline);
|
|
4808
|
-
}
|
|
4809
|
-
} catch (_) { /* keep snapshot path resilient */ }
|
|
4796
|
+
const html = streaming ? this.renderer.renderStreamingSnapshot(src) : this.renderer.renderFinalSnapshot(src);
|
|
4810
4797
|
|
|
4811
|
-
|
|
4812
|
-
|
|
4798
|
+
let frag = null;
|
|
4799
|
+
try {
|
|
4800
|
+
const range = document.createRange();
|
|
4801
|
+
range.selectNodeContents(snap);
|
|
4802
|
+
frag = range.createContextualFragment(html);
|
|
4803
|
+
} catch (_) {
|
|
4804
|
+
const tmp = document.createElement('div');
|
|
4805
|
+
tmp.innerHTML = html;
|
|
4806
|
+
frag = document.createDocumentFragment();
|
|
4807
|
+
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
4808
|
+
}
|
|
4813
4809
|
|
|
4814
|
-
|
|
4815
|
-
|
|
4810
|
+
// (stream-aware custom markup):
|
|
4811
|
+
try {
|
|
4812
|
+
if (this.renderer && this.renderer.customMarkup && this.renderer.customMarkup.hasStreamRules()) {
|
|
4813
|
+
const MDinline = this.renderer.MD_STREAM || this.renderer.MD || null;
|
|
4814
|
+
this.renderer.customMarkup.applyStream(frag, MDinline);
|
|
4815
|
+
}
|
|
4816
|
+
} catch (_) { /* keep snapshot path resilient */ }
|
|
4816
4817
|
|
|
4817
|
-
|
|
4818
|
-
this.renderer.restoreCollapsedCode(snap);
|
|
4819
|
-
this._ensureBottomForJustFinalized(snap);
|
|
4818
|
+
this.preserveStableClosedCodes(snap, frag, this.fenceOpen === true);
|
|
4820
4819
|
|
|
4821
|
-
|
|
4822
|
-
const prevAC = this.activeCode; // remember previous active streaming state (if any)
|
|
4820
|
+
snap.replaceChildren(frag);
|
|
4823
4821
|
|
|
4824
|
-
|
|
4825
|
-
|
|
4822
|
+
this.renderer.restoreCollapsedCode(snap);
|
|
4823
|
+
this._ensureBottomForJustFinalized(snap);
|
|
4826
4824
|
|
|
4827
|
-
|
|
4828
|
-
if (prevAC && newAC) {
|
|
4829
|
-
this.rehydrateActiveCode(prevAC, newAC);
|
|
4830
|
-
this.stabilizeHeaderLabel(prevAC, newAC);
|
|
4831
|
-
}
|
|
4825
|
+
const prevAC = this.activeCode;
|
|
4832
4826
|
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
this.activeCode = null;
|
|
4836
|
-
}
|
|
4827
|
+
if (this.fenceOpen) {
|
|
4828
|
+
const newAC = this.setupActiveCodeFromSnapshot(snap);
|
|
4837
4829
|
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
this.
|
|
4830
|
+
if (prevAC && newAC) {
|
|
4831
|
+
this.rehydrateActiveCode(prevAC, newAC);
|
|
4832
|
+
this.stabilizeHeaderLabel(prevAC, newAC);
|
|
4841
4833
|
}
|
|
4842
|
-
|
|
4834
|
+
|
|
4835
|
+
this.activeCode = newAC || null;
|
|
4836
|
+
} else {
|
|
4837
|
+
this.activeCode = null;
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
if (!this.fenceOpen) {
|
|
4841
|
+
this.codeScroll.initScrollableBlocks(snap);
|
|
4842
|
+
}
|
|
4843
|
+
this.highlighter.observeNewCode(snap, {
|
|
4844
|
+
deferLastIfStreaming: true,
|
|
4845
|
+
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
4846
|
+
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
4847
|
+
}, this.activeCode);
|
|
4848
|
+
this.highlighter.observeMsgBoxes(snap, (box) => {
|
|
4849
|
+
this.highlighter.observeNewCode(box, {
|
|
4843
4850
|
deferLastIfStreaming: true,
|
|
4844
4851
|
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
4845
4852
|
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
4846
4853
|
}, this.activeCode);
|
|
4847
|
-
this.
|
|
4848
|
-
|
|
4849
|
-
deferLastIfStreaming: true,
|
|
4850
|
-
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
4851
|
-
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
4852
|
-
}, this.activeCode);
|
|
4853
|
-
this.codeScroll.initScrollableBlocks(box);
|
|
4854
|
-
});
|
|
4854
|
+
this.codeScroll.initScrollableBlocks(box);
|
|
4855
|
+
});
|
|
4855
4856
|
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
if (
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
}
|
|
4857
|
+
const mm = getMathMode();
|
|
4858
|
+
if (!this.suppressPostFinalizePass) {
|
|
4859
|
+
if (mm === 'idle') this.math.schedule(snap);
|
|
4860
|
+
else if (mm === 'always') this.math.schedule(snap, 0, true);
|
|
4861
|
+
}
|
|
4862
4862
|
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
}
|
|
4863
|
+
if (this.fenceOpen && this.activeCode && this.activeCode.codeEl) {
|
|
4864
|
+
this.codeScroll.attachHandlers(this.activeCode.codeEl);
|
|
4865
|
+
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4866
|
+
} else if (!this.fenceOpen) {
|
|
4867
|
+
this.codeScroll.initScrollableBlocks(snap);
|
|
4868
|
+
}
|
|
4870
4869
|
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
this.lastSnapshotTs = Utils.now();
|
|
4870
|
+
window.__lastSnapshotLen = this.getStreamLength();
|
|
4871
|
+
this.lastSnapshotTs = Utils.now();
|
|
4874
4872
|
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4873
|
+
const prof = this.profile();
|
|
4874
|
+
if (prof.adaptiveStep) {
|
|
4875
|
+
const maxStep = this.cfg.STREAM.SNAPSHOT_MAX_STEP || 8000;
|
|
4876
|
+
this.nextSnapshotStep = Math.min(Math.ceil(this.nextSnapshotStep * prof.growth), maxStep);
|
|
4877
|
+
} else {
|
|
4878
|
+
this.nextSnapshotStep = prof.base;
|
|
4879
|
+
}
|
|
4882
4880
|
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
this.scrollMgr.scheduleScrollFabUpdate();
|
|
4881
|
+
this.scrollMgr.scheduleScroll(true);
|
|
4882
|
+
this.scrollMgr.fabFreezeUntil = Utils.now() + this.cfg.FAB.TOGGLE_DEBOUNCE_MS;
|
|
4883
|
+
this.scrollMgr.scheduleScrollFabUpdate();
|
|
4887
4884
|
|
|
4888
|
-
|
|
4885
|
+
if (this.suppressPostFinalizePass) this.suppressPostFinalizePass = false;
|
|
4889
4886
|
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4887
|
+
const dt = Utils.now() - t0;
|
|
4888
|
+
this._d('SNAPSHOT', { fenceOpen: this.fenceOpen, activeCode: !!this.activeCode, bufLen: this.getStreamLength(), timeMs: Math.round(dt), streaming });
|
|
4889
|
+
}
|
|
4893
4890
|
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4891
|
+
getMsg(create, name_header) { return this.dom.getStreamMsg(create, name_header); }
|
|
4892
|
+
beginStream(chunk = false) {
|
|
4893
|
+
this.isStreaming = true;
|
|
4894
|
+
if (chunk) runtime.loading.hide();
|
|
4895
|
+
this.scrollMgr.userInteracted = false;
|
|
4896
|
+
this.dom.clearOutput();
|
|
4897
|
+
this.reset();
|
|
4898
|
+
this.scrollMgr.forceScrollToBottomImmediate();
|
|
4899
|
+
this.scrollMgr.scheduleScroll();
|
|
4900
|
+
this._d('BEGIN_STREAM', { chunkFlag: !!chunk });
|
|
4901
|
+
}
|
|
4902
|
+
endStream() {
|
|
4903
|
+
this.isStreaming = false;
|
|
4904
|
+
|
|
4905
|
+
const msg = this.getMsg(false, '');
|
|
4906
|
+
if (msg) this.renderSnapshot(msg);
|
|
4907
|
+
|
|
4908
|
+
this.snapshotScheduled = false;
|
|
4909
|
+
try { this.raf.cancel('SE:snapshot'); } catch (_) {}
|
|
4910
|
+
this.snapshotRAF = 0;
|
|
4911
|
+
|
|
4912
|
+
const hadActive = !!this.activeCode;
|
|
4913
|
+
if (this.activeCode) this.finalizeActiveCode();
|
|
4914
|
+
|
|
4915
|
+
if (!hadActive) {
|
|
4916
|
+
if (this.highlighter.hlQueue && this.highlighter.hlQueue.length) {
|
|
4917
|
+
this.highlighter.flush(this.activeCode);
|
|
4918
|
+
}
|
|
4919
|
+
const snap = msg ? this.getMsgSnapshotRoot(msg) : null;
|
|
4920
|
+
if (snap) this.math.renderAsync(snap);
|
|
4906
4921
|
}
|
|
4907
|
-
// End streaming session, finalize active code if present, and complete math/highlight.
|
|
4908
|
-
endStream() {
|
|
4909
|
-
// Switch to final mode before the last snapshot to allow full renderer (linkify etc.)
|
|
4910
|
-
this.isStreaming = false;
|
|
4911
4922
|
|
|
4912
|
-
|
|
4913
|
-
|
|
4923
|
+
this.fenceOpen = false; this.codeStream.open = false; this.activeCode = null; this.lastSnapshotTs = Utils.now();
|
|
4924
|
+
this.suppressPostFinalizePass = false;
|
|
4925
|
+
this._d('END_STREAM', { hadActive });
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
// NEW: eager snapshot detection for custom stream openers (e.g., <think>, <tool>)
|
|
4929
|
+
_maybeEagerSnapshotForCustomOpeners(msg, chunkStr) {
|
|
4930
|
+
try {
|
|
4931
|
+
const CM = this.renderer && this.renderer.customMarkup;
|
|
4932
|
+
if (!CM || !CM.hasStreamRules()) return;
|
|
4914
4933
|
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
this.snapshotRAF = 0;
|
|
4934
|
+
// Do not interfere with code fence streaming.
|
|
4935
|
+
if (this.fenceOpen || this.codeStream.open) return;
|
|
4918
4936
|
|
|
4919
|
-
const
|
|
4920
|
-
if (this.activeCode) this.finalizeActiveCode();
|
|
4937
|
+
const isFirstSnapshot = ((window.__lastSnapshotLen || 0) === 0);
|
|
4921
4938
|
|
|
4922
|
-
if (
|
|
4923
|
-
if
|
|
4924
|
-
|
|
4939
|
+
if (isFirstSnapshot) {
|
|
4940
|
+
// Cheap first-chunk check: if stream starts with a custom opener, snapshot immediately.
|
|
4941
|
+
let head;
|
|
4942
|
+
try { head = this.getStreamText(); } catch (_) { head = String(chunkStr || ''); }
|
|
4943
|
+
if (CM.hasStreamOpenerAtStart(head)) {
|
|
4944
|
+
this._d('CM_EAGER_SNAPSHOT_START', { openerAtStart: true });
|
|
4945
|
+
this.scheduleSnapshot(msg, true);
|
|
4946
|
+
return;
|
|
4925
4947
|
}
|
|
4926
|
-
const snap = msg ? this.getMsgSnapshotRoot(msg) : null;
|
|
4927
|
-
if (snap) this.math.renderAsync(snap); // ensure math completes eagerly but async
|
|
4928
4948
|
}
|
|
4929
4949
|
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
if (!this.activeCode && !this.fenceOpen) {
|
|
4937
|
-
try { if (document.querySelector('pre code[data-_active_stream="1"]')) this.defuseOrphanActiveBlocks(); } catch (_) {}
|
|
4950
|
+
// For later chunks: if this chunk contains any stream opener token, snapshot soon
|
|
4951
|
+
// to let CustomMarkup.applyStreamPartialOpeners extend the pending block.
|
|
4952
|
+
const rules = (CM.getRules() || []).filter(r => r && r.stream && typeof r.open === 'string');
|
|
4953
|
+
if (rules.length && CM.hasAnyOpenToken(String(chunkStr || ''), rules)) {
|
|
4954
|
+
this._d('CM_EAGER_SNAPSHOT_CHUNK', { tokenFound: true, chunkLen: (chunkStr || '').length });
|
|
4955
|
+
this.scheduleSnapshot(msg); // normal (coalesced) schedule is enough
|
|
4938
4956
|
}
|
|
4939
|
-
|
|
4957
|
+
} catch (_) {
|
|
4958
|
+
// Keep streaming resilient; never throw here.
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4940
4961
|
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4962
|
+
// Apply incoming chunk to stream buffer and update DOM when needed.
|
|
4963
|
+
applyStream(name_header, chunk, alreadyBuffered = false) {
|
|
4964
|
+
if (!this.activeCode && !this.fenceOpen) {
|
|
4965
|
+
try { if (document.querySelector('pre code[data-_active_stream="1"]')) this.defuseOrphanActiveBlocks(); } catch (_) {}
|
|
4966
|
+
}
|
|
4967
|
+
if (this.snapshotScheduled && !this.raf.isScheduled('SE:snapshot')) this.snapshotScheduled = false;
|
|
4944
4968
|
|
|
4945
|
-
|
|
4946
|
-
|
|
4969
|
+
const msg = this.getMsg(true, name_header); if (!msg || !chunk) return;
|
|
4970
|
+
const s = String(chunk);
|
|
4971
|
+
if (!alreadyBuffered) this._appendChunk(s);
|
|
4947
4972
|
|
|
4948
|
-
|
|
4973
|
+
const change = this.updateFenceHeuristic(s);
|
|
4974
|
+
const nlCount = Utils.countNewlines(s); const chunkHasNL = nlCount > 0;
|
|
4949
4975
|
|
|
4950
|
-
|
|
4951
|
-
let didImmediateOpenSnap = false;
|
|
4976
|
+
this._d('APPLY_CHUNK', { len: s.length, nl: nlCount, opened: change.opened, closed: change.closed, splitAt: change.splitAt, fenceOpenBefore: this.fenceOpen || false, codeOpenBefore: this.codeStream.open || false, rebroadcast: !!alreadyBuffered });
|
|
4952
4977
|
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4978
|
+
// Eager snapshot for custom stream openers (non-code contexts).
|
|
4979
|
+
// This ensures tags like <think> immediately turn on "pending block" behavior across the rest of the snapshot.
|
|
4980
|
+
if (!change.opened && !this.fenceOpen) {
|
|
4981
|
+
this._maybeEagerSnapshotForCustomOpeners(msg, s);
|
|
4982
|
+
}
|
|
4958
4983
|
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
}
|
|
4984
|
+
// Track if we just materialized the first code-open snapshot synchronously.
|
|
4985
|
+
let didImmediateOpenSnap = false;
|
|
4986
|
+
|
|
4987
|
+
if (change.opened) {
|
|
4988
|
+
this.codeStream.open = true; this.codeStream.lines = 0; this.codeStream.chars = 0;
|
|
4989
|
+
this.resetBudget();
|
|
4990
|
+
this.scheduleSnapshot(msg);
|
|
4991
|
+
this._d('CODE_STREAM_OPEN', { });
|
|
4992
|
+
|
|
4993
|
+
if (!this._firstCodeOpenSnapDone && !this.activeCode && ((window.__lastSnapshotLen || 0) === 0)) {
|
|
4994
|
+
try {
|
|
4995
|
+
this.renderSnapshot(msg);
|
|
4996
|
+
try { this.raf.cancel('SE:snapshot'); } catch (_) {}
|
|
4997
|
+
this.snapshotScheduled = false;
|
|
4998
|
+
this._firstCodeOpenSnapDone = true;
|
|
4999
|
+
didImmediateOpenSnap = true;
|
|
5000
|
+
this._d('CODE_OPEN_IMMEDIATE_SNAPSHOT', { bufLen: this.getStreamLength() });
|
|
5001
|
+
} catch (_) {
|
|
5002
|
+
// Normal scheduled snapshot will land soon.
|
|
4972
5003
|
}
|
|
4973
5004
|
}
|
|
5005
|
+
}
|
|
4974
5006
|
|
|
4975
|
-
|
|
4976
|
-
|
|
5007
|
+
if (this.codeStream.open) {
|
|
5008
|
+
this.codeStream.lines += nlCount; this.codeStream.chars += s.length;
|
|
4977
5009
|
|
|
4978
|
-
|
|
4979
|
-
|
|
5010
|
+
if (this.activeCode && this.activeCode.codeEl && this.activeCode.codeEl.isConnected) {
|
|
5011
|
+
let partForCode = s; let remainder = '';
|
|
4980
5012
|
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
}
|
|
5013
|
+
if (didImmediateOpenSnap) {
|
|
5014
|
+
partForCode = '';
|
|
5015
|
+
} else {
|
|
5016
|
+
if (change.closed && change.splitAt >= 0 && change.splitAt <= s.length) {
|
|
5017
|
+
partForCode = s.slice(0, change.splitAt); remainder = s.slice(change.splitAt);
|
|
4987
5018
|
}
|
|
5019
|
+
}
|
|
4988
5020
|
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
5021
|
+
if (partForCode) {
|
|
5022
|
+
this.appendToActiveTail(partForCode);
|
|
5023
|
+
this.activeCode.lines += Utils.countNewlines(partForCode);
|
|
4992
5024
|
|
|
4993
|
-
|
|
4994
|
-
|
|
5025
|
+
this.maybePromoteLanguageFromDirective();
|
|
5026
|
+
this.enforceHLStopBudget();
|
|
4995
5027
|
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
}
|
|
5028
|
+
if (!this.activeCode.haltHL) {
|
|
5029
|
+
if (partForCode.indexOf('\n') >= 0 || (this.activeCode.tailEl.textContent || '').length >= this.cfg.PROFILE_CODE.minCharsForHL) {
|
|
5030
|
+
this.schedulePromoteTail(false);
|
|
5000
5031
|
}
|
|
5001
5032
|
}
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
}
|
|
5033
|
+
}
|
|
5034
|
+
this.scrollMgr.scrollFabUpdateScheduled = false;
|
|
5035
|
+
this.scrollMgr.scheduleScroll(true);
|
|
5036
|
+
this.scrollMgr.fabFreezeUntil = Utils.now() + this.cfg.FAB.TOGGLE_DEBOUNCE_MS;
|
|
5037
|
+
this.scrollMgr.scheduleScrollFabUpdate();
|
|
5038
|
+
|
|
5039
|
+
if (change.closed) {
|
|
5040
|
+
this.finalizeActiveCode();
|
|
5041
|
+
this.codeStream.open = false; this.resetBudget(); this.scheduleSnapshot(msg);
|
|
5042
|
+
this._d('CODE_STREAM_CLOSE_FINALIZED', { remainderLen: remainder.length });
|
|
5043
|
+
if (remainder && remainder.length) { this.applyStream(name_header, remainder, true); }
|
|
5044
|
+
}
|
|
5045
|
+
return;
|
|
5046
|
+
} else {
|
|
5047
|
+
if (!this.activeCode && (this.codeStream.lines >= 2 || this.codeStream.chars >= 80)) {
|
|
5048
|
+
this.scheduleSnapshot(msg, true);
|
|
5013
5049
|
return;
|
|
5050
|
+
}
|
|
5051
|
+
if (change.closed) {
|
|
5052
|
+
this.codeStream.open = false; this.resetBudget(); this.scheduleSnapshot(msg);
|
|
5053
|
+
this._d('CODE_CLOSED_WITHOUT_ACTIVE', { sinceLastSnapMs: Math.round(Utils.now() - this.lastSnapshotTs), snapshotScheduled: this.snapshotScheduled });
|
|
5014
5054
|
} else {
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
}
|
|
5019
|
-
if (change.closed) {
|
|
5020
|
-
this.codeStream.open = false; this.resetBudget(); this.scheduleSnapshot(msg);
|
|
5021
|
-
this._d('CODE_CLOSED_WITHOUT_ACTIVE', { sinceLastSnapMs: Math.round(Utils.now() - this.lastSnapshotTs), snapshotScheduled: this.snapshotScheduled });
|
|
5022
|
-
} else {
|
|
5023
|
-
const boundary = this.hasStructuralBoundary(s);
|
|
5024
|
-
if (this.shouldSnapshotOnChunk(s, chunkHasNL, boundary)) this.scheduleSnapshot(msg);
|
|
5025
|
-
else this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
5026
|
-
}
|
|
5027
|
-
return;
|
|
5055
|
+
const boundary = this.hasStructuralBoundary(s);
|
|
5056
|
+
if (this.shouldSnapshotOnChunk(s, chunkHasNL, boundary)) this.scheduleSnapshot(msg);
|
|
5057
|
+
else this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
5028
5058
|
}
|
|
5059
|
+
return;
|
|
5029
5060
|
}
|
|
5061
|
+
}
|
|
5030
5062
|
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5063
|
+
if (change.closed) {
|
|
5064
|
+
this.codeStream.open = false; this.resetBudget(); this.scheduleSnapshot(msg);
|
|
5065
|
+
this._d('CODE_STREAM_CLOSE', { });
|
|
5066
|
+
} else {
|
|
5067
|
+
const boundary = this.hasStructuralBoundary(s);
|
|
5068
|
+
if (this.shouldSnapshotOnChunk(s, chunkHasNL, boundary)) {
|
|
5069
|
+
this.scheduleSnapshot(msg);
|
|
5070
|
+
this._d('SCHEDULE_SNAPSHOT_BOUNDARY', { boundary });
|
|
5034
5071
|
} else {
|
|
5035
|
-
|
|
5036
|
-
if (this.shouldSnapshotOnChunk(s, chunkHasNL, boundary)) {
|
|
5037
|
-
this.scheduleSnapshot(msg);
|
|
5038
|
-
this._d('SCHEDULE_SNAPSHOT_BOUNDARY', { boundary });
|
|
5039
|
-
} else {
|
|
5040
|
-
this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
5041
|
-
}
|
|
5072
|
+
this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
5042
5073
|
}
|
|
5043
5074
|
}
|
|
5044
5075
|
}
|
|
5076
|
+
}
|
|
5045
5077
|
|
|
5046
5078
|
// ==========================================================================
|
|
5047
5079
|
// 12) Stream queue
|