pygpt-net 2.6.45__py3-none-any.whl → 2.6.47__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 +12 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/controller/__init__.py +1 -3
- pygpt_net/controller/audio/audio.py +2 -0
- pygpt_net/controller/chat/text.py +2 -1
- pygpt_net/controller/debug/debug.py +11 -9
- pygpt_net/controller/dialogs/debug.py +40 -29
- pygpt_net/controller/notepad/notepad.py +0 -2
- pygpt_net/controller/theme/theme.py +5 -5
- pygpt_net/controller/ui/tabs.py +40 -2
- pygpt_net/core/debug/agent.py +19 -14
- pygpt_net/core/debug/assistants.py +22 -24
- pygpt_net/core/debug/attachments.py +11 -7
- pygpt_net/core/debug/config.py +22 -23
- pygpt_net/core/debug/context.py +63 -63
- pygpt_net/core/debug/db.py +1 -4
- pygpt_net/core/debug/events.py +14 -11
- pygpt_net/core/debug/indexes.py +41 -76
- pygpt_net/core/debug/kernel.py +11 -8
- pygpt_net/core/debug/models.py +20 -15
- pygpt_net/core/debug/plugins.py +9 -6
- pygpt_net/core/debug/presets.py +16 -11
- pygpt_net/core/debug/tabs.py +28 -22
- pygpt_net/core/debug/ui.py +25 -22
- pygpt_net/core/render/web/renderer.py +5 -2
- pygpt_net/core/tabs/tab.py +16 -3
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/config/settings.json +15 -17
- pygpt_net/data/css/style.dark.css +6 -0
- pygpt_net/data/css/web-blocks.css +4 -0
- pygpt_net/data/css/web-blocks.light.css +1 -1
- pygpt_net/data/css/web-chatgpt.css +4 -0
- pygpt_net/data/css/web-chatgpt.light.css +1 -1
- pygpt_net/data/css/web-chatgpt_wide.css +4 -0
- pygpt_net/data/css/web-chatgpt_wide.light.css +1 -1
- pygpt_net/data/js/app.js +1804 -1688
- pygpt_net/data/locale/locale.de.ini +1 -1
- pygpt_net/data/locale/locale.en.ini +1 -1
- pygpt_net/data/locale/locale.es.ini +1 -1
- pygpt_net/data/locale/locale.fr.ini +1 -1
- pygpt_net/data/locale/locale.it.ini +1 -1
- pygpt_net/data/locale/locale.pl.ini +2 -2
- pygpt_net/data/locale/locale.uk.ini +1 -1
- pygpt_net/data/locale/locale.zh.ini +1 -1
- pygpt_net/item/model.py +4 -1
- pygpt_net/js_rc.py +14303 -14540
- pygpt_net/provider/api/anthropic/__init__.py +3 -1
- pygpt_net/provider/api/anthropic/tools.py +1 -1
- pygpt_net/provider/api/google/__init__.py +7 -1
- pygpt_net/provider/api/x_ai/__init__.py +5 -1
- pygpt_net/provider/core/config/patch.py +14 -1
- pygpt_net/provider/llms/anthropic.py +37 -5
- pygpt_net/provider/llms/azure_openai.py +3 -1
- pygpt_net/provider/llms/base.py +13 -1
- pygpt_net/provider/llms/deepseek_api.py +13 -3
- pygpt_net/provider/llms/google.py +14 -1
- pygpt_net/provider/llms/hugging_face_api.py +105 -24
- pygpt_net/provider/llms/hugging_face_embedding.py +88 -0
- pygpt_net/provider/llms/hugging_face_router.py +28 -16
- pygpt_net/provider/llms/local.py +2 -0
- pygpt_net/provider/llms/mistral.py +60 -3
- pygpt_net/provider/llms/open_router.py +4 -2
- pygpt_net/provider/llms/openai.py +4 -1
- pygpt_net/provider/llms/perplexity.py +66 -5
- pygpt_net/provider/llms/utils.py +39 -0
- pygpt_net/provider/llms/voyage.py +50 -0
- pygpt_net/provider/llms/x_ai.py +70 -10
- pygpt_net/ui/widget/lists/db.py +1 -0
- pygpt_net/ui/widget/lists/debug.py +1 -0
- pygpt_net/ui/widget/tabs/body.py +23 -4
- pygpt_net/ui/widget/textarea/notepad.py +0 -4
- {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.47.dist-info}/METADATA +16 -4
- {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.47.dist-info}/RECORD +77 -74
- {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.47.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.47.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.45.dist-info → pygpt_net-2.6.47.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
|
|
@@ -1206,643 +1210,793 @@
|
|
|
1206
1210
|
this.hlQueueSet.clear(); this.hlQueue.length = 0;
|
|
1207
1211
|
}
|
|
1208
1212
|
}
|
|
1213
|
+
// ==========================================================================
|
|
1214
|
+
// 4) Custom Markup Processor
|
|
1215
|
+
// ==========================================================================
|
|
1216
|
+
|
|
1217
|
+
class CustomMarkup {
|
|
1218
|
+
constructor(cfg, logger) {
|
|
1219
|
+
this.cfg = cfg || { CUSTOM_MARKUP_RULES: [] };
|
|
1220
|
+
this.logger = logger || new Logger(cfg);
|
|
1221
|
+
this.__compiled = null;
|
|
1222
|
+
this.__hasStreamRules = false; // Fast flag to skip stream work if not needed
|
|
1223
|
+
}
|
|
1224
|
+
_d(line, ctx) { try { this.logger.debug('CM', line, ctx); } catch (_) {} }
|
|
1225
|
+
|
|
1226
|
+
// Decode HTML entities once (safe)
|
|
1227
|
+
decodeEntitiesOnce(s) {
|
|
1228
|
+
if (!s || s.indexOf('&') === -1) return String(s || '');
|
|
1229
|
+
const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
|
|
1230
|
+
ta.innerHTML = s;
|
|
1231
|
+
return ta.value;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Escape helpers
|
|
1235
|
+
_escHtml(s) {
|
|
1236
|
+
try { return Utils.escapeHtml(s); } catch (_) {
|
|
1237
|
+
return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
1238
|
+
}
|
|
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
|
+
}
|
|
1249
|
+
|
|
1250
|
+
hasAnyOpenToken(text, rules) {
|
|
1251
|
+
if (!text || !rules || !rules.length) return false;
|
|
1252
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1253
|
+
const r = rules[i];
|
|
1254
|
+
if (!r || !r.open) continue;
|
|
1255
|
+
if (text.indexOf(r.open) !== -1) return true;
|
|
1256
|
+
}
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
_materializeInnerHTML(rule, text, MD) {
|
|
1261
|
+
let payload = String(text || '');
|
|
1262
|
+
const wantsBr = !!(rule && (rule.nl2br || rule.allowBr));
|
|
1263
|
+
|
|
1264
|
+
if (rule && rule.decodeEntities && payload && payload.indexOf('&') !== -1) {
|
|
1265
|
+
try { payload = this.decodeEntitiesOnce(payload); } catch (_) { /* keep original */ }
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (wantsBr) {
|
|
1269
|
+
try { return this._escapeHtmlAllowBr(payload, { convertNewlines: !!rule.nl2br }); }
|
|
1270
|
+
catch (_) { return this._escHtml(payload); }
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
if (rule && rule.innerMode === 'markdown-inline' && MD && typeof MD.renderInline === 'function') {
|
|
1274
|
+
try { return MD.renderInline(payload); } catch (_) { return this._escHtml(payload); }
|
|
1275
|
+
}
|
|
1276
|
+
return this._escHtml(payload);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
_fragmentFromHTML(html, ctxNode) {
|
|
1280
|
+
let frag = null;
|
|
1281
|
+
try {
|
|
1282
|
+
const range = document.createRange();
|
|
1283
|
+
const ctx = (ctxNode && ctxNode.parentNode) ? ctxNode.parentNode : (document.body || document.documentElement);
|
|
1284
|
+
range.selectNode(ctx);
|
|
1285
|
+
frag = range.createContextualFragment(String(html || ''));
|
|
1286
|
+
return frag;
|
|
1287
|
+
} catch (_) {
|
|
1288
|
+
const tmp = document.createElement('div');
|
|
1289
|
+
tmp.innerHTML = String(html || '');
|
|
1290
|
+
frag = document.createDocumentFragment();
|
|
1291
|
+
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
1292
|
+
return frag;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
_replaceElementWithHTML(el, html) {
|
|
1296
|
+
if (!el || !el.parentNode) return;
|
|
1297
|
+
const parent = el.parentNode;
|
|
1298
|
+
const frag = this._fragmentFromHTML(html, el);
|
|
1299
|
+
try {
|
|
1300
|
+
parent.insertBefore(frag, el);
|
|
1301
|
+
parent.removeChild(el);
|
|
1302
|
+
} catch (_) {
|
|
1303
|
+
const tmp = document.createElement('span');
|
|
1304
|
+
tmp.innerHTML = String(html || '');
|
|
1305
|
+
while (tmp.firstChild) parent.insertBefore(tmp.firstChild, el);
|
|
1306
|
+
parent.removeChild(el);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
compile(rules) {
|
|
1311
|
+
const src = Array.isArray(rules) ? rules : (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
|
|
1312
|
+
const compiled = [];
|
|
1313
|
+
let hasStream = false;
|
|
1314
|
+
|
|
1315
|
+
for (const r of src) {
|
|
1316
|
+
if (!r || typeof r.open !== 'string' || typeof r.close !== 'string') continue;
|
|
1317
|
+
|
|
1318
|
+
const tag = (r.tag || 'span').toLowerCase();
|
|
1319
|
+
const className = (r.className || r.class || '').trim();
|
|
1320
|
+
const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
|
|
1321
|
+
|
|
1322
|
+
const stream = !!(r.stream === true);
|
|
1323
|
+
const openReplace = String((r.openReplace != null ? r.openReplace : (r.openReplace || '')) || '');
|
|
1324
|
+
const closeReplace = String((r.closeReplace != null ? r.closeReplace : (r.closeReplace || '')) || '');
|
|
1325
|
+
|
|
1326
|
+
const decodeEntities = (typeof r.decodeEntities === 'boolean')
|
|
1327
|
+
? r.decodeEntities
|
|
1328
|
+
: ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
|
|
1329
|
+
|
|
1330
|
+
let phaseRaw = (typeof r.phase === 'string') ? r.phase.toLowerCase() : '';
|
|
1331
|
+
if (phaseRaw !== 'source' && phaseRaw !== 'html' && phaseRaw !== 'both') phaseRaw = '';
|
|
1332
|
+
const looksLikeFence = (openReplace.indexOf('```') !== -1) || (closeReplace.indexOf('```') !== -1);
|
|
1333
|
+
const phase = phaseRaw || (looksLikeFence ? 'source' : 'html');
|
|
1334
|
+
|
|
1335
|
+
const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
|
|
1336
|
+
const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
|
|
1337
|
+
const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
|
|
1338
|
+
|
|
1339
|
+
const nl2br = !!r.nl2br;
|
|
1340
|
+
const allowBr = !!r.allowBr;
|
|
1341
|
+
|
|
1342
|
+
const item = {
|
|
1343
|
+
name: r.name || tag,
|
|
1344
|
+
tag, className, innerMode,
|
|
1345
|
+
open: r.open, close: r.close,
|
|
1346
|
+
decodeEntities,
|
|
1347
|
+
re, reFull, reFullTrim,
|
|
1348
|
+
stream,
|
|
1349
|
+
openReplace, closeReplace,
|
|
1350
|
+
phase,
|
|
1351
|
+
isSourceFence: looksLikeFence,
|
|
1352
|
+
nl2br, allowBr
|
|
1353
|
+
};
|
|
1354
|
+
compiled.push(item);
|
|
1355
|
+
if (stream) hasStream = true;
|
|
1356
|
+
this._d('COMPILE_RULE', { name: item.name, phase: item.phase, stream: item.stream, nl2br: item.nl2br, allowBr: item.allowBr });
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (compiled.length === 0) {
|
|
1360
|
+
const open = '[!cmd]', close = '[/!cmd]';
|
|
1361
|
+
const item = {
|
|
1362
|
+
name: 'cmd', tag: 'p', className: 'cmd', innerMode: 'text', open, close,
|
|
1363
|
+
decodeEntities: true,
|
|
1364
|
+
re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
|
|
1365
|
+
reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
|
|
1366
|
+
reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$'),
|
|
1367
|
+
stream: false,
|
|
1368
|
+
openReplace: '', closeReplace: '',
|
|
1369
|
+
phase: 'html', isSourceFence: false,
|
|
1370
|
+
nl2br: false, allowBr: false
|
|
1371
|
+
};
|
|
1372
|
+
compiled.push(item);
|
|
1373
|
+
this._d('COMPILE_RULE_FALLBACK', { name: item.name });
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
this.__hasStreamRules = hasStream;
|
|
1377
|
+
return compiled;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
transformSource(src, opts) {
|
|
1381
|
+
let s = String(src || '');
|
|
1382
|
+
this.ensureCompiled();
|
|
1383
|
+
const rules = this.__compiled;
|
|
1384
|
+
if (!rules || !rules.length) return s;
|
|
1385
|
+
|
|
1386
|
+
const candidates = [];
|
|
1387
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1388
|
+
const r = rules[i];
|
|
1389
|
+
if (!r) continue;
|
|
1390
|
+
if ((r.phase === 'source' || r.phase === 'both') && (r.openReplace || r.closeReplace)) candidates.push(r);
|
|
1391
|
+
}
|
|
1392
|
+
if (!candidates.length) return s;
|
|
1393
|
+
|
|
1394
|
+
const fences = this._findFenceRanges(s);
|
|
1395
|
+
if (!fences.length) {
|
|
1396
|
+
return this._applySourceReplacementsInChunk(s, s, 0, candidates);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
let out = '';
|
|
1400
|
+
let last = 0;
|
|
1401
|
+
for (let k = 0; k < fences.length; k++) {
|
|
1402
|
+
const [a, b] = fences[k];
|
|
1403
|
+
if (a > last) {
|
|
1404
|
+
const chunk = s.slice(last, a);
|
|
1405
|
+
out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
|
|
1406
|
+
}
|
|
1407
|
+
out += s.slice(a, b);
|
|
1408
|
+
last = b;
|
|
1409
|
+
}
|
|
1410
|
+
if (last < s.length) {
|
|
1411
|
+
const tail = s.slice(last);
|
|
1412
|
+
out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
|
|
1413
|
+
}
|
|
1414
|
+
return out;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
getSourceFenceSpecs() {
|
|
1418
|
+
this.ensureCompiled();
|
|
1419
|
+
const rules = this.__compiled || [];
|
|
1420
|
+
const out = [];
|
|
1421
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1422
|
+
const r = rules[i];
|
|
1423
|
+
if (!r || !r.isSourceFence) continue;
|
|
1424
|
+
if (r.phase !== 'source' && r.phase !== 'both') continue;
|
|
1425
|
+
out.push({ open: r.open, close: r.close });
|
|
1426
|
+
}
|
|
1427
|
+
return out;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
ensureCompiled() {
|
|
1431
|
+
if (!this.__compiled) {
|
|
1432
|
+
this.__compiled = this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
|
|
1433
|
+
this._d('ENSURE_COMPILED', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
|
|
1434
|
+
}
|
|
1435
|
+
return this.__compiled;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
setRules(rules) {
|
|
1439
|
+
this.__compiled = this.compile(rules);
|
|
1440
|
+
window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() : (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
|
|
1441
|
+
this._d('SET_RULES', { count: this.__compiled.length, hasStream: this.__hasStreamRules });
|
|
1442
|
+
}
|
|
1443
|
+
getRules() {
|
|
1444
|
+
const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice()
|
|
1445
|
+
: (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
|
|
1446
|
+
this._d('GET_RULES', { count: list.length });
|
|
1447
|
+
return list;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
hasStreamRules() {
|
|
1451
|
+
this.ensureCompiled();
|
|
1452
|
+
return !!this.__hasStreamRules;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
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
|
+
|
|
1469
|
+
isInsideForbiddenContext(node) {
|
|
1470
|
+
const p = node.parentElement; if (!p) return true;
|
|
1471
|
+
return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
|
|
1472
|
+
}
|
|
1473
|
+
isInsideForbiddenElement(el) {
|
|
1474
|
+
if (!el) return true;
|
|
1475
|
+
return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
findNextMatch(text, from, rules) {
|
|
1479
|
+
let best = null;
|
|
1480
|
+
for (const rule of rules) {
|
|
1481
|
+
rule.re.lastIndex = from;
|
|
1482
|
+
const m = rule.re.exec(text);
|
|
1483
|
+
if (m) {
|
|
1484
|
+
const start = m.index, end = rule.re.lastIndex;
|
|
1485
|
+
if (!best || start < best.start) best = { rule, start, end, inner: m[1] || '' };
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return best;
|
|
1489
|
+
}
|
|
1490
|
+
findFullMatch(text, rules) {
|
|
1491
|
+
for (const rule of rules) {
|
|
1492
|
+
if (rule.reFull) {
|
|
1493
|
+
const m = rule.reFull.exec(text);
|
|
1494
|
+
if (m) return { rule, inner: m[1] || '' };
|
|
1495
|
+
} else {
|
|
1496
|
+
rule.re.lastIndex = 0;
|
|
1497
|
+
const m = rule.re.exec(text);
|
|
1498
|
+
if (m && m.index === 0 && (rule.re.lastIndex === text.length)) {
|
|
1499
|
+
const m2 = rule.re.exec(text);
|
|
1500
|
+
if (!m2) return { rule: rule, inner: m[1] || '' };
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
setInnerByMode(el, mode, text, MD, decodeEntities = false, rule = null) {
|
|
1508
|
+
let payload = String(text || '');
|
|
1509
|
+
const wantsBr = !!(rule && (rule.nl2br || rule.allowBr));
|
|
1510
|
+
|
|
1511
|
+
if (decodeEntities && payload && payload.indexOf('&') !== -1) {
|
|
1512
|
+
try { payload = this.decodeEntitiesOnce(payload); } catch (_) {}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (wantsBr) {
|
|
1516
|
+
el.innerHTML = this._escapeHtmlAllowBr(payload, { convertNewlines: !!(rule && rule.nl2br) });
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (mode === 'markdown-inline' && typeof window.markdownit !== 'undefined') {
|
|
1521
|
+
try {
|
|
1522
|
+
if (MD && typeof MD.renderInline === 'function') { el.innerHTML = MD.renderInline(payload); return; }
|
|
1523
|
+
const tempMD = window.markdownit({ html: false, linkify: true, breaks: true, highlight: () => '' });
|
|
1524
|
+
el.innerHTML = tempMD.renderInline(payload); return;
|
|
1525
|
+
} catch (_) {}
|
|
1526
|
+
}
|
|
1527
|
+
el.textContent = payload;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
_tryReplaceFullParagraph(el, rules, MD) {
|
|
1531
|
+
if (!el || el.tagName !== 'P') return false;
|
|
1532
|
+
if (this.isInsideForbiddenElement(el)) {
|
|
1533
|
+
this._d('P_SKIP_FORBIDDEN', { tag: el.tagName });
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
const t = el.textContent || '';
|
|
1537
|
+
if (!this.hasAnyOpenToken(t, rules)) return false;
|
|
1538
|
+
|
|
1539
|
+
for (const rule of rules) {
|
|
1540
|
+
if (!rule) continue;
|
|
1541
|
+
const m = rule.reFullTrim ? rule.reFullTrim.exec(t) : null;
|
|
1542
|
+
if (!m) continue;
|
|
1543
|
+
|
|
1544
|
+
const innerText = m[1] || '';
|
|
1545
|
+
if (rule.phase !== 'html' && rule.phase !== 'both') continue;
|
|
1546
|
+
|
|
1547
|
+
if (rule.openReplace || rule.closeReplace) {
|
|
1548
|
+
const innerHTML = this._materializeInnerHTML(rule, innerText, MD);
|
|
1549
|
+
const html = String(rule.openReplace || '') + innerHTML + String(rule.closeReplace || '');
|
|
1550
|
+
this._replaceElementWithHTML(el, html);
|
|
1551
|
+
this._d('P_REPLACED_AS_HTML', { rule: rule.name });
|
|
1552
|
+
return true;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const outTag = (rule.tag && typeof rule.tag === 'string') ? rule.tag.toLowerCase() : 'span';
|
|
1556
|
+
const out = document.createElement(outTag === 'p' ? 'p' : outTag);
|
|
1557
|
+
if (rule.className) out.className = rule.className;
|
|
1558
|
+
out.setAttribute('data-cm', rule.name);
|
|
1559
|
+
this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities, rule);
|
|
1560
|
+
|
|
1561
|
+
try { el.replaceWith(out); } catch (_) {
|
|
1562
|
+
const par = el.parentNode; if (par) par.replaceChild(out, el);
|
|
1563
|
+
}
|
|
1564
|
+
this._d('P_REPLACED', { rule: rule.name, asTag: outTag });
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
this._d('P_NO_FULL_MATCH', { preview: this.logger.pv(t, 160) });
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
applyRules(root, MD, rules) {
|
|
1572
|
+
if (!root || !rules || !rules.length) return;
|
|
1573
|
+
|
|
1574
|
+
const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
|
|
1575
|
+
|
|
1576
|
+
// Phase 1: tolerant <p> replacements
|
|
1577
|
+
try {
|
|
1578
|
+
const paragraphs = (typeof scope.querySelectorAll === 'function') ? scope.querySelectorAll('p') : [];
|
|
1579
|
+
this._d('P_TOLERANT_SCAN_START', { count: paragraphs.length });
|
|
1580
|
+
|
|
1581
|
+
if (paragraphs && paragraphs.length) {
|
|
1582
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
1583
|
+
const p = paragraphs[i];
|
|
1584
|
+
if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
|
|
1585
|
+
const tc = p && (p.textContent || '');
|
|
1586
|
+
if (!tc || !this.hasAnyOpenToken(tc, rules)) continue;
|
|
1587
|
+
if (this.isInsideForbiddenElement(p)) continue;
|
|
1588
|
+
this._tryReplaceFullParagraph(p, rules, MD);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
} catch (e) {
|
|
1592
|
+
this._d('P_TOLERANT_SCAN_ERR', String(e));
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Phase 2: per-text-node inline replacements
|
|
1596
|
+
const self = this;
|
|
1597
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
1598
|
+
acceptNode: (node) => {
|
|
1599
|
+
const val = node && node.nodeValue ? node.nodeValue : '';
|
|
1600
|
+
if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
|
|
1601
|
+
if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
|
|
1602
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
let node;
|
|
1607
|
+
while ((node = walker.nextNode())) {
|
|
1608
|
+
const text = node.nodeValue;
|
|
1609
|
+
if (!text || !this.hasAnyOpenToken(text, rules)) continue;
|
|
1610
|
+
const parent = node.parentElement;
|
|
1611
|
+
|
|
1612
|
+
if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
|
|
1613
|
+
const fm = this.findFullMatch(text, rules);
|
|
1614
|
+
if (fm) {
|
|
1615
|
+
if ((fm.rule.phase === 'html' || fm.rule.phase === 'both') && (fm.rule.openReplace || fm.rule.closeReplace)) {
|
|
1616
|
+
const innerHTML = this._materializeInnerHTML(fm.rule, fm.inner, MD);
|
|
1617
|
+
const html = String(fm.rule.openReplace || '') + innerHTML + String(fm.rule.closeReplace || '');
|
|
1618
|
+
this._replaceElementWithHTML(parent, html);
|
|
1619
|
+
this._d('WALKER_FULL_REPLACE_HTML', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (fm.rule.tag === 'p') {
|
|
1623
|
+
const out = document.createElement('p');
|
|
1624
|
+
if (fm.rule.className) out.className = fm.rule.className;
|
|
1625
|
+
out.setAttribute('data-cm', fm.rule.name);
|
|
1626
|
+
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities, fm.rule);
|
|
1627
|
+
try { parent.replaceWith(out); } catch (_) {
|
|
1628
|
+
const par = parent.parentNode; if (par) par.replaceChild(out, parent);
|
|
1629
|
+
}
|
|
1630
|
+
this._d('WALKER_FULL_REPLACE', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1209
1635
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
this._d('P_TOLERANT_SCAN_START', { count: paragraphs.length });
|
|
1575
|
-
|
|
1576
|
-
if (paragraphs && paragraphs.length) {
|
|
1577
|
-
for (let i = 0; i < paragraphs.length; i++) {
|
|
1578
|
-
const p = paragraphs[i];
|
|
1579
|
-
if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
|
|
1580
|
-
const tc = p && (p.textContent || '');
|
|
1581
|
-
if (!tc || !this.hasAnyOpenToken(tc, rules)) continue;
|
|
1582
|
-
// Skip paragraphs inside forbidden contexts (includes lists now)
|
|
1583
|
-
if (this.isInsideForbiddenElement(p)) continue;
|
|
1584
|
-
this._tryReplaceFullParagraph(p, rules, MD);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
} catch (e) {
|
|
1588
|
-
this._d('P_TOLERANT_SCAN_ERR', String(e));
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
// Phase 2: legacy per-text-node pass for partial inline cases.
|
|
1592
|
-
const self = this;
|
|
1593
|
-
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
1594
|
-
acceptNode: (node) => {
|
|
1595
|
-
const val = node && node.nodeValue ? node.nodeValue : '';
|
|
1596
|
-
if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
|
|
1597
|
-
if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
|
|
1598
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
|
|
1602
|
-
let node;
|
|
1603
|
-
while ((node = walker.nextNode())) {
|
|
1604
|
-
const text = node.nodeValue;
|
|
1605
|
-
if (!text || !this.hasAnyOpenToken(text, rules)) continue; // quick skip
|
|
1606
|
-
const parent = node.parentElement;
|
|
1607
|
-
|
|
1608
|
-
// Entire text node equals one full match and parent is <p>.
|
|
1609
|
-
if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
|
|
1610
|
-
const fm = this.findFullMatch(text, rules);
|
|
1611
|
-
if (fm) {
|
|
1612
|
-
// If explicit HTML replacements are provided, swap <p> for exact HTML (only for html/both phase).
|
|
1613
|
-
if ((fm.rule.phase === 'html' || fm.rule.phase === 'both') && (fm.rule.openReplace || fm.rule.closeReplace)) {
|
|
1614
|
-
const innerHTML = this._materializeInnerHTML(fm.rule, fm.inner, MD);
|
|
1615
|
-
const html = String(fm.rule.openReplace || '') + innerHTML + String(fm.rule.closeReplace || '');
|
|
1616
|
-
this._replaceElementWithHTML(parent, html);
|
|
1617
|
-
this._d('WALKER_FULL_REPLACE_HTML', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1618
|
-
continue;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
// Backward-compatible: only replace as <p> when rule tag is 'p'
|
|
1622
|
-
if (fm.rule.tag === 'p') {
|
|
1623
|
-
const out = document.createElement('p');
|
|
1624
|
-
if (fm.rule.className) out.className = fm.rule.className;
|
|
1625
|
-
out.setAttribute('data-cm', fm.rule.name);
|
|
1626
|
-
this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities);
|
|
1627
|
-
try { parent.replaceWith(out); } catch (_) {
|
|
1628
|
-
const par = parent.parentNode; if (par) par.replaceChild(out, parent);
|
|
1629
|
-
}
|
|
1630
|
-
this._d('WALKER_FULL_REPLACE', { rule: fm.rule.name, preview: this.logger.pv(text, 160) });
|
|
1631
|
-
continue;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// General inline replacement inside the text node (span-like or HTML-replace).
|
|
1637
|
-
let i = 0;
|
|
1638
|
-
let didReplace = false;
|
|
1639
|
-
const frag = document.createDocumentFragment();
|
|
1640
|
-
|
|
1641
|
-
while (i < text.length) {
|
|
1642
|
-
const m = this.findNextMatch(text, i, rules);
|
|
1643
|
-
if (!m) break;
|
|
1644
|
-
|
|
1645
|
-
if (m.start > i) {
|
|
1646
|
-
frag.appendChild(document.createTextNode(text.slice(i, m.start)));
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
// If HTML replacements are provided, build exact HTML around processed inner – only for html/both phase.
|
|
1650
|
-
if ((m.rule.openReplace || m.rule.closeReplace) && (m.rule.phase === 'html' || m.rule.phase === 'both')) {
|
|
1651
|
-
const innerHTML = this._materializeInnerHTML(m.rule, m.inner, MD);
|
|
1652
|
-
const html = String(m.rule.openReplace || '') + innerHTML + String(m.rule.closeReplace || '');
|
|
1653
|
-
const part = this._fragmentFromHTML(html, node);
|
|
1654
|
-
frag.appendChild(part);
|
|
1655
|
-
this._d('WALKER_INLINE_MATCH_HTML', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1656
|
-
i = m.end; didReplace = true; continue;
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
// If rule is not html-phase, do NOT inject open/close replacements here (source-only rules are handled pre-md).
|
|
1660
|
-
if (m.rule.openReplace || m.rule.closeReplace) {
|
|
1661
|
-
// Source-only replacement met in DOM pass – keep original text verbatim for this match.
|
|
1662
|
-
frag.appendChild(document.createTextNode(text.slice(m.start, m.end)));
|
|
1663
|
-
this._d('WALKER_INLINE_SKIP_SOURCE_PHASE_HTML', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1664
|
-
i = m.end; didReplace = true; continue;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// Element-based inline replacement (original behavior).
|
|
1668
|
-
const tag = (m.rule.tag === 'p') ? 'span' : m.rule.tag;
|
|
1669
|
-
const el = document.createElement(tag);
|
|
1670
|
-
if (m.rule.className) el.className = m.rule.className;
|
|
1671
|
-
el.setAttribute('data-cm', m.rule.name);
|
|
1672
|
-
this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities);
|
|
1673
|
-
frag.appendChild(el);
|
|
1674
|
-
this._d('WALKER_INLINE_MATCH', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1675
|
-
|
|
1676
|
-
i = m.end;
|
|
1677
|
-
didReplace = true;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
if (!didReplace) continue;
|
|
1681
|
-
|
|
1682
|
-
if (i < text.length) {
|
|
1683
|
-
frag.appendChild(document.createTextNode(text.slice(i)));
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
const parentNode = node.parentNode;
|
|
1687
|
-
if (parentNode) {
|
|
1688
|
-
parentNode.replaceChild(frag, node);
|
|
1689
|
-
this._d('WALKER_INLINE_DONE', { preview: this.logger.pv(text, 120) });
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// Public API: apply custom markup for full (static) paths – unchanged behavior.
|
|
1695
|
-
apply(root, MD) {
|
|
1696
|
-
this.ensureCompiled();
|
|
1697
|
-
this.applyRules(root, MD, this.__compiled);
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// Public API: apply only stream-enabled rules (used in snapshots).
|
|
1701
|
-
applyStream(root, MD) {
|
|
1702
|
-
this.ensureCompiled();
|
|
1703
|
-
if (!this.__hasStreamRules) return;
|
|
1704
|
-
const rules = this.__compiled.filter(r => !!r.stream);
|
|
1705
|
-
if (!rules.length) return;
|
|
1706
|
-
this.applyRules(root, MD, rules);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
// -----------------------------
|
|
1710
|
-
// INTERNAL HELPERS (NEW)
|
|
1711
|
-
// -----------------------------
|
|
1712
|
-
|
|
1713
|
-
// Scan source and return ranges [start, end) of fenced code blocks (``` or ~~~).
|
|
1714
|
-
// Matches Markdown fences at line-start with up to 3 spaces/tabs indentation.
|
|
1715
|
-
_findFenceRanges(s) {
|
|
1716
|
-
const ranges = [];
|
|
1717
|
-
const n = s.length;
|
|
1718
|
-
let i = 0;
|
|
1719
|
-
let inFence = false;
|
|
1720
|
-
let fenceMark = '';
|
|
1721
|
-
let fenceLen = 0;
|
|
1722
|
-
let startLineStart = 0;
|
|
1723
|
-
|
|
1724
|
-
while (i < n) {
|
|
1725
|
-
const lineStart = i;
|
|
1726
|
-
// Find line end and newline length
|
|
1727
|
-
let j = lineStart;
|
|
1728
|
-
while (j < n && s.charCodeAt(j) !== 10 && s.charCodeAt(j) !== 13) j++;
|
|
1729
|
-
const lineEnd = j;
|
|
1730
|
-
let nl = 0;
|
|
1731
|
-
if (j < n) {
|
|
1732
|
-
if (s.charCodeAt(j) === 13 && j + 1 < n && s.charCodeAt(j + 1) === 10) nl = 2;
|
|
1733
|
-
else nl = 1;
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// Compute indentation up to 3 "spaces" (tabs count as 1 here – safe heuristic)
|
|
1737
|
-
let k = lineStart;
|
|
1738
|
-
let indent = 0;
|
|
1739
|
-
while (k < lineEnd) {
|
|
1740
|
-
const c = s.charCodeAt(k);
|
|
1741
|
-
if (c === 32 /* space */) { indent++; if (indent > 3) break; k++; }
|
|
1742
|
-
else if (c === 9 /* tab */) { indent++; if (indent > 3) break; k++; }
|
|
1743
|
-
else break;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
if (!inFence) {
|
|
1747
|
-
if (indent <= 3 && k < lineEnd) {
|
|
1748
|
-
const ch = s.charCodeAt(k);
|
|
1749
|
-
if (ch === 0x60 /* ` */ || ch === 0x7E /* ~ */) {
|
|
1750
|
-
const mark = String.fromCharCode(ch);
|
|
1751
|
-
let m = k;
|
|
1752
|
-
while (m < lineEnd && s.charCodeAt(m) === ch) m++;
|
|
1753
|
-
const run = m - k;
|
|
1754
|
-
if (run >= 3) {
|
|
1755
|
-
inFence = true;
|
|
1756
|
-
fenceMark = mark;
|
|
1757
|
-
fenceLen = run;
|
|
1758
|
-
startLineStart = lineStart;
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
} else {
|
|
1763
|
-
if (indent <= 3 && k < lineEnd && s.charCodeAt(k) === fenceMark.charCodeAt(0)) {
|
|
1764
|
-
let m = k;
|
|
1765
|
-
while (m < lineEnd && s.charCodeAt(m) === fenceMark.charCodeAt(0)) m++;
|
|
1766
|
-
const run = m - k;
|
|
1767
|
-
if (run >= fenceLen) {
|
|
1768
|
-
// Only whitespace is allowed after closing fence on the same line
|
|
1769
|
-
let onlyWS = true;
|
|
1770
|
-
for (let t = m; t < lineEnd; t++) {
|
|
1771
|
-
const cc = s.charCodeAt(t);
|
|
1772
|
-
if (cc !== 32 && cc !== 9) { onlyWS = false; break; }
|
|
1773
|
-
}
|
|
1774
|
-
if (onlyWS) {
|
|
1775
|
-
const endIdx = lineEnd + nl; // include trailing newline if present
|
|
1776
|
-
ranges.push([startLineStart, endIdx]);
|
|
1777
|
-
inFence = false; fenceMark = ''; fenceLen = 0; startLineStart = 0;
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
i = lineEnd + nl;
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
// If EOF while still in fence, mark until end of string.
|
|
1786
|
-
if (inFence) ranges.push([startLineStart, n]);
|
|
1787
|
-
return ranges;
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// Check if match starts at "top-level" of a line:
|
|
1791
|
-
// - up to 3 leading spaces/tabs allowed
|
|
1792
|
-
// - not a list item marker ("- ", "+ ", "* ", "1. ", "1) ") and not a blockquote ("> ")
|
|
1793
|
-
// - nothing else precedes the token on the same line
|
|
1794
|
-
_isTopLevelLineInSource(s, absIdx) {
|
|
1795
|
-
let ls = absIdx;
|
|
1796
|
-
while (ls > 0) {
|
|
1797
|
-
const ch = s.charCodeAt(ls - 1);
|
|
1798
|
-
if (ch === 10 /* \n */ || ch === 13 /* \r */) break;
|
|
1799
|
-
ls--;
|
|
1800
|
-
}
|
|
1801
|
-
const prefix = s.slice(ls, absIdx);
|
|
1802
|
-
|
|
1803
|
-
// Strip up to 3 leading "spaces" (tabs treated as 1 – acceptable heuristic)
|
|
1804
|
-
let i = 0, indent = 0;
|
|
1805
|
-
while (i < prefix.length) {
|
|
1806
|
-
const c = prefix.charCodeAt(i);
|
|
1807
|
-
if (c === 32) { indent++; if (indent > 3) break; i++; }
|
|
1808
|
-
else if (c === 9) { indent++; if (indent > 3) break; i++; }
|
|
1809
|
-
else break;
|
|
1810
|
-
}
|
|
1811
|
-
if (indent > 3) return false;
|
|
1812
|
-
const rest = prefix.slice(i);
|
|
1813
|
-
|
|
1814
|
-
// Reject lists/blockquote
|
|
1815
|
-
if (/^>\s?/.test(rest)) return false;
|
|
1816
|
-
if (/^[-+*]\s/.test(rest)) return false;
|
|
1817
|
-
if (/^\d+[.)]\s/.test(rest)) return false;
|
|
1818
|
-
|
|
1819
|
-
// If any other non-whitespace text precedes the token on this line – not top-level
|
|
1820
|
-
if (rest.trim().length > 0) return false;
|
|
1821
|
-
|
|
1822
|
-
return true;
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
// Apply source-phase replacements to one outside-of-fence chunk with top-level guard.
|
|
1826
|
-
_applySourceReplacementsInChunk(full, chunk, baseOffset, rules) {
|
|
1827
|
-
let t = chunk;
|
|
1828
|
-
for (let i = 0; i < rules.length; i++) {
|
|
1829
|
-
const r = rules[i];
|
|
1830
|
-
if (!r || !(r.openReplace || r.closeReplace)) continue;
|
|
1831
|
-
try {
|
|
1832
|
-
r.re.lastIndex = 0;
|
|
1833
|
-
t = t.replace(r.re, (match, inner, offset /*, ...rest*/) => {
|
|
1834
|
-
const abs = baseOffset + (offset | 0);
|
|
1835
|
-
// Only apply when opener is at top-level on that line (not in lists/blockquote)
|
|
1836
|
-
if (!this._isTopLevelLineInSource(full, abs)) return match;
|
|
1837
|
-
const open = r.openReplace || '';
|
|
1838
|
-
const close = r.closeReplace || '';
|
|
1839
|
-
return open + (inner || '') + close;
|
|
1840
|
-
});
|
|
1841
|
-
} catch (_) { /* keep chunk as is on any error */ }
|
|
1842
|
-
}
|
|
1843
|
-
return t;
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1636
|
+
let i = 0;
|
|
1637
|
+
let didReplace = false;
|
|
1638
|
+
const frag = document.createDocumentFragment();
|
|
1639
|
+
|
|
1640
|
+
while (i < text.length) {
|
|
1641
|
+
const m = this.findNextMatch(text, i, rules);
|
|
1642
|
+
if (!m) break;
|
|
1643
|
+
|
|
1644
|
+
if (m.start > i) {
|
|
1645
|
+
frag.appendChild(document.createTextNode(text.slice(i, m.start)));
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if ((m.rule.openReplace || m.rule.closeReplace) && (m.rule.phase === 'html' || m.rule.phase === 'both')) {
|
|
1649
|
+
const innerHTML = this._materializeInnerHTML(m.rule, m.inner, MD);
|
|
1650
|
+
const html = String(m.rule.openReplace || '') + innerHTML + String(m.rule.closeReplace || '');
|
|
1651
|
+
const part = this._fragmentFromHTML(html, node);
|
|
1652
|
+
frag.appendChild(part);
|
|
1653
|
+
this._d('WALKER_INLINE_MATCH_HTML', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1654
|
+
i = m.end; didReplace = true; continue;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
if (m.rule.openReplace || m.rule.closeReplace) {
|
|
1658
|
+
frag.appendChild(document.createTextNode(text.slice(m.start, m.end)));
|
|
1659
|
+
this._d('WALKER_INLINE_SKIP_SOURCE_PHASE_HTML', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1660
|
+
i = m.end; didReplace = true; continue;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const tag = (m.rule.tag === 'p') ? 'span' : m.rule.tag;
|
|
1664
|
+
const el = document.createElement(tag);
|
|
1665
|
+
if (m.rule.className) el.className = m.rule.className;
|
|
1666
|
+
el.setAttribute('data-cm', m.rule.name);
|
|
1667
|
+
this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities, m.rule);
|
|
1668
|
+
frag.appendChild(el);
|
|
1669
|
+
this._d('WALKER_INLINE_MATCH', { rule: m.rule.name, start: m.start, end: m.end });
|
|
1670
|
+
|
|
1671
|
+
i = m.end;
|
|
1672
|
+
didReplace = true;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (!didReplace) continue;
|
|
1676
|
+
if (i < text.length) frag.appendChild(document.createTextNode(text.slice(i)));
|
|
1677
|
+
|
|
1678
|
+
const parentNode = node.parentNode;
|
|
1679
|
+
if (parentNode) {
|
|
1680
|
+
parentNode.replaceChild(frag, node);
|
|
1681
|
+
this._d('WALKER_INLINE_DONE', { preview: this.logger.pv(text, 120) });
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Public API: full pass (static)
|
|
1687
|
+
apply(root, MD) {
|
|
1688
|
+
this.ensureCompiled();
|
|
1689
|
+
this.applyRules(root, MD, this.__compiled);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Public API: stream pass (stream-enabled rules only)
|
|
1693
|
+
applyStream(root, MD) {
|
|
1694
|
+
this.ensureCompiled();
|
|
1695
|
+
if (!this.__hasStreamRules) return;
|
|
1696
|
+
const rules = this.__compiled.filter(r => !!r.stream);
|
|
1697
|
+
if (!rules.length) return;
|
|
1698
|
+
|
|
1699
|
+
// 1) Normal html-phase replacements where both tokens are within one node/paragraph
|
|
1700
|
+
this.applyRules(root, MD, rules);
|
|
1701
|
+
|
|
1702
|
+
// 2) If only opener is present, start pending block and wrap the remainder of the snapshot
|
|
1703
|
+
try { this.applyStreamPartialOpeners(root, MD, rules); } catch (_) {}
|
|
1704
|
+
|
|
1705
|
+
// 3) If a pending block exists and we can see a closer now – finalize immediately
|
|
1706
|
+
try { this.applyStreamFinalizeClosers(root, rules); } catch (_) {}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// Streaming: begin pending wrapper when opener is unmatched in the same text node.
|
|
1710
|
+
applyStreamPartialOpeners(root, MD, rulesAll) {
|
|
1711
|
+
if (!root) return;
|
|
1712
|
+
|
|
1713
|
+
const rules = (rulesAll || []).filter(r =>
|
|
1714
|
+
(r && (r.phase === 'html' || r.phase === 'both') && !(r.openReplace || r.closeReplace) && r.open && r.close)
|
|
1715
|
+
);
|
|
1716
|
+
if (!rules.length) return;
|
|
1717
|
+
|
|
1718
|
+
const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
|
|
1719
|
+
const self = this;
|
|
1720
|
+
|
|
1721
|
+
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT, {
|
|
1722
|
+
acceptNode(node) {
|
|
1723
|
+
const val = node && node.nodeValue ? node.nodeValue : '';
|
|
1724
|
+
if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
|
|
1725
|
+
if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
|
|
1726
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
let node;
|
|
1731
|
+
while ((node = walker.nextNode())) {
|
|
1732
|
+
const text = node.nodeValue || '';
|
|
1733
|
+
if (!text) continue;
|
|
1734
|
+
|
|
1735
|
+
let best = null; // { rule, start }
|
|
1736
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1737
|
+
const r = rules[i];
|
|
1738
|
+
if (!r || !r.open || !r.close) continue;
|
|
1739
|
+
|
|
1740
|
+
const idx = text.lastIndexOf(r.open);
|
|
1741
|
+
if (idx === -1) continue;
|
|
1742
|
+
|
|
1743
|
+
const after = text.indexOf(r.close, idx + r.open.length);
|
|
1744
|
+
if (after !== -1) continue;
|
|
1745
|
+
|
|
1746
|
+
if (!best || idx > best.start) best = { rule: r, start: idx };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
if (!best) continue;
|
|
1750
|
+
|
|
1751
|
+
const r = best.rule;
|
|
1752
|
+
const start = best.start;
|
|
1753
|
+
const openLen = r.open.length;
|
|
1754
|
+
const prefixText = text.slice(0, start);
|
|
1755
|
+
const fromOffset = start + openLen;
|
|
1756
|
+
|
|
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);
|
|
1770
|
+
|
|
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;
|
|
1816
|
+
|
|
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
|
+
});
|
|
1840
|
+
|
|
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
|
|
1849
|
+
|
|
1850
|
+
try {
|
|
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).
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Source fence scan (unchanged)
|
|
1889
|
+
_findFenceRanges(s) {
|
|
1890
|
+
const ranges = [];
|
|
1891
|
+
const n = s.length;
|
|
1892
|
+
let i = 0;
|
|
1893
|
+
let inFence = false;
|
|
1894
|
+
let fenceMark = '';
|
|
1895
|
+
let fenceLen = 0;
|
|
1896
|
+
let startLineStart = 0;
|
|
1897
|
+
|
|
1898
|
+
while (i < n) {
|
|
1899
|
+
const lineStart = i;
|
|
1900
|
+
let j = lineStart;
|
|
1901
|
+
while (j < n && s.charCodeAt(j) !== 10 && s.charCodeAt(j) !== 13) j++;
|
|
1902
|
+
const lineEnd = j;
|
|
1903
|
+
let nl = 0;
|
|
1904
|
+
if (j < n) {
|
|
1905
|
+
if (s.charCodeAt(j) === 13 && j + 1 < n && s.charCodeAt(j + 1) === 10) nl = 2;
|
|
1906
|
+
else nl = 1;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
let k = lineStart;
|
|
1910
|
+
let indent = 0;
|
|
1911
|
+
while (k < lineEnd) {
|
|
1912
|
+
const c = s.charCodeAt(k);
|
|
1913
|
+
if (c === 32) { indent++; if (indent > 3) break; k++; }
|
|
1914
|
+
else if (c === 9) { indent++; if (indent > 3) break; k++; }
|
|
1915
|
+
else break;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (!inFence) {
|
|
1919
|
+
if (indent <= 3 && k < lineEnd) {
|
|
1920
|
+
const ch = s.charCodeAt(k);
|
|
1921
|
+
if (ch === 0x60 || ch === 0x7E) {
|
|
1922
|
+
const mark = String.fromCharCode(ch);
|
|
1923
|
+
let m = k;
|
|
1924
|
+
while (m < lineEnd && s.charCodeAt(m) === ch) m++;
|
|
1925
|
+
const run = m - k;
|
|
1926
|
+
if (run >= 3) { inFence = true; fenceMark = mark; fenceLen = run; startLineStart = lineStart; }
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
} else {
|
|
1930
|
+
if (indent <= 3 && k < lineEnd && s.charCodeAt(k) === fenceMark.charCodeAt(0)) {
|
|
1931
|
+
let m = k;
|
|
1932
|
+
while (m < lineEnd && s.charCodeAt(m) === fenceMark.charCodeAt(0)) m++;
|
|
1933
|
+
const run = m - k;
|
|
1934
|
+
if (run >= fenceLen) {
|
|
1935
|
+
let onlyWS = true;
|
|
1936
|
+
for (let t = m; t < lineEnd; t++) {
|
|
1937
|
+
const cc = s.charCodeAt(t);
|
|
1938
|
+
if (cc !== 32 && cc !== 9) { onlyWS = false; break; }
|
|
1939
|
+
}
|
|
1940
|
+
if (onlyWS) {
|
|
1941
|
+
const endIdx = lineEnd + nl;
|
|
1942
|
+
ranges.push([startLineStart, endIdx]);
|
|
1943
|
+
inFence = false; fenceMark = ''; fenceLen = 0; startLineStart = 0;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
i = lineEnd + nl;
|
|
1949
|
+
}
|
|
1950
|
+
if (inFence) ranges.push([startLineStart, n]);
|
|
1951
|
+
return ranges;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
_isTopLevelLineInSource(s, absIdx) {
|
|
1955
|
+
let ls = absIdx;
|
|
1956
|
+
while (ls > 0) {
|
|
1957
|
+
const ch = s.charCodeAt(ls - 1);
|
|
1958
|
+
if (ch === 10 || ch === 13) break;
|
|
1959
|
+
ls--;
|
|
1960
|
+
}
|
|
1961
|
+
const prefix = s.slice(ls, absIdx);
|
|
1962
|
+
|
|
1963
|
+
let i = 0, indent = 0;
|
|
1964
|
+
while (i < prefix.length) {
|
|
1965
|
+
const c = prefix.charCodeAt(i);
|
|
1966
|
+
if (c === 32) { indent++; if (indent > 3) break; i++; }
|
|
1967
|
+
else if (c === 9) { indent++; if (indent > 3) break; i++; }
|
|
1968
|
+
else break;
|
|
1969
|
+
}
|
|
1970
|
+
if (indent > 3) return false;
|
|
1971
|
+
const rest = prefix.slice(i);
|
|
1972
|
+
|
|
1973
|
+
if (/^>\s?/.test(rest)) return false;
|
|
1974
|
+
if (/^[-+*]\s/.test(rest)) return false;
|
|
1975
|
+
if (/^\d+[.)]\s/.test(rest)) return false;
|
|
1976
|
+
|
|
1977
|
+
if (rest.trim().length > 0) return false;
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
_applySourceReplacementsInChunk(full, chunk, baseOffset, rules) {
|
|
1982
|
+
let t = chunk;
|
|
1983
|
+
for (let i = 0; i < rules.length; i++) {
|
|
1984
|
+
const r = rules[i];
|
|
1985
|
+
if (!r || !(r.openReplace || r.closeReplace)) continue;
|
|
1986
|
+
try {
|
|
1987
|
+
r.re.lastIndex = 0;
|
|
1988
|
+
t = t.replace(r.re, (match, inner, offset) => {
|
|
1989
|
+
const abs = baseOffset + (offset | 0);
|
|
1990
|
+
if (!this._isTopLevelLineInSource(full, abs)) return match;
|
|
1991
|
+
const open = r.openReplace || '';
|
|
1992
|
+
const close = r.closeReplace || '';
|
|
1993
|
+
return open + (inner || '') + close;
|
|
1994
|
+
});
|
|
1995
|
+
} catch (_) {}
|
|
1996
|
+
}
|
|
1997
|
+
return t;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1846
2000
|
|
|
1847
2001
|
// ==========================================================================
|
|
1848
2002
|
// 5) Markdown runtime (markdown-it + code wrapper + math placeholders)
|
|
@@ -3892,1072 +4046,1034 @@
|
|
|
3892
4046
|
// ==========================================================================
|
|
3893
4047
|
|
|
3894
4048
|
class StreamEngine {
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
// Streaming buffer (rope-like) – avoids O(n^2) string concatenation when many small chunks arrive.
|
|
3902
|
-
// streamBuf holds the already materialized prefix; _sbParts keeps recent tail parts; _sbLen tracks their length.
|
|
3903
|
-
this.streamBuf = ''; // materialized prefix (string used by render)
|
|
3904
|
-
this._sbParts = []; // pending string chunks (array) not yet joined
|
|
3905
|
-
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);
|
|
3906
4054
|
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
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
|
|
3911
4060
|
|
|
3912
|
-
|
|
3913
|
-
|
|
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;
|
|
3914
4065
|
|
|
3915
|
-
|
|
4066
|
+
this.codeStream = { open: false, lines: 0, chars: 0 };
|
|
4067
|
+
this.activeCode = null;
|
|
3916
4068
|
|
|
3917
|
-
|
|
4069
|
+
this.suppressPostFinalizePass = false;
|
|
3918
4070
|
|
|
3919
|
-
|
|
3920
|
-
this._firstCodeOpenSnapDone = false;
|
|
4071
|
+
this._promoteScheduled = false;
|
|
3921
4072
|
|
|
3922
|
-
|
|
3923
|
-
|
|
4073
|
+
// Guard to ensure first fence-open is materialized immediately when stream starts with code.
|
|
4074
|
+
this._firstCodeOpenSnapDone = false;
|
|
3924
4075
|
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
this._lastInjectedEOL = false;
|
|
4076
|
+
// Streaming mode flag – controls reduced rendering (no linkify etc.) on hot path.
|
|
4077
|
+
this.isStreaming = false;
|
|
3928
4078
|
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
_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;
|
|
3933
4082
|
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
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); }
|
|
3937
4087
|
|
|
3938
|
-
|
|
4088
|
+
setCustomFenceSpecs(specs) {
|
|
4089
|
+
this._customFenceSpecs = Array.isArray(specs) ? specs.slice() : [];
|
|
4090
|
+
}
|
|
3939
4091
|
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
// Single-part fast path avoids a temporary array join.
|
|
3955
|
-
this.streamBuf += (this._sbParts.length === 1 ? this._sbParts[0] : this._sbParts.join(''));
|
|
3956
|
-
this._sbParts.length = 0;
|
|
3957
|
-
this._sbLen = 0;
|
|
3958
|
-
}
|
|
3959
|
-
return this.streamBuf;
|
|
3960
|
-
}
|
|
3961
|
-
// Reset the rope to an empty state.
|
|
3962
|
-
_clearStreamBuffer() {
|
|
3963
|
-
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(''));
|
|
3964
4106
|
this._sbParts.length = 0;
|
|
3965
4107
|
this._sbLen = 0;
|
|
3966
4108
|
}
|
|
4109
|
+
return this.streamBuf;
|
|
4110
|
+
}
|
|
4111
|
+
_clearStreamBuffer() {
|
|
4112
|
+
this.streamBuf = '';
|
|
4113
|
+
this._sbParts.length = 0;
|
|
4114
|
+
this._sbLen = 0;
|
|
4115
|
+
}
|
|
3967
4116
|
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
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;
|
|
3993
4162
|
codeEl.removeAttribute('data-highlighted');
|
|
3994
4163
|
codeEl.classList.remove('hljs');
|
|
3995
4164
|
codeEl.dataset._active_stream = '0';
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
this.
|
|
4000
|
-
}
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
const tail = codeEl.querySelector('.hl-tail');
|
|
4012
|
-
if (frozen || tail) text = (frozen?.textContent || '') + (tail?.textContent || '');
|
|
4013
|
-
else text = codeEl.textContent || '';
|
|
4014
|
-
codeEl.textContent = text;
|
|
4015
|
-
codeEl.removeAttribute('data-highlighted');
|
|
4016
|
-
codeEl.classList.remove('hljs');
|
|
4017
|
-
codeEl.dataset._active_stream = '0';
|
|
4018
|
-
try { this.codeScroll.attachHandlers(codeEl); } catch (_) {}
|
|
4019
|
-
n++;
|
|
4020
|
-
});
|
|
4021
|
-
if (n) this._d('DEFUSE_ORPHAN_ACTIVE_BLOCKS', { count: n });
|
|
4022
|
-
} catch (e) { this._d('DEFUSE_ORPHAN_ACTIVE_ERR', String(e)); }
|
|
4023
|
-
}
|
|
4024
|
-
// Abort streaming and clear state with options.
|
|
4025
|
-
abortAndReset(opts) {
|
|
4026
|
-
const o = Object.assign({
|
|
4027
|
-
finalizeActive: true,
|
|
4028
|
-
clearBuffer: true,
|
|
4029
|
-
clearMsg: false,
|
|
4030
|
-
defuseOrphans: true,
|
|
4031
|
-
reason: '',
|
|
4032
|
-
suppressLog: false
|
|
4033
|
-
}, (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 || {}));
|
|
4034
4180
|
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4181
|
+
try { this.raf.cancelGroup('StreamEngine'); } catch (_) {}
|
|
4182
|
+
try { this.raf.cancel('SE:snapshot'); } catch (_) {}
|
|
4183
|
+
this.snapshotScheduled = false; this.snapshotRAF = 0;
|
|
4038
4184
|
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
}
|
|
4045
|
-
} catch (e) {
|
|
4046
|
-
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();
|
|
4047
4190
|
}
|
|
4191
|
+
} catch (e) {
|
|
4192
|
+
this._d('ABORT_FINALIZE_ERR', String(e));
|
|
4193
|
+
}
|
|
4048
4194
|
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4195
|
+
if (o.defuseOrphans) {
|
|
4196
|
+
try { this.defuseOrphanActiveBlocks(); }
|
|
4197
|
+
catch (e) { this._d('ABORT_DEFUSE_ORPHANS_ERR', String(e)); }
|
|
4198
|
+
}
|
|
4053
4199
|
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
}
|
|
4061
|
-
if (o.clearMsg === true) {
|
|
4062
|
-
try { this.dom.resetEphemeral(); } catch (_) {}
|
|
4063
|
-
}
|
|
4064
|
-
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;
|
|
4065
4206
|
}
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
// Reset adaptive snapshot budget to base.
|
|
4069
|
-
resetBudget() { this.nextSnapshotStep = this.profile().base; }
|
|
4070
|
-
// Check whether [from, end) contains only spaces/tabs.
|
|
4071
|
-
onlyTrailingWhitespace(s, from, end) {
|
|
4072
|
-
for (let i = from; i < end; i++) { const c = s.charCodeAt(i); if (c !== 0x20 && c !== 0x09) return false; }
|
|
4073
|
-
return true;
|
|
4207
|
+
if (o.clearMsg === true) {
|
|
4208
|
+
try { this.dom.resetEphemeral(); } catch (_) {}
|
|
4074
4209
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
}
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
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; }
|
|
4115
4250
|
}
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
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
|
|
4132
4273
|
}
|
|
4133
4274
|
}
|
|
4134
|
-
}
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
} else {
|
|
4153
|
-
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
|
|
4154
4293
|
}
|
|
4294
|
+
} else {
|
|
4295
|
+
this._d('FENCE_CLOSE_REJECTED_CUSTOM_NON_WS_AFTER', { close, idxStart: j, idxEnd: k });
|
|
4155
4296
|
}
|
|
4156
4297
|
}
|
|
4298
|
+
}
|
|
4157
4299
|
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
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;
|
|
4161
4303
|
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
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' });
|
|
4167
4322
|
continue;
|
|
4168
|
-
}
|
|
4169
|
-
|
|
4170
|
-
if (mark === this.fenceMark && run >= this.fenceLen) {
|
|
4171
|
-
if (!inNewOrCrosses(j, k)) { i = k; continue; }
|
|
4172
|
-
let eol = k; while (eol < n && s[eol] !== '\n' && s[eol] !== '\r') eol++;
|
|
4173
|
-
if (this.onlyTrailingWhitespace(s, k, eol)) {
|
|
4174
|
-
this.fenceOpen = false; closed = true;
|
|
4175
|
-
const endInS = k;
|
|
4176
|
-
const rel = endInS - preLen;
|
|
4177
|
-
splitAt = Math.max(0, Math.min((chunk ? chunk.length : 0), rel));
|
|
4178
|
-
i = k;
|
|
4179
|
-
this._d('FENCE_CLOSE_DETECTED', { mark, run, idxStart: j, idxEnd: k, splitAt, region: (j >= preLen) ? 'new' : 'cross' });
|
|
4180
|
-
continue;
|
|
4181
|
-
} else {
|
|
4182
|
-
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k });
|
|
4183
|
-
}
|
|
4323
|
+
} else {
|
|
4324
|
+
this._d('FENCE_CLOSE_REJECTED_NON_WS_AFTER', { mark, run, idxStart: j, idxEnd: k });
|
|
4184
4325
|
}
|
|
4185
4326
|
}
|
|
4186
4327
|
}
|
|
4187
|
-
|
|
4188
|
-
i = j + 1;
|
|
4189
4328
|
}
|
|
4190
4329
|
|
|
4191
|
-
|
|
4192
|
-
this.fenceBuf = s.slice(-MAX_TAIL);
|
|
4193
|
-
this.fenceTail = s.slice(-3);
|
|
4194
|
-
return { opened, closed, splitAt };
|
|
4330
|
+
i = j + 1;
|
|
4195
4331
|
}
|
|
4196
4332
|
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
return snap;
|
|
4203
|
-
}
|
|
4204
|
-
// Detect structural boundaries in a chunk (for snapshot decisions).
|
|
4205
|
-
hasStructuralBoundary(chunk) { if (!chunk) return false; return /\n(\n|[-*]\s|\d+\.\s|#{1,6}\s|>\s)/.test(chunk); }
|
|
4206
|
-
// Decide whether we should snapshot on this chunk.
|
|
4207
|
-
shouldSnapshotOnChunk(chunk, chunkHasNL, hasBoundary) {
|
|
4208
|
-
const prof = this.profile(); const now = Utils.now();
|
|
4209
|
-
if (this.activeCode && this.fenceOpen) return false;
|
|
4210
|
-
if ((now - this.lastSnapshotTs) < prof.minInterval) return false;
|
|
4211
|
-
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
|
+
}
|
|
4212
4338
|
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
}
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
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;
|
|
4221
4368
|
if (this.activeCode && this.fenceOpen) return;
|
|
4222
|
-
|
|
4223
|
-
if (
|
|
4224
|
-
}
|
|
4225
|
-
// Schedule snapshot rendering (coalesced via rAF).
|
|
4226
|
-
scheduleSnapshot(msg, force = false) {
|
|
4227
|
-
if (this.snapshotScheduled && !this.raf.isScheduled('SE:snapshot')) this.snapshotScheduled = false;
|
|
4228
|
-
if (!force) {
|
|
4229
|
-
if (this.snapshotScheduled) return;
|
|
4230
|
-
if (this.activeCode && this.fenceOpen) return;
|
|
4231
|
-
} else {
|
|
4232
|
-
if (this.snapshotScheduled && this.raf.isScheduled('SE:snapshot')) return;
|
|
4233
|
-
}
|
|
4234
|
-
this.snapshotScheduled = true;
|
|
4235
|
-
this.raf.schedule('SE:snapshot', () => { this.snapshotScheduled = false; this.renderSnapshot(msg); }, 'StreamEngine', 0);
|
|
4236
|
-
}
|
|
4237
|
-
// Split code element into frozen and tail spans if needed.
|
|
4238
|
-
ensureSplitCodeEl(codeEl) {
|
|
4239
|
-
if (!codeEl) return null;
|
|
4240
|
-
let frozen = codeEl.querySelector('.hl-frozen'); let tail = codeEl.querySelector('.hl-tail');
|
|
4241
|
-
if (frozen && tail) return { codeEl, frozenEl: frozen, tailEl: tail };
|
|
4242
|
-
const text = codeEl.textContent || ''; codeEl.innerHTML = '';
|
|
4243
|
-
frozen = document.createElement('span'); frozen.className = 'hl-frozen';
|
|
4244
|
-
tail = document.createElement('span'); tail.className = 'hl-tail';
|
|
4245
|
-
codeEl.appendChild(frozen); codeEl.appendChild(tail);
|
|
4246
|
-
if (text) tail.textContent = text; return { codeEl, frozenEl: frozen, tailEl: tail };
|
|
4369
|
+
} else {
|
|
4370
|
+
if (this.snapshotScheduled && this.raf.isScheduled('SE:snapshot')) return;
|
|
4247
4371
|
}
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
const last = codes[codes.length - 1];
|
|
4252
|
-
const cls = Array.from(last.classList).find(c => c.startsWith('language-')) || 'language-plaintext';
|
|
4253
|
-
const lang = (cls.replace('language-', '') || 'plaintext');
|
|
4254
|
-
const parts = this.ensureSplitCodeEl(last); if (!parts) return null;
|
|
4255
|
-
|
|
4256
|
-
// If we injected a synthetic EOL for parsing an open fence, remove it from the streaming tail now.
|
|
4257
|
-
// This prevents breaking the very first code line into "#\n foo" when the next chunk starts with " foo".
|
|
4258
|
-
if (this._lastInjectedEOL && parts.tailEl && parts.tailEl.textContent && parts.tailEl.textContent.endsWith('\n')) {
|
|
4259
|
-
parts.tailEl.textContent = parts.tailEl.textContent.slice(0, -1);
|
|
4260
|
-
// Reset the marker so we don't accidentally trim again in this snapshot lifecycle.
|
|
4261
|
-
this._lastInjectedEOL = false;
|
|
4262
|
-
}
|
|
4263
|
-
|
|
4264
|
-
const st = this.codeScroll.state(parts.codeEl); st.autoFollow = true; st.userInteracted = false;
|
|
4265
|
-
parts.codeEl.dataset._active_stream = '1';
|
|
4266
|
-
const baseFrozenNL = Utils.countNewlines(parts.frozenEl.textContent || ''); const baseTailNL = Utils.countNewlines(parts.tailEl.textContent || '');
|
|
4267
|
-
const ac = { codeEl: parts.codeEl, frozenEl: parts.frozenEl, tailEl: parts.tailEl, lang, frozenLen: parts.frozenEl.textContent.length, lastPromoteTs: 0,
|
|
4268
|
-
lines: 0, tailLines: baseTailNL, linesSincePromote: 0, initialLines: baseFrozenNL + baseTailNL, haltHL: false, plainStream: false };
|
|
4269
|
-
this._d('ACTIVE_CODE_SETUP', { lang, frozenLen: ac.frozenLen, tailLines: ac.tailLines, initialLines: ac.initialLines });
|
|
4270
|
-
return ac;
|
|
4271
|
-
}
|
|
4272
|
-
// Copy previous active code state into the new one (after snapshot).
|
|
4273
|
-
rehydrateActiveCode(oldAC, newAC) {
|
|
4274
|
-
if (!oldAC || !newAC) return;
|
|
4275
|
-
newAC.frozenEl.innerHTML = oldAC.frozenEl ? oldAC.frozenEl.innerHTML : '';
|
|
4276
|
-
const fullText = newAC.codeEl.textContent || ''; const remainder = fullText.slice(oldAC.frozenLen);
|
|
4277
|
-
newAC.tailEl.textContent = remainder;
|
|
4278
|
-
newAC.frozenLen = oldAC.frozenLen; newAC.lang = oldAC.lang;
|
|
4279
|
-
newAC.lines = oldAC.lines; newAC.tailLines = Utils.countNewlines(remainder);
|
|
4280
|
-
newAC.lastPromoteTs = oldAC.lastPromoteTs; newAC.linesSincePromote = oldAC.linesSincePromote || 0;
|
|
4281
|
-
newAC.initialLines = oldAC.initialLines || 0; newAC.haltHL = !!oldAC.haltHL;
|
|
4282
|
-
newAC.plainStream = !!oldAC.plainStream;
|
|
4283
|
-
this._d('ACTIVE_CODE_REHYDRATE', { lang: newAC.lang, frozenLen: newAC.frozenLen, tailLines: newAC.tailLines, initialLines: newAC.initialLines, halted: newAC.haltHL, plainStream: newAC.plainStream });
|
|
4284
|
-
}
|
|
4285
|
-
// Append text to active tail span and update counters.
|
|
4286
|
-
appendToActiveTail(text) {
|
|
4287
|
-
if (!this.activeCode || !this.activeCode.tailEl || !text) return;
|
|
4288
|
-
this.activeCode.tailEl.insertAdjacentText('beforeend', text);
|
|
4289
|
-
const nl = Utils.countNewlines(text);
|
|
4290
|
-
this.activeCode.tailLines += nl; this.activeCode.linesSincePromote += nl;
|
|
4291
|
-
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4292
|
-
if (this.logger.isEnabled('STREAM') && (nl > 0 || text.length >= 64)) {
|
|
4293
|
-
this._d('TAIL_APPEND', { addLen: text.length, addNL: nl, totalTailNL: this.activeCode.tailLines });
|
|
4294
|
-
}
|
|
4295
|
-
}
|
|
4296
|
-
// Enforce budgets: stop incremental hljs and switch to plain streaming if needed.
|
|
4297
|
-
enforceHLStopBudget() {
|
|
4298
|
-
if (!this.activeCode) return;
|
|
4299
|
-
// If global disable was requested, halt early and switch to plain streaming.
|
|
4300
|
-
if (this.cfg.HL.DISABLE_ALL) { this.activeCode.haltHL = true; this.activeCode.plainStream = true; return; }
|
|
4301
|
-
const stop = (this.cfg.PROFILE_CODE.stopAfterLines | 0);
|
|
4302
|
-
const streamPlainLines = (this.cfg.PROFILE_CODE.streamPlainAfterLines | 0);
|
|
4303
|
-
const streamPlainChars = (this.cfg.PROFILE_CODE.streamPlainAfterChars | 0);
|
|
4304
|
-
const maxFrozenChars = (this.cfg.PROFILE_CODE.maxFrozenChars | 0);
|
|
4305
|
-
|
|
4306
|
-
const totalLines = (this.activeCode.initialLines || 0) + (this.activeCode.lines || 0);
|
|
4307
|
-
const frozenChars = this.activeCode.frozenLen | 0;
|
|
4308
|
-
const tailChars = (this.activeCode.tailEl?.textContent || '').length | 0;
|
|
4309
|
-
const totalStreamedChars = frozenChars + tailChars;
|
|
4310
|
-
|
|
4311
|
-
// Switch to plain streaming after budgets – no incremental hljs
|
|
4312
|
-
if ((streamPlainLines > 0 && totalLines >= streamPlainLines) ||
|
|
4313
|
-
(streamPlainChars > 0 && totalStreamedChars >= streamPlainChars) ||
|
|
4314
|
-
(maxFrozenChars > 0 && frozenChars >= maxFrozenChars)) {
|
|
4315
|
-
this.activeCode.haltHL = true;
|
|
4316
|
-
this.activeCode.plainStream = true;
|
|
4317
|
-
try { this.activeCode.codeEl.dataset.hlStreamSuspended = '1'; } catch (_) {}
|
|
4318
|
-
this._d('STREAM_HL_SUSPENDED', { totalLines, totalStreamedChars, frozenChars, reason: 'budget' });
|
|
4319
|
-
return;
|
|
4320
|
-
}
|
|
4372
|
+
this.snapshotScheduled = true;
|
|
4373
|
+
this.raf.schedule('SE:snapshot', () => { this.snapshotScheduled = false; this.renderSnapshot(msg); }, 'StreamEngine', 0);
|
|
4374
|
+
}
|
|
4321
4375
|
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
}
|
|
4344
|
-
_isHLJSSupported(lang) {
|
|
4345
|
-
try { return !!(window.hljs && hljs.getLanguage && hljs.getLanguage(lang)); } catch (_) { return false; }
|
|
4346
|
-
}
|
|
4347
|
-
// Try to detect language from a "language: X" style first line directive.
|
|
4348
|
-
_detectDirectiveLangFromText(text) {
|
|
4349
|
-
if (!text) return null;
|
|
4350
|
-
let s = String(text);
|
|
4351
|
-
if (s.charCodeAt(0) === 0xFEFF) s = s.slice(1);
|
|
4352
|
-
const lines = s.split(/\r?\n/);
|
|
4353
|
-
let i = 0; while (i < lines.length && !lines[i].trim()) i++;
|
|
4354
|
-
if (i >= lines.length) return null;
|
|
4355
|
-
let first = lines[i].trim();
|
|
4356
|
-
first = first.replace(/^\s*lang(?:uage)?\s*[:=]\s*/i, '').trim();
|
|
4357
|
-
let token = first.split(/\s+/)[0].replace(/:$/, '');
|
|
4358
|
-
if (!/^[A-Za-z][\w#+\-\.]{0,30}$/.test(token)) return null;
|
|
4359
|
-
|
|
4360
|
-
let cand = this._aliasLang(token);
|
|
4361
|
-
const rest = lines.slice(i + 1).join('\n');
|
|
4362
|
-
if (!rest.trim()) return null;
|
|
4363
|
-
|
|
4364
|
-
let pos = 0, seen = 0;
|
|
4365
|
-
while (seen < i && pos < s.length) { const nl = s.indexOf('\n', pos); if (nl === -1) return null; pos = nl + 1; seen++; }
|
|
4366
|
-
let end = s.indexOf('\n', pos);
|
|
4367
|
-
if (end === -1) end = s.length; else end = end + 1;
|
|
4368
|
-
return { lang: cand, deleteUpto: end };
|
|
4369
|
-
}
|
|
4370
|
-
// Update code element class to reflect new lang (language-xxx).
|
|
4371
|
-
_updateCodeLangClass(codeEl, newLang) {
|
|
4372
|
-
try {
|
|
4373
|
-
Array.from(codeEl.classList).forEach(c => { if (c.startsWith('language-')) codeEl.classList.remove(c); });
|
|
4374
|
-
codeEl.classList.add('language-' + (newLang || 'plaintext'));
|
|
4375
|
-
} 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;
|
|
4376
4397
|
}
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
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 });
|
|
4386
4427
|
}
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
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;
|
|
4391
4517
|
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
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;
|
|
4396
4522
|
|
|
4397
|
-
|
|
4398
|
-
|
|
4523
|
+
const det = this._detectDirectiveLangFromText(combined);
|
|
4524
|
+
if (!det || !det.lang) return;
|
|
4399
4525
|
|
|
4400
|
-
|
|
4401
|
-
|
|
4526
|
+
const newLang = det.lang;
|
|
4527
|
+
const newCombined = combined.slice(det.deleteUpto);
|
|
4402
4528
|
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
this.activeCode.lang = newLang;
|
|
4416
|
-
this._updateCodeLangClass(codeEl, newLang);
|
|
4417
|
-
this._updateCodeHeaderLabel(codeEl, newLang, newLang);
|
|
4418
|
-
|
|
4419
|
-
this._d('LANG_PROMOTE', { to: newLang, removedChars: det.deleteUpto, tailLines: this.activeCode.tailLines });
|
|
4420
|
-
this.schedulePromoteTail(true);
|
|
4421
|
-
} catch (e) {
|
|
4422
|
-
this._d('LANG_PROMOTE_ERR', String(e));
|
|
4423
|
-
}
|
|
4424
|
-
}
|
|
4425
|
-
// Highlight a small piece of text based on language (safe fallback to escapeHtml).
|
|
4426
|
-
highlightDeltaText(lang, text) {
|
|
4427
|
-
if (this.cfg.HL.DISABLE_ALL) return Utils.escapeHtml(text);
|
|
4428
|
-
if (window.hljs && lang && hljs.getLanguage && hljs.getLanguage(lang)) {
|
|
4429
|
-
try { return hljs.highlight(text, { language: lang, ignoreIllegals: true }).value; }
|
|
4430
|
-
catch (_) { return Utils.escapeHtml(text); }
|
|
4431
|
-
}
|
|
4432
|
-
return Utils.escapeHtml(text);
|
|
4433
|
-
}
|
|
4434
|
-
// Schedule cooperative tail promotion (async) to avoid blocking UI on each chunk.
|
|
4435
|
-
schedulePromoteTail(force = false) {
|
|
4436
|
-
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4437
|
-
if (this._promoteScheduled) return;
|
|
4438
|
-
this._promoteScheduled = true;
|
|
4439
|
-
this.raf.schedule('SE:promoteTail', () => {
|
|
4440
|
-
this._promoteScheduled = false;
|
|
4441
|
-
this._promoteTailWork(force);
|
|
4442
|
-
}, 'StreamEngine', 1);
|
|
4443
|
-
}
|
|
4444
|
-
// Move a full-line part of tail into frozen region (with highlight if budgets allow).
|
|
4445
|
-
async _promoteTailWork(force = false) {
|
|
4446
|
-
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4447
|
-
|
|
4448
|
-
// If plain streaming mode is on, or incremental hljs is disabled, promote as plain text only.
|
|
4449
|
-
const now = Utils.now(); const prof = this.cfg.PROFILE_CODE;
|
|
4450
|
-
const tailText0 = this.activeCode.tailEl.textContent || ''; if (!tailText0) return;
|
|
4451
|
-
|
|
4452
|
-
if (!force) {
|
|
4453
|
-
if ((now - this.activeCode.lastPromoteTs) < prof.promoteMinInterval) return;
|
|
4454
|
-
const enoughLines = (this.activeCode.linesSincePromote || 0) >= (prof.promoteMinLines || 10);
|
|
4455
|
-
const enoughChars = tailText0.length >= prof.minCharsForHL;
|
|
4456
|
-
if (!enoughLines && !enoughChars) return;
|
|
4457
|
-
}
|
|
4458
|
-
|
|
4459
|
-
// Cut at last full line to avoid moving partial tokens
|
|
4460
|
-
const idx = tailText0.lastIndexOf('\n');
|
|
4461
|
-
if (idx <= -1 && !force) return;
|
|
4462
|
-
const cut = (idx >= 0) ? (idx + 1) : tailText0.length;
|
|
4463
|
-
const delta = tailText0.slice(0, cut); if (!delta) return;
|
|
4464
|
-
|
|
4465
|
-
// Re-evaluate budgets before performing any heavy work
|
|
4466
|
-
this.enforceHLStopBudget();
|
|
4467
|
-
const usePlain = this.activeCode.haltHL || this.activeCode.plainStream || !this._isHLJSSupported(this.activeCode.lang);
|
|
4468
|
-
|
|
4469
|
-
// Cooperative rAF yield before heavy highlight
|
|
4470
|
-
if (!usePlain) await this.asyncer.yield();
|
|
4471
|
-
|
|
4472
|
-
// If tail changed since we captured it, validate prefix to avoid duplication
|
|
4473
|
-
if (!this.activeCode || !this.activeCode.tailEl) return;
|
|
4474
|
-
const tailNow = this.activeCode.tailEl.textContent || '';
|
|
4475
|
-
if (!tailNow.startsWith(delta)) {
|
|
4476
|
-
// New data arrived; reschedule for next frame without touching DOM
|
|
4477
|
-
this.schedulePromoteTail(false);
|
|
4478
|
-
return;
|
|
4479
|
-
}
|
|
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;
|
|
4480
4540
|
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
this.activeCode.frozenEl.insertAdjacentHTML('beforeend', html);
|
|
4490
|
-
}
|
|
4491
|
-
|
|
4492
|
-
// Update tail and counters
|
|
4493
|
-
this.activeCode.tailEl.textContent = tailNow.slice(delta.length);
|
|
4494
|
-
this.activeCode.frozenLen += delta.length;
|
|
4495
|
-
const promotedLines = Utils.countNewlines(delta);
|
|
4496
|
-
this.activeCode.tailLines = Math.max(0, (this.activeCode.tailLines || 0) - promotedLines);
|
|
4497
|
-
this.activeCode.linesSincePromote = Math.max(0, (this.activeCode.linesSincePromote || 0) - promotedLines);
|
|
4498
|
-
this.activeCode.lastPromoteTs = Utils.now();
|
|
4499
|
-
this.codeScroll.scheduleScroll(this.activeCode.codeEl, true, false);
|
|
4500
|
-
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));
|
|
4501
4549
|
}
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
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;
|
|
4509
4570
|
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
codeEl.innerHTML = '';
|
|
4513
|
-
codeEl.textContent = fullText;
|
|
4514
|
-
codeEl.classList.add('hljs'); // keep visual parity until highlight applies
|
|
4515
|
-
codeEl.removeAttribute('data-highlighted');
|
|
4516
|
-
} catch (_) {}
|
|
4571
|
+
const now = Utils.now(); const prof = this.cfg.PROFILE_CODE;
|
|
4572
|
+
const tailText0 = this.activeCode.tailEl.textContent || ''; if (!tailText0) return;
|
|
4517
4573
|
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
const
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
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
|
+
}
|
|
4524
4580
|
|
|
4525
|
-
|
|
4526
|
-
|
|
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;
|
|
4527
4585
|
|
|
4528
|
-
|
|
4529
|
-
|
|
4586
|
+
this.enforceHLStopBudget();
|
|
4587
|
+
const usePlain = this.activeCode.haltHL || this.activeCode.plainStream || !this._isHLJSSupported(this.activeCode.lang);
|
|
4530
4588
|
|
|
4531
|
-
|
|
4589
|
+
if (!usePlain) await this.asyncer.yield();
|
|
4532
4590
|
|
|
4533
|
-
|
|
4534
|
-
|
|
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;
|
|
4596
|
+
}
|
|
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);
|
|
4535
4604
|
}
|
|
4536
|
-
|
|
4537
|
-
|
|
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;
|
|
4538
4655
|
const cls = Array.from(codeEl.classList).find(c => c.startsWith('language-')) || 'language-plaintext';
|
|
4539
|
-
const lang = cls.replace('language-', '') || 'plaintext';
|
|
4540
|
-
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;
|
|
4541
4661
|
return `${lang}|${len}|${head}|${tail}`;
|
|
4662
|
+
} catch (_) {
|
|
4663
|
+
return null;
|
|
4542
4664
|
}
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
if (
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
const
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
if (
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
let reuseCount = 0;
|
|
4578
|
-
for (let i = 0; i < end; i++) {
|
|
4579
|
-
const nc = newCodes[i];
|
|
4580
|
-
if (nc.getAttribute('data-highlighted') === 'yes') continue;
|
|
4581
|
-
// Fingerprint new code: prefer wrapper meta (no .textContent read)
|
|
4582
|
-
let fp = this.codeFingerprintFromWrapper(nc);
|
|
4583
|
-
if (!fp) fp = this.codeFingerprint(nc);
|
|
4584
|
-
const arr = map.get(fp);
|
|
4585
|
-
if (arr && arr.length) {
|
|
4586
|
-
const oldEl = arr.shift();
|
|
4587
|
-
if (oldEl && oldEl.isConnected) {
|
|
4588
|
-
try {
|
|
4589
|
-
nc.replaceWith(oldEl);
|
|
4590
|
-
this.codeScroll.attachHandlers(oldEl);
|
|
4591
|
-
// Preserve whatever final state the old element had
|
|
4592
|
-
if (!oldEl.getAttribute('data-highlighted')) oldEl.setAttribute('data-highlighted', 'yes');
|
|
4593
|
-
const st = this.codeScroll.state(oldEl); st.autoFollow = false;
|
|
4594
|
-
reuseCount++;
|
|
4595
|
-
} catch (_) {}
|
|
4596
|
-
}
|
|
4597
|
-
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 (_) {}
|
|
4598
4699
|
}
|
|
4700
|
+
if (!arr.length) map.delete(fp);
|
|
4599
4701
|
}
|
|
4600
|
-
if (reuseCount) this._d('PRESERVE_CODES_REUSED', { reuseCount, skipLastIfStreaming });
|
|
4601
|
-
} catch (e) {
|
|
4602
|
-
this._d('PRESERVE_CODES_ERROR', String(e));
|
|
4603
4702
|
}
|
|
4703
|
+
if (reuseCount) this._d('PRESERVE_CODES_REUSED', { reuseCount, skipLastIfStreaming });
|
|
4704
|
+
} catch (e) {
|
|
4705
|
+
this._d('PRESERVE_CODES_ERROR', String(e));
|
|
4604
4706
|
}
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
this.schedulePromoteTail(true);
|
|
4634
|
-
}
|
|
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);
|
|
4635
4735
|
}
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
try {
|
|
4641
|
-
if (!newAC || !newAC.codeEl || !newAC.codeEl.isConnected) return;
|
|
4736
|
+
}
|
|
4737
|
+
stabilizeHeaderLabel(prevAC, newAC) {
|
|
4738
|
+
try {
|
|
4739
|
+
if (!newAC || !newAC.codeEl || !newAC.codeEl.isConnected) return;
|
|
4642
4740
|
|
|
4643
|
-
|
|
4644
|
-
|
|
4741
|
+
const wrap = newAC.codeEl.closest('.code-wrapper');
|
|
4742
|
+
if (!wrap) return;
|
|
4645
4743
|
|
|
4646
|
-
|
|
4647
|
-
|
|
4744
|
+
const span = wrap.querySelector('.code-header-lang');
|
|
4745
|
+
const curLabel = (span && span.textContent ? span.textContent.trim() : '').toLowerCase();
|
|
4648
4746
|
|
|
4649
|
-
|
|
4650
|
-
if (curLabel === 'output') return;
|
|
4747
|
+
if (curLabel === 'output') return;
|
|
4651
4748
|
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
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() : '';
|
|
4655
4752
|
|
|
4656
|
-
|
|
4753
|
+
const valid = (t) => !!t && t !== 'plaintext' && this._isHLJSSupported(t);
|
|
4657
4754
|
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4755
|
+
let finalTok = '';
|
|
4756
|
+
if (valid(tokNow)) finalTok = tokNow;
|
|
4757
|
+
else if (valid(prev)) finalTok = prev;
|
|
4758
|
+
else if (valid(sticky)) finalTok = sticky;
|
|
4662
4759
|
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
span.textContent = 'code';
|
|
4674
|
-
}
|
|
4675
|
-
}
|
|
4676
|
-
} catch (_) { /* defensive: never break streaming path */ }
|
|
4677
|
-
}
|
|
4678
|
-
// Render a snapshot of current stream buffer into the DOM.
|
|
4679
|
-
renderSnapshot(msg) {
|
|
4680
|
-
const streaming = !!this.isStreaming;
|
|
4681
|
-
const snap = this.getMsgSnapshotRoot(msg); if (!snap) return;
|
|
4682
|
-
|
|
4683
|
-
// No-op if nothing changed and no active code
|
|
4684
|
-
const prevLen = (window.__lastSnapshotLen || 0);
|
|
4685
|
-
const curLen = this.getStreamLength();
|
|
4686
|
-
if (!this.fenceOpen && !this.activeCode && curLen === prevLen) {
|
|
4687
|
-
this.lastSnapshotTs = Utils.now();
|
|
4688
|
-
this._d('SNAPSHOT_SKIPPED_NO_DELTA', { bufLen: curLen });
|
|
4689
|
-
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
|
+
}
|
|
4690
4770
|
}
|
|
4771
|
+
} catch (_) { /* defensive: never break streaming path */ }
|
|
4772
|
+
}
|
|
4691
4773
|
|
|
4692
|
-
|
|
4774
|
+
renderSnapshot(msg) {
|
|
4775
|
+
const streaming = !!this.isStreaming;
|
|
4776
|
+
const snap = this.getMsgSnapshotRoot(msg); if (!snap) return;
|
|
4693
4777
|
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
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
|
+
}
|
|
4701
4785
|
|
|
4702
|
-
|
|
4703
|
-
const html = streaming ? this.renderer.renderStreamingSnapshot(src) : this.renderer.renderFinalSnapshot(src);
|
|
4786
|
+
const t0 = Utils.now();
|
|
4704
4787
|
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
const tmp = document.createElement('div');
|
|
4713
|
-
tmp.innerHTML = html;
|
|
4714
|
-
frag = document.createDocumentFragment();
|
|
4715
|
-
while (tmp.firstChild) frag.appendChild(tmp.firstChild);
|
|
4716
|
-
}
|
|
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;
|
|
4717
4795
|
|
|
4718
|
-
|
|
4719
|
-
// Apply Custom Markup on the fragment only if at least one rule opted-in for stream.
|
|
4720
|
-
try {
|
|
4721
|
-
if (this.renderer && this.renderer.customMarkup && this.renderer.customMarkup.hasStreamRules()) {
|
|
4722
|
-
const MDinline = this.renderer.MD_STREAM || this.renderer.MD || null;
|
|
4723
|
-
this.renderer.customMarkup.applyStream(frag, MDinline);
|
|
4724
|
-
}
|
|
4725
|
-
} catch (_) { /* keep snapshot path resilient */ }
|
|
4796
|
+
const html = streaming ? this.renderer.renderStreamingSnapshot(src) : this.renderer.renderFinalSnapshot(src);
|
|
4726
4797
|
|
|
4727
|
-
|
|
4728
|
-
|
|
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
|
+
}
|
|
4729
4809
|
|
|
4730
|
-
|
|
4731
|
-
|
|
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 */ }
|
|
4732
4817
|
|
|
4733
|
-
|
|
4734
|
-
this.renderer.restoreCollapsedCode(snap);
|
|
4735
|
-
this._ensureBottomForJustFinalized(snap);
|
|
4818
|
+
this.preserveStableClosedCodes(snap, frag, this.fenceOpen === true);
|
|
4736
4819
|
|
|
4737
|
-
|
|
4738
|
-
const prevAC = this.activeCode; // remember previous active streaming state (if any)
|
|
4820
|
+
snap.replaceChildren(frag);
|
|
4739
4821
|
|
|
4740
|
-
|
|
4741
|
-
|
|
4822
|
+
this.renderer.restoreCollapsedCode(snap);
|
|
4823
|
+
this._ensureBottomForJustFinalized(snap);
|
|
4742
4824
|
|
|
4743
|
-
|
|
4744
|
-
if (prevAC && newAC) {
|
|
4745
|
-
this.rehydrateActiveCode(prevAC, newAC);
|
|
4746
|
-
this.stabilizeHeaderLabel(prevAC, newAC);
|
|
4747
|
-
}
|
|
4825
|
+
const prevAC = this.activeCode;
|
|
4748
4826
|
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
this.activeCode = null;
|
|
4752
|
-
}
|
|
4827
|
+
if (this.fenceOpen) {
|
|
4828
|
+
const newAC = this.setupActiveCodeFromSnapshot(snap);
|
|
4753
4829
|
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
this.
|
|
4830
|
+
if (prevAC && newAC) {
|
|
4831
|
+
this.rehydrateActiveCode(prevAC, newAC);
|
|
4832
|
+
this.stabilizeHeaderLabel(prevAC, newAC);
|
|
4757
4833
|
}
|
|
4758
|
-
|
|
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, {
|
|
4759
4850
|
deferLastIfStreaming: true,
|
|
4760
4851
|
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
4761
4852
|
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
4762
4853
|
}, this.activeCode);
|
|
4763
|
-
this.
|
|
4764
|
-
|
|
4765
|
-
deferLastIfStreaming: true,
|
|
4766
|
-
minLinesForLast: this.cfg.PROFILE_CODE.minLinesForHL,
|
|
4767
|
-
minCharsForLast: this.cfg.PROFILE_CODE.minCharsForHL
|
|
4768
|
-
}, this.activeCode);
|
|
4769
|
-
this.codeScroll.initScrollableBlocks(box);
|
|
4770
|
-
});
|
|
4854
|
+
this.codeScroll.initScrollableBlocks(box);
|
|
4855
|
+
});
|
|
4771
4856
|
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
if (
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
}
|
|
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
|
+
}
|
|
4778
4862
|
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
}
|
|
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
|
+
}
|
|
4786
4869
|
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
this.lastSnapshotTs = Utils.now();
|
|
4870
|
+
window.__lastSnapshotLen = this.getStreamLength();
|
|
4871
|
+
this.lastSnapshotTs = Utils.now();
|
|
4790
4872
|
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
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
|
+
}
|
|
4798
4880
|
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
this.scrollMgr.scheduleScrollFabUpdate();
|
|
4881
|
+
this.scrollMgr.scheduleScroll(true);
|
|
4882
|
+
this.scrollMgr.fabFreezeUntil = Utils.now() + this.cfg.FAB.TOGGLE_DEBOUNCE_MS;
|
|
4883
|
+
this.scrollMgr.scheduleScrollFabUpdate();
|
|
4803
4884
|
|
|
4804
|
-
|
|
4885
|
+
if (this.suppressPostFinalizePass) this.suppressPostFinalizePass = false;
|
|
4805
4886
|
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
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
|
+
}
|
|
4809
4890
|
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
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);
|
|
4822
4921
|
}
|
|
4823
|
-
// End streaming session, finalize active code if present, and complete math/highlight.
|
|
4824
|
-
endStream() {
|
|
4825
|
-
// Switch to final mode before the last snapshot to allow full renderer (linkify etc.)
|
|
4826
|
-
this.isStreaming = false;
|
|
4827
4922
|
|
|
4828
|
-
|
|
4829
|
-
|
|
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;
|
|
4830
4933
|
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
this.snapshotRAF = 0;
|
|
4934
|
+
// Do not interfere with code fence streaming.
|
|
4935
|
+
if (this.fenceOpen || this.codeStream.open) return;
|
|
4834
4936
|
|
|
4835
|
-
const
|
|
4836
|
-
if (this.activeCode) this.finalizeActiveCode();
|
|
4937
|
+
const isFirstSnapshot = ((window.__lastSnapshotLen || 0) === 0);
|
|
4837
4938
|
|
|
4838
|
-
if (
|
|
4839
|
-
if
|
|
4840
|
-
|
|
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;
|
|
4841
4947
|
}
|
|
4842
|
-
const snap = msg ? this.getMsgSnapshotRoot(msg) : null;
|
|
4843
|
-
if (snap) this.math.renderAsync(snap); // ensure math completes eagerly but async
|
|
4844
4948
|
}
|
|
4845
4949
|
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
if (!this.activeCode && !this.fenceOpen) {
|
|
4853
|
-
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
|
|
4854
4956
|
}
|
|
4855
|
-
|
|
4957
|
+
} catch (_) {
|
|
4958
|
+
// Keep streaming resilient; never throw here.
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4856
4961
|
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
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;
|
|
4860
4968
|
|
|
4861
|
-
|
|
4862
|
-
|
|
4969
|
+
const msg = this.getMsg(true, name_header); if (!msg || !chunk) return;
|
|
4970
|
+
const s = String(chunk);
|
|
4971
|
+
if (!alreadyBuffered) this._appendChunk(s);
|
|
4863
4972
|
|
|
4864
|
-
|
|
4973
|
+
const change = this.updateFenceHeuristic(s);
|
|
4974
|
+
const nlCount = Utils.countNewlines(s); const chunkHasNL = nlCount > 0;
|
|
4865
4975
|
|
|
4866
|
-
|
|
4867
|
-
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 });
|
|
4868
4977
|
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
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
|
+
}
|
|
4874
4983
|
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
}
|
|
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.
|
|
4888
5003
|
}
|
|
4889
5004
|
}
|
|
5005
|
+
}
|
|
4890
5006
|
|
|
4891
|
-
|
|
4892
|
-
|
|
5007
|
+
if (this.codeStream.open) {
|
|
5008
|
+
this.codeStream.lines += nlCount; this.codeStream.chars += s.length;
|
|
4893
5009
|
|
|
4894
|
-
|
|
4895
|
-
|
|
5010
|
+
if (this.activeCode && this.activeCode.codeEl && this.activeCode.codeEl.isConnected) {
|
|
5011
|
+
let partForCode = s; let remainder = '';
|
|
4896
5012
|
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
}
|
|
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);
|
|
4903
5018
|
}
|
|
5019
|
+
}
|
|
4904
5020
|
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
5021
|
+
if (partForCode) {
|
|
5022
|
+
this.appendToActiveTail(partForCode);
|
|
5023
|
+
this.activeCode.lines += Utils.countNewlines(partForCode);
|
|
4908
5024
|
|
|
4909
|
-
|
|
4910
|
-
|
|
5025
|
+
this.maybePromoteLanguageFromDirective();
|
|
5026
|
+
this.enforceHLStopBudget();
|
|
4911
5027
|
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
}
|
|
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);
|
|
4916
5031
|
}
|
|
4917
5032
|
}
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
}
|
|
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);
|
|
4929
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 });
|
|
4930
5054
|
} else {
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
}
|
|
4935
|
-
if (change.closed) {
|
|
4936
|
-
this.codeStream.open = false; this.resetBudget(); this.scheduleSnapshot(msg);
|
|
4937
|
-
this._d('CODE_CLOSED_WITHOUT_ACTIVE', { sinceLastSnapMs: Math.round(Utils.now() - this.lastSnapshotTs), snapshotScheduled: this.snapshotScheduled });
|
|
4938
|
-
} else {
|
|
4939
|
-
const boundary = this.hasStructuralBoundary(s);
|
|
4940
|
-
if (this.shouldSnapshotOnChunk(s, chunkHasNL, boundary)) this.scheduleSnapshot(msg);
|
|
4941
|
-
else this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
4942
|
-
}
|
|
4943
|
-
return;
|
|
5055
|
+
const boundary = this.hasStructuralBoundary(s);
|
|
5056
|
+
if (this.shouldSnapshotOnChunk(s, chunkHasNL, boundary)) this.scheduleSnapshot(msg);
|
|
5057
|
+
else this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
4944
5058
|
}
|
|
5059
|
+
return;
|
|
4945
5060
|
}
|
|
5061
|
+
}
|
|
4946
5062
|
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
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 });
|
|
4950
5071
|
} else {
|
|
4951
|
-
|
|
4952
|
-
if (this.shouldSnapshotOnChunk(s, chunkHasNL, boundary)) {
|
|
4953
|
-
this.scheduleSnapshot(msg);
|
|
4954
|
-
this._d('SCHEDULE_SNAPSHOT_BOUNDARY', { boundary });
|
|
4955
|
-
} else {
|
|
4956
|
-
this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
4957
|
-
}
|
|
5072
|
+
this.maybeScheduleSoftSnapshot(msg, chunkHasNL);
|
|
4958
5073
|
}
|
|
4959
5074
|
}
|
|
4960
5075
|
}
|
|
5076
|
+
}
|
|
4961
5077
|
|
|
4962
5078
|
// ==========================================================================
|
|
4963
5079
|
// 12) Stream queue
|