├── LICENSE
├── README.md
├── chat.php
├── class
├── Class.ChatGPT.php
├── Class.DFA.php
└── Class.StreamHandler.php
├── cloudflare.worker.js
├── favicon.ico
├── index.html
├── log
└── README.md
├── sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt
└── static
├── css
├── chat.css
└── monokai-sublime.css
├── img
├── WechatIMG197.jpeg
└── wxqrcode.0420.jpeg
└── js
├── chat.js
├── highlight.min.js
└── marked.min.js
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2023, qiayue https://github.com/qiayue/
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
10 |
11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # php-openai-gpt-stream-chat-api-webui
2 |
3 | 由 [@qiayue](https://github.com/qiayue/) 开源的 [纯 PHP 实现 GPT 流式调用和前端实时打印 webui ](https://github.com/qiayue/php-openai-gpt-stream-chat-api-webui) 。
4 |
5 | **4月13日更新:**
6 |
7 | 1、最近速度慢是因为 OpenAI 对于免费账号限速了,在 [platform.openai.com](https://platform.openai.com/) 绑定了信用卡的才是之前的正常速度;
8 |
9 | 2、限速指的是流式请求时,首个 token 返回需要 20 秒左右,而绑定了信用卡的账号,在 2 秒左右;
10 |
11 |
12 | ## 目录结构
13 |
14 | ```
15 | /
16 | ├─ /class
17 | │ ├─ Class.ChatGPT.php
18 | │ ├─ Class.DFA.php
19 | │ ├─ Class.StreamHandler.php
20 | ├─ /static
21 | │ ├─ css
22 | │ │ ├─ chat.css
23 | │ │ ├─ monokai-sublime.css
24 | │ ├─ js
25 | │ │ ├─ chat.js
26 | │ │ ├─ highlight.min.js
27 | │ │ ├─ marked.min.js
28 | ├─ /chat.php
29 | ├─ /index.html
30 | ├─ /README.md
31 | ├─ /sensitive_words.txt
32 | ```
33 |
34 | | 目录/文件 | 说明 |
35 | | --- | --- |
36 | | / | 程序根目录 |
37 | | /class | php类文件目录 |
38 | | /class/Class.ChatGPT.php | ChatGPT 类,用于处理前端请求,并向 OpenAI 接口提交请求 |
39 | | /class/Class.DFA.php | DFA 类,用于敏感词校验和替换 |
40 | | /class/Class.StreamHandler.php | StreamHandler 类,用于实时处理 OpenAI 流式返回的数据 |
41 | | /static | 存放所有前端页面所需的静态文件 |
42 | | /static/css | 存放前端页面所有的 css 文件 |
43 | | /static/css/chat.css | 前端页面聊天样式文件 |
44 | | /static/css/monokai-sublime.css | highlight 代码高亮插件的主题样式文件 |
45 | | /static/js | 存放前端页面所有的 js 文件 |
46 | | /static/js/chat.js | 前端聊天交互 js 代码 |
47 | | /static/js/highlight.min.js | 代码高亮 js 库 |
48 | | /static/js/marked.min.js | markdown 解析 js 库 |
49 | | /chat.php | 前端聊天请求的后端入口文件,在这里引入 php 类文件 |
50 | | /index.html | 前端页面 html 代码 |
51 | | /README.md | 仓库描述文件 |
52 | | /sensitive_words.txt | 敏感词文件,一行一个敏感词,需要你自己收集敏感词,也可以加我微信(同 GitHub id)找我要 |
53 |
54 | ## 使用方法
55 |
56 |
57 | 本项目代码,没有使用任何框架,也没有引入任何第三方后端库,前端引入了代码高亮库 [highlight](https://github.com/highlightjs/highlight.js) 和 markdown 解析库 [marked](https://github.com/markedjs/marked) 都已经下载项目内了,所以拿到代码不用任何安装即可直接使用。
58 |
59 | 唯二要做的就是把你自己的 api key 填进去。
60 |
61 | 获取源码后,修改 `chat.php` ,填写 OpenAI 的 api key 进去,具体请见:
62 | ```php
63 | $chat = new ChatGPT([
64 | 'api_key' => '此处需要填入 openai 的 api key ',
65 | ]);
66 | ```
67 |
68 | 如果开启敏感词检测功能,需要把敏感词一行一个放入 `sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt` 文件中。
69 |
70 | 开了一个微信群,欢迎入群交流:
71 |
72 | 
73 |
74 |
75 | ## 原理说明
76 |
77 | ### 流式接收 OpenAI 的返回数据
78 |
79 | 后端 Class.ChatGPT.php 中用 curl 向 OpenAI 发起请求,使用 curl 的 `CURLOPT_WRITEFUNCTION` 设置回调函数,同时请求参数里 `'stream' => true` 告诉 OpenAI 开启流式传输。
80 |
81 | 我们通过 `curl_setopt($ch, CURLOPT_WRITEFUNCTION, [$this->streamHandler, 'callback']);` 设置使用 StreamHandler 类的实例化对象 `$this->streamHandler` 的 `callback` 方法来处理 OpenAI 返回的数据。
82 |
83 | OpenAI 会在模型每次输出时返回 `data: {"id":"","object":"","created":1679616251,"model":"","choices":[{"delta":{"content":""},"index":0,"finish_reason":null}]}` 格式字符串,其中我们需要的回答就在 `choices[0]['delta']['content']` 里,当然我们也要做好异常判断,不能直接这样获取数据。
84 |
85 | 另外,实际因为网络传输问题,每次 `callback` 函数收到的数据并不一定只有一条 `data: {"key":"value"}` 格式的数据,有可能只有半条,也有可能有多条,还有可能有N条半。
86 |
87 | 所以我们在 `StreamHandler` 类中增加了 `data_buffer` 属性来存储无法解析的半条数据。
88 |
89 | 这里根据 OpenAI 的返回数据格式,做了一些特殊处理,具体代码如下:
90 |
91 | ```php
92 | public function callback($ch, $data) {
93 | $this->counter += 1;
94 | file_put_contents('./log/data.'.$this->qmd5.'.log', $this->counter.'=='.$data.PHP_EOL.'--------------------'.PHP_EOL, FILE_APPEND);
95 |
96 | $result = json_decode($data, TRUE);
97 | if(is_array($result)){
98 | $this->end('openai 请求错误:'.json_encode($result));
99 | return strlen($data);
100 | }
101 |
102 | /*
103 | 此处步骤仅针对 openai 接口而言
104 | 每次触发回调函数时,里边会有多条data数据,需要分割
105 | 如某次收到 $data 如下所示:
106 | data: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"以下"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"是"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"使用"},"index":0,"finish_reason":null}]}
107 |
108 | 最后两条一般是这样的:
109 | data: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}\n\ndata: [DONE]
110 |
111 | 根据以上 openai 的数据格式,分割步骤如下:
112 | */
113 |
114 | // 0、把上次缓冲区内数据拼接上本次的data
115 | $buffer = $this->data_buffer.$data;
116 |
117 | //拼接完之后,要把缓冲字符串清空
118 | $this->data_buffer = '';
119 |
120 | // 1、把所有的 'data: {' 替换为 '{' ,'data: [' 换成 '['
121 | $buffer = str_replace('data: {', '{', $buffer);
122 | $buffer = str_replace('data: [', '[', $buffer);
123 |
124 | // 2、把所有的 '}\n\n{' 替换维 '}[br]{' , '}\n\n[' 替换为 '}[br]['
125 | $buffer = str_replace('}'.PHP_EOL.PHP_EOL.'{', '}[br]{', $buffer);
126 | $buffer = str_replace('}'.PHP_EOL.PHP_EOL.'[', '}[br][', $buffer);
127 |
128 | // 3、用 '[br]' 分割成多行数组
129 | $lines = explode('[br]', $buffer);
130 |
131 | // 4、循环处理每一行,对于最后一行需要判断是否是完整的json
132 | $line_c = count($lines);
133 | foreach($lines as $li=>$line){
134 | if(trim($line) == '[DONE]'){
135 | //数据传输结束
136 | $this->data_buffer = '';
137 | $this->counter = 0;
138 | $this->sensitive_check();
139 | $this->end();
140 | break;
141 | }
142 | $line_data = json_decode(trim($line), TRUE);
143 | if( !is_array($line_data) || !isset($line_data['choices']) || !isset($line_data['choices'][0]) ){
144 | if($li == ($line_c - 1)){
145 | //如果是最后一行
146 | $this->data_buffer = $line;
147 | break;
148 | }
149 | //如果是中间行无法json解析,则写入错误日志中
150 | file_put_contents('./log/error.'.$this->qmd5.'.log', json_encode(['i'=>$this->counter, 'line'=>$line, 'li'=>$li], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT).PHP_EOL.PHP_EOL, FILE_APPEND);
151 | continue;
152 | }
153 |
154 | if( isset($line_data['choices'][0]['delta']) && isset($line_data['choices'][0]['delta']['content']) ){
155 | $this->sensitive_check($line_data['choices'][0]['delta']['content']);
156 | }
157 | }
158 |
159 | return strlen($data);
160 | }
161 | ```
162 |
163 |
164 | ### 敏感词检测
165 |
166 | 我们使用了 DFA 算法来实现敏感词检测,按照 ChatGPT 的解释,`"DFA"是指“确定性有限自动机”(Deterministic Finite Automaton)` ,`DfaFilter(确定有限自动机过滤器)通常是指一种用于文本处理和匹配的算法`。
167 |
168 | Class.DFA.php 类代码是 GPT4 写的,具体实现代码见源码。
169 |
170 | 这里介绍一下使用方法,创建一个 DFA 实例需要传入敏感词文件路径:
171 |
172 | ```php
173 | $dfa = new DFA([
174 | 'words_file' => './sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt',
175 | ]);
176 | ```
177 |
178 | ***特别说明:这里特意用乱码字符串文件名是为了防止他人下载敏感词文件,请你部署后也自己改一个别的乱码文件名,不要使用我这里公开了的文件名***
179 |
180 | 之后就可以用 `$dfa->containsSensitiveWords($inputText)` 来判断 `$inputText` 是否包含敏感词,返回值是 `TRUE` 或 `FALSE` 的布尔值,也可以用 `$outputText = $dfa->replaceWords($inputText)` 来进行敏感词替换,所有在 `sensitive_words.txt` 中指定的敏感词都会被替换为三个`*`号。
181 |
182 | 如果不想开启敏感词检测,把 `chat.php` 中的以下三句注释掉即可:
183 |
184 | ```php
185 | $dfa = new DFA([
186 | 'words_file' => './sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt',
187 | ]);
188 | $chat->set_dfa($dfa);
189 | ```
190 |
191 | 如果没有开启敏感词检测,那么每次 OpenAI 的返回都会实时返回给前端。
192 |
193 | 如果开启了敏感词检测,会查找 OpenAI 返回中的换行符和停顿符号 `[',', '。', ';', '?', '!', '……']` 等来进行分句,每一句都使用 `$outputText = $dfa->replaceWords($inputText)` 来替换敏感词,之后整句返回给前端。
194 |
195 | 开启敏感词后,加载敏感词文件需要时间,每次检测时也是逐句检测,而不是逐词检测,也会导致返回变慢。
196 |
197 | 所以如果是自用,可以不开启敏感词检测,如果是部署出去给其他人用,为了保护你的域名安全和你的安全,最好开启敏感词检测。
198 |
199 | ### 流式返回给前端
200 |
201 | 直接看 `chat.php` 的注释会更清楚:
202 |
203 | ```php
204 | /*
205 | 以下几行注释由 GPT4 生成
206 | */
207 |
208 | // 这行代码用于关闭输出缓冲。关闭后,脚本的输出将立即发送到浏览器,而不是等待缓冲区填满或脚本执行完毕。
209 | ini_set('output_buffering', 'off');
210 |
211 | // 这行代码禁用了 zlib 压缩。通常情况下,启用 zlib 压缩可以减小发送到浏览器的数据量,但对于服务器发送事件来说,实时性更重要,因此需要禁用压缩。
212 | ini_set('zlib.output_compression', false);
213 |
214 | // 这行代码使用循环来清空所有当前激活的输出缓冲区。ob_end_flush() 函数会刷新并关闭最内层的输出缓冲区,@ 符号用于抑制可能出现的错误或警告。
215 | while (@ob_end_flush()) {}
216 |
217 | // 这行代码设置 HTTP 响应的 Content-Type 为 text/event-stream,这是服务器发送事件(SSE)的 MIME 类型。
218 | header('Content-Type: text/event-stream');
219 |
220 | // 这行代码设置 HTTP 响应的 Cache-Control 为 no-cache,告诉浏览器不要缓存此响应。
221 | header('Cache-Control: no-cache');
222 |
223 | // 这行代码设置 HTTP 响应的 Connection 为 keep-alive,保持长连接,以便服务器可以持续发送事件到客户端。
224 | header('Connection: keep-alive');
225 |
226 | // 这行代码设置 HTTP 响应的自定义头部 X-Accel-Buffering 为 no,用于禁用某些代理或 Web 服务器(如 Nginx)的缓冲。
227 | // 这有助于确保服务器发送事件在传输过程中不会受到缓冲影响。
228 | header('X-Accel-Buffering: no');
229 | ```
230 |
231 | 之后我们每次想给前端返回数据,用以下代码即可:
232 | ```php
233 | echo 'data: '.json_encode(['time'=>date('Y-m-d H:i:s'), 'content'=>'答: ']).PHP_EOL.PHP_EOL;
234 | flush();
235 | ```
236 |
237 | 这里我们定义了我们自己使用的一个数据格式,里边只放了 time 和 content ,不用解释都懂,time 是时间, content 就是我们要返回给前端的内容。
238 |
239 | 注意,回答全部传输完毕后,我们需要关闭连接,可以用以下代码:
240 |
241 | ```php
242 | echo 'retry: 86400000'.PHP_EOL; // 告诉前端如果发生错误,隔多久之后才轮询一次
243 | echo 'event: close'.PHP_EOL; // 告诉前端,结束了,该说再见了
244 | echo 'data: Connection closed'.PHP_EOL.PHP_EOL; // 告诉前端,连接已关闭
245 | flush();
246 | ```
247 |
248 | ### EventSource
249 |
250 |
251 | 前端 js 通过 `const eventSource = new EventSource(url);` 开启一个 [EventSource ](https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource) 请求。
252 |
253 | 之后服务器按照 `data: {"kev1":"value1","kev2":"value2"}` 格式向前端发送数据,前端就可以在 EventSource 的 message 回调事件中的 `event.data` 里获取 `{"kev1":"value1","kev2":"value2"}` 字符串形式 json 数据,再通过 `JSON.parse(event.data)` 就可以得到 js 对象。
254 |
255 | 具体代码在 getAnswer 函数中,如下所示:
256 |
257 | ```js
258 | function getAnswer(inputValue){
259 | inputValue = inputValue.replace('+', '{[$add$]}');
260 | const url = "./chat.php?q="+inputValue;
261 | const eventSource = new EventSource(url);
262 |
263 | eventSource.addEventListener("open", (event) => {
264 | console.log("连接已建立", JSON.stringify(event));
265 | });
266 |
267 | eventSource.addEventListener("message", (event) => {
268 | //console.log("接收数据:", event);
269 | try {
270 | var result = JSON.parse(event.data);
271 | if(result.time && result.content ){
272 | answerWords.push(result.content);
273 | contentIdx += 1;
274 | }
275 | } catch (error) {
276 | console.log(error);
277 | }
278 | });
279 |
280 | eventSource.addEventListener("error", (event) => {
281 | console.error("发生错误:", JSON.stringify(event));
282 | });
283 |
284 | eventSource.addEventListener("close", (event) => {
285 | console.log("连接已关闭", JSON.stringify(event.data));
286 | eventSource.close();
287 | contentEnd = true;
288 | console.log((new Date().getTime()), 'answer end');
289 | });
290 | }
291 | ```
292 |
293 | 说明一下,原生的 `EventSource` 请求,只能是 `GET` 请求,所以这里演示时,直接把提问放到 `GET` 的 `URL` 参数里了。
294 | 如果要想用 `POST` 请求,一般有两种办法:
295 |
296 | 1. 前后端一起改:【先发 `POST` 后发 `GET` 】用 `POST` 向后端提问,后端根据提问和时间生成一个唯一 key 随着 `POST` 请求返回给前端,前端拿到后,再发起一个 `GET` 请求,在参数里携带问题 key ,获取回答,这种方式需要修改后端代码;
297 |
298 | 2. 只改前端:【只发一个 `POST` 请求】后端代码不用大改,只需要把 `chat.php` 中 `$question = urldecode($_GET['q'] ?? '')` 改为 `$question = urldecode($_POST['q'] ?? '')` 即可,但是前端需要改造,不能用原生 `EventSource` 请求,需要用 fetch ,设置流式接收,具体可见下方 GPT4 给出的代码示例。
299 |
300 | ```js
301 | async function fetchAiResponse(message) {
302 | try {
303 | const response = await fetch("./chat.php", {
304 | method: "POST",
305 | headers: { "Content-Type": "application/json" },
306 | body: JSON.stringify({ messages: [{ role: "user", content: message }] }),
307 | });
308 |
309 | if (!response.ok) {
310 | throw new Error(response.statusText);
311 | }
312 |
313 | const reader = response.body.getReader();
314 | const decoder = new TextDecoder("utf-8");
315 |
316 | while (true) {
317 | const { value, done } = await reader.read();
318 | if (value) {
319 | const partialResponse = decoder.decode(value, { stream: true });
320 | displayMessage("assistant", partialResponse);
321 | }
322 | if (done) {
323 | break;
324 | }
325 | }
326 | } catch (error) {
327 | console.error("Error fetching AI response:", error);
328 | displayMessage("assistant", "Error: Failed to fetch AI response.");
329 | }
330 | }
331 | ```
332 |
333 | 上方代码,关键点在于 `const partialResponse = decoder.decode(value, { stream: true })` 中的 `{ stream: true }` 。
334 |
335 |
336 | ### 打字机效果
337 |
338 | 对于后端返回的所有回复内容,我们需要用打字机形式打印出来。
339 |
340 | 最初的方案是每次接收到后端的返回后就立即显示到页面里,后来发现这样速度太快了,眨眼就显示完了,没有打印机效果。
341 | 所以后来的方案就改成了用定时器实现定时打印,那么就需要把收到的先放进数组里缓存起来,然后定时每 50 毫秒执行一次,打印一个内容出来。
342 | 具体实现代码如下:
343 |
344 | ```js
345 | function typingWords(){
346 | if(contentEnd && contentIdx==typingIdx){
347 | clearInterval(typingTimer);
348 | answerContent = '';
349 | answerWords = [];
350 | answers = [];
351 | qaIdx += 1;
352 | typingIdx = 0;
353 | contentIdx = 0;
354 | contentEnd = false;
355 | lastWord = '';
356 | lastLastWord = '';
357 | input.disabled = false;
358 | sendButton.disabled = false;
359 | console.log((new Date().getTime()), 'typing end');
360 | return;
361 | }
362 | if(contentIdx<=typingIdx){
363 | return;
364 | }
365 | if(typing){
366 | return;
367 | }
368 | typing = true;
369 |
370 | if(!answers[qaIdx]){
371 | answers[qaIdx] = document.getElementById('answer-'+qaIdx);
372 | }
373 |
374 | const content = answerWords[typingIdx];
375 | if(content.indexOf('`') != -1){
376 | if(content.indexOf('```') != -1){
377 | codeStart = !codeStart;
378 | }else if(content.indexOf('``') != -1 && (lastWord + content).indexOf('```') != -1){
379 | codeStart = !codeStart;
380 | }else if(content.indexOf('`') != -1 && (lastLastWord + lastWord + content).indexOf('```') != -1){
381 | codeStart = !codeStart;
382 | }
383 | }
384 |
385 | lastLastWord = lastWord;
386 | lastWord = content;
387 |
388 | answerContent += content;
389 | answers[qaIdx].innerHTML = marked.parse(answerContent+(codeStart?'\n\n```':''));
390 |
391 | typingIdx += 1;
392 | typing = false;
393 | }
394 | ```
395 |
396 | ### 代码渲染
397 |
398 | 如果严格按照输出什么打印什么的话,那么当正在打印一段代码,需要等到代码全部打完,才能被格式化为代码块,才能高亮显示代码。
399 | 那这个体验也太差了。
400 | 有什么办法能够解决这个问题呢?
401 | 答案就在问题里,既然是因为代码块有开始标记没有结束标记,那就我们给他补全结束标记就好了,直到真的结束标记来了,才不需要补全。
402 |
403 | 具体的实现就是下面几行代码:
404 |
405 | ```js
406 | if(content.indexOf('`') != -1){
407 | if(content.indexOf('```') != -1){
408 | codeStart = !codeStart;
409 | }else if(content.indexOf('``') != -1 && (lastWord + content).indexOf('```') != -1){
410 | codeStart = !codeStart;
411 | }else if(content.indexOf('`') != -1 && (lastLastWord + lastWord + content).indexOf('```') != -1){
412 | codeStart = !codeStart;
413 | }
414 | }
415 |
416 | lastLastWord = lastWord;
417 | lastWord = content;
418 |
419 | answerContent += content;
420 | answers[qaIdx].innerHTML = marked.parse(answerContent+(codeStart?'\n\n```':''));
421 | ```
422 |
423 | ### 其它
424 |
425 | 更多其它细节请看代码,如果对代码有疑问的,请加我微信(同 GitHub id)
426 |
427 | ## License
428 |
429 | [BSD 2-Clause](https://github.com/qiayue/php-openai-gpt-stream-chat-api-webui/blob/main/LICENSE)
--------------------------------------------------------------------------------
/chat.php:
--------------------------------------------------------------------------------
1 | date('Y-m-d H:i:s'), 'content'=>'']).PHP_EOL.PHP_EOL;
45 | flush();
46 |
47 | // 从 get 中获取提问
48 | $question = urldecode($_GET['q'] ?? '');
49 | if(empty($question)) {
50 | echo "event: close".PHP_EOL;
51 | echo "data: Connection closed".PHP_EOL.PHP_EOL;
52 | flush();
53 | exit();
54 | }
55 | $question = str_ireplace('{[$add$]}', '+', $question);
56 |
57 |
58 | // 此处需要填入 openai 的 api key
59 | $chat = new ChatGPT([
60 | 'api_key' => '',
61 | ]);
62 |
63 | /*
64 | // 如果把下面三行注释掉,则不会启用敏感词检测
65 | // 特别注意,这里特意用乱码字符串文件名是为了防止他人下载敏感词文件,请你部署后也自己改一个别的乱码文件名
66 | $dfa = new DFA([
67 | 'words_file' => './sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt',
68 | ]);
69 | $chat->set_dfa($dfa);
70 | */
71 |
72 | // 开始提问
73 | $chat->qa([
74 | 'system' => '你是一个智能机器人',
75 | 'question' => $question,
76 | ]);
77 |
--------------------------------------------------------------------------------
/class/Class.ChatGPT.php:
--------------------------------------------------------------------------------
1 | api_key = $params['api_key'] ?? '';
14 | }
15 |
16 | public function set_dfa(&$dfa){
17 | $this->dfa = $dfa;
18 | if(!empty($this->dfa) && $this->dfa->is_available()){
19 | $this->check_sensitive = TRUE;
20 | }
21 | }
22 |
23 | public function qa($params){
24 | $this->question = $params['question'];
25 | $this->streamHandler = new StreamHandler([
26 | 'qmd5' => md5($this->question.''.time())
27 | ]);
28 | if($this->check_sensitive){
29 | $this->streamHandler->set_dfa($this->dfa);
30 | }
31 |
32 |
33 | if(empty($this->api_key)){
34 | $this->streamHandler->end('OpenAI 的 api key 还没填');
35 | return;
36 | }
37 |
38 |
39 | // 开启检测且提问包含敏感词
40 | if($this->check_sensitive && $this->dfa->containsSensitiveWords($this->question)){
41 | $this->streamHandler->end('您的问题不合适,AI暂时无法回答');
42 | return;
43 | }
44 |
45 | $messages = [
46 | [
47 | 'role' => 'system',
48 | 'content' => $params['system'] ?? '',
49 | ],
50 | [
51 | 'role' => 'user',
52 | 'content' => $this->question
53 | ]
54 | ];
55 |
56 | $json = json_encode([
57 | 'model' => 'gpt-3.5-turbo-0613',
58 | 'messages' => $messages,
59 | 'temperature' => 0.6,
60 | 'stream' => true,
61 | ]);
62 |
63 | $headers = array(
64 | "Content-Type: application/json",
65 | "Authorization: Bearer ".$this->api_key,
66 | );
67 |
68 | $this->openai($json, $headers);
69 |
70 | }
71 |
72 | private function openai($json, $headers){
73 | $ch = curl_init();
74 |
75 | curl_setopt($ch, CURLOPT_URL, $this->api_url);
76 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
77 | curl_setopt($ch, CURLOPT_HEADER, false);
78 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
79 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
80 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
81 | curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
82 | curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
83 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
84 |
85 | curl_setopt($ch, CURLOPT_WRITEFUNCTION, [$this->streamHandler, 'callback']);
86 |
87 | $response = curl_exec($ch);
88 |
89 | if (curl_errno($ch)) {
90 | file_put_contents('./log/curl.error.log', curl_error($ch).PHP_EOL.PHP_EOL, FILE_APPEND);
91 | }
92 |
93 | curl_close($ch);
94 | }
95 |
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/class/Class.DFA.php:
--------------------------------------------------------------------------------
1 |
2 | words_file = $params['words_file'] ?? '';
13 |
14 | $this->root = new DFA_Node();
15 |
16 | $this->load_words_file();
17 | }
18 |
19 | private function load_words_file(){
20 | if(!file_exists($this->words_file)){
21 | return;
22 | }
23 | $words = file($this->words_file);
24 | foreach ($words as $word) {
25 | $word = trim($word);
26 | if(empty($word)){
27 | continue;
28 | }
29 | $this->words_count += 1;
30 | $this->addWord($word);
31 | }
32 | }
33 |
34 | public function is_available(){
35 | return $this->words_count>0;
36 | }
37 |
38 | public function addWord($word)
39 | {
40 | $node = $this->root;
41 | for ($i = 0; $i < strlen($word); $i++) {
42 | $char = $word[$i];
43 | if (!isset($node->children[$char])) {
44 | $node->children[$char] = new DFA_Node();
45 | }
46 | $node = $node->children[$char];
47 | }
48 | $node->isEndOfWord = true;
49 | }
50 |
51 | public function replaceWords($text)
52 | {
53 | $result = '';
54 | $length = strlen($text);
55 | for ($i = 0; $i < $length;) {
56 | $node = $this->root;
57 | $j = $i;
58 | $lastMatched = -1;
59 | while ($j < $length && isset($node->children[$text[$j]])) {
60 | $node = $node->children[$text[$j]];
61 | if ($node->isEndOfWord) {
62 | $lastMatched = $j;
63 | }
64 | $j++;
65 | }
66 |
67 | if ($lastMatched >= 0) {
68 | $result .= '\*\*\*';
69 | $i = $lastMatched + 1;
70 | } else {
71 | $result .= $text[$i];
72 | $i++;
73 | }
74 | }
75 | return $result;
76 | }
77 |
78 | public function containsSensitiveWords($text)
79 | {
80 | $length = strlen($text);
81 | for ($i = 0; $i < $length;) {
82 | $node = $this->root;
83 | $j = $i;
84 | while ($j < $length && isset($node->children[$text[$j]])) {
85 | $node = $node->children[$text[$j]];
86 | if ($node->isEndOfWord) {
87 | return true;
88 | }
89 | $j++;
90 | }
91 | $i++;
92 | }
93 | return false;
94 | }
95 | }
96 |
97 | class DFA_Node
98 | {
99 | public $isEndOfWord;
100 | public $children;
101 |
102 | public function __construct()
103 | {
104 | $this->isEndOfWord = false;
105 | $this->children = [];
106 | }
107 | }
108 |
109 |
110 |
111 | /*
112 | $inputText = "需要检测的句子";
113 | $isContain = $dfa->containsSensitiveWords($inputText);
114 |
115 | echo "Original Text: \n" . $inputText . "\n";
116 | echo "isContain: " . json_encode($isContain) . "\n";
117 |
118 |
119 | $outputText = $dfa->replaceWords($inputText);
120 |
121 | echo "Original Text: \n" . $inputText . "\n";
122 | echo "Replaced Text: \n" . $outputText . "\n";
123 | */
124 |
--------------------------------------------------------------------------------
/class/Class.StreamHandler.php:
--------------------------------------------------------------------------------
1 | buffer = '';
15 | $this->counter = 0;
16 | $this->qmd5 = $params['qmd5'] ?? time();
17 | $this->chars = [];
18 | $this->lines = [];
19 | $this->punctuation = [',', '。', ';', '?', '!', '……'];
20 | }
21 |
22 | public function set_dfa(&$dfa){
23 | $this->dfa = $dfa;
24 | if(!empty($this->dfa) && $this->dfa->is_available()){
25 | $this->check_sensitive = TRUE;
26 | }
27 | }
28 |
29 | public function callback($ch, $data) {
30 | $this->counter += 1;
31 | file_put_contents('./log/data.'.$this->qmd5.'.log', $this->counter.'=='.$data.PHP_EOL.'--------------------'.PHP_EOL, FILE_APPEND);
32 |
33 | $result = json_decode($data, TRUE);
34 | if(is_array($result)){
35 | $this->end('openai 请求错误:'.json_encode($result));
36 | return strlen($data);
37 | }
38 |
39 | /*
40 | 此处步骤仅针对 openai 接口而言
41 | 每次触发回调函数时,里边会有多条data数据,需要分割
42 | 如某次收到 $data 如下所示:
43 | data: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"以下"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"是"},"index":0,"finish_reason":null}]}\n\ndata: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":"使用"},"index":0,"finish_reason":null}]}
44 |
45 | 最后两条一般是这样的:
46 | data: {"id":"chatcmpl-6wimHHBt4hKFHEpFnNT2ryUeuRRJC","object":"chat.completion.chunk","created":1679453169,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}\n\ndata: [DONE]
47 |
48 | 根据以上 openai 的数据格式,分割步骤如下:
49 | */
50 |
51 | // 0、把上次缓冲区内数据拼接上本次的data
52 | $buffer = $this->data_buffer.$data;
53 |
54 | //拼接完之后,要把缓冲字符串清空
55 | $this->data_buffer = '';
56 |
57 | // 1、把所有的 'data: {' 替换为 '{' ,'data: [' 换成 '['
58 | $buffer = str_replace('data: {', '{', $buffer);
59 | $buffer = str_replace('data: [', '[', $buffer);
60 |
61 | // 2、把所有的 '}\n\n{' 替换维 '}[br]{' , '}\n\n[' 替换为 '}[br]['
62 | $buffer = str_replace("}\n\n{", '}[br]{', $buffer);
63 | $buffer = str_replace("}\n\n[", '}[br][', $buffer);
64 |
65 | // 3、用 '[br]' 分割成多行数组
66 | $lines = explode('[br]', $buffer);
67 |
68 | // 4、循环处理每一行,对于最后一行需要判断是否是完整的json
69 | $line_c = count($lines);
70 | foreach($lines as $li=>$line){
71 | if(trim($line) == '[DONE]'){
72 | //数据传输结束
73 | $this->data_buffer = '';
74 | $this->counter = 0;
75 | $this->sensitive_check();
76 | $this->end();
77 | break;
78 | }
79 | $line_data = json_decode(trim($line), TRUE);
80 | if( !is_array($line_data) || !isset($line_data['choices']) || !isset($line_data['choices'][0]) ){
81 | if($li == ($line_c - 1)){
82 | //如果是最后一行
83 | $this->data_buffer = $line;
84 | break;
85 | }
86 | //如果是中间行无法json解析,则写入错误日志中
87 | file_put_contents('./log/error.'.$this->qmd5.'.log', json_encode(['i'=>$this->counter, 'line'=>$line, 'li'=>$li], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT).PHP_EOL.PHP_EOL, FILE_APPEND);
88 | continue;
89 | }
90 |
91 | if( isset($line_data['choices'][0]['delta']) && isset($line_data['choices'][0]['delta']['content']) ){
92 | $this->sensitive_check($line_data['choices'][0]['delta']['content']);
93 | }
94 | }
95 |
96 | return strlen($data);
97 | }
98 |
99 | private function sensitive_check($content = NULL){
100 | // 如果不检测敏感词,则直接返回给前端
101 | if(!$this->check_sensitive){
102 | $this->write($content);
103 | return;
104 | }
105 | //每个 content 都检测是否包含换行或者停顿符号,如有,则成为一个新行
106 | if(!$this->has_pause($content)){
107 | $this->chars[] = $content;
108 | return;
109 | }
110 | $this->chars[] = $content;
111 | $content = implode('', $this->chars);
112 | if($this->dfa->containsSensitiveWords($content)){
113 | $content = $this->dfa->replaceWords($content);
114 | $this->write($content);
115 | }else{
116 | foreach($this->chars as $char){
117 | $this->write($char);
118 | }
119 | }
120 | $this->chars = [];
121 | }
122 |
123 | private function has_pause($content){
124 | if($content == NULL){
125 | return TRUE;
126 | }
127 | $has_p = false;
128 | if(is_numeric(strripos(json_encode($content), '\n'))){
129 | $has_p = true;
130 | }else{
131 | foreach($this->punctuation as $p){
132 | if( is_numeric(strripos($content, $p)) ){
133 | $has_p = true;
134 | break;
135 | }
136 | }
137 | }
138 | return $has_p;
139 | }
140 |
141 | private function write($content = NULL, $flush=TRUE){
142 | if($content != NULL){
143 | echo 'data: '.json_encode(['time'=>date('Y-m-d H:i:s'), 'content'=>$content], JSON_UNESCAPED_UNICODE).PHP_EOL.PHP_EOL;
144 | }
145 |
146 | if($flush){
147 | flush();
148 | }
149 | }
150 |
151 | public function end($content = NULL){
152 | if(!empty($content)){
153 | $this->write($content, FALSE);
154 | }
155 |
156 | echo 'retry: 86400000'.PHP_EOL;
157 | echo 'event: close'.PHP_EOL;
158 | echo 'data: Connection closed'.PHP_EOL.PHP_EOL;
159 | flush();
160 |
161 | }
162 | }
163 |
164 |
165 |
--------------------------------------------------------------------------------
/cloudflare.worker.js:
--------------------------------------------------------------------------------
1 | /*
2 | 感谢@赖嘉伟Gary https://github.com/paicha 提供 Worker 代码
3 |
4 | 用途:
5 | 使用此方式,不需要购买海外服务器,替换 /class/Class.ChatGPT.php 中的 api.openai.com 域名后,直接将本项目开源代码部署到国内服务器即可使用。
6 |
7 | 使用方法:
8 | 1、在 CloudFlare 添加一个域名,注意只能填一级域名,不能填子域名,所以可以新注册一个便宜域名专门做这个事情;
9 | 2、添加域名后,在域名注册商处把域名的 dns 设为 CloudFlare 的 dns 地址;
10 | 3、等待域名生效,大概需要十几分钟到一个小时不等;
11 | 4、添加一个 Worker ,把以下代码复制到进去;
12 | 5、点击 Worker 的 Trigger 界面,设置自定义域名,选择第一步添加的域名;
13 | 6、等全部生效后,就可以用你的自定义域名替换 /class/Class.ChatGPT.php 中的 api.openai.com 域名。
14 |
15 | */
16 | async function handleRequest(request) {
17 | const url = new URL(request.url)
18 | const fetchAPI = request.url.replace(url.host, 'api.openai.com')
19 |
20 | // 添加跨域处理
21 | const corsHeaders = {
22 | 'Access-Control-Allow-Origin': '*',
23 | 'Access-Control-Allow-Methods': 'OPTIONS',
24 | 'Access-Control-Allow-Headers': '*',
25 | };
26 | if (request.method === 'OPTIONS') return new Response(null, { headers: corsHeaders });
27 |
28 | return fetch(fetchAPI, { headers: request.headers, method: request.method, body: request.body })
29 | }
30 |
31 | addEventListener("fetch", (event) => {
32 | event.respondWith(handleRequest(event.request))
33 | })
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiayue/php-openai-gpt-stream-chat-api-webui/1135cd9c8dedb0986fe2fc5eb76a2c4a056fb33b/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Chat Stream Demo by @qiayue
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/log/README.md:
--------------------------------------------------------------------------------
1 | 用于日志文件
--------------------------------------------------------------------------------
/sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiayue/php-openai-gpt-stream-chat-api-webui/1135cd9c8dedb0986fe2fc5eb76a2c4a056fb33b/sensitive_words_sdfdsfvdfs5v56v5dfvdf.txt
--------------------------------------------------------------------------------
/static/css/chat.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Helvetica Neue", "Luxi Sans", "Segoe UI", "Hiragino Sans GB", "Microsoft Yahei", sans-serif, "Apple Logo";
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | #app {
8 | display: flex;
9 | flex-direction: column;
10 | height: 100vh;
11 | max-width: 900px;
12 | margin: 0 auto;
13 | }
14 |
15 | #app, .message.question, .input-area{
16 | background-color: #f3f6fc;
17 | }
18 |
19 | .messages-container {
20 | flex: 1;
21 | overflow-y: auto;
22 | padding: 20px;
23 | padding-top: 40px;
24 | }
25 |
26 | .message{
27 | width: 100%;
28 | padding: 10px 20px;
29 | border:solid 1px #c3c6cb;
30 | box-sizing: border-box;
31 | background-color: white;
32 | }
33 | .message p, .message pre{
34 | margin: 10px 0;
35 | }
36 |
37 | .message.question{
38 | border-radius: 10px 10px 0 0;
39 | }
40 | .message.answer{
41 | margin-top: -1px;
42 | margin-bottom: 20px;
43 | border-radius: 0 0 10px 10px;
44 | }
45 |
46 | .message pre{
47 | width: 100%;
48 | padding: 10px;
49 | background-color: #23241f;
50 | border-radius: 6px;
51 | color: #f8f8f2;
52 | box-sizing: border-box;
53 | overflow-x: auto;
54 | }
55 |
56 | .input-area {
57 | display: flex;
58 | align-items: center;
59 | padding: 10px 20px;
60 | border-top: 1px solid #ffffff;
61 | }
62 |
63 | textarea {
64 | flex: 1;
65 | height: 52px;
66 | padding: 5px 10px;
67 | line-height: 20px;
68 | resize: none;
69 | border: 1px solid #c3c6cb;
70 | outline: none;
71 | box-sizing: border-box;
72 | border-radius: 6px;
73 | }
74 |
75 | button {
76 | margin-left: 10px;
77 | height: 40px;
78 | padding: 0 20px;
79 | line-height: 40px;
80 | background-color: #007bff;
81 | color: #fff;
82 | border: none;
83 | cursor: pointer;
84 | border-radius: 6px;
85 | }
86 |
87 | button:disabled {
88 | background-color: #c3c6cb;
89 | cursor: not-allowed;
90 | }
--------------------------------------------------------------------------------
/static/css/monokai-sublime.css:
--------------------------------------------------------------------------------
1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#23241f;color:#f8f8f2}.hljs-subst,.hljs-tag{color:#f8f8f2}.hljs-emphasis,.hljs-strong{color:#a8a8a2}.hljs-bullet,.hljs-link,.hljs-literal,.hljs-number,.hljs-quote,.hljs-regexp{color:#ae81ff}.hljs-code,.hljs-section,.hljs-selector-class,.hljs-title{color:#a6e22e}.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}.hljs-attr,.hljs-keyword,.hljs-name,.hljs-selector-tag{color:#f92672}.hljs-attribute,.hljs-symbol{color:#66d9ef}.hljs-class .hljs-title,.hljs-params,.hljs-title.class_{color:#f8f8f2}.hljs-addition,.hljs-built_in,.hljs-selector-attr,.hljs-selector-id,.hljs-selector-pseudo,.hljs-string,.hljs-template-variable,.hljs-type,.hljs-variable{color:#e6db74}.hljs-comment,.hljs-deletion,.hljs-meta{color:#75715e}
--------------------------------------------------------------------------------
/static/img/WechatIMG197.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiayue/php-openai-gpt-stream-chat-api-webui/1135cd9c8dedb0986fe2fc5eb76a2c4a056fb33b/static/img/WechatIMG197.jpeg
--------------------------------------------------------------------------------
/static/img/wxqrcode.0420.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qiayue/php-openai-gpt-stream-chat-api-webui/1135cd9c8dedb0986fe2fc5eb76a2c4a056fb33b/static/img/wxqrcode.0420.jpeg
--------------------------------------------------------------------------------
/static/js/chat.js:
--------------------------------------------------------------------------------
1 | const messagesContainer = document.getElementById('messages');
2 | const input = document.getElementById('input');
3 | const sendButton = document.getElementById('send');
4 | var qaIdx = 0,answers={},answerContent='',answerWords=[];
5 | var codeStart=false,lastWord='',lastLastWord='';
6 | var typingTimer=null,typing=false,typingIdx=0,contentIdx=0,contentEnd=false;
7 |
8 | //markdown解析,代码高亮设置
9 | marked.setOptions({
10 | highlight: function (code, language) {
11 | const validLanguage = hljs.getLanguage(language) ? language : 'javascript';
12 | return hljs.highlight(code, { language: validLanguage }).value;
13 | },
14 | });
15 |
16 |
17 | //在输入时和获取焦点后自动调整输入框高度
18 | input.addEventListener('input', adjustInputHeight);
19 | input.addEventListener('focus', adjustInputHeight);
20 |
21 | // 自动调整输入框高度
22 | function adjustInputHeight() {
23 | input.style.height = 'auto'; // 将高度重置为 auto
24 | input.style.height = (input.scrollHeight+2) + 'px';
25 | }
26 |
27 | function sendMessage() {
28 | const inputValue = input.value;
29 | if (!inputValue) {
30 | return;
31 | }
32 |
33 | const question = document.createElement('div');
34 | question.setAttribute('class', 'message question');
35 | question.setAttribute('id', 'question-'+qaIdx);
36 | question.innerHTML = marked.parse(inputValue);
37 | messagesContainer.appendChild(question);
38 |
39 | const answer = document.createElement('div');
40 | answer.setAttribute('class', 'message answer');
41 | answer.setAttribute('id', 'answer-'+qaIdx);
42 | answer.innerHTML = marked.parse('AI思考中……');
43 | messagesContainer.appendChild(answer);
44 |
45 | answers[qaIdx] = document.getElementById('answer-'+qaIdx);
46 |
47 | input.value = '';
48 | input.disabled = true;
49 | sendButton.disabled = true;
50 | adjustInputHeight();
51 |
52 | typingTimer = setInterval(typingWords, 50);
53 |
54 | getAnswer(inputValue);
55 | }
56 |
57 | function getAnswer(inputValue){
58 | inputValue = encodeURIComponent(inputValue.replace(/\+/g, '{[$add$]}'));
59 | const url = "./chat.php?q="+inputValue;
60 | const eventSource = new EventSource(url);
61 |
62 | eventSource.addEventListener("open", (event) => {
63 | console.log("连接已建立", JSON.stringify(event));
64 | });
65 |
66 | eventSource.addEventListener("message", (event) => {
67 | //console.log("接收数据:", event);
68 | try {
69 | var result = JSON.parse(event.data);
70 | if(result.time && result.content ){
71 | answerWords.push(result.content);
72 | contentIdx += 1;
73 | }
74 | } catch (error) {
75 | console.log(error);
76 | }
77 | });
78 |
79 | eventSource.addEventListener("error", (event) => {
80 | console.error("发生错误:", JSON.stringify(event));
81 | });
82 |
83 | eventSource.addEventListener("close", (event) => {
84 | console.log("连接已关闭", JSON.stringify(event.data));
85 | eventSource.close();
86 | contentEnd = true;
87 | console.log((new Date().getTime()), 'answer end');
88 | });
89 | }
90 |
91 |
92 | function typingWords(){
93 | if(contentEnd && contentIdx==typingIdx){
94 | clearInterval(typingTimer);
95 | answerContent = '';
96 | answerWords = [];
97 | answers = [];
98 | qaIdx += 1;
99 | typingIdx = 0;
100 | contentIdx = 0;
101 | contentEnd = false;
102 | lastWord = '';
103 | lastLastWord = '';
104 | input.disabled = false;
105 | sendButton.disabled = false;
106 | console.log((new Date().getTime()), 'typing end');
107 | return;
108 | }
109 | if(contentIdx<=typingIdx){
110 | return;
111 | }
112 | if(typing){
113 | return;
114 | }
115 | typing = true;
116 |
117 | if(!answers[qaIdx]){
118 | answers[qaIdx] = document.getElementById('answer-'+qaIdx);
119 | }
120 |
121 | const content = answerWords[typingIdx];
122 | if(content.indexOf('`') != -1){
123 | if(content.indexOf('```') != -1){
124 | codeStart = !codeStart;
125 | }else if(content.indexOf('``') != -1 && (lastWord + content).indexOf('```') != -1){
126 | codeStart = !codeStart;
127 | }else if(content.indexOf('`') != -1 && (lastLastWord + lastWord + content).indexOf('```') != -1){
128 | codeStart = !codeStart;
129 | }
130 | }
131 |
132 | lastLastWord = lastWord;
133 | lastWord = content;
134 |
135 | answerContent += content;
136 | answers[qaIdx].innerHTML = marked.parse(answerContent+(codeStart?'\n\n```':''));
137 |
138 | typingIdx += 1;
139 | typing = false;
140 | }
141 |
--------------------------------------------------------------------------------
/static/js/highlight.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Highlight.js v11.3.1 (git: 2a972d8658)
3 | (c) 2006-2021 Ivan Sagalaev and other contributors
4 | License: BSD-3-Clause
5 | */
6 | var hljs=function(){"use strict";var e={exports:{}};function n(e){
7 | return e instanceof Map?e.clear=e.delete=e.set=()=>{
8 | throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{
9 | throw Error("set is read-only")
10 | }),Object.freeze(e),Object.getOwnPropertyNames(e).forEach((t=>{var a=e[t]
11 | ;"object"!=typeof a||Object.isFrozen(a)||n(a)})),e}
12 | e.exports=n,e.exports.default=n;var t=e.exports;class a{constructor(e){
13 | void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
14 | ignoreMatch(){this.isMatchIgnored=!0}}function i(e){
15 | return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")
16 | }function r(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n]
17 | ;return n.forEach((e=>{for(const n in e)t[n]=e[n]})),t}const s=e=>!!e.kind
18 | ;class o{constructor(e,n){
19 | this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){
20 | this.buffer+=i(e)}openNode(e){if(!s(e))return;let n=e.kind
21 | ;n=e.sublanguage?"language-"+n:((e,{prefix:n})=>{if(e.includes(".")){
22 | const t=e.split(".")
23 | ;return[`${n}${t.shift()}`,...t.map(((e,n)=>`${e}${"_".repeat(n+1)}`))].join(" ")
24 | }return`${n}${e}`})(n,{prefix:this.classPrefix}),this.span(n)}closeNode(e){
25 | s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){
26 | this.buffer+=``}}class l{constructor(){this.rootNode={
27 | children:[]},this.stack=[this.rootNode]}get top(){
28 | return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
29 | this.top.children.push(e)}openNode(e){const n={kind:e,children:[]}
30 | ;this.add(n),this.stack.push(n)}closeNode(){
31 | if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
32 | for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
33 | walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){
34 | return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),
35 | n.children.forEach((n=>this._walk(e,n))),e.closeNode(n)),e}static _collapse(e){
36 | "string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
37 | l._collapse(e)})))}}class c extends l{constructor(e){super(),this.options=e}
38 | addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}
39 | addText(e){""!==e&&this.add(e)}addSublanguage(e,n){const t=e.root
40 | ;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){
41 | return new o(this,this.options).value()}finalize(){return!0}}function d(e){
42 | return e?"string"==typeof e?e:e.source:null}function g(e){return m("(?=",e,")")}
43 | function u(e){return m("(?:",e,")*")}function b(e){return m("(?:",e,")?")}
44 | function m(...e){return e.map((e=>d(e))).join("")}function p(...e){const n=(e=>{
45 | const n=e[e.length-1]
46 | ;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{}
47 | })(e);return"("+(n.capture?"":"?:")+e.map((e=>d(e))).join("|")+")"}
48 | function _(e){return RegExp(e.toString()+"|").exec("").length-1}
49 | const h=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
50 | ;function f(e,{joinWith:n}){let t=0;return e.map((e=>{t+=1;const n=t
51 | ;let a=d(e),i="";for(;a.length>0;){const e=h.exec(a);if(!e){i+=a;break}
52 | i+=a.substring(0,e.index),
53 | a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0],
54 | "("===e[0]&&t++)}return i})).map((e=>`(${e})`)).join(n)}
55 | const E="[a-zA-Z]\\w*",y="[a-zA-Z_]\\w*",N="\\b\\d+(\\.\\d+)?",w="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",v="\\b(0b[01]+)",O={
56 | begin:"\\\\[\\s\\S]",relevance:0},x={scope:"string",begin:"'",end:"'",
57 | illegal:"\\n",contains:[O]},M={scope:"string",begin:'"',end:'"',illegal:"\\n",
58 | contains:[O]},k=(e,n,t={})=>{const a=r({scope:"comment",begin:e,end:n,
59 | contains:[]},t);a.contains.push({scope:"doctag",
60 | begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
61 | end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
62 | ;const i=p("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
63 | ;return a.contains.push({begin:m(/[ ]+/,"(",i,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),a
64 | },S=k("//","$"),A=k("/\\*","\\*/"),C=k("#","$");var T=Object.freeze({
65 | __proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:E,UNDERSCORE_IDENT_RE:y,
66 | NUMBER_RE:N,C_NUMBER_RE:w,BINARY_NUMBER_RE:v,
67 | RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
68 | SHEBANG:(e={})=>{const n=/^#![ ]*\//
69 | ;return e.binary&&(e.begin=m(n,/.*\b/,e.binary,/\b.*/)),r({scope:"meta",begin:n,
70 | end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},
71 | BACKSLASH_ESCAPE:O,APOS_STRING_MODE:x,QUOTE_STRING_MODE:M,PHRASAL_WORDS_MODE:{
72 | begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
73 | },COMMENT:k,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:A,HASH_COMMENT_MODE:C,
74 | NUMBER_MODE:{scope:"number",begin:N,relevance:0},C_NUMBER_MODE:{scope:"number",
75 | begin:w,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:v,relevance:0},
76 | REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//,
77 | end:/\/[gimuy]*/,illegal:/\n/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,
78 | contains:[O]}]}]},TITLE_MODE:{scope:"title",begin:E,relevance:0},
79 | UNDERSCORE_TITLE_MODE:{scope:"title",begin:y,relevance:0},METHOD_GUARD:{
80 | begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{
81 | "on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{
82 | n.data._beginMatch!==e[1]&&n.ignoreMatch()}})});function R(e,n){
83 | "."===e.input[e.index-1]&&n.ignoreMatch()}function D(e,n){
84 | void 0!==e.className&&(e.scope=e.className,delete e.className)}function I(e,n){
85 | n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
86 | e.__beforeBegin=R,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
87 | void 0===e.relevance&&(e.relevance=0))}function L(e,n){
88 | Array.isArray(e.illegal)&&(e.illegal=p(...e.illegal))}function B(e,n){
89 | if(e.match){
90 | if(e.begin||e.end)throw Error("begin & end are not supported with match")
91 | ;e.begin=e.match,delete e.match}}function $(e,n){
92 | void 0===e.relevance&&(e.relevance=1)}const F=(e,n)=>{if(!e.beforeMatch)return
93 | ;if(e.starts)throw Error("beforeMatch cannot be used with starts")
94 | ;const t=Object.assign({},e);Object.keys(e).forEach((n=>{delete e[n]
95 | })),e.keywords=t.keywords,e.begin=m(t.beforeMatch,g(t.begin)),e.starts={
96 | relevance:0,contains:[Object.assign(t,{endsParent:!0})]
97 | },e.relevance=0,delete t.beforeMatch
98 | },z=["of","and","for","in","not","or","if","then","parent","list","value"]
99 | ;function U(e,n,t="keyword"){const a=Object.create(null)
100 | ;return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach((t=>{
101 | Object.assign(a,U(e[t],n,t))})),a;function i(e,t){
102 | n&&(t=t.map((e=>e.toLowerCase()))),t.forEach((n=>{const t=n.split("|")
103 | ;a[t[0]]=[e,j(t[0],t[1])]}))}}function j(e,n){
104 | return n?Number(n):(e=>z.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{
105 | console.error(e)},q=(e,...n)=>{console.log("WARN: "+e,...n)},H=(e,n)=>{
106 | P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0)
107 | },Z=Error();function G(e,n,{key:t}){let a=0;const i=e[t],r={},s={}
108 | ;for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=_(n[e-1])
109 | ;e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{
110 | e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
111 | delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
112 | _wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
113 | }),(e=>{if(Array.isArray(e.begin)){
114 | if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
115 | Z
116 | ;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"),
117 | Z;G(e,e.begin,{key:"beginScope"}),e.begin=f(e.begin,{joinWith:""})}})(e),(e=>{
118 | if(Array.isArray(e.end)){
119 | if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
120 | Z
121 | ;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"),
122 | Z;G(e,e.end,{key:"endScope"}),e.end=f(e.end,{joinWith:""})}})(e)}function Q(e){
123 | function n(n,t){
124 | return RegExp(d(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":""))
125 | }class t{constructor(){
126 | this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
127 | addRule(e,n){
128 | n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),
129 | this.matchAt+=_(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
130 | ;const e=this.regexes.map((e=>e[1]));this.matcherRe=n(f(e,{joinWith:"|"
131 | }),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
132 | ;const n=this.matcherRe.exec(e);if(!n)return null
133 | ;const t=n.findIndex(((e,n)=>n>0&&void 0!==e)),a=this.matchIndexes[t]
134 | ;return n.splice(0,t),Object.assign(n,a)}}class a{constructor(){
135 | this.rules=[],this.multiRegexes=[],
136 | this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
137 | if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t
138 | ;return this.rules.slice(e).forEach((([e,t])=>n.addRule(e,t))),
139 | n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){
140 | return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){
141 | this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){
142 | const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex
143 | ;let t=n.exec(e)
144 | ;if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{
145 | const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)}
146 | return t&&(this.regexIndex+=t.position+1,
147 | this.regexIndex===this.count&&this.considerAll()),t}}
148 | if(e.compilerExtensions||(e.compilerExtensions=[]),
149 | e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
150 | ;return e.classNameAliases=r(e.classNameAliases||{}),function t(i,s){const o=i
151 | ;if(i.isCompiled)return o
152 | ;[D,B,W,F].forEach((e=>e(i,s))),e.compilerExtensions.forEach((e=>e(i,s))),
153 | i.__beforeBegin=null,[I,L,$].forEach((e=>e(i,s))),i.isCompiled=!0;let l=null
154 | ;return"object"==typeof i.keywords&&i.keywords.$pattern&&(i.keywords=Object.assign({},i.keywords),
155 | l=i.keywords.$pattern,
156 | delete i.keywords.$pattern),l=l||/\w+/,i.keywords&&(i.keywords=U(i.keywords,e.case_insensitive)),
157 | o.keywordPatternRe=n(l,!0),
158 | s&&(i.begin||(i.begin=/\B|\b/),o.beginRe=n(o.begin),i.end||i.endsWithParent||(i.end=/\B|\b/),
159 | i.end&&(o.endRe=n(o.end)),
160 | o.terminatorEnd=d(o.end)||"",i.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(i.end?"|":"")+s.terminatorEnd)),
161 | i.illegal&&(o.illegalRe=n(i.illegal)),
162 | i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((n=>r(e,{
163 | variants:null},n)))),e.cachedVariants?e.cachedVariants:X(e)?r(e,{
164 | starts:e.starts?r(e.starts):null
165 | }):Object.isFrozen(e)?r(e):e))("self"===e?i:e)))),i.contains.forEach((e=>{t(e,o)
166 | })),i.starts&&t(i.starts,s),o.matcher=(e=>{const n=new a
167 | ;return e.contains.forEach((e=>n.addRule(e.begin,{rule:e,type:"begin"
168 | }))),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end"
169 | }),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){
170 | return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{
171 | constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}}
172 | const J=i,Y=r,ee=Symbol("nomatch");var ne=(e=>{
173 | const n=Object.create(null),i=Object.create(null),r=[];let s=!0
174 | ;const o="Could not find the language '{}', did you forget to load/include a language module?",l={
175 | disableAutodetect:!0,name:"Plain text",contains:[]};let d={
176 | ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
177 | languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
178 | cssSelector:"pre code",languages:null,__emitter:c};function _(e){
179 | return d.noHighlightRe.test(e)}function h(e,n,t){let a="",i=""
180 | ;"object"==typeof n?(a=e,
181 | t=n.ignoreIllegals,i=n.language):(H("10.7.0","highlight(lang, code, ...args) has been deprecated."),
182 | H("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
183 | i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};M("before:highlight",r)
184 | ;const s=r.result?r.result:f(r.language,r.code,t)
185 | ;return s.code=r.code,M("after:highlight",s),s}function f(e,t,i,r){
186 | const l=Object.create(null);function c(){if(!x.keywords)return void k.addText(S)
187 | ;let e=0;x.keywordPatternRe.lastIndex=0;let n=x.keywordPatternRe.exec(S),t=""
188 | ;for(;n;){t+=S.substring(e,n.index)
189 | ;const i=N.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,x.keywords[a]);if(r){
190 | const[e,a]=r
191 | ;if(k.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(A+=a),e.startsWith("_"))t+=n[0];else{
192 | const t=N.classNameAliases[e]||e;k.addKeyword(n[0],t)}}else t+=n[0]
193 | ;e=x.keywordPatternRe.lastIndex,n=x.keywordPatternRe.exec(S)}var a
194 | ;t+=S.substr(e),k.addText(t)}function g(){null!=x.subLanguage?(()=>{
195 | if(""===S)return;let e=null;if("string"==typeof x.subLanguage){
196 | if(!n[x.subLanguage])return void k.addText(S)
197 | ;e=f(x.subLanguage,S,!0,M[x.subLanguage]),M[x.subLanguage]=e._top
198 | }else e=E(S,x.subLanguage.length?x.subLanguage:null)
199 | ;x.relevance>0&&(A+=e.relevance),k.addSublanguage(e._emitter,e.language)
200 | })():c(),S=""}function u(e,n){let t=1;for(;void 0!==n[t];){if(!e._emit[t]){t++
201 | ;continue}const a=N.classNameAliases[e[t]]||e[t],i=n[t]
202 | ;a?k.addKeyword(i,a):(S=i,c(),S=""),t++}}function b(e,n){
203 | return e.scope&&"string"==typeof e.scope&&k.openNode(N.classNameAliases[e.scope]||e.scope),
204 | e.beginScope&&(e.beginScope._wrap?(k.addKeyword(S,N.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
205 | S=""):e.beginScope._multi&&(u(e.beginScope,n),S="")),x=Object.create(e,{parent:{
206 | value:x}}),x}function m(e,n,t){let i=((e,n)=>{const t=e&&e.exec(n)
207 | ;return t&&0===t.index})(e.endRe,t);if(i){if(e["on:end"]){const t=new a(e)
208 | ;e["on:end"](n,t),t.isMatchIgnored&&(i=!1)}if(i){
209 | for(;e.endsParent&&e.parent;)e=e.parent;return e}}
210 | if(e.endsWithParent)return m(e.parent,n,t)}function p(e){
211 | return 0===x.matcher.regexIndex?(S+=e[0],1):(R=!0,0)}function _(e){
212 | const n=e[0],a=t.substr(e.index),i=m(x,e,a);if(!i)return ee;const r=x
213 | ;x.endScope&&x.endScope._wrap?(g(),
214 | k.addKeyword(n,x.endScope._wrap)):x.endScope&&x.endScope._multi?(g(),
215 | u(x.endScope,e)):r.skip?S+=n:(r.returnEnd||r.excludeEnd||(S+=n),
216 | g(),r.excludeEnd&&(S=n));do{
217 | x.scope&&k.closeNode(),x.skip||x.subLanguage||(A+=x.relevance),x=x.parent
218 | }while(x!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length}
219 | let h={};function y(n,r){const o=r&&r[0];if(S+=n,null==o)return g(),0
220 | ;if("begin"===h.type&&"end"===r.type&&h.index===r.index&&""===o){
221 | if(S+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`)
222 | ;throw n.languageName=e,n.badRule=h.rule,n}return 1}
223 | if(h=r,"begin"===r.type)return(e=>{
224 | const n=e[0],t=e.rule,i=new a(t),r=[t.__beforeBegin,t["on:begin"]]
225 | ;for(const t of r)if(t&&(t(e,i),i.isMatchIgnored))return p(n)
226 | ;return t.skip?S+=n:(t.excludeBegin&&(S+=n),
227 | g(),t.returnBegin||t.excludeBegin||(S=n)),b(t,e),t.returnBegin?0:n.length})(r)
228 | ;if("illegal"===r.type&&!i){
229 | const e=Error('Illegal lexeme "'+o+'" for mode "'+(x.scope||"")+'"')
230 | ;throw e.mode=x,e}if("end"===r.type){const e=_(r);if(e!==ee)return e}
231 | if("illegal"===r.type&&""===o)return 1
232 | ;if(T>1e5&&T>3*r.index)throw Error("potential infinite loop, way more iterations than matches")
233 | ;return S+=o,o.length}const N=v(e)
234 | ;if(!N)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"')
235 | ;const w=Q(N);let O="",x=r||w;const M={},k=new d.__emitter(d);(()=>{const e=[]
236 | ;for(let n=x;n!==N;n=n.parent)n.scope&&e.unshift(n.scope)
237 | ;e.forEach((e=>k.openNode(e)))})();let S="",A=0,C=0,T=0,R=!1;try{
238 | for(x.matcher.considerAll();;){
239 | T++,R?R=!1:x.matcher.considerAll(),x.matcher.lastIndex=C
240 | ;const e=x.matcher.exec(t);if(!e)break;const n=y(t.substring(C,e.index),e)
241 | ;C=e.index+n}return y(t.substr(C)),k.closeAllNodes(),k.finalize(),O=k.toHTML(),{
242 | language:e,value:O,relevance:A,illegal:!1,_emitter:k,_top:x}}catch(n){
243 | if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t),
244 | illegal:!0,relevance:0,_illegalBy:{message:n.message,index:C,
245 | context:t.slice(C-100,C+100),mode:n.mode,resultSoFar:O},_emitter:k};if(s)return{
246 | language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:k,_top:x}
247 | ;throw n}}function E(e,t){t=t||d.languages||Object.keys(n);const a=(e=>{
248 | const n={value:J(e),illegal:!1,relevance:0,_top:l,_emitter:new d.__emitter(d)}
249 | ;return n._emitter.addText(e),n})(e),i=t.filter(v).filter(x).map((n=>f(n,e,!1)))
250 | ;i.unshift(a);const r=i.sort(((e,n)=>{
251 | if(e.relevance!==n.relevance)return n.relevance-e.relevance
252 | ;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1
253 | ;if(v(n.language).supersetOf===e.language)return-1}return 0})),[s,o]=r,c=s
254 | ;return c.secondBest=o,c}function y(e){let n=null;const t=(e=>{
255 | let n=e.className+" ";n+=e.parentNode?e.parentNode.className:""
256 | ;const t=d.languageDetectRe.exec(n);if(t){const n=v(t[1])
257 | ;return n||(q(o.replace("{}",t[1])),
258 | q("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"}
259 | return n.split(/\s+/).find((e=>_(e)||v(e)))})(e);if(_(t))return
260 | ;if(M("before:highlightElement",{el:e,language:t
261 | }),e.children.length>0&&(d.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
262 | console.warn("https://github.com/highlightjs/highlight.js/issues/2886"),
263 | console.warn(e)),
264 | d.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML)
265 | ;n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a)
266 | ;e.innerHTML=r.value,((e,n,t)=>{const a=n&&i[n]||t
267 | ;e.classList.add("hljs"),e.classList.add("language-"+a)
268 | })(e,t,r.language),e.result={language:r.language,re:r.relevance,
269 | relevance:r.relevance},r.secondBest&&(e.secondBest={
270 | language:r.secondBest.language,relevance:r.secondBest.relevance
271 | }),M("after:highlightElement",{el:e,result:r,text:a})}let N=!1;function w(){
272 | "loading"!==document.readyState?document.querySelectorAll(d.cssSelector).forEach(y):N=!0
273 | }function v(e){return e=(e||"").toLowerCase(),n[e]||n[i[e]]}
274 | function O(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
275 | i[e.toLowerCase()]=n}))}function x(e){const n=v(e)
276 | ;return n&&!n.disableAutodetect}function M(e,n){const t=e;r.forEach((e=>{
277 | e[t]&&e[t](n)}))}
278 | "undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
279 | N&&w()}),!1),Object.assign(e,{highlight:h,highlightAuto:E,highlightAll:w,
280 | highlightElement:y,
281 | highlightBlock:e=>(H("10.7.0","highlightBlock will be removed entirely in v12.0"),
282 | H("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{d=Y(d,e)},
283 | initHighlighting:()=>{
284 | w(),H("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
285 | initHighlightingOnLoad:()=>{
286 | w(),H("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
287 | },registerLanguage:(t,a)=>{let i=null;try{i=a(e)}catch(e){
288 | if(K("Language definition for '{}' could not be registered.".replace("{}",t)),
289 | !s)throw e;K(e),i=l}
290 | i.name||(i.name=t),n[t]=i,i.rawDefinition=a.bind(null,e),i.aliases&&O(i.aliases,{
291 | languageName:t})},unregisterLanguage:e=>{delete n[e]
292 | ;for(const n of Object.keys(i))i[n]===e&&delete i[n]},
293 | listLanguages:()=>Object.keys(n),getLanguage:v,registerAliases:O,
294 | autoDetection:x,inherit:Y,addPlugin:e=>{(e=>{
295 | e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{
296 | e["before:highlightBlock"](Object.assign({block:n.el},n))
297 | }),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{
298 | e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}
299 | }),e.debugMode=()=>{s=!1},e.safeMode=()=>{s=!0
300 | },e.versionString="11.3.1",e.regex={concat:m,lookahead:g,either:p,optional:b,
301 | anyNumberOfTimes:u};for(const e in T)"object"==typeof T[e]&&t(T[e])
302 | ;return Object.assign(e,T),e})({});const te=e=>({IMPORTANT:{scope:"meta",
303 | begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{
304 | scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},
305 | FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},
306 | ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",
307 | contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{
308 | scope:"number",
309 | begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",
310 | relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/}
311 | }),ae=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],ie=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],re=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],se=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],oe=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-height","max-width","min-height","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),le=re.concat(se)
312 | ;var ce="\\.([0-9](_*[0-9])*)",de="[0-9a-fA-F](_*[0-9a-fA-F])*",ge={
313 | className:"number",variants:[{
314 | begin:`(\\b([0-9](_*[0-9])*)((${ce})|\\.)?|(${ce}))[eE][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`
315 | },{begin:`\\b([0-9](_*[0-9])*)((${ce})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{
316 | begin:`(${ce})[fFdD]?\\b`},{begin:"\\b([0-9](_*[0-9])*)[fFdD]\\b"},{
317 | begin:`\\b0[xX]((${de})\\.?|(${de})?\\.(${de}))[pP][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`
318 | },{begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${de})[lL]?\\b`},{
319 | begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}],
320 | relevance:0};function ue(e,n,t){return-1===t?"":e.replace(n,(a=>ue(e,n,t-1)))}
321 | const be="[A-Za-z$_][0-9A-Za-z$_]*",me=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],pe=["true","false","null","undefined","NaN","Infinity"],_e=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],he=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],fe=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],Ee=["arguments","this","super","console","window","document","localStorage","module","global"],ye=[].concat(fe,_e,he)
322 | ;function Ne(e){const n=e.regex,t=be,a={begin:/<[A-Za-z0-9\\._:-]+/,
323 | end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{
324 | const t=e[0].length+e.index,a=e.input[t]
325 | ;if("<"===a||","===a)return void n.ignoreMatch();let i
326 | ;">"===a&&(((e,{after:n})=>{const t=""+e[0].slice(1)
327 | ;return-1!==e.input.indexOf(t,n)})(e,{after:t
328 | })||n.ignoreMatch()),(i=e.input.substr(t).match(/^\s+extends\s+/))&&0===i.index&&n.ignoreMatch()
329 | }},i={$pattern:be,keyword:me,literal:pe,built_in:ye,"variable.language":Ee
330 | },r="\\.([0-9](_?[0-9])*)",s="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",o={
331 | className:"number",variants:[{
332 | begin:`(\\b(${s})((${r})|\\.)?|(${r}))[eE][+-]?([0-9](_?[0-9])*)\\b`},{
333 | begin:`\\b(${s})\\b((${r})\\b|\\.)?|(${r})\\b`},{
334 | begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{
335 | begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{
336 | begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{
337 | begin:"\\b0[0-7]+n?\\b"}],relevance:0},l={className:"subst",begin:"\\$\\{",
338 | end:"\\}",keywords:i,contains:[]},c={begin:"html`",end:"",starts:{end:"`",
339 | returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,l],subLanguage:"xml"}},d={
340 | begin:"css`",end:"",starts:{end:"`",returnEnd:!1,
341 | contains:[e.BACKSLASH_ESCAPE,l],subLanguage:"css"}},g={className:"string",
342 | begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,l]},u={className:"comment",
343 | variants:[e.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{
344 | begin:"(?=@[A-Za-z]+)",relevance:0,contains:[{className:"doctag",
345 | begin:"@[A-Za-z]+"},{className:"type",begin:"\\{",end:"\\}",excludeEnd:!0,
346 | excludeBegin:!0,relevance:0},{className:"variable",begin:t+"(?=\\s*(-)|$)",
347 | endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]
348 | }),e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]
349 | },b=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,c,d,g,o];l.contains=b.concat({
350 | begin:/\{/,end:/\}/,keywords:i,contains:["self"].concat(b)})
351 | ;const m=[].concat(u,l.contains),p=m.concat([{begin:/\(/,end:/\)/,keywords:i,
352 | contains:["self"].concat(m)}]),_={className:"params",begin:/\(/,end:/\)/,
353 | excludeBegin:!0,excludeEnd:!0,keywords:i,contains:p},h={variants:[{
354 | match:[/class/,/\s+/,t,/\s+/,/extends/,/\s+/,n.concat(t,"(",n.concat(/\./,t),")*")],
355 | scope:{1:"keyword",3:"title.class",5:"keyword",7:"title.class.inherited"}},{
356 | match:[/class/,/\s+/,t],scope:{1:"keyword",3:"title.class"}}]},f={relevance:0,
357 | match:n.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]+|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+/),
358 | className:"title.class",keywords:{_:[..._e,...he]}},E={variants:[{
359 | match:[/function/,/\s+/,t,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}],
360 | className:{1:"keyword",3:"title.function"},label:"func.def",contains:[_],
361 | illegal:/%/},y={
362 | match:n.concat(/\b/,(N=[...fe,"super"],n.concat("(?!",N.join("|"),")")),t,n.lookahead(/\(/)),
363 | className:"title.function",relevance:0};var N;const w={
364 | begin:n.concat(/\./,n.lookahead(n.concat(t,/(?![0-9A-Za-z$_(])/))),end:t,
365 | excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},v={
366 | match:[/get|set/,/\s+/,t,/(?=\()/],className:{1:"keyword",3:"title.function"},
367 | contains:[{begin:/\(\)/},_]
368 | },O="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",x={
369 | match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,n.lookahead(O)],className:{
370 | 1:"keyword",3:"title.function"},contains:[_]};return{name:"Javascript",
371 | aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{PARAMS_CONTAINS:p,
372 | CLASS_REFERENCE:f},illegal:/#(?![$_A-z])/,contains:[e.SHEBANG({label:"shebang",
373 | binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,
374 | begin:/^\s*['"]use (strict|asm)['"]/
375 | },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,c,d,g,u,o,f,{className:"attr",
376 | begin:t+n.lookahead(":"),relevance:0},x,{
377 | begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",
378 | keywords:"return throw case",relevance:0,contains:[u,e.REGEXP_MODE,{
379 | className:"function",begin:O,returnBegin:!0,end:"\\s*=>",contains:[{
380 | className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{
381 | className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,
382 | excludeEnd:!0,keywords:i,contains:p}]}]},{begin:/,/,relevance:0},{match:/\s+/,
383 | relevance:0},{variants:[{begin:"<>",end:">"},{
384 | match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin,
385 | "on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{
386 | begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},E,{
387 | beginKeywords:"while if switch catch for"},{
388 | begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",
389 | returnBegin:!0,label:"func.def",contains:[_,e.inherit(e.TITLE_MODE,{begin:t,
390 | className:"title.function"})]},{match:/\.\.\./,relevance:0},w,{match:"\\$"+t,
391 | relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},
392 | contains:[_]},y,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,
393 | className:"variable.constant"},h,v,{match:/\$[(.]/}]}}
394 | const we=e=>m(/\b/,e,/\w$/.test(e)?/\b/:/\B/),ve=["Protocol","Type"].map(we),Oe=["init","self"].map(we),xe=["Any","Self"],Me=["actor","associatedtype","async","await",/as\?/,/as!/,"as","break","case","catch","class","continue","convenience","default","defer","deinit","didSet","do","dynamic","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],ke=["false","nil","true"],Se=["assignment","associativity","higherThan","left","lowerThan","none","right"],Ae=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warn_unqualified_access","#warning"],Ce=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Te=p(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Re=p(Te,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),De=m(Te,Re,"*"),Ie=p(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),Le=p(Ie,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Be=m(Ie,Le,"*"),$e=m(/[A-Z]/,Le,"*"),Fe=["autoclosure",m(/convention\(/,p("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",m(/objc\(/,Be,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","testable","UIApplicationMain","unknown","usableFromInline"],ze=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"]
395 | ;var Ue=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={
396 | begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]}
397 | ;Object.assign(t,{className:"variable",variants:[{
398 | begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={
399 | className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={
400 | begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,
401 | end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/,
402 | contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$\(\(/,
403 | end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t]
404 | },l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10
405 | }),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,
406 | contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{
407 | name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z._-]+\b/,
408 | keyword:["if","then","else","elif","fi","for","while","in","do","done","case","esac","function"],
409 | literal:["true","false"],
410 | built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"]
411 | },contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{
412 | className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},t]}},
413 | grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}]
414 | }),a="[a-zA-Z_]\\w*::",i="(decltype\\(auto\\)|"+n.optional(a)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",r={
415 | className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{
416 | match:/\batomic_[a-z]{3,6}\b/}]},s={className:"string",variants:[{
417 | begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{
418 | begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
419 | end:"'",illegal:"."},e.END_SAME_AS_BEGIN({
420 | begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={
421 | className:"number",variants:[{begin:"\\b(0b[01']+)"},{
422 | begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
423 | },{
424 | begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
425 | }],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
426 | keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
427 | },contains:[{begin:/\\\n/,relevance:0},e.inherit(s,{className:"string"}),{
428 | className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},c={
429 | className:"title",begin:n.optional(a)+e.IDENT_RE,relevance:0
430 | },d=n.optional(a)+e.IDENT_RE+"\\s*\\(",g={
431 | keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"],
432 | type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"],
433 | literal:"true false NULL",
434 | built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr"
435 | },u=[l,r,t,e.C_BLOCK_COMMENT_MODE,o,s],b={variants:[{begin:/=/,end:/;/},{
436 | begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],
437 | keywords:g,contains:u.concat([{begin:/\(/,end:/\)/,keywords:g,
438 | contains:u.concat(["self"]),relevance:0}]),relevance:0},m={
439 | begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,
440 | keywords:g,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:"decltype\\(auto\\)",
441 | keywords:g,relevance:0},{begin:d,returnBegin:!0,contains:[e.inherit(c,{
442 | className:"title.function"})],relevance:0},{relevance:0,match:/,/},{
443 | className:"params",begin:/\(/,end:/\)/,keywords:g,relevance:0,
444 | contains:[t,e.C_BLOCK_COMMENT_MODE,s,o,r,{begin:/\(/,end:/\)/,keywords:g,
445 | relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,s,o,r]}]
446 | },r,t,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C",aliases:["h"],keywords:g,
447 | disableAutodetect:!0,illegal:"",contains:[].concat(b,m,u,[l,{
448 | begin:e.IDENT_RE+"::",keywords:g},{className:"class",
449 | beginKeywords:"enum class struct union",end:/[{;:<>=]/,contains:[{
450 | beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:l,
451 | strings:s,keywords:g}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{
452 | contains:[{begin:/\\\n/}]
453 | }),a="[a-zA-Z_]\\w*::",i="(?!struct)(decltype\\(auto\\)|"+n.optional(a)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",r={
454 | className:"type",begin:"\\b[a-z\\d_]*_t\\b"},s={className:"string",variants:[{
455 | begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{
456 | begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",
457 | end:"'",illegal:"."},e.END_SAME_AS_BEGIN({
458 | begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={
459 | className:"number",variants:[{begin:"\\b(0b[01']+)"},{
460 | begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"
461 | },{
462 | begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
463 | }],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{
464 | keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"
465 | },contains:[{begin:/\\\n/,relevance:0},e.inherit(s,{className:"string"}),{
466 | className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},c={
467 | className:"title",begin:n.optional(a)+e.IDENT_RE,relevance:0
468 | },d=n.optional(a)+e.IDENT_RE+"\\s*\\(",g={
469 | type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"],
470 | keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"],
471 | literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"],
472 | _type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"]
473 | },u={className:"function.dispatch",relevance:0,keywords:{
474 | _hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"]
475 | },
476 | begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/))
477 | },b=[u,l,r,t,e.C_BLOCK_COMMENT_MODE,o,s],m={variants:[{begin:/=/,end:/;/},{
478 | begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],
479 | keywords:g,contains:b.concat([{begin:/\(/,end:/\)/,keywords:g,
480 | contains:b.concat(["self"]),relevance:0}]),relevance:0},p={className:"function",
481 | begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,
482 | keywords:g,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:"decltype\\(auto\\)",
483 | keywords:g,relevance:0},{begin:d,returnBegin:!0,contains:[c],relevance:0},{
484 | begin:/::/,relevance:0},{begin:/:/,endsWithParent:!0,contains:[s,o]},{
485 | relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/,keywords:g,
486 | relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,s,o,r,{begin:/\(/,end:/\)/,
487 | keywords:g,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,s,o,r]}]
488 | },r,t,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C++",
489 | aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:g,illegal:"",
490 | classNameAliases:{"function.dispatch":"built_in"},
491 | contains:[].concat(m,p,u,b,[l,{
492 | begin:"\\b(deque|list|queue|priority_queue|pair|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array|tuple|optional|variant|function)\\s*<",
493 | end:">",keywords:g,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:g},{
494 | match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/],
495 | className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={
496 | keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]),
497 | built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"],
498 | literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{
499 | begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{
500 | begin:"\\b(0b[01']+)"},{
501 | begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{
502 | begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"
503 | }],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]
504 | },r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/,
505 | keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/,
506 | end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/
507 | },e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{
508 | begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/,
509 | contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]})
510 | ;s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],
511 | o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{
512 | illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]
513 | },u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t]
514 | },b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={
515 | begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],
516 | keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,
517 | contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{
518 | begin:"\x3c!--|--\x3e"},{begin:"?",end:">"}]}]
519 | }),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",
520 | end:"$",keywords:{
521 | keyword:"if else elif endif define undef warning error line region endregion pragma checksum"
522 | }},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/,
523 | illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"
524 | },t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",
525 | relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
526 | contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
527 | beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,
528 | contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",
529 | begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{
530 | className:"string",begin:/"/,end:/"/}]},{
531 | beginKeywords:"new return throw await else",relevance:0},{className:"function",
532 | begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,
533 | end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{
534 | beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial",
535 | relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,
536 | contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params",
537 | begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,
538 | contains:[g,a,e.C_BLOCK_COMMENT_MODE]
539 | },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{
540 | const n=e.regex,t=te(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{
541 | name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{
542 | keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"},
543 | contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/
544 | },t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0
545 | },{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0
546 | },t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{
547 | begin:":("+re.join("|")+")"},{begin:":(:)?("+se.join("|")+")"}]
548 | },t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+oe.join("|")+")\\b"},{
549 | begin:/:/,end:/[;}{]/,
550 | contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{
551 | begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"
552 | },contains:[{className:"string",begin:/[^)]/,endsWithParent:!0,excludeEnd:!0}]
553 | },t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]",relevance:0,
554 | illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/},{
555 | begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{
556 | $pattern:/[a-z-]+/,keyword:"and or not only",attribute:ie.join(" ")},contains:[{
557 | begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{
558 | className:"selector-tag",begin:"\\b("+ae.join("|")+")\\b"}]}},grmr_diff:e=>{
559 | const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{
560 | className:"meta",relevance:10,
561 | match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/)
562 | },{className:"comment",variants:[{
563 | begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/),
564 | end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{
565 | className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/,
566 | end:/$/}]}},grmr_go:e=>{const n={
567 | keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"],
568 | type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"],
569 | literal:["true","false","iota","nil"],
570 | built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"]
571 | };return{name:"Go",aliases:["golang"],keywords:n,illegal:"",
572 | contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"string",
573 | variants:[e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,{begin:"`",end:"`"}]},{
574 | className:"number",variants:[{begin:e.C_NUMBER_RE+"[i]",relevance:1
575 | },e.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",
576 | end:"\\s*(\\{|$)",excludeEnd:!0,contains:[e.TITLE_MODE,{className:"params",
577 | begin:/\(/,end:/\)/,endsParent:!0,keywords:n,illegal:/["']/}]}]}},grmr_ini:e=>{
578 | const n=e.regex,t={className:"number",relevance:0,variants:[{
579 | begin:/([+-]+)?[\d]+_[\d_]+/},{begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{
580 | begin:/;/,end:/$/},{begin:/#/,end:/$/}];const i={className:"variable",
581 | variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)\}/}]},r={
582 | className:"literal",begin:/\bon|off|true|false|yes|no\b/},s={className:"string",
583 | contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{
584 | begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]
585 | },o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0
586 | },l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{
587 | name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,
588 | contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{
589 | begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)),
590 | className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{
591 | e.regex
592 | ;const n="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",t=n+ue("(?:<"+n+"~~~(?:\\s*,\\s*"+n+"~~~)*>)?",/~~~/g,2),a={
593 | keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do"],
594 | literal:["false","true","null"],
595 | type:["char","boolean","long","float","int","byte","short","double"],
596 | built_in:["super","this"]},i={className:"meta",begin:"@"+n,contains:[{
597 | begin:/\(/,end:/\)/,contains:["self"]}]},r={className:"params",begin:/\(/,
598 | end:/\)/,keywords:a,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0}
599 | ;return{name:"Java",aliases:["jsp"],keywords:a,illegal:/<\/|#/,
600 | contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,
601 | relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{
602 | begin:/import java\.[a-z]+\./,keywords:"import",relevance:2
603 | },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/,
604 | className:"string",contains:[e.BACKSLASH_ESCAPE]
605 | },e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{
606 | match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,n],className:{
607 | 1:"keyword",3:"title.class"}},{begin:[n,/\s+/,n,/\s+/,/=/],className:{1:"type",
608 | 3:"variable",5:"operator"}},{begin:[/record/,/\s+/,n],className:{1:"keyword",
609 | 3:"title.class"},contains:[r,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{
610 | beginKeywords:"new throw return else",relevance:0},{
611 | begin:["(?:"+t+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{
612 | 2:"title.function"},keywords:a,contains:[{className:"params",begin:/\(/,
613 | end:/\)/,keywords:a,relevance:0,
614 | contains:[i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,ge,e.C_BLOCK_COMMENT_MODE]
615 | },e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},ge,i]}},grmr_javascript:Ne,
616 | grmr_json:e=>({name:"JSON",contains:[{className:"attr",
617 | begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{match:/[{}[\],:]/,
618 | className:"punctuation",relevance:0},e.QUOTE_STRING_MODE,{
619 | beginKeywords:"true false null"
620 | },e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"}),
621 | grmr_kotlin:e=>{const n={
622 | keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual",
623 | built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",
624 | literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"
625 | },a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={
626 | className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string",
627 | variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'",
628 | illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,
629 | contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={
630 | className:"meta",
631 | begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"
632 | },o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,
633 | end:/\)/,contains:[e.inherit(r,{className:"string"})]}]
634 | },l=ge,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={
635 | variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,
636 | contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g],
637 | {name:"Kotlin",aliases:["kt","kts"],keywords:n,
638 | contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",
639 | begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",
640 | begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",
641 | begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$",
642 | returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{
643 | begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,
644 | contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/,end:/>/,
645 | keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,
646 | endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,
647 | endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0
648 | },e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{className:"class",
649 | beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,
650 | illegal:"extends implements",contains:[{
651 | beginKeywords:"public protected internal private constructor"
652 | },e.UNDERSCORE_TITLE_MODE,{className:"type",begin:/,end:/>/,excludeBegin:!0,
653 | excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,
654 | excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env",
655 | end:"$",illegal:"\n"},l]}},grmr_less:e=>{
656 | const n=te(e),t=le,a="([\\w-]+|@\\{[\\w-]+\\})",i=[],r=[],s=e=>({
657 | className:"string",begin:"~?"+e+".*?"+e}),o=(e,n,t)=>({className:e,begin:n,
658 | relevance:t}),l={$pattern:/[a-z-]+/,keyword:"and or not only",
659 | attribute:ie.join(" ")},c={begin:"\\(",end:"\\)",contains:r,keywords:l,
660 | relevance:0}
661 | ;r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s("'"),s('"'),n.CSS_NUMBER_MODE,{
662 | begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",
663 | excludeEnd:!0}
664 | },n.HEXCOLOR,c,o("variable","@@?[\\w-]+",10),o("variable","@\\{[\\w-]+\\}"),o("built_in","~?`[^`]*?`"),{
665 | className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0
666 | },n.IMPORTANT);const d=r.concat({begin:/\{/,end:/\}/,contains:i}),g={
667 | beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"
668 | }].concat(r)},u={begin:a+"\\s*:",returnBegin:!0,end:/[;}]/,relevance:0,
669 | contains:[{begin:/-(webkit|moz|ms|o)-/},n.CSS_VARIABLE,{className:"attribute",
670 | begin:"\\b("+oe.join("|")+")\\b",end:/(?=:)/,starts:{endsWithParent:!0,
671 | illegal:"[<=$]",relevance:0,contains:r}}]},b={className:"keyword",
672 | begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",
673 | starts:{end:"[;{}]",keywords:l,returnEnd:!0,contains:r,relevance:0}},m={
674 | className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{
675 | begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:d}},p={variants:[{
676 | begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:a,end:/\{/}],returnBegin:!0,
677 | returnEnd:!0,illegal:"[<='$\"]",relevance:0,
678 | contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,o("keyword","all\\b"),o("variable","@\\{[\\w-]+\\}"),{
679 | begin:"\\b("+ae.join("|")+")\\b",className:"selector-tag"
680 | },n.CSS_NUMBER_MODE,o("selector-tag",a,0),o("selector-id","#"+a),o("selector-class","\\."+a,0),o("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{
681 | className:"selector-pseudo",begin:":("+re.join("|")+")"},{
682 | className:"selector-pseudo",begin:":(:)?("+se.join("|")+")"},{begin:/\(/,
683 | end:/\)/,relevance:0,contains:d},{begin:"!important"},n.FUNCTION_DISPATCH]},_={
684 | begin:`[\\w-]+:(:)?(${t.join("|")})`,returnBegin:!0,contains:[p]}
685 | ;return i.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,b,m,_,u,p),{
686 | name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:i}},grmr_lua:e=>{
687 | const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"]
688 | },i=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[",t,{contains:[a],
689 | relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,
690 | literal:"true false nil",
691 | keyword:"and break do else elseif end for goto if in local not or repeat return then until while",
692 | built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"
693 | },contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)",
694 | contains:[e.inherit(e.TITLE_MODE,{
695 | begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",
696 | begin:"\\(",endsWithParent:!0,contains:i}].concat(i)
697 | },e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",
698 | begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={
699 | className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",
700 | contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%\^\+\*]/}]},t={className:"string",
701 | begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,n]},a={className:"variable",
702 | begin:/\$\([\w-]+\s/,end:/\)/,keywords:{
703 | built_in:"subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value"
704 | },contains:[n]},i={begin:"^"+e.UNDERSCORE_IDENT_RE+"\\s*(?=[:+?]?=)"},r={
705 | className:"section",begin:/^[^\s]+:/,end:/$/,contains:[n]};return{
706 | name:"Makefile",aliases:["mk","mak","make"],keywords:{$pattern:/[\w-]+/,
707 | keyword:"define endef undefine ifdef ifndef ifeq ifneq else endif include -include sinclude override export unexport private vpath"
708 | },contains:[e.HASH_COMMENT_MODE,n,t,a,i,{className:"meta",begin:/^\.PHONY:/,
709 | end:/$/,keywords:{$pattern:/[\.\w]+/,keyword:".PHONY"}},r]}},grmr_xml:e=>{
710 | const n=e.regex,t=n.concat(/[A-Z_]/,n.optional(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),a={
711 | className:"symbol",begin:/&[a-z]+;|[0-9]+;|[a-f0-9]+;/},i={begin:/\s/,
712 | contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
713 | },r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{
714 | className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={
715 | endsWithParent:!0,illegal:/,relevance:0,contains:[{className:"attr",
716 | begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\s*/,relevance:0,contains:[{
717 | className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[a]},{
718 | begin:/'/,end:/'/,contains:[a]},{begin:/[^\s"'=<>`]+/}]}]}]};return{
719 | name:"HTML, XML",
720 | aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
721 | case_insensitive:!0,contains:[{className:"meta",begin://,
722 | relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{className:"meta",
723 | begin://,contains:[i,r,o,s]}]}]},e.COMMENT(//,{
724 | relevance:10}),{begin://,relevance:10},a,{
725 | className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",
726 | begin:/