pygpt-net 2.6.55__py3-none-any.whl → 2.6.57__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.
Files changed (218) hide show
  1. pygpt_net/CHANGELOG.txt +12 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +26 -22
  4. pygpt_net/config.py +44 -0
  5. pygpt_net/controller/audio/audio.py +0 -0
  6. pygpt_net/controller/calendar/calendar.py +0 -0
  7. pygpt_net/controller/calendar/note.py +0 -0
  8. pygpt_net/controller/chat/chat.py +0 -0
  9. pygpt_net/controller/chat/handler/openai_stream.py +2 -1
  10. pygpt_net/controller/chat/handler/worker.py +0 -0
  11. pygpt_net/controller/chat/remote_tools.py +5 -3
  12. pygpt_net/controller/chat/render.py +0 -0
  13. pygpt_net/controller/chat/text.py +0 -0
  14. pygpt_net/controller/ctx/common.py +0 -0
  15. pygpt_net/controller/debug/debug.py +26 -2
  16. pygpt_net/controller/debug/fixtures.py +1 -1
  17. pygpt_net/controller/dialogs/confirm.py +15 -1
  18. pygpt_net/controller/dialogs/debug.py +2 -0
  19. pygpt_net/controller/lang/mapping.py +0 -0
  20. pygpt_net/controller/launcher/launcher.py +0 -0
  21. pygpt_net/controller/mode/mode.py +0 -0
  22. pygpt_net/controller/presets/presets.py +0 -0
  23. pygpt_net/controller/realtime/realtime.py +0 -0
  24. pygpt_net/controller/theme/theme.py +0 -0
  25. pygpt_net/controller/ui/mode.py +5 -3
  26. pygpt_net/controller/ui/tabs.py +0 -0
  27. pygpt_net/core/agents/agents.py +3 -1
  28. pygpt_net/core/agents/custom.py +150 -0
  29. pygpt_net/core/agents/provider.py +0 -0
  30. pygpt_net/core/builder/__init__.py +12 -0
  31. pygpt_net/core/builder/graph.py +478 -0
  32. pygpt_net/core/calendar/calendar.py +0 -0
  33. pygpt_net/core/ctx/ctx.py +0 -0
  34. pygpt_net/core/ctx/output.py +0 -0
  35. pygpt_net/core/debug/agent.py +0 -0
  36. pygpt_net/core/debug/agent_builder.py +29 -0
  37. pygpt_net/core/debug/console/console.py +0 -0
  38. pygpt_net/core/debug/db.py +0 -0
  39. pygpt_net/core/debug/debug.py +0 -0
  40. pygpt_net/core/debug/events.py +0 -0
  41. pygpt_net/core/debug/indexes.py +0 -0
  42. pygpt_net/core/debug/kernel.py +0 -0
  43. pygpt_net/core/debug/tabs.py +0 -0
  44. pygpt_net/core/filesystem/filesystem.py +0 -0
  45. pygpt_net/core/fixtures/__init__ +0 -0
  46. pygpt_net/core/fixtures/stream/__init__.py +0 -0
  47. pygpt_net/core/fixtures/stream/generator.py +0 -0
  48. pygpt_net/core/models/models.py +2 -1
  49. pygpt_net/core/plugins/plugins.py +60 -0
  50. pygpt_net/core/render/plain/pid.py +0 -0
  51. pygpt_net/core/render/plain/renderer.py +26 -4
  52. pygpt_net/core/render/web/body.py +46 -4
  53. pygpt_net/core/render/web/debug.py +0 -0
  54. pygpt_net/core/render/web/helpers.py +0 -0
  55. pygpt_net/core/render/web/pid.py +0 -0
  56. pygpt_net/core/render/web/renderer.py +15 -20
  57. pygpt_net/core/tabs/tab.py +0 -0
  58. pygpt_net/core/tabs/tabs.py +0 -0
  59. pygpt_net/core/text/utils.py +0 -0
  60. pygpt_net/css.qrc +0 -0
  61. pygpt_net/css_rc.py +0 -0
  62. pygpt_net/data/config/config.json +8 -7
  63. pygpt_net/data/config/models.json +3 -3
  64. pygpt_net/data/config/settings.json +14 -0
  65. pygpt_net/data/css/web-blocks.css +9 -0
  66. pygpt_net/data/css/web-blocks.dark.css +6 -0
  67. pygpt_net/data/css/web-blocks.darkest.css +6 -0
  68. pygpt_net/data/css/web-chatgpt.css +14 -6
  69. pygpt_net/data/css/web-chatgpt.dark.css +6 -0
  70. pygpt_net/data/css/web-chatgpt.darkest.css +6 -0
  71. pygpt_net/data/css/web-chatgpt.light.css +6 -0
  72. pygpt_net/data/css/web-chatgpt_wide.css +14 -6
  73. pygpt_net/data/css/web-chatgpt_wide.dark.css +6 -0
  74. pygpt_net/data/css/web-chatgpt_wide.darkest.css +6 -0
  75. pygpt_net/data/css/web-chatgpt_wide.light.css +6 -0
  76. pygpt_net/data/fixtures/fake_stream.txt +14 -1
  77. pygpt_net/data/icons/case.svg +0 -0
  78. pygpt_net/data/icons/chat1.svg +0 -0
  79. pygpt_net/data/icons/chat2.svg +0 -0
  80. pygpt_net/data/icons/chat3.svg +0 -0
  81. pygpt_net/data/icons/chat4.svg +0 -0
  82. pygpt_net/data/icons/fit.svg +0 -0
  83. pygpt_net/data/icons/note1.svg +0 -0
  84. pygpt_net/data/icons/note2.svg +0 -0
  85. pygpt_net/data/icons/note3.svg +0 -0
  86. pygpt_net/data/icons/stt.svg +0 -0
  87. pygpt_net/data/icons/translate.svg +0 -0
  88. pygpt_net/data/icons/tts.svg +0 -0
  89. pygpt_net/data/icons/url.svg +0 -0
  90. pygpt_net/data/icons/vision.svg +0 -0
  91. pygpt_net/data/icons/web_off.svg +0 -0
  92. pygpt_net/data/icons/web_on.svg +0 -0
  93. pygpt_net/data/js/app/async.js +166 -0
  94. pygpt_net/data/js/app/bridge.js +88 -0
  95. pygpt_net/data/js/app/common.js +212 -0
  96. pygpt_net/data/js/app/config.js +223 -0
  97. pygpt_net/data/js/app/custom.js +961 -0
  98. pygpt_net/data/js/app/data.js +84 -0
  99. pygpt_net/data/js/app/dom.js +322 -0
  100. pygpt_net/data/js/app/events.js +400 -0
  101. pygpt_net/data/js/app/highlight.js +542 -0
  102. pygpt_net/data/js/app/logger.js +305 -0
  103. pygpt_net/data/js/app/markdown.js +1137 -0
  104. pygpt_net/data/js/app/math.js +167 -0
  105. pygpt_net/data/js/app/nodes.js +395 -0
  106. pygpt_net/data/js/app/queue.js +260 -0
  107. pygpt_net/data/js/app/raf.js +250 -0
  108. pygpt_net/data/js/app/runtime.js +582 -0
  109. pygpt_net/data/js/app/scroll.js +433 -0
  110. pygpt_net/data/js/app/stream.js +2708 -0
  111. pygpt_net/data/js/app/template.js +287 -0
  112. pygpt_net/data/js/app/tool.js +87 -0
  113. pygpt_net/data/js/app/ui.js +86 -0
  114. pygpt_net/data/js/app/user.js +380 -0
  115. pygpt_net/data/js/app/utils.js +64 -0
  116. pygpt_net/data/js/app.min.js +880 -0
  117. pygpt_net/data/js/markdown-it/markdown-it-katex.min.js +1 -1
  118. pygpt_net/data/js/markdown-it/markdown-it.min.js +0 -0
  119. pygpt_net/data/locale/locale.de.ini +3 -1
  120. pygpt_net/data/locale/locale.en.ini +8 -0
  121. pygpt_net/data/locale/locale.es.ini +2 -0
  122. pygpt_net/data/locale/locale.fr.ini +2 -0
  123. pygpt_net/data/locale/locale.it.ini +2 -0
  124. pygpt_net/data/locale/locale.pl.ini +3 -1
  125. pygpt_net/data/locale/locale.uk.ini +3 -1
  126. pygpt_net/data/locale/locale.zh.ini +2 -0
  127. pygpt_net/data/locale/plugin.osm.en.ini +24 -24
  128. pygpt_net/data/locale/plugin.wolfram.en.ini +9 -9
  129. pygpt_net/fonts.qrc +0 -0
  130. pygpt_net/fonts_rc.py +0 -0
  131. pygpt_net/icons.qrc +0 -0
  132. pygpt_net/icons_rc.py +0 -0
  133. pygpt_net/item/agent.py +62 -0
  134. pygpt_net/item/builder_layout.py +62 -0
  135. pygpt_net/js.qrc +24 -1
  136. pygpt_net/js_rc.py +51394 -33687
  137. pygpt_net/plugin/base/worker.py +0 -0
  138. pygpt_net/plugin/cmd_web/config.py +17 -17
  139. pygpt_net/plugin/cmd_web/worker.py +325 -171
  140. pygpt_net/plugin/mcp/__init__.py +0 -0
  141. pygpt_net/plugin/mcp/config.py +0 -0
  142. pygpt_net/plugin/mcp/plugin.py +0 -0
  143. pygpt_net/plugin/mcp/worker.py +0 -0
  144. pygpt_net/plugin/osm/__init__.py +0 -0
  145. pygpt_net/plugin/osm/config.py +0 -0
  146. pygpt_net/plugin/osm/plugin.py +0 -0
  147. pygpt_net/plugin/osm/worker.py +0 -0
  148. pygpt_net/plugin/wolfram/__init__.py +0 -0
  149. pygpt_net/plugin/wolfram/config.py +0 -0
  150. pygpt_net/plugin/wolfram/plugin.py +0 -0
  151. pygpt_net/plugin/wolfram/worker.py +0 -0
  152. pygpt_net/provider/api/anthropic/tools.py +0 -0
  153. pygpt_net/provider/api/google/__init__.py +0 -0
  154. pygpt_net/provider/api/google/video.py +0 -0
  155. pygpt_net/provider/api/openai/agents/experts.py +0 -0
  156. pygpt_net/provider/api/openai/agents/remote_tools.py +0 -0
  157. pygpt_net/provider/api/openai/remote_tools.py +0 -0
  158. pygpt_net/provider/api/openai/responses.py +0 -0
  159. pygpt_net/provider/api/x_ai/__init__.py +2 -0
  160. pygpt_net/provider/api/x_ai/remote.py +0 -0
  161. pygpt_net/provider/core/agent/__init__.py +10 -0
  162. pygpt_net/provider/core/agent/base.py +51 -0
  163. pygpt_net/provider/core/agent/json_file.py +200 -0
  164. pygpt_net/provider/core/config/patch.py +33 -0
  165. pygpt_net/provider/core/config/patches/__init__.py +0 -0
  166. pygpt_net/provider/core/config/patches/patch_before_2_6_42.py +1 -0
  167. pygpt_net/provider/core/ctx/db_sqlite/storage.py +0 -0
  168. pygpt_net/provider/core/model/patches/__init__.py +0 -0
  169. pygpt_net/provider/core/model/patches/patch_before_2_6_42.py +0 -0
  170. pygpt_net/provider/core/preset/patch.py +0 -0
  171. pygpt_net/provider/core/preset/patches/__init__.py +0 -0
  172. pygpt_net/provider/core/preset/patches/patch_before_2_6_42.py +0 -0
  173. pygpt_net/provider/llms/anthropic.py +4 -0
  174. pygpt_net/provider/llms/base.py +2 -0
  175. pygpt_net/provider/llms/deepseek_api.py +2 -0
  176. pygpt_net/provider/llms/google.py +2 -0
  177. pygpt_net/provider/llms/hugging_face_api.py +4 -0
  178. pygpt_net/provider/llms/hugging_face_embedding.py +0 -0
  179. pygpt_net/provider/llms/hugging_face_router.py +2 -0
  180. pygpt_net/provider/llms/local.py +0 -0
  181. pygpt_net/provider/llms/mistral.py +4 -0
  182. pygpt_net/provider/llms/open_router.py +0 -0
  183. pygpt_net/provider/llms/perplexity.py +2 -0
  184. pygpt_net/provider/llms/utils.py +0 -0
  185. pygpt_net/provider/llms/voyage.py +0 -0
  186. pygpt_net/provider/llms/x_ai.py +2 -0
  187. pygpt_net/tools/agent_builder/__init__.py +12 -0
  188. pygpt_net/tools/agent_builder/tool.py +292 -0
  189. pygpt_net/tools/agent_builder/ui/__init__.py +0 -0
  190. pygpt_net/tools/agent_builder/ui/dialogs.py +152 -0
  191. pygpt_net/tools/agent_builder/ui/list.py +228 -0
  192. pygpt_net/tools/code_interpreter/ui/html.py +0 -0
  193. pygpt_net/tools/code_interpreter/ui/widgets.py +0 -0
  194. pygpt_net/tools/html_canvas/tool.py +23 -6
  195. pygpt_net/tools/html_canvas/ui/widgets.py +224 -2
  196. pygpt_net/ui/layout/chat/chat.py +0 -0
  197. pygpt_net/ui/layout/chat/output.py +5 -5
  198. pygpt_net/ui/main.py +10 -9
  199. pygpt_net/ui/menu/debug.py +39 -1
  200. pygpt_net/ui/widget/builder/__init__.py +12 -0
  201. pygpt_net/ui/widget/builder/editor.py +2001 -0
  202. pygpt_net/ui/widget/dialog/base.py +4 -1
  203. pygpt_net/ui/widget/draw/painter.py +0 -0
  204. pygpt_net/ui/widget/element/labels.py +9 -4
  205. pygpt_net/ui/widget/lists/db.py +0 -0
  206. pygpt_net/ui/widget/lists/debug.py +0 -0
  207. pygpt_net/ui/widget/tabs/body.py +0 -0
  208. pygpt_net/ui/widget/textarea/html.py +1 -0
  209. pygpt_net/ui/widget/textarea/input.py +28 -10
  210. pygpt_net/ui/widget/textarea/output.py +21 -1
  211. pygpt_net/ui/widget/textarea/web.py +31 -3
  212. pygpt_net/utils.py +40 -0
  213. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/METADATA +16 -2
  214. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/RECORD +116 -77
  215. pygpt_net/data/js/app.js +0 -5869
  216. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/LICENSE +0 -0
  217. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/WHEEL +0 -0
  218. {pygpt_net-2.6.55.dist-info → pygpt_net-2.6.57.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,961 @@
1
+ // custom.js
2
+
3
+
4
+ // ==========================================================================
5
+ // Custom Markup Processor
6
+ // ==========================================================================
7
+
8
+ class CustomMarkup {
9
+
10
+ // Constructor: set config, logger and initialize internal caches/flags
11
+ constructor(cfg, logger) {
12
+ this.cfg = cfg || {
13
+ CUSTOM_MARKUP_RULES: []
14
+ };
15
+ this.logger = logger || new Logger(cfg);
16
+
17
+ // Compiled state
18
+ this.__compiled = null; // all rules
19
+ this.__streamRules = null; // subset of stream rules (cache)
20
+ this.__streamWrapRules = null; // subset: stream + html-phase, without open/closeReplace (cache)
21
+ this.__hasStreamRules = false; // quick flag
22
+
23
+ // Quick tests for the presence of "open" (avoid O(N*M) indexOf on hot path)
24
+ this.__openReAll = null; // RegExp for all html-phase/both
25
+ this.__openReStream = null; // RegExp for stream html-phase/both
26
+ }
27
+
28
+ // Debug helper: write structured log lines for the stream engine.
29
+ _d(tag, data) {
30
+ try {
31
+ const lg = this.logger || (this.cfg && this.cfg.logger) || (window.runtime && runtime.logger) || null;
32
+ if (!lg || typeof lg.debug !== 'function') return;
33
+ lg.debug_obj("CM", tag, data);
34
+ } catch (_) {}
35
+ }
36
+
37
+ // Decode HTML entities once using a <textarea> trick
38
+ decodeEntitiesOnce(s) {
39
+ if (!s || !s.indexOf || s.indexOf('&') === -1) return String(s || '');
40
+ const ta = CustomMarkup._decTA || (CustomMarkup._decTA = document.createElement('textarea'));
41
+ ta.innerHTML = s;
42
+ return ta.value;
43
+ }
44
+
45
+ // Escape HTML safely (fallback if Utils.escapeHtml is missing)
46
+ _escHtml(s) {
47
+ try {
48
+ return Utils.escapeHtml(s);
49
+ } catch (_) {
50
+ return String(s || '').replace(/[&<>"']/g, m => ({
51
+ '&': '&amp;',
52
+ '<': '&lt;',
53
+ '>': '&gt;',
54
+ '"': '&quot;',
55
+ "'": '&#039;'
56
+ } [m]));
57
+ }
58
+ }
59
+
60
+ // Escape HTML but preserve <br> and optionally convert newlines to <br>
61
+ _escapeHtmlAllowBr(text, { convertNewlines = true } = {}) {
62
+ const PLACEHOLDER = '\u0001__BR__\u0001';
63
+ let s = String(text || '');
64
+ s = s.replace(/<br\s*\/?>/gi, PLACEHOLDER);
65
+ s = this._escHtml(s);
66
+ if (convertNewlines) s = s.replace(/\r\n|\r|\n/g, '<br>');
67
+ s = s.replaceAll(PLACEHOLDER, '<br>');
68
+ return s;
69
+ }
70
+
71
+ // Fast check if any "open" token from rules exists in text
72
+ hasAnyOpenToken(text, rules) {
73
+ if (!text || !rules || !rules.length) return false;
74
+ if (rules === this.__compiled && this.__openReAll) {
75
+ return this.__openReAll.test(text);
76
+ }
77
+ if (rules === this.__streamRules && this.__openReStream) {
78
+ return this.__openReStream.test(text);
79
+ }
80
+ for (let i = 0; i < rules.length; i++) {
81
+ const r = rules[i];
82
+ if (!r || !r.open) continue;
83
+ if (text.indexOf(r.open) !== -1) return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ // fast check against stream rules’ open tokens
89
+ hasAnyStreamOpenToken(text) {
90
+ this.ensureCompiled();
91
+ if (!this.__hasStreamRules) return false;
92
+ const t = String(text || '');
93
+ if (this.__openReStream) return this.__openReStream.test(t);
94
+ const rules = this.__streamRules || [];
95
+ return this.hasAnyOpenToken(t, rules);
96
+ }
97
+
98
+ // Convert inner text to HTML depending on rule options and Markdown renderer
99
+ _materializeInnerHTML(rule, text, MD) {
100
+ let payload = String(text || '');
101
+ const wantsBr = !!(rule && (rule.nl2br || rule.allowBr));
102
+ if (rule && rule.decodeEntities && payload && payload.indexOf('&') !== -1) {
103
+ try { payload = this.decodeEntitiesOnce(payload); } catch (_) {}
104
+ }
105
+ if (wantsBr) {
106
+ try {
107
+ return this._escapeHtmlAllowBr(payload, { convertNewlines: !!rule.nl2br });
108
+ } catch (_) {
109
+ return this._escHtml(payload);
110
+ }
111
+ }
112
+ if (rule && rule.innerMode === 'markdown-inline' && MD && typeof MD.renderInline === 'function') {
113
+ try {
114
+ return MD.renderInline(payload);
115
+ } catch (_) {
116
+ return this._escHtml(payload);
117
+ }
118
+ }
119
+ return this._escHtml(payload);
120
+ }
121
+
122
+ // Get a DocumentFragment from HTML string
123
+ _fragmentFromHTML(html) {
124
+ const tpl = document.createElement('template');
125
+ tpl.innerHTML = String(html || '');
126
+ return tpl.content;
127
+ }
128
+
129
+ // Replace an element with HTML content
130
+ _replaceElementWithHTML(el, html) {
131
+ if (!el || !el.parentNode) return;
132
+ const parent = el.parentNode;
133
+ const frag = this._fragmentFromHTML(html);
134
+ parent.insertBefore(frag, el);
135
+ parent.removeChild(el);
136
+ }
137
+
138
+ // Build a RegExp that matches any of the "open" tokens in the given rules
139
+ _buildOpenRegex(rules) {
140
+ if (!rules || !rules.length) return null;
141
+ const tokens = [];
142
+ let patternLen = 0;
143
+ const LIMIT_TOKENS = 200;
144
+ const LIMIT_PATTERN_LEN = 4000;
145
+
146
+ for (const r of rules) {
147
+ if (!r || !r.open) continue;
148
+ const esc = Utils.reEscape(r.open);
149
+ tokens.push(esc);
150
+ patternLen += esc.length + 1;
151
+ if (tokens.length > LIMIT_TOKENS || patternLen > LIMIT_PATTERN_LEN) return null;
152
+ }
153
+ if (!tokens.length) return null;
154
+ tokens.sort((a, b) => b.length - a.length);
155
+ try {
156
+ return new RegExp('(?:' + tokens.join('|') + ')');
157
+ } catch (_) {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ // Compile rules from given array or config/global override
163
+ compile(rules) {
164
+ const src = Array.isArray(rules) ? rules :
165
+ (window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES || []);
166
+ const compiled = [];
167
+ let hasStream = false;
168
+
169
+ for (const r of src) {
170
+ if (!r || typeof r.open !== 'string' || typeof r.close !== 'string') continue;
171
+
172
+ const tag = (r.tag || 'span').toLowerCase();
173
+ const className = (r.className || r.class || '').trim();
174
+ const innerMode = (r.innerMode === 'markdown-inline' || r.innerMode === 'text') ? r.innerMode : 'text';
175
+
176
+ const stream = !!(r.stream === true);
177
+ const openReplace = String((r.openReplace != null ? r.openReplace : (r.openReplace || '')) || '');
178
+ const closeReplace = String((r.closeReplace != null ? r.closeReplace : (r.closeReplace || '')) || '');
179
+
180
+ const decodeEntities = (typeof r.decodeEntities === 'boolean') ?
181
+ r.decodeEntities :
182
+ ((r.name || '').toLowerCase() === 'cmd' || className === 'cmd');
183
+
184
+ let phaseRaw = (typeof r.phase === 'string') ? r.phase.toLowerCase() : '';
185
+ if (phaseRaw !== 'source' && phaseRaw !== 'html' && phaseRaw !== 'both') phaseRaw = '';
186
+ const looksLikeFence = (openReplace.indexOf('```') !== -1) || (closeReplace.indexOf('```') !== -1);
187
+ const phase = phaseRaw || (looksLikeFence ? 'source' : 'html');
188
+
189
+ const re = new RegExp(Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close), 'g');
190
+ const reFull = new RegExp('^' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '$');
191
+ const reFullTrim = new RegExp('^\\s*' + Utils.reEscape(r.open) + '([\\s\\S]*?)' + Utils.reEscape(r.close) + '\\s*$');
192
+
193
+ const nl2br = !!r.nl2br;
194
+ const allowBr = !!r.allowBr;
195
+
196
+ const item = {
197
+ name: r.name || tag,
198
+ tag,
199
+ className,
200
+ innerMode,
201
+ open: r.open,
202
+ close: r.close,
203
+ decodeEntities,
204
+ re,
205
+ reFull,
206
+ reFullTrim,
207
+ stream,
208
+ openReplace,
209
+ closeReplace,
210
+ phase,
211
+ isSourceFence: looksLikeFence,
212
+ nl2br,
213
+ allowBr
214
+ };
215
+ compiled.push(item);
216
+ if (stream) hasStream = true;
217
+ }
218
+
219
+ if (compiled.length === 0) {
220
+ const open = '[!cmd]', close = '[/!cmd]';
221
+ const item = {
222
+ name: 'cmd',
223
+ tag: 'p',
224
+ className: 'cmd',
225
+ innerMode: 'text',
226
+ open,
227
+ close,
228
+ decodeEntities: true,
229
+ re: new RegExp(Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close), 'g'),
230
+ reFull: new RegExp('^' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '$'),
231
+ reFullTrim: new RegExp('^\\s*' + Utils.reEscape(open) + '([\\s\\S]*?)' + Utils.reEscape(close) + '\\s*$'),
232
+ stream: false,
233
+ openReplace: '',
234
+ closeReplace: '',
235
+ phase: 'html',
236
+ isSourceFence: false,
237
+ nl2br: false,
238
+ allowBr: false
239
+ };
240
+ compiled.push(item);
241
+ }
242
+
243
+ this.__compiled = compiled;
244
+ this.__hasStreamRules = hasStream;
245
+ this.__streamRules = compiled.filter(r => !!r.stream);
246
+ this.__streamWrapRules = this.__streamRules.filter(
247
+ r => (r.phase === 'html' || r.phase === 'both') && !(r.openReplace || r.closeReplace) && r.open && r.close
248
+ );
249
+
250
+ const htmlPhaseAll = compiled.filter(r => (r.phase === 'html' || r.phase === 'both'));
251
+ const htmlPhaseStream = this.__streamRules.filter(r => (r.phase === 'html' || r.phase === 'both'));
252
+ this.__openReAll = this._buildOpenRegex(htmlPhaseAll);
253
+ this.__openReStream = this._buildOpenRegex(htmlPhaseStream);
254
+
255
+ this._d('cm.compile', { rules: compiled.length, streamRules: this.__streamRules.length });
256
+ return compiled;
257
+ }
258
+
259
+ // Transform source text by applying source-phase rules outside fenced code blocks
260
+ transformSource(src, opts) {
261
+ let s = String(src || '');
262
+ this.ensureCompiled();
263
+ const rules = this.__compiled;
264
+ if (!rules || !rules.length) return s;
265
+
266
+ const candidates = [];
267
+ for (let i = 0; i < rules.length; i++) {
268
+ const r = rules[i];
269
+ if (!r) continue;
270
+ if ((r.phase === 'source' || r.phase === 'both') && (r.openReplace || r.closeReplace)) candidates.push(r);
271
+ }
272
+ if (!candidates.length) return s;
273
+
274
+ const fences = this._findFenceRanges(s);
275
+ let result = '';
276
+ if (!fences.length) {
277
+ result = this._applySourceReplacementsInChunk(s, s, 0, candidates);
278
+ } else {
279
+ let out = '', last = 0;
280
+ for (let k = 0; k < fences.length; k++) {
281
+ const [a, b] = fences[k];
282
+ if (a > last) {
283
+ const chunk = s.slice(last, a);
284
+ out += this._applySourceReplacementsInChunk(s, chunk, last, candidates);
285
+ }
286
+ out += s.slice(a, b);
287
+ last = b;
288
+ }
289
+ if (last < s.length) {
290
+ const tail = s.slice(last);
291
+ out += this._applySourceReplacementsInChunk(s, tail, last, candidates);
292
+ }
293
+ result = out;
294
+ }
295
+
296
+ if (opts && opts.streaming === true) {
297
+ const fenceRules = candidates.filter(r => !!r.isSourceFence);
298
+ if (fenceRules.length) result = this._injectUnmatchedSourceOpeners(result, fenceRules);
299
+ }
300
+
301
+ if (result !== src) this._d('cm.transformSource', { streaming: !!(opts && opts.streaming), delta: result.length - String(src || '').length });
302
+ return result;
303
+ }
304
+
305
+ // Get specs of all source fence rules
306
+ getSourceFenceSpecs() {
307
+ this.ensureCompiled();
308
+ const rules = this.__compiled || [];
309
+ const out = [];
310
+ for (let i = 0; i < rules.length; i++) {
311
+ const r = rules[i];
312
+ if (!r || !r.isSourceFence) continue;
313
+ if (r.phase !== 'source' && r.phase !== 'both') continue;
314
+ out.push({ open: r.open, close: r.close });
315
+ }
316
+ return out;
317
+ }
318
+
319
+ // Ensure the rules are compiled (from global override or config)
320
+ ensureCompiled() {
321
+ if (!this.__compiled) {
322
+ this.compile(window.CUSTOM_MARKUP_RULES || this.cfg.CUSTOM_MARKUP_RULES);
323
+ }
324
+ return this.__compiled;
325
+ }
326
+
327
+ // Set a new set of rules (overrides global and config)
328
+ setRules(rules) {
329
+ this.compile(rules);
330
+ window.CUSTOM_MARKUP_RULES = Array.isArray(rules) ? rules.slice() :
331
+ (this.cfg.CUSTOM_MARKUP_RULES || []).slice();
332
+ this._d('cm.setRules', { count: (window.CUSTOM_MARKUP_RULES || []).length });
333
+ }
334
+
335
+ // Get the current set of rules (from global override or config)
336
+ getRules() {
337
+ const list = (window.CUSTOM_MARKUP_RULES ? window.CUSTOM_MARKUP_RULES.slice() :
338
+ (this.cfg.CUSTOM_MARKUP_RULES || []).slice());
339
+ return list;
340
+ }
341
+
342
+ // Quick check if any stream rules are defined
343
+ hasStreamRules() {
344
+ this.ensureCompiled();
345
+ return !!this.__hasStreamRules;
346
+ }
347
+
348
+ // Quick check if text starts with any known stream opener
349
+ hasStreamOpenerAtStart(text) {
350
+ if (!text) return false;
351
+ this.ensureCompiled();
352
+ if (!this.__hasStreamRules) return false;
353
+ const rules = this.__streamRules || [];
354
+ if (!rules.length) return false;
355
+ const t = String(text).trimStart();
356
+ for (let i = 0; i < rules.length; i++) {
357
+ const r = rules[i];
358
+ if (!r || !r.open) continue;
359
+ if (t.startsWith(r.open)) return true;
360
+ }
361
+ return false;
362
+ }
363
+
364
+ // Apply to stream if the delta text contains any known opener.
365
+ maybeApplyStreamOnDelta(root, deltaText, MD) {
366
+ try {
367
+ this.ensureCompiled();
368
+ if (!this.__hasStreamRules) return;
369
+ const t = String(deltaText || '');
370
+
371
+ // If the delta contains any known stream opener, run full stream pass quickly.
372
+ if (t && this.hasAnyStreamOpenToken(t)) {
373
+ this._d('cm.stream.delta', { len: t.length, head: t.slice(0, 64) });
374
+ this.applyStream(root, MD);
375
+ return;
376
+ }
377
+
378
+ // If there are pending wrappers in the snapshot root and the delta likely carries closers,
379
+ // finalize them immediately (cheap pass limited to pending subtrees).
380
+ if (root && root.querySelector && root.querySelector('[data-cm-pending="1"]')) {
381
+ if (t.indexOf('>') !== -1 || t.indexOf(']') !== -1) {
382
+ this.applyStreamFinalizeClosers(root, this.__streamRules);
383
+ this._d('cm.stream.delta.finalize', {});
384
+ }
385
+ }
386
+ } catch (_) { return; }
387
+ }
388
+
389
+ // Apply stream processing to the given subtree
390
+ applyStream(root, MD) {
391
+ this.ensureCompiled();
392
+ if (!this.__hasStreamRules) return;
393
+ const rules = this.__streamRules;
394
+ if (!rules || !rules.length) return;
395
+
396
+ this.applyRules(root, MD, rules);
397
+
398
+ try {
399
+ this.applyStreamPartialOpeners(root, MD, this.__streamWrapRules);
400
+ } catch (_) {}
401
+
402
+ try {
403
+ this.applyStreamFinalizeClosers(root, rules);
404
+ } catch (_) {}
405
+ }
406
+
407
+ // Finalize any openers that lack a closer in the current subtree
408
+ isInsideForbiddenContext(node) {
409
+ const p = node.parentElement;
410
+ if (!p) return true;
411
+ return !!p.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
412
+ }
413
+
414
+ // Check if an element is inside a forbidden element
415
+ isInsideForbiddenElement(el) {
416
+ if (!el) return true;
417
+ return !!el.closest('pre, code, kbd, samp, var, script, style, textarea, .math-pending, .hljs, .code-wrapper, ul, ol, li, dl, dt, dd');
418
+ }
419
+
420
+ // Find all source fence ranges in the text
421
+ findNextMatch(text, from, rules) {
422
+ let best = null;
423
+ for (const rule of rules) {
424
+ rule.re.lastIndex = from;
425
+ const m = rule.re.exec(text);
426
+ if (m) {
427
+ const start = m.index, end = rule.re.lastIndex;
428
+ if (!best || start < best.start) best = { rule, start, end, inner: m[1] || '' };
429
+ }
430
+ }
431
+ return best;
432
+ }
433
+
434
+ // Find a full match that spans the entire text
435
+ findFullMatch(text, rules) {
436
+ for (const rule of rules) {
437
+ if (rule.reFull) {
438
+ const m = rule.reFull.exec(text);
439
+ if (m) return { rule, inner: m[1] || '' };
440
+ } else {
441
+ rule.re.lastIndex = 0;
442
+ const m = rule.re.exec(text);
443
+ if (m && m.index === 0 && (rule.re.lastIndex === text.length)) {
444
+ const m2 = rule.re.exec(text);
445
+ if (!m2) return { rule, inner: m[1] || '' };
446
+ }
447
+ }
448
+ }
449
+ return null;
450
+ }
451
+
452
+ // Set inner content of an element according to mode and rule options
453
+ setInnerByMode(el, mode, text, MD, decodeEntities = false, rule = null) {
454
+ let payload = String(text || '');
455
+ const wantsBr = !!(rule && (rule.nl2br || rule.allowBr));
456
+
457
+ if (decodeEntities && payload && payload.indexOf('&') !== -1) {
458
+ try {
459
+ payload = this.decodeEntitiesOnce(payload);
460
+ } catch (_) {}
461
+ }
462
+
463
+ if (wantsBr) {
464
+ el.innerHTML = this._escapeHtmlAllowBr(payload, { convertNewlines: !!(rule && rule.nl2br) });
465
+ return;
466
+ }
467
+
468
+ if (mode === 'markdown-inline' && MD && typeof MD.renderInline === 'function') {
469
+ try {
470
+ el.innerHTML = MD.renderInline(payload);
471
+ return;
472
+ } catch (_) {}
473
+ }
474
+ el.textContent = payload;
475
+ }
476
+
477
+ // Finalize any openers that lack a closer in the current subtree
478
+ _tryReplaceFullParagraph(el, rules, MD) {
479
+ if (!el || el.tagName !== 'P') return false;
480
+ if (this.isInsideForbiddenElement(el)) {
481
+ return false;
482
+ }
483
+ const t = el.textContent || '';
484
+ if (!this.hasAnyOpenToken(t, rules)) return false;
485
+
486
+ for (const rule of rules) {
487
+ if (!rule) continue;
488
+ const m = rule.reFullTrim ? rule.reFullTrim.exec(t) : null;
489
+ if (!m) continue;
490
+
491
+ const innerText = m[1] || '';
492
+ if (rule.phase !== 'html' && rule.phase !== 'both') continue;
493
+
494
+ if (rule.openReplace || rule.closeReplace) {
495
+ const innerHTML = this._materializeInnerHTML(rule, innerText, MD);
496
+ const html = String(rule.openReplace || '') + innerHTML + String(rule.closeReplace || '');
497
+ this._replaceElementWithHTML(el, html);
498
+ this._d('cm.replace.full', { name: rule.name });
499
+ return true;
500
+ }
501
+
502
+ const outTag = (rule.tag && typeof rule.tag === 'string') ? rule.tag.toLowerCase() : 'span';
503
+ const out = document.createElement(outTag === 'p' ? 'p' : outTag);
504
+ if (rule.className) out.className = rule.className;
505
+ out.setAttribute('data-cm', rule.name);
506
+ this.setInnerByMode(out, rule.innerMode, innerText, MD, !!rule.decodeEntities, rule);
507
+
508
+ try {
509
+ el.replaceWith(out);
510
+ } catch (_) {
511
+ const par = el.parentNode;
512
+ if (par) par.replaceChild(out, el);
513
+ }
514
+ this._d('cm.wrap.full', { name: rule.name });
515
+ return true;
516
+ }
517
+ return false;
518
+ }
519
+
520
+ // Finalize any openers that lack a closer in the current subtree
521
+ applyRules(root, MD, rules) {
522
+ if (!root || !rules || !rules.length) return;
523
+
524
+ const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
525
+
526
+ try {
527
+ const paragraphs = (typeof scope.querySelectorAll === 'function') ? scope.querySelectorAll('p') : [];
528
+ if (paragraphs && paragraphs.length) {
529
+ for (let i = 0; i < paragraphs.length; i++) {
530
+ const p = paragraphs[i];
531
+ if (p && p.getAttribute && p.getAttribute('data-cm')) continue;
532
+ const tc = p && (p.textContent || '');
533
+ if (!tc || !this.hasAnyOpenToken(tc, rules)) continue;
534
+ if (this.isInsideForbiddenElement(p)) continue;
535
+ this._tryReplaceFullParagraph(p, rules, MD);
536
+ }
537
+ }
538
+ } catch (e) {}
539
+
540
+ const self = this;
541
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
542
+ acceptNode: (node) => {
543
+ const val = node && node.nodeValue ? node.nodeValue : '';
544
+ if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
545
+ if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
546
+ return NodeFilter.FILTER_ACCEPT;
547
+ }
548
+ });
549
+
550
+ let node;
551
+ while ((node = walker.nextNode())) {
552
+ const text = node.nodeValue;
553
+ if (!text || !this.hasAnyOpenToken(text, rules)) continue;
554
+ const parent = node.parentElement;
555
+
556
+ if (parent && parent.tagName === 'P' && parent.childNodes.length === 1) {
557
+ const fm = this.findFullMatch(text, rules);
558
+ if (fm) {
559
+ if ((fm.rule.phase === 'html' || fm.rule.phase === 'both') && (fm.rule.openReplace || fm.rule.closeReplace)) {
560
+ const innerHTML = this._materializeInnerHTML(fm.rule, fm.inner, MD);
561
+ const html = String(fm.rule.openReplace || '') + innerHTML + String(fm.rule.closeReplace || '');
562
+ this._replaceElementWithHTML(parent, html);
563
+ this._d('cm.replace.inlineFull', { name: fm.rule.name });
564
+ continue;
565
+ }
566
+ if (fm.rule.tag === 'p') {
567
+ const out = document.createElement('p');
568
+ if (fm.rule.className) out.className = fm.rule.className;
569
+ out.setAttribute('data-cm', fm.rule.name);
570
+ this.setInnerByMode(out, fm.rule.innerMode, fm.inner, MD, !!fm.rule.decodeEntities, fm.rule);
571
+ try {
572
+ parent.replaceWith(out);
573
+ } catch (_) {
574
+ const par = parent.parentNode;
575
+ if (par) par.replaceChild(out, parent);
576
+ }
577
+ this._d('cm.wrap.paragraph', { name: fm.rule.name });
578
+ continue;
579
+ }
580
+ }
581
+ }
582
+
583
+ let i = 0;
584
+ let didReplace = false;
585
+ const frag = document.createDocumentFragment();
586
+
587
+ while (i < text.length) {
588
+ const m = this.findNextMatch(text, i, rules);
589
+ if (!m) break;
590
+
591
+ if (m.start > i) {
592
+ frag.appendChild(document.createTextNode(text.slice(i, m.start)));
593
+ }
594
+
595
+ if ((m.rule.openReplace || m.rule.closeReplace) && (m.rule.phase === 'html' || m.rule.phase === 'both')) {
596
+ const innerHTML = this._materializeInnerHTML(m.rule, m.inner, MD);
597
+ const html = String(m.rule.openReplace || '') + innerHTML + String(m.rule.closeReplace || '');
598
+ const part = this._fragmentFromHTML(html);
599
+ frag.appendChild(part);
600
+ i = m.end;
601
+ didReplace = true;
602
+ this._d('cm.replace.inline', { name: m.rule.name });
603
+ continue;
604
+ }
605
+
606
+ if (m.rule.openReplace || m.rule.closeReplace) {
607
+ frag.appendChild(document.createTextNode(text.slice(m.start, m.end)));
608
+ i = m.end;
609
+ didReplace = true;
610
+ continue;
611
+ }
612
+
613
+ const tag = (m.rule.tag === 'p') ? 'span' : m.rule.tag;
614
+ const el = document.createElement(tag);
615
+ if (m.rule.className) el.className = m.rule.className;
616
+ el.setAttribute('data-cm', m.rule.name);
617
+ this.setInnerByMode(el, m.rule.innerMode, m.inner, MD, !!m.rule.decodeEntities, m.rule);
618
+ frag.appendChild(el);
619
+ this._d('cm.wrap.inline', { name: m.rule.name });
620
+
621
+ i = m.end;
622
+ didReplace = true;
623
+ }
624
+
625
+ if (!didReplace) continue;
626
+ if (i < text.length) frag.appendChild(document.createTextNode(text.slice(i)));
627
+ const parentNode = node.parentNode;
628
+ if (parentNode) {
629
+ parentNode.replaceChild(frag, node);
630
+ }
631
+ }
632
+ }
633
+
634
+ // Apply all rules to the given subtree
635
+ apply(root, MD) {
636
+ this.ensureCompiled();
637
+ this.applyRules(root, MD, this.__compiled);
638
+ }
639
+
640
+ // Handle partial openers that lack a closer in the current subtree
641
+ applyStreamPartialOpeners(root, MD, rules) {
642
+ if (!root) return;
643
+ rules = (rules || this.__streamWrapRules || []).slice();
644
+ if (!rules.length) return;
645
+
646
+ const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
647
+ const self = this;
648
+
649
+ const walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT, {
650
+ acceptNode(node) {
651
+ const val = node && node.nodeValue ? node.nodeValue : '';
652
+ if (!val || !self.hasAnyOpenToken(val, rules)) return NodeFilter.FILTER_SKIP;
653
+ if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
654
+ return NodeFilter.FILTER_ACCEPT;
655
+ }
656
+ });
657
+
658
+ let node;
659
+ while ((node = walker.nextNode())) {
660
+ const text = node.nodeValue || '';
661
+ if (!text) continue;
662
+
663
+ let best = null; // { rule, start }
664
+ for (let i = 0; i < rules.length; i++) {
665
+ const r = rules[i];
666
+ if (!r || !r.open || !r.close) continue;
667
+
668
+ const idx = text.lastIndexOf(r.open);
669
+ if (idx === -1) continue;
670
+
671
+ const after = text.indexOf(r.close, idx + r.open.length);
672
+ if (after !== -1) continue;
673
+
674
+ if (!best || idx > best.start) best = { rule: r, start: idx };
675
+ }
676
+
677
+ if (!best) continue;
678
+
679
+ const r = best.rule;
680
+ const start = best.start;
681
+ const openLen = r.open.length;
682
+ const prefixText = text.slice(0, start);
683
+ const fromOffset = start + openLen;
684
+
685
+ try {
686
+ const range = document.createRange();
687
+ range.setStart(node, Math.min(fromOffset, node.nodeValue.length));
688
+
689
+ let endNode = root;
690
+ try {
691
+ endNode = (root.nodeType === 11 || root.nodeType === 1) ? root : node.parentNode;
692
+ while (endNode && endNode.lastChild) endNode = endNode.lastChild;
693
+ } catch (_) {}
694
+ if (endNode && endNode.nodeType === 3) range.setEnd(endNode, endNode.nodeValue.length);
695
+ else if (endNode) range.setEndAfter(endNode);
696
+ else range.setEndAfter(node);
697
+
698
+ const remainder = range.extractContents();
699
+
700
+ const outTag = (r.tag && typeof r.tag === 'string') ? r.tag.toLowerCase() : 'span';
701
+ const hostTag = (outTag === 'p') ? 'span' : outTag;
702
+ const el = document.createElement(hostTag);
703
+ if (r.className) el.className = r.className;
704
+ el.setAttribute('data-cm', r.name);
705
+ el.setAttribute('data-cm-pending', '1');
706
+
707
+ el.appendChild(remainder);
708
+ range.insertNode(el);
709
+ range.detach();
710
+
711
+ try { node.nodeValue = prefixText; } catch (_) {}
712
+ this._d('cm.stream.open.pending', { name: r.name });
713
+ return;
714
+ } catch (err) {
715
+ try {
716
+ const tail = text.slice(start + r.open.length);
717
+ const frag = document.createDocumentFragment();
718
+
719
+ if (prefixText) frag.appendChild(document.createTextNode(prefixText));
720
+
721
+ const el = document.createElement((r.tag === 'p') ? 'span' : r.tag);
722
+ if (r.className) el.className = r.className;
723
+ el.setAttribute('data-cm', r.name);
724
+ el.setAttribute('data-cm-pending', '1');
725
+
726
+ this.setInnerByMode(el, r.innerMode, tail, MD, !!r.decodeEntities, r);
727
+ frag.appendChild(el);
728
+
729
+ node.parentNode.replaceChild(frag, node);
730
+ this._d('cm.stream.open.pending.fallback', { name: r.name });
731
+ return;
732
+ } catch (_) {}
733
+ }
734
+ }
735
+ }
736
+
737
+ // Finalize any pending openers if their closer token is found in the subtree
738
+ applyStreamFinalizeClosers(root, rulesAll) {
739
+ if (!root) return;
740
+
741
+ const scope = (root.nodeType === 1 || root.nodeType === 11) ? root : document;
742
+ const pending = scope.querySelectorAll('[data-cm][data-cm-pending="1"]');
743
+ if (!pending || !pending.length) return;
744
+
745
+ const rulesByName = new Map();
746
+ (rulesAll || []).forEach(r => { if (r && r.name) rulesByName.set(r.name, r); });
747
+
748
+ for (let i = 0; i < pending.length; i++) {
749
+ const el = pending[i];
750
+ const name = el.getAttribute('data-cm') || '';
751
+ const rule = rulesByName.get(name);
752
+ if (!rule || !rule.close) continue;
753
+
754
+ const self = this;
755
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
756
+ acceptNode(node) {
757
+ const val = node && node.nodeValue ? node.nodeValue : '';
758
+ if (!val || val.indexOf(rule.close) === -1) return NodeFilter.FILTER_SKIP;
759
+ if (self.isInsideForbiddenContext(node)) return NodeFilter.FILTER_REJECT;
760
+ return NodeFilter.FILTER_ACCEPT;
761
+ }
762
+ });
763
+
764
+ let nodeWithClose = null;
765
+ let idxInNode = -1;
766
+ let tn;
767
+ while ((tn = walker.nextNode())) {
768
+ const idx = tn.nodeValue.indexOf(rule.close);
769
+ if (idx !== -1) {
770
+ nodeWithClose = tn;
771
+ idxInNode = idx;
772
+ break;
773
+ }
774
+ }
775
+ if (!nodeWithClose) continue;
776
+
777
+ try {
778
+ const tokenLen = rule.close.length;
779
+
780
+ const afterRange = document.createRange();
781
+ afterRange.setStart(nodeWithClose, idxInNode + tokenLen);
782
+ let endNode = el;
783
+ while (endNode && endNode.lastChild) endNode = endNode.lastChild;
784
+ if (endNode && endNode.nodeType === 3) afterRange.setEnd(endNode, endNode.nodeValue.length);
785
+ else afterRange.setEndAfter(el.lastChild || el);
786
+
787
+ const tail = afterRange.extractContents();
788
+ afterRange.detach();
789
+
790
+ const tok = document.createRange();
791
+ tok.setStart(nodeWithClose, idxInNode);
792
+ tok.setEnd(nodeWithClose, idxInNode + tokenLen);
793
+ tok.deleteContents();
794
+ tok.detach();
795
+
796
+ if (el.parentNode && tail && tail.childNodes.length) {
797
+ el.parentNode.insertBefore(tail, el.nextSibling);
798
+ }
799
+
800
+ el.removeAttribute('data-cm-pending');
801
+ this._d('cm.stream.close.finalize', { name });
802
+ } catch (err) {}
803
+ }
804
+ }
805
+
806
+ // Find all fenced code block ranges in source text
807
+ _findFenceRanges(s) {
808
+ const ranges = [];
809
+ const n = s.length;
810
+ let i = 0;
811
+ let inFence = false;
812
+ let fenceMark = '';
813
+ let fenceLen = 0;
814
+ let startLineStart = 0;
815
+
816
+ while (i < n) {
817
+ const lineStart = i;
818
+ let j = lineStart;
819
+ while (j < n && s.charCodeAt(j) !== 10 && s.charCodeAt(j) !== 13) j++;
820
+ const lineEnd = j;
821
+ let nl = 0;
822
+ if (j < n) {
823
+ if (s.charCodeAt(j) === 13 && j + 1 < n && s.charCodeAt(j + 1) === 10) nl = 2;
824
+ else nl = 1;
825
+ }
826
+
827
+ let k = lineStart;
828
+ let indent = 0;
829
+ while (k < lineEnd) {
830
+ const c = s.charCodeAt(k);
831
+ if (c === 32) {
832
+ indent++;
833
+ if (indent > 3) break;
834
+ k++;
835
+ } else if (c === 9) {
836
+ indent++;
837
+ if (indent > 3) break;
838
+ k++;
839
+ } else break;
840
+ }
841
+
842
+ if (!inFence) {
843
+ if (indent <= 3 && k < lineEnd) {
844
+ const ch = s.charCodeAt(k);
845
+ if (ch === 0x60 || ch === 0x7E) {
846
+ const mark = String.fromCharCode(ch);
847
+ let m = k;
848
+ while (m < lineEnd && s.charCodeAt(m) === ch) m++;
849
+ const run = m - k;
850
+ if (run >= 3) {
851
+ inFence = true;
852
+ fenceMark = mark;
853
+ fenceLen = run;
854
+ startLineStart = lineStart;
855
+ }
856
+ }
857
+ }
858
+ } else {
859
+ if (indent <= 3 && k < lineEnd && s.charCodeAt(k) === fenceMark.charCodeAt(0)) {
860
+ let m = k;
861
+ while (m < lineEnd && s.charCodeAt(m) === fenceMark.charCodeAt(0)) m++;
862
+ const run = m - k;
863
+ if (run >= fenceLen) {
864
+ let onlyWS = true;
865
+ for (let t = m; t < lineEnd; t++) {
866
+ const cc = s.charCodeAt(t);
867
+ if (cc !== 32 && cc !== 9) { onlyWS = false; break; }
868
+ }
869
+ if (onlyWS) {
870
+ const endIdx = lineEnd + nl;
871
+ ranges.push([startLineStart, endIdx]);
872
+ inFence = false;
873
+ fenceMark = '';
874
+ fenceLen = 0;
875
+ startLineStart = 0;
876
+ }
877
+ }
878
+ }
879
+ }
880
+ i = lineEnd + nl;
881
+ }
882
+ if (inFence) ranges.push([startLineStart, n]);
883
+ return ranges;
884
+ }
885
+
886
+ // Check if a given absolute index in source text is at top-level line (not indented >3, not in list/quote)
887
+ _isTopLevelLineInSource(s, absIdx) {
888
+ let ls = absIdx;
889
+ while (ls > 0) {
890
+ const ch = s.charCodeAt(ls - 1);
891
+ if (ch === 10 || ch === 13) break;
892
+ ls--;
893
+ }
894
+ const prefix = s.slice(ls, absIdx);
895
+ let i = 0, indent = 0;
896
+ while (i < prefix.length) {
897
+ const c = prefix.charCodeAt(i);
898
+ if (c === 32) { indent++; if (indent > 3) break; i++; }
899
+ else if (c === 9) { indent++; if (indent > 3) break; i++; }
900
+ else break;
901
+ }
902
+ if (indent > 3) return false;
903
+ const rest = prefix.slice(i);
904
+ if (/^>\s?/.test(rest)) return false;
905
+ if (/^[-+*]\s/.test(rest)) return false;
906
+ if (/^\d+[.)]\s/.test(rest)) return false;
907
+ if (rest.trim().length > 0) return false;
908
+ return true;
909
+ }
910
+
911
+ // Apply source replacements in a given chunk of text, checking line starts against full text
912
+ _applySourceReplacementsInChunk(full, chunk, baseOffset, rules) {
913
+ let t = chunk;
914
+ for (let i = 0; i < rules.length; i++) {
915
+ const r = rules[i];
916
+ if (!r || !(r.openReplace || r.closeReplace)) continue;
917
+ try {
918
+ r.re.lastIndex = 0;
919
+ t = t.replace(r.re, (match, inner, offset) => {
920
+ const abs = baseOffset + (offset | 0);
921
+ if (!this._isTopLevelLineInSource(full, abs)) return match;
922
+ const open = r.openReplace || '';
923
+ const close = r.closeReplace || '';
924
+ return open + (inner || '') + close;
925
+ });
926
+ } catch (_) {}
927
+ }
928
+ return t;
929
+ }
930
+
931
+ // In streaming mode, if there is any unmatched opener at the end of the text,
932
+ _injectUnmatchedSourceOpeners(text, fenceRules) {
933
+ let s = String(text || '');
934
+ if (!s || !fenceRules || !fenceRules.length) return s;
935
+
936
+ let best = null; // { r, idx }
937
+ for (let i = 0; i < fenceRules.length; i++) {
938
+ const r = fenceRules[i];
939
+ if (!r || !r.open || !r.close || !r.openReplace) continue;
940
+
941
+ const idx = s.lastIndexOf(r.open);
942
+ if (idx === -1) continue;
943
+
944
+ if (!this._isTopLevelLineInSource(s, idx)) continue;
945
+
946
+ const after = s.indexOf(r.close, idx + r.open.length);
947
+ if (after !== -1) continue;
948
+
949
+ if (!best || idx > best.idx) best = { r, idx };
950
+ }
951
+
952
+ if (!best) return s;
953
+
954
+ const r = best.r, i = best.idx;
955
+ const before = s.slice(0, i);
956
+ const after = s.slice(i + r.open.length);
957
+ const injected = String(r.openReplace || '');
958
+ this._d('cm.inject.open', { name: r.name });
959
+ return before + injected + after;
960
+ }
961
+ }