diff --git a/README.md b/README.md index e3cd650..0758d40 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ The ModelScope image generation interface only requires you to fill in the corre The node supports text, image, and video inputs. Video is sent directly using the current protocol's content format; if the target API or protocol does not support video, its error is surfaced as an explicit video-unsupported message. File input is not implemented in this version. The `OpenAI Text Advanced Options` node accepts a JSON object for protocol/API-specific parameters, for example `{"temperature":0.7,"max_output_tokens":4096}`. JSON options cannot override request fields such as `model`, `messages`, `input`, `instructions`, `stream`, `api_key`, `base_url`, or `timeout`. +When `stream` is enabled, the model response is streamed to the client as it is generated using server-sent events (SSE). Streaming and `skill_options` cannot currently be enabled together. + ### skills The `OpenAI Text Skill Options` node discovers local skills organized around `SKILL.md`. Select a Skill and connect the node to `OpenAI Text API` through `skill_options`. Configure Skill locations with `skills.paths`; relative paths resolve from this plugin directory, and the default location is `skills/`. Set `skills.allow_call` to `true` to enable Skill calls. diff --git a/README_zh_CN.md b/README_zh_CN.md index cd9fb4c..6c202a2 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -33,6 +33,8 @@ git clone https://github.com/ycyy/ComfyUI-YCYY-API.git 节点支持文本、图像和视频输入。视频会按当前协议的格式直接发送;如果目标 API 或协议不支持视频,接口错误会转换为明确的视频不支持提示。`files` 输入当前版本暂不支持。`OpenAI 文本高级选项(JSON)` 节点接受协议或 API 特有参数,例如 `{"temperature":0.7,"max_output_tokens":4096}`。JSON 参数不能覆盖 `model`、`messages`、`input`、`instructions`、`stream`、`api_key`、`base_url` 或 `timeout` 等请求字段。 +启用 `stream` 后,模型响应会在生成过程中通过服务器发送事件(SSE)流式传输到客户端。当前流式模式不能与 `skill_options` 同时启用。 + ### skills `OpenAI 文本 Skill 选项` 节点可以发现以 `SKILL.md` 组织的本地 Skill。选择 Skill 后,通过 `skill_options` 连接到 `OpenAI 文本 API` 节点。使用 `skills.paths` 配置 Skill 位置;相对路径以本插件目录为基准,默认位置为 `skills/`。将 `skills.allow_call` 设置为 `true` 后即可启用 Skill 调用。 diff --git a/__init__.py b/__init__.py index fe42f8e..58830a5 100644 --- a/__init__.py +++ b/__init__.py @@ -19,6 +19,7 @@ from .openai.openai_text_node import * from .options.openai_text_advanced_options_node import * from .options.openai_text_skill_options_node import * from .images.image_compare import ImageCompare +from .text.preview_api_result_node import PreviewAPIResult WEB_DIRECTORY = "./web/js" @@ -44,6 +45,7 @@ class APIExtension(ComfyExtension): OpenAITextAPI, OpenAITextAdvancedOptions, OpenAITextSkillOptions, + PreviewAPIResult, ] diff --git a/locales/en/nodeDefs.json b/locales/en/nodeDefs.json index febc459..9c7a91b 100644 --- a/locales/en/nodeDefs.json +++ b/locales/en/nodeDefs.json @@ -9,6 +9,7 @@ "user_prompt": {"name": "user_prompt"}, "persist_context": {"name": "persist_context", "tooltip": "Persist chat context between calls"}, "clear_history": {"name": "clear_history", "tooltip": "Clear the current conversation history"}, + "stream": {"name": "stream", "tooltip": "If true, the model response is streamed to the client as it is generated using server-sent events (SSE)."}, "images": {"name": "images", "tooltip": "Optional image input"}, "videos": {"name": "videos", "tooltip": "Optional video input; unsupported APIs return an error"}, "config_options": {"name": "config_options", "tooltip": "Configuration override"}, @@ -22,6 +23,16 @@ "2": {"name": "Skill Trace"} } }, + "YCYY_Preview_API_Result": { + "display_name": "Preview API Result", + "description": "Render text as a Markdown preview with copy support.", + "inputs": { + "source": {"name": "source", "tooltip": "Markdown or plain text to preview"} + }, + "outputs": { + "0": {"name": "text"} + } + }, "YCYY_OpenAI_Text_Skill_Options": { "display_name": "OpenAI Text Skill Options", "description": "Load a local SKILL.md package for progressive use by OpenAI Text API.", diff --git a/locales/zh/nodeDefs.json b/locales/zh/nodeDefs.json index e63f33d..305bad0 100644 --- a/locales/zh/nodeDefs.json +++ b/locales/zh/nodeDefs.json @@ -9,6 +9,7 @@ "user_prompt": {"name": "用户提示词"}, "persist_context": {"name": "保持上下文", "tooltip": "在多次调用之间保持对话上下文"}, "clear_history": {"name": "清除历史", "tooltip": "清除当前会话历史"}, + "stream": {"name": "流式输出", "tooltip": "设为 true 时,模型响应会在生成过程中通过服务器发送事件(SSE)流式传输到客户端。"}, "images": {"name": "图像", "tooltip": "可选图像输入"}, "videos": {"name": "视频", "tooltip": "可选视频输入;不支持视频的接口会返回错误"}, "config_options": {"name": "配置选项", "tooltip": "配置覆盖"}, @@ -22,6 +23,16 @@ "2": {"name": "Skill 调用记录"} } }, + "YCYY_Preview_API_Result": { + "display_name": "Preview API Result", + "description": "将文本渲染为 Markdown 网页预览,并支持复制。", + "inputs": { + "source": {"name": "文本", "tooltip": "要预览的 Markdown 或纯文本"} + }, + "outputs": { + "0": {"name": "文本"} + } + }, "YCYY_OpenAI_Text_Skill_Options": { "display_name": "OpenAI 文本 Skill 选项", "description": "加载本地 SKILL.md,并供 OpenAI 文本 API 渐进式读取引用资料。", diff --git a/openai/openai_text_node.py b/openai/openai_text_node.py index b180b0e..4c761ce 100644 --- a/openai/openai_text_node.py +++ b/openai/openai_text_node.py @@ -1,5 +1,7 @@ import json import hashlib +import re +from uuid import uuid4 from aiohttp import web from server import PromptServer from comfy_api.latest import io @@ -9,12 +11,77 @@ from ..utils.image_utils import tensor_to_base64_string from ..utils.request_utils import ( get_proxy_config, post_openai_json, + post_openai_stream, resolve_endpoint, video_to_data_uri, ) from ..utils.skill_utils import SkillRequestContext +STREAM_EVENT = "ycyy_openai_text_stream" + + +def _safe_stream_error(exc): + detail = str(exc).replace("\r", " ").replace("\n", " ") + detail = re.sub(r"(?i)bearer\s+[a-z0-9._~+/-]+", "Bearer ", detail) + detail = re.sub(r"(?i)(api[_-]?key[=:]\s*)[^\s&]+", r"\1", detail) + detail = re.sub(r"\bsk-[a-zA-Z0-9_-]{8,}\b", "sk-", detail) + return detail[:300] + + +class _TextStreamSink: + """Route text deltas to the executing ComfyUI client immediately.""" + + def __init__(self, node_id): + try: + from comfy_execution.utils import get_executing_context + context = get_executing_context() + except (ImportError, RuntimeError): + context = None + server = PromptServer.instance + context_node_id = getattr(context, "node_id", None) + effective_node_id = node_id if node_id is not None else context_node_id + self.node_id = str(effective_node_id) if effective_node_id is not None else "" + self.prompt_id = getattr(context, "prompt_id", None) + self.run_id = uuid4().hex + self.client_id = getattr(server, "client_id", None) + self.seq = 0 + self.activities = set() + + def _send(self, phase, **extra): + PromptServer.instance.send_sync( + STREAM_EVENT, + { + "node_id": self.node_id, + "prompt_id": self.prompt_id, + "run_id": self.run_id, + "seq": self.seq, + "phase": phase, + **extra, + }, + self.client_id, + ) + self.seq += 1 + + def start(self): + self._send("start") + + def delta(self, value): + if value: + self._send("delta", delta=value) + + def activity(self, kind): + if kind and kind not in self.activities: + self.activities.add(kind) + self._send("activity", activity=kind) + + def end(self, value): + self._send("end", text=value) + + def error(self, exc): + self._send("error", message=_safe_stream_error(exc)) + + @PromptServer.instance.routes.get("/ycyy/openai/apis/all") async def get_all_openai_apis(request): try: @@ -43,6 +110,21 @@ class OpenAITextAPI(io.ComfyNode): _conversation_history = {} _max_history_items = 40 + @classmethod + def _runtime_node_id(cls, explicit_id=None): + """Resolve the client graph node id for ComfyUI V3 and direct tests.""" + if explicit_id is not None: + return explicit_id + hidden_id = getattr(getattr(cls, "hidden", None), "unique_id", None) + if hidden_id is not None: + return hidden_id + try: + from comfy_execution.utils import get_executing_context + context = get_executing_context() + except (ImportError, RuntimeError): + context = None + return getattr(context, "node_id", None) + @classmethod def define_schema(cls) -> io.Schema: apis = get_openai_apis() @@ -62,6 +144,14 @@ class OpenAITextAPI(io.ComfyNode): io.String.Input(id="user_prompt", multiline=True), io.Boolean.Input(id="persist_context", default=True), io.Boolean.Input(id="clear_history", default=False), + io.Boolean.Input( + id="stream", + default=False, + tooltip=( + "If true, the model response is streamed to the client as it is " + "generated using server-sent events (SSE)." + ), + ), io.Image.Input("images", optional=True, tooltip="Optional image input"), io.Video.Input("videos", optional=True, tooltip="Optional video input"), io.AnyType.Input(id="config_options", optional=True), @@ -190,7 +280,7 @@ class OpenAITextAPI(io.ComfyNode): @classmethod def execute(cls, api_name, model, system_prompt, user_prompt, persist_context, - clear_history=False, images=None, videos=None, config_options=None, + clear_history=False, stream=False, images=None, videos=None, config_options=None, proxy_options=None, advanced_options=None, skill_options=None, unique_id=None) -> io.NodeOutput: if not user_prompt or not user_prompt.strip(): @@ -199,9 +289,15 @@ class OpenAITextAPI(io.ComfyNode): base_url, api_key, timeout, protocol = cls._resolve_api_settings(api, config_options) endpoint = resolve_endpoint(base_url, protocol) skill = SkillRequestContext.create(skill_options, protocol) + stream_enabled = bool(stream) + runtime_node_id = cls._runtime_node_id(unique_id) + if stream_enabled and skill.enabled: + raise ValueError( + "stream cannot be enabled together with skill_options in this version" + ) session_prompt = skill.session_discriminator(system_prompt) key = cls._session_key( - unique_id, endpoint, protocol, model, session_prompt, + runtime_node_id, endpoint, protocol, model, session_prompt, ) if clear_history: cls._conversation_history.pop(key, None) @@ -226,7 +322,7 @@ class OpenAITextAPI(io.ComfyNode): request_history.append(user_message) else: history.append(user_message) - payload = {"model": model, "messages": request_history, "stream": False} + payload = {"model": model, "messages": request_history, "stream": stream_enabled} else: history = list(cls._conversation_history.get(key, [])) if persist_context and not skill.enabled else [] content = [{"type": "input_text", "text": user_prompt}] + _image_parts(images, protocol) @@ -238,7 +334,7 @@ class OpenAITextAPI(io.ComfyNode): instructions = system_prompt else: instructions = None - payload = {"model": model, "input": history, "stream": False} + payload = {"model": model, "input": history, "stream": stream_enabled} if instructions: payload["instructions"] = instructions skill.validate_advanced_options(advanced_options) @@ -259,6 +355,26 @@ class OpenAITextAPI(io.ComfyNode): key, persist_context=persist_context, ) + elif stream_enabled: + sink = _TextStreamSink(runtime_node_id) + sink.start() + try: + result = post_openai_stream( + endpoint, + headers, + payload, + timeout, + proxies, + protocol, + sink.delta, + final_response_parser=cls._parse_responses, + on_activity=sink.activity, + ) + except Exception as exc: + sink.error(exc) + raise + else: + sink.end(result) else: data = post_openai_json(endpoint, headers, payload, timeout, proxies) result = cls._parse_completions(data) if protocol == "openai-completions" else cls._parse_responses(data) diff --git a/text/preview_api_result_node.py b/text/preview_api_result_node.py new file mode 100644 index 0000000..fdba9cf --- /dev/null +++ b/text/preview_api_result_node.py @@ -0,0 +1,29 @@ +from comfy_api.latest import io, ui + + +class PreviewAPIResult(io.ComfyNode): + """Render a string in the frontend Markdown preview widget.""" + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="YCYY_Preview_API_Result", + display_name="Preview API Result", + category="YCYY/API/utils", + inputs=[ + io.String.Input( + id="source", + force_input=True, + multiline=True, + tooltip="Markdown or plain text to preview", + ), + ], + outputs=[io.String.Output(id="text", display_name="text")], + is_output_node=True, + description="Render text as a Markdown preview with copy support.", + ) + + @classmethod + def execute(cls, source) -> io.NodeOutput: + text = source if isinstance(source, str) else ("" if source is None else str(source)) + return io.NodeOutput(text, ui=ui.PreviewText(text)) diff --git a/utils/request_utils.py b/utils/request_utils.py index 53760c6..c026c42 100644 --- a/utils/request_utils.py +++ b/utils/request_utils.py @@ -18,40 +18,46 @@ class FunctionToolsRejected(RuntimeError): """The provider explicitly rejected the requested function tool schema.""" +def _raise_openai_http_error(response, payload): + """Raise the same stable error classes for JSON and streaming requests.""" + if 200 <= response.status_code < 300: + return + detail = response.text[:1000] + lower_detail = detail.lower() + choice_rejected = ( + response.status_code in (400, 404, 415, 422) + and "tool_choice" in payload + and any(marker in lower_detail for marker in ("tool_choice", "tool choice")) + and any( + marker in lower_detail + for marker in ( + "unsupported", "unknown", "unrecognized", "invalid", + "not support", "not allowed", "not permitted", "extra field", + ) + ) + ) + if choice_rejected: + raise ToolChoiceRejected( + f"API request rejected tool_choice ({response.status_code}): {detail}" + ) + tool_error = response.status_code in (400, 404, 415, 422) and any( + marker in lower_detail + for marker in ("tool", "function", "unsupported", "unknown field") + ) + if payload.get("tools") and tool_error: + raise FunctionToolsRejected( + f"API request failed ({response.status_code}); this API/model may not support " + f"the requested Skill tools: {detail}" + ) + raise RuntimeError(f"API request failed ({response.status_code}): {detail}") + + def post_openai_json(endpoint, headers, payload, timeout, proxies): """POST an OpenAI-compatible JSON request with stable error classification.""" response = requests.post( endpoint, headers=headers, json=payload, timeout=timeout, proxies=proxies ) - if response.status_code < 200 or response.status_code >= 300: - detail = response.text[:1000] - lower_detail = detail.lower() - choice_rejected = ( - response.status_code in (400, 404, 415, 422) - and "tool_choice" in payload - and any(marker in lower_detail for marker in ("tool_choice", "tool choice")) - and any( - marker in lower_detail - for marker in ( - "unsupported", "unknown", "unrecognized", "invalid", - "not support", "not allowed", "not permitted", "extra field", - ) - ) - ) - if choice_rejected: - raise ToolChoiceRejected( - f"API request rejected tool_choice ({response.status_code}): {detail}" - ) - tool_error = response.status_code in (400, 404, 415, 422) and any( - marker in lower_detail - for marker in ("tool", "function", "unsupported", "unknown field") - ) - if payload.get("tools") and tool_error: - raise FunctionToolsRejected( - f"API request failed ({response.status_code}); this API/model may not support " - f"the requested Skill tools: {detail}" - ) - raise RuntimeError(f"API request failed ({response.status_code}): {detail}") + _raise_openai_http_error(response, payload) if not response.text.strip(): raise ValueError("API returned an empty response") try: @@ -60,6 +66,162 @@ def post_openai_json(endpoint, headers, payload, timeout, proxies): raise ValueError("API returned invalid JSON") from exc +def _iter_sse_data(response): + """Yield complete SSE data payloads, independent of HTTP chunk boundaries.""" + data_lines = [] + # The SSE specification mandates UTF-8. Requests may otherwise infer + # ISO-8859-1 for text/event-stream responses without a charset. + response.encoding = "utf-8" + # requests defaults to 512-byte buffering here. Small token events can + # otherwise sit in that buffer until generation is nearly complete, + # making a real SSE response appear non-streaming in the UI. + for line in response.iter_lines(chunk_size=1, decode_unicode=True): + if line is None: + continue + if isinstance(line, bytes): + line = line.decode(response.encoding or "utf-8", errors="replace") + line = line.rstrip("\r") + if line == "": + if data_lines: + yield "\n".join(data_lines) + data_lines.clear() + continue + if line.startswith(":"): + continue + if line.startswith("data:"): + data_lines.append(line[5:].lstrip()) + if data_lines: + yield "\n".join(data_lines) + + +def _completion_stream_event(event): + """Return the provider-neutral meaning of a Chat Completions event.""" + choices = event.get("choices") + if not isinstance(choices, list) or not choices or not isinstance(choices[0], dict): + return None, None, False + choice = choices[0] + delta = choice.get("delta") + delta = delta if isinstance(delta, dict) else {} + content = delta.get("content") + reasoning = delta.get("reasoning_content") + return ( + content if isinstance(content, str) and content else None, + "reasoning" if isinstance(reasoning, str) and reasoning else None, + choice.get("finish_reason") is not None, + ) + + +def post_openai_stream( + endpoint, + headers, + payload, + timeout, + proxies, + protocol, + on_delta, + final_response_parser=None, + on_activity=None, +): + """POST an OpenAI-compatible SSE request and return its complete text.""" + if protocol not in ("openai-completions", "openai-responses"): + raise ValueError(f"Unsupported streaming protocol: {protocol}") + + chunks = [] + completed_response = None + reasoning_notified = False + completed = False + stream_headers = { + **headers, + "Accept": "text/event-stream", + "Cache-Control": "no-cache", + # Some OpenAI-compatible gateways gzip SSE responses and only flush + # compressed blocks occasionally. Identity encoding keeps token-sized + # events observable as soon as the provider sends them. + "Accept-Encoding": "identity", + } + with requests.post( + endpoint, + headers=stream_headers, + json=payload, + timeout=timeout, + proxies=proxies, + stream=True, + ) as response: + _raise_openai_http_error(response, payload) + content_type = response.headers.get("Content-Type", "").lower() + if "text/event-stream" not in content_type: + raise ValueError("Streaming API returned a non-SSE response") + + for raw_data in _iter_sse_data(response): + if raw_data.strip() == "[DONE]": + completed = True + break + try: + event = json.loads(raw_data) + except json.JSONDecodeError as exc: + raise ValueError("Streaming API returned invalid SSE JSON") from exc + if not isinstance(event, dict): + raise ValueError("Streaming API returned a non-object SSE event") + if event.get("error"): + raise ValueError(f"Streaming API failed: {event['error']}") + + if protocol == "openai-completions": + text, activity, event_completed = _completion_stream_event(event) + completed = completed or event_completed + else: + event_type = event.get("type") + text = ( + event.get("delta") + if event_type == "response.output_text.delta" + else None + ) + activity = ( + "reasoning" + if event_type == "response.reasoning_text.delta" + and isinstance(event.get("delta"), str) + and event.get("delta") + else None + ) + if event_type == "response.completed": + completed_response = event.get("response") + completed = True + if event_type in ("error", "response.failed", "response.incomplete"): + detail = event.get("error") or event.get("response") or event + raise ValueError(f"Streaming API failed: {detail}") + + if activity == "reasoning" and not reasoning_notified: + reasoning_notified = True + if on_activity is not None: + on_activity("reasoning") + + if isinstance(text, str) and text: + chunks.append(text) + on_delta(text) + + if not completed: + raise ValueError("Streaming API ended before a completion marker") + + result = "".join(chunks) + if ( + protocol == "openai-responses" + and isinstance(completed_response, dict) + and final_response_parser is not None + ): + try: + final_result = final_response_parser(completed_response) + except (TypeError, ValueError): + if not result: + raise + else: + if isinstance(final_result, str) and final_result.strip(): + result = final_result + if not isinstance(result, str) or not result.strip(): + if reasoning_notified: + raise ValueError("Streaming API returned no formal text content") + raise ValueError("Streaming API returned no text content") + return result + + def parse_json_options(options_json): if not options_json or not str(options_json).strip(): return {} diff --git a/web/js/marked.umd.js b/web/js/marked.umd.js new file mode 100644 index 0000000..e6a0d6f --- /dev/null +++ b/web/js/marked.umd.js @@ -0,0 +1,80 @@ +/** + * marked v18.0.11 - a markdown parser + * Copyright (c) 2018-2026, MarkedJS. (MIT License) + * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License) + * https://github.com/markedjs/marked + */ + +/** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ +(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports}; +"use strict";var j=Object.defineProperty;var we=Object.getOwnPropertyDescriptor;var ye=Object.getOwnPropertyNames;var Pe=Object.prototype.hasOwnProperty;var Se=(l,e)=>{for(var t in e)j(l,t,{get:e[t],enumerable:!0})},_e=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of ye(e))!Pe.call(l,s)&&s!==t&&j(l,s,{get:()=>e[s],enumerable:!(n=we(e,s))||n.enumerable});return l};var $e=l=>_e(j({},"__esModule",{value:!0}),l);var Lt={};Se(Lt,{Hooks:()=>P,Lexer:()=>x,Marked:()=>D,Parser:()=>b,Renderer:()=>y,TextRenderer:()=>_,Tokenizer:()=>w,defaults:()=>R,getDefaults:()=>z,lexer:()=>$t,marked:()=>g,options:()=>Tt,parse:()=>St,parseInline:()=>Pt,parser:()=>_t,setOptions:()=>wt,use:()=>Re,walkTokens:()=>yt});module.exports=$e(Lt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var R=z();function F(l){R=l}var M={exec:()=>null};function I(l){let e=[];return t=>{let n=Math.max(0,Math.min(3,t-1)),s=e[n];return s||(s=l(n),e[n]=s),s}}function k(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,r)=>{let i=typeof r=="string"?r:r.source;return i=i.replace(m.caret,"$1"),t=t.replace(s,i),n},getRegex:()=>new RegExp(t,e)};return n}var Le=((l="")=>{try{return!!new RegExp("(?<=1)(?/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] +\S/,listReplaceTask:/^\[[ xX]\] +/,listTaskCheckbox:/\[[ xX]\]/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:I(l=>new RegExp(`^ {0,${l}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`)),hrRegex:I(l=>new RegExp(`^ {0,${l}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`)),fencesBeginRegex:I(l=>new RegExp(`^ {0,${l}}(?:\`\`\`|~~~)`)),headingBeginRegex:I(l=>new RegExp(`^ {0,${l}}#`)),htmlBeginRegex:I(l=>new RegExp(`^ {0,${l}}<(?:[a-z].*>|!--)`,"i")),blockquoteBeginRegex:I(l=>new RegExp(`^ {0,${l}}>`))},Ee=/^(?:[ \t]*(?:\n|$))+/,ze=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Me=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,v=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Ae=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,K=/ {0,3}(?:[*+-]|\d{1,9}[.)])/,ae=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,le=k(ae).replace(/bull/g,K).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}(?:\s|$)/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Ie=k(ae).replace(/bull/g,K).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}(?:\s|$)/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),W=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table|[ \t]+\n)[^\n]+)*)/,Ce=/^[^\n]+/,X=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,Be=k(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",X).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),De=k(/^(bull)([ \t][^\n]*?)?(?:\n|$)/).replace(/bull/g,K).getRegex(),Q="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",J=/|$))/,qe=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n*|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>[^\\n]*\\n*|$)|[^\\n]*\\n*|$)|[^\\n]*\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",J).replace("tag",Q).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),ue=l=>k(W).replace("hr",v).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*(?:\\n|$))|~~~)[^\\n]*(?:\\n|$)").replace("list",l).replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Q).getRegex(),ve=ue(/ {0,3}(?:[*+-]|1[.)])[ \t]+[^ \t\n]/),He=ue(/ {0,3}(?:[*+-]|\d{1,9}[.)])(?:[ \t]|\n|$)/),Ze=k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",He).getRegex(),V={blockquote:Ze,code:ze,def:Be,fences:Me,heading:Ae,hr:v,html:qe,lheading:le,list:De,newline:Ee,paragraph:ve,table:M,text:Ce},ie=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",v).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*(?:\\n|$))|~~~)[^\\n]*(?:\\n|$)").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Q).getRegex(),Ge={...V,lheading:Ie,table:ie,paragraph:k(W).replace("hr",v).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",ie).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*(?:\\n|$))|~~~)[^\\n]*(?:\\n|$)").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",Q).getRegex()},Qe={...V,html:k(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",J).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:M,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(W).replace("hr",v).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",le).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Ne=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,je=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,pe=/^( {2,}|\\)\n(?!\s*$)/,Fe=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",Le?"(?`+)[^`]+\k(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),he=/^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/,Ve=k(he,"u").replace(/punct/g,$).getRegex(),Ye=k(he,"u").replace(/punct/g,ce).getRegex(),et=/^(?:\*+(?:((?!\*)(?!openQuote)punct)|([^\s*]))?)|^_+(?:((?!_)(?!openQuote)punct)|([^\s_]))?/,tt=k(et,"u").replace(/openQuote/g,Ke).replace(/punct/g,$).getRegex(),ke="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",nt=k(ke,"gu").replace(/notPunctSpace/g,H).replace(/punctSpace/g,C).replace(/punct/g,$).getRegex(),rt=k(ke,"gu").replace(/notPunctSpace/g,Xe).replace(/punctSpace/g,We).replace(/punct/g,ce).getRegex(),st="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)[\\s](\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|(?:(?!\\*)punct|notPunctSpace)(\\*+)(?!\\*)(?=notPunctSpace)",it=k(st,"gu").replace(/notPunctSpace/g,H).replace(/punctSpace/g,C).replace(/punct/g,$).getRegex(),ot=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,H).replace(/punctSpace/g,C).replace(/punct/g,$).getRegex(),at="^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)[\\s](_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)|(?:(?!_)punct|notPunctSpace)(_+)(?!_)(?=notPunctSpace)",lt=k(at,"gu").replace(/notPunctSpace/g,H).replace(/punctSpace/g,C).replace(/punct/g,$).getRegex(),ut=k(/^~~?(?:((?!~)punct)|[^\s~])/,"u").replace(/punct/g,$).getRegex(),pt="^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)",ct=k(pt,"gu").replace(/notPunctSpace/g,H).replace(/punctSpace/g,C).replace(/punct/g,$).getRegex(),ht=k(/\\(punct)/,"gu").replace(/punct/g,$).getRegex(),kt=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),dt=k(J).replace("(?:-->|$)","-->").getRegex(),gt=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",dt).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),G=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/,ft=k(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/).replace("label",G).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]+|(?=\))/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),de=k(/^!?\[(label)\]\[(ref)\]/).replace("label",G).replace("ref",X).getRegex(),ge=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",X).getRegex(),mt=k("reflink|nolink(?!\\()","g").replace("reflink",de).replace("nolink",ge).getRegex(),oe=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,Y={_backpedal:M,anyPunctuation:ht,autolink:kt,blockSkip:Je,br:pe,code:je,del:M,delLDelim:M,delRDelim:M,emStrongLDelim:Ve,emStrongRDelimAst:nt,emStrongRDelimUnd:ot,escape:Ne,link:ft,nolink:ge,punctuation:Ue,reflink:de,reflinkSearch:mt,tag:gt,text:Fe,url:M},xt={...Y,emStrongLDelim:tt,emStrongRDelimAst:it,emStrongRDelimUnd:lt,link:k(/^!?\[(label)\]\((.*?)\)/).replace("label",G).getRegex(),reflink:k(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",G).getRegex()},U={...Y,emStrongRDelimAst:rt,emStrongLDelim:Ye,delLDelim:ut,delRDelim:ct,url:k(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol",oe).replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:k(/^(`+|~+|[^`~])(?:(?=[`~])|(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},fe=l=>Rt[l];function T(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function ee(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function te(l,e){let t=l.replace(m.findPipe,(r,i,o)=>{let u=!1,a=i;for(;--a>=0&&o[a]==="\\";)u=!u;return u?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length=0&&m.blankLine.test(e[t]);)t--;return e.length-t<=2?l:e.slice(0,t+1).join(` +`)}function me(l,e){if(l.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n0?-2:-1}function xe(l,e=0){let t=e,n="";for(let s of l)if(s===" "){let r=4-t%4;n+=" ".repeat(r),t+=r}else n+=s,t++;return n}function be(l,e,t,n,s){let r=e.href,i=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1"),u=l[0].charAt(0)==="!";n.state.inLink=!0;let a=n.state.linkEmitted,p=n.state.inRawBlock;n.state.linkEmitted=!1;let c=n.inlineTokens(o),h=n.state.linkEmitted;if(n.state.linkEmitted=a,n.state.inLink=!1,!u){if(h){n.state.inRawBlock=p;return}n.state.linkEmitted=!0}return{type:u?"image":"link",raw:t,href:r,title:i,text:o,tokens:c}}function Ot(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(` +`).map(r=>{let i=r.match(t.other.beginningSpace);if(i===null)return r;let[o]=i;return o.length>=s.length?r.slice(s.length):r}).join(` +`)}var w=class{options;rules;lexer;constructor(e){this.options=e||R}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=this.options.pedantic?t[0]:ne(t[0]),s=n.replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:n,codeBlockStyle:"indented",text:s}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=Ot(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=L(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:L(t[0],` +`),depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:L(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=L(t[0],` +`).split(` +`),s="",r="",i=[];for(;n.length>0;){let o=!1,u=[],a;for(a=0;a1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let i=this.rules.other.listItemRegex(n),o=!1;for(;e;){let a=!1,p="",c="";if(!(t=i.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let h=xe(t[2].split(` +`,1)[0],t[1].length),d=e.split(` +`,1)[0],O=!h.trim(),f=0;if(this.options.pedantic?(f=2,c=h.trimStart()):O?f=t[1].length+1:(f=h.search(this.rules.other.nonSpaceChar),f=f>4?1:f,c=h.slice(f),f+=t[1].length),O&&this.rules.other.blankLine.test(d)&&(p+=d+` +`,e=e.substring(d.length+1),a=!0),!a){let S=this.rules.other.nextBulletRegex(f),E=this.rules.other.hrRegex(f),re=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),Oe=this.rules.other.htmlBeginRegex(f),Te=this.rules.other.blockquoteBeginRegex(f);for(;e;){let N=e.split(` +`,1)[0],q;if(d=N,this.options.pedantic?(d=d.replace(this.rules.other.listReplaceNesting," "),q=d):q=d.replace(this.rules.other.tabCharGlobal," "),re.test(d)||se.test(d)||Oe.test(d)||Te.test(d)||S.test(d)||E.test(d))break;if(q.search(this.rules.other.nonSpaceChar)>=f||!d.trim())c+=` +`+q.slice(f);else{if(O||h.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||re.test(h)||se.test(h)||E.test(h))break;c+=` +`+d}O=!d.trim(),p+=N+` +`,e=e.substring(N.length+1),h=q.slice(f)}}r.loose||(o?r.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0)),r.items.push({type:"list_item",raw:p,task:!!this.options.gfm&&this.rules.other.listIsTask.test(c),loose:!1,text:c,tokens:[]}),r.raw+=p}let u=r.items.at(-1);if(u)u.raw=u.raw.trimEnd(),u.text=u.text.trimEnd();else return;r.raw=r.raw.trimEnd();for(let a of r.items)if(this.lexer.state.top=!1,a.tokens=this.lexer.blockTokens(a.text,[]),!r.loose){let p=a.tokens.filter(h=>h.type==="space"),c=p.length>0&&p.some(h=>this.rules.other.anyLine.test(h.raw));r.loose=c}for(let a of r.items){let p=a.tokens[0];if(a.task&&(p?.type==="text"||p?.type==="paragraph")){a.text=a.text.replace(this.rules.other.listReplaceTask,""),p.raw=p.raw.replace(this.rules.other.listReplaceTask,""),p.text=p.text.replace(this.rules.other.listReplaceTask,"");for(let h=this.lexer.inlineQueue.length-1;h>=0;h--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[h].src)){this.lexer.inlineQueue[h].src=this.lexer.inlineQueue[h].src.replace(this.rules.other.listReplaceTask,"");break}let c=this.rules.other.listTaskCheckbox.exec(a.raw);if(c){let h={type:"checkbox",raw:c[0]+" ",checked:c[0]!=="[ ]"};a.checked=h.checked,r.loose?a.tokens[0]&&["paragraph","text"].includes(a.tokens[0].type)&&"tokens"in a.tokens[0]&&a.tokens[0].tokens?(a.tokens[0].raw=h.raw+a.tokens[0].raw,a.tokens[0].text=h.raw+a.tokens[0].text,a.tokens[0].tokens.unshift(h)):a.tokens.unshift({type:"paragraph",raw:h.raw,text:h.raw,tokens:[h]}):a.tokens.unshift(h)}}else a.task&&(a.task=!1)}if(r.loose)for(let a of r.items){a.loose=!0;for(let p of a.tokens)p.type==="text"&&(p.type="paragraph")}return r}}html(e){let t=this.rules.block.html.exec(e);if(t){let n=ne(t[0]);return{type:"html",block:!0,raw:n,pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:n}}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",r=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:L(t[0],` +`),href:s,title:r}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=te(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),r=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(` +`):[],i={type:"table",raw:L(t[0],` +`),header:[],align:[],rows:[]};if(n.length===s.length){for(let o of s)this.rules.other.tableAlignRight.test(o)?i.align.push("right"):this.rules.other.tableAlignCenter.test(o)?i.align.push("center"):this.rules.other.tableAlignLeft.test(o)?i.align.push("left"):i.align.push(null);for(let o=0;o({text:u,tokens:this.lexer.inline(u),header:!1,align:i.align[a]})));return i}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t){let n=t[1].trim();return{type:"heading",raw:L(t[0],` +`),depth:t[2].charAt(0)==="="?1:2,text:n,tokens:this.lexer.inline(n)}}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let i=L(n.slice(0,-1),"\\");if((n.length-i.length)%2===0)return}else{let i=me(t[2],"()");if(i===-2)return;if(i>-1){let u=(t[0].indexOf("!")===0?5:4)+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,u).trim(),t[3]=""}}let s=t[2],r="";if(this.options.pedantic){let i=this.rules.other.pedanticHrefTitle.exec(s);i&&(s=i[1],r=i[3])}else r=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),be(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:r&&r.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),r=t[s.toLowerCase()];if(!r){let i=n[0].charAt(0);return{type:"text",raw:i,text:i}}return be(n,r,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||!s[1]&&!s[2]&&!s[3]&&!s[4]||s[4]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[3]||"")||!n||this.rules.inline.punctuation.exec(n)){let i=[...s[0]].length-1,o,u,a=i,p=0,c=s[0][0],h=n===c,d=c==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(d.lastIndex=0,t=t.slice(-1*e.length+i);(s=d.exec(t))!==null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(u=[...o].length,s[3]||s[4]){a+=u;continue}else if(s[5]||s[6]){if(i%3&&!((i+u)%3)){p+=u;continue}if(h)break}if(a-=u,a>0)continue;u=Math.min(u,u+a+p);let O=[...s[0]][0].length,f=e.slice(0,i+s.index+O+u);if(Math.min(i,u)%2){let E=f.slice(1,-1);return{type:"em",raw:f,text:E,tokens:this.lexer.inlineTokens(E)}}let S=f.slice(2,-2);return{type:"strong",raw:f,text:S,tokens:this.lexer.inlineTokens(S)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),r=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&r&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e,t,n=""){let s=this.rules.inline.delLDelim.exec(e);if(!s)return;if(!(s[1]||"")||!n||this.rules.inline.punctuation.exec(n)){let i=[...s[0]].length-1,o,u,a=i,p=this.rules.inline.delRDelim;for(p.lastIndex=0,t=t.slice(-1*e.length+i);(s=p.exec(t))!==null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o||(u=[...o].length,u!==i))continue;if(s[3]||s[4]){a+=u;continue}if(a-=u,a>0)continue;u=Math.min(u,u+a);let c=[...s[0]][0].length,h=e.slice(0,i+s.index+c+u),d=h.slice(i,-i);return{type:"del",raw:h,text:d,tokens:this.lexer.inlineTokens(d)}}}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let r;do r=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(r!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;inlineQueue;tokenizer;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||R,this.options.tokenizer=this.options.tokenizer||new w,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,linkEmitted:!1,top:!0};let t={other:m,block:Z.normal,inline:B.normal};this.options.pedantic?(t.block=Z.pedantic,t.inline=B.pedantic):this.options.gfm&&(t.block=Z.gfm,this.options.breaks?t.inline=B.breaks:t.inline=B.gfm),this.tokenizer.rules=t}static get rules(){return{block:Z,inline:B}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` +`),this.blockTokens(e,this.tokens);for(let t=0;t(r=o.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let o=t.at(-1);r.raw.length===1&&o!==void 0?o.raw+=` +`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.text,this.inlineQueue.at(-1).src=o.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.raw,this.inlineQueue.at(-1).src=o.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let o=1/0,u=e.slice(1),a;this.options.extensions.startBlock.forEach(p=>{a=p.call({lexer:this},u),typeof a=="number"&&a>=0&&(o=Math.min(o,a))}),o<1/0&&o>=0&&(i=e.substring(0,o+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let o=t.at(-1);n&&o?.type==="paragraph"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="text"?(o.raw+=(o.raw.endsWith(` +`)?"":` +`)+r.raw,o.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):t.push(r);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}linkInText(e){if(!e.includes("["))return!1;let t=this.tokenizer.rules.inline.link;for(let n of e.matchAll(this.tokenizer.rules.inline.blockSkip))if(t.test(n[0])&&e.charAt(n.index-1)!=="!")return!0;for(let n of e.matchAll(this.tokenizer.rules.inline.reflinkSearch)){let s=n[0],r=s.lastIndexOf("[");if(!(s.charAt(0)==="!"||!Object.hasOwn(this.tokens.links,s.slice(r+1,-1)))&&!(r>1&&this.linkInText(s.slice(1,r-1))))return!0}return!1}inlineTokens(e,t=[]){this.tokenizer.lexer=this;let n=e;if(this.tokens.links&&e.includes("[")){let o=this.tokenizer.rules.inline.reflinkSearch,u=a=>{let p=a.lastIndexOf("[");if(!Object.hasOwn(this.tokens.links,a.slice(p+1,-1)))return a;if(p>1&&a.charAt(0)!=="!"){let c=a.slice(1,p-1);if(this.linkInText(c))return"["+c.replace(o,u)+"]["+"a".repeat(a.length-p-2)+"]"}return"["+"a".repeat(a.length-2)+"]"};n=n.replace(o,u)}n=n.replace(this.tokenizer.rules.inline.anyPunctuation,o=>"+".repeat(o.length)),n=n.replace(this.tokenizer.rules.inline.blockSkip,(o,u,a)=>{let p=a?a.length:0;return o.slice(0,p)+"["+"a".repeat(o.length-p-2)+"]"}),n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let s=!1,r="",i=1/0;for(;e;){if(e.length(o=a.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let a=t.at(-1);o.type==="text"&&a?.type==="text"?(a.raw+=o.raw,a.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let u=e;if(this.options.extensions?.startInline){let a=1/0,p=e.slice(1),c;this.options.extensions.startInline.forEach(h=>{c=h.call({lexer:this},p),typeof c=="number"&&c>=0&&(a=Math.min(a,c))}),a<1/0&&a>=0&&(u=e.substring(0,a+1))}if(o=this.tokenizer.inlineText(u)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),s=!0;let a=t.at(-1);a?.type==="text"?(a.raw+=o.raw,a.text+=o.text):t.push(o);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return t}infiniteLoopError(e){let t="Infinite loop on byte: "+e;if(this.options.silent)console.error(t);else throw new Error(t)}};var y=class{options;parser;constructor(e){this.options=e||R}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],r=e.replace(m.endingNewline,"")+` +`;return s?'
'+(n?r:T(r,!0))+`
+`:"
"+(n?r:T(r,!0))+`
+`}blockquote({tokens:e}){return`
+${this.parser.parse(e)}
+`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} +`}hr(e){return`
+`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o +`+s+" +`}listitem(e){return`
  • ${this.parser.parse(e.tokens)}
  • +`}checkbox({checked:e}){return" '}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    +`}table(e){let t="",n="";for(let r=0;r${s}`),` + +`+t+` +`+s+`
    +`}tablerow({text:e}){return` +${e} +`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${T(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),r=ee(e);if(r===null)return s;e=r;let i='
    ",i}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let r=ee(e);if(r===null)return T(n);e=r;let i=`${T(n)}{let o=r[i].flat(1/0);n=n.concat(this.walkTokens(o,t))}):r.tokens&&(n=n.concat(this.walkTokens(r.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(r=>{if(!r.name)throw new Error("extension name required");if("renderer"in r){let i=t.renderers[r.name];i?t.renderers[r.name]=function(...o){let u=r.renderer.apply(this,o);return u===!1&&(u=i.apply(this,o)),u}:t.renderers[r.name]=r.renderer}if("tokenizer"in r){if(!r.level||r.level!=="block"&&r.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let i=t[r.level];i?i.unshift(r.tokenizer):t[r.level]=[r.tokenizer],r.start&&(r.level==="block"?t.startBlock?t.startBlock.push(r.start):t.startBlock=[r.start]:r.level==="inline"&&(t.startInline?t.startInline.push(r.start):t.startInline=[r.start]))}"childTokens"in r&&r.childTokens&&(t.childTokens[r.name]=r.childTokens)}),s.extensions=t),n.renderer){let r=this.defaults.renderer||new y(this.defaults);for(let i in n.renderer){if(!(i in r))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;let o=i,u=n.renderer[o],a=r[o];r[o]=(...p)=>{let c=u.apply(r,p);return c===!1&&(c=a.apply(r,p)),c||""}}s.renderer=r}if(n.tokenizer){let r=this.defaults.tokenizer||new w(this.defaults);for(let i in n.tokenizer){if(!(i in r))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;let o=i,u=n.tokenizer[o],a=r[o];r[o]=(...p)=>{let c=u.apply(r,p);return c===!1&&(c=a.apply(r,p)),c}}s.tokenizer=r}if(n.hooks){let r=this.defaults.hooks||new P;for(let i in n.hooks){if(!(i in r))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;let o=i,u=n.hooks[o],a=r[o];P.passThroughHooks.has(i)?r[o]=p=>{if(this.defaults.async&&P.passThroughHooksRespectAsync.has(i))return(async()=>{let h=await u.call(r,p);return a.call(r,h)})();let c=u.call(r,p);return a.call(r,c)}:r[o]=(...p)=>{if(this.defaults.async)return(async()=>{let h=await u.apply(r,p);return h===!1&&(h=await a.apply(r,p)),h})();let c=u.apply(r,p);return c===!1&&(c=a.apply(r,p)),c}}s.hooks=r}if(n.walkTokens){let r=this.defaults.walkTokens,i=n.walkTokens;s.walkTokens=function(o){let u=[];return u.push(i.call(this,o)),r&&(u=u.concat(r.call(this,o))),u}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let r={...s},i={...this.defaults,...r},o=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&r.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i,i.hooks.block=e),i.async)return(async()=>{let u=i.hooks?await i.hooks.preprocess(n):n,p=await(i.hooks?await i.hooks.provideLexer(e):e?x.lex:x.lexInline)(u,i),c=i.hooks?await i.hooks.processAllTokens(p):p;i.walkTokens&&await Promise.all(this.walkTokens(c,i.walkTokens));let d=await(i.hooks?await i.hooks.provideParser(e):e?b.parse:b.parseInline)(c,i);return i.hooks?await i.hooks.postprocess(d):d})().catch(o);try{i.hooks&&(n=i.hooks.preprocess(n));let a=(i.hooks?i.hooks.provideLexer(e):e?x.lex:x.lexInline)(n,i);i.hooks&&(a=i.hooks.processAllTokens(a)),i.walkTokens&&this.walkTokens(a,i.walkTokens);let c=(i.hooks?i.hooks.provideParser(e):e?b.parse:b.parseInline)(a,i);return i.hooks&&(c=i.hooks.postprocess(c)),c}catch(u){return o(u)}}}onError(e,t){return n=>{if(n.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let s="

    An error occurred:

    "+T(n.message+"",!0)+"
    ";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var A=new D;function g(l,e){return A.parse(l,e)}g.options=g.setOptions=function(l){return A.setOptions(l),g.defaults=A.defaults,F(g.defaults),g};g.getDefaults=z;g.defaults=R;function Re(...l){return A.use(...l),g.defaults=A.defaults,F(g.defaults),g}g.use=Re;g.walkTokens=function(l,e){return A.walkTokens(l,e)};g.parseInline=A.parseInline;g.Parser=b;g.parser=b.parse;g.Renderer=y;g.TextRenderer=_;g.Lexer=x;g.lexer=x.lex;g.Tokenizer=w;g.Hooks=P;g.parse=g;var Tt=g.options,wt=g.setOptions,yt=g.walkTokens,Pt=g.parseInline,St=g,_t=b.parse,$t=x.lex; + +if(__exports != exports)module.exports = exports;return module.exports})); +//# sourceMappingURL=marked.umd.js.map diff --git a/web/js/preview_api_result.js b/web/js/preview_api_result.js new file mode 100644 index 0000000..f7af78f --- /dev/null +++ b/web/js/preview_api_result.js @@ -0,0 +1,680 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; +import "./marked.umd.js"; + +const NODE_CLASS = "YCYY_Preview_API_Result"; +const OPENAI_NODE_CLASS = "YCYY_OpenAI_Text_API"; +const STREAM_EVENT = "ycyy_openai_text_stream"; +const LOCALE_SETTING = "Comfy.Locale"; +const MIN_WIDTH = 360; +const MIN_HEIGHT = 260; +const MAX_TYPING_UNITS_PER_FRAME = 18; +const RENDER_INTERVAL_MS = 40; + +const MESSAGES = { + en: { + empty: "Connect an API result and run the workflow", + ready: "Ready", + waiting: "Waiting for response…", + reasoning: "Thinking…", + generating: "Generating…", + displaying: "Displaying result…", + complete: "Complete", + copy: "Copy", + copied: "Copied", + copyFailed: "Copy failed", + success: "Success", + copiedDetail: "Text copied to clipboard", + error: "Error", + }, + zh: { + empty: "连接 API 结果并运行工作流", + ready: "就绪", + waiting: "等待响应…", + reasoning: "正在思考…", + generating: "正在生成…", + displaying: "正在显示结果…", + complete: "完成", + copy: "复制", + copied: "已复制", + copyFailed: "复制失败", + success: "成功", + copiedDetail: "文本已复制到剪贴板", + error: "错误", + }, +}; + +const ALLOWED_TAGS = new Set([ + "P", "BR", "H1", "H2", "H3", "H4", "H5", "H6", + "UL", "OL", "LI", "BLOCKQUOTE", "PRE", "CODE", "HR", + "STRONG", "EM", "DEL", "A", "TABLE", "THEAD", "TBODY", + "TR", "TH", "TD", +]); +const DROP_CONTENT_TAGS = new Set([ + "SCRIPT", "STYLE", "IFRAME", "OBJECT", "EMBED", "FORM", + "SVG", "MATH", "META", "LINK", "BASE", "IMG", "VIDEO", "AUDIO", +]); + +let currentLocale = "en"; +const graphemeSegmenter = globalThis.Intl?.Segmenter + ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) + : null; +const reducedMotion = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)"); + +function normalizeLocale(locale) { + return String(locale || "en").toLowerCase().startsWith("zh") ? "zh" : "en"; +} + +function message(key) { + return MESSAGES[normalizeLocale(currentLocale)]?.[key] ?? MESSAGES.en[key] ?? key; +} + +function nodeClass(node) { + return node?.constructor?.comfyClass || node?.comfyClass || node?.type; +} + +function getLink(graph, linkId) { + return graph?.links?.get?.(linkId) ?? graph?.links?.[linkId] ?? null; +} + +function safeHref(value) { + try { + const url = new URL(value, window.location.href); + return ["http:", "https:", "mailto:"].includes(url.protocol) ? value : null; + } catch { + return null; + } +} + +function sanitizeMarkedHtml(html) { + const template = document.createElement("template"); + template.innerHTML = html; + + for (const element of [...template.content.querySelectorAll("*")]) { + if (DROP_CONTENT_TAGS.has(element.tagName)) { + element.remove(); + continue; + } + if (!ALLOWED_TAGS.has(element.tagName)) { + element.replaceWith(...element.childNodes); + continue; + } + + const href = element.tagName === "A" + ? safeHref(element.getAttribute("href") || "") + : null; + const title = element.getAttribute("title"); + for (const attribute of [...element.attributes]) { + element.removeAttribute(attribute.name); + } + if (href) { + element.setAttribute("href", href); + element.setAttribute("target", "_blank"); + element.setAttribute("rel", "noopener noreferrer"); + } + if (title) element.setAttribute("title", title); + } + return template.innerHTML; +} + +function markedApi() { + return globalThis.marked; +} + +function isNearBottom(element) { + return element.scrollHeight - element.scrollTop - element.clientHeight < 48; +} + +function splitTextUnits(value) { + const text = String(value ?? ""); + if (!text) return []; + if (graphemeSegmenter) { + return [...graphemeSegmenter.segment(text)].map(part => part.segment); + } + return Array.from(text); +} + +function typingRate(state) { + const queueLength = state.pendingUnits.length; + if (state.sourceEnded) { + if (queueLength > 500) return 720; + if (queueLength > 200) return 480; + return 260; + } + if (queueLength > 500) return 260; + if (queueLength > 200) return 180; + if (queueLength > 80) return 110; + return 48; +} + +function renderNow(state) { + const followTail = isNearBottom(state.content); + const raw = state.displayedText || ""; + state.host.dataset.hasContent = raw ? "true" : "false"; + if (!raw) { + state.content.replaceChildren(); + if (!["waiting", "reasoning", "error"].includes(state.statusKey)) { + const placeholder = document.createElement("div"); + placeholder.className = "empty"; + placeholder.textContent = message("empty"); + state.content.append(placeholder); + } + } else { + const parser = markedApi(); + if (!parser?.parse) { + state.content.textContent = raw; + setStatus(state, "error", "marked.umd.js unavailable"); + } else { + const html = parser.parse(raw, { gfm: true, breaks: true }); + state.content.innerHTML = sanitizeMarkedHtml(html); + } + } + if (followTail) state.content.scrollTop = state.content.scrollHeight; +} + +function scheduleRender(state, immediate = false) { + if (immediate) { + clearTimeout(state.renderTimer); + state.renderTimer = null; + state.lastRenderTime = performance.now(); + renderNow(state); + return; + } + if (state.renderTimer !== null) return; + const elapsed = performance.now() - state.lastRenderTime; + const delay = Math.max(0, RENDER_INTERVAL_MS - elapsed); + state.renderTimer = setTimeout(() => { + state.renderTimer = null; + state.lastRenderTime = performance.now(); + renderNow(state); + }, delay); +} + +function setStatus(state, key, detail = "") { + state.statusKey = key; + state.statusDetail = detail; + const visible = ["waiting", "reasoning", "generating", "displaying", "error"].includes(key); + state.host.dataset.state = key; + state.status.textContent = key === "error" && detail + ? `${message("error")}: ${detail}` + : message(key); + state.status.hidden = !visible; +} + +function updateCopyAvailability(state) { + state.copyButton.disabled = !(state.finalText || state.receivedText || state.displayedText); +} + +function setCopyState(state, copyState) { + state.copyButton.dataset.copyState = copyState; + const label = copyState === "copied" + ? message("copied") + : (copyState === "failed" ? message("copyFailed") : message("copy")); + state.copyButton.title = label; + state.copyButton.setAttribute("aria-label", label); +} + +function showCopyToast(success) { + const toast = app.extensionManager?.toast; + if (!toast?.add) return; + toast.add(success + ? { + severity: "success", + summary: message("success"), + detail: message("copiedDetail"), + life: 3000, + } + : { + severity: "error", + summary: message("error"), + detail: message("copyFailed"), + }); +} + +async function copyRawText(state) { + const text = state.sourceEnded ? state.finalText : (state.receivedText || state.displayedText); + if (!text) return; + try { + if (navigator.clipboard?.writeText && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.append(textarea); + try { + textarea.select(); + if (!document.execCommand("copy")) throw new Error("copy command failed"); + } finally { + textarea.remove(); + } + } + setCopyState(state, "copied"); + showCopyToast(true); + } catch (error) { + console.error("[YCYY] Failed to copy preview text:", error); + setCopyState(state, "failed"); + showCopyToast(false); + } finally { + clearTimeout(state.copyTimer); + state.copyTimer = setTimeout(() => { + setCopyState(state, "idle"); + }, 1200); + } +} + +function stopTyping(state) { + if (state.typingFrame !== null) cancelAnimationFrame(state.typingFrame); + state.typingFrame = null; + state.lastTypingTime = 0; + state.typingBudget = 0; +} + +function finishTypingIfReady(state) { + if (!state.sourceEnded || state.pendingUnits.length > 0) return false; + + // Normally the queue already produced finalText. This assignment also + // reconciles unusual providers whose final response differs from deltas. + if (state.displayedText !== state.finalText) { + state.displayedText = state.finalText; + } + state.receivedText = state.finalText; + state.streaming = false; + state.activeRunId = null; + state.promptId = null; + if (state.terminalError) { + setStatus(state, "error", state.terminalError); + } else { + setStatus(state, "complete"); + } + updateCopyAvailability(state); + scheduleRender(state, true); + return true; +} + +function typingTick(state, timestamp) { + state.typingFrame = null; + if (!state.streaming) return; + + if (!state.lastTypingTime) state.lastTypingTime = timestamp; + const elapsed = Math.min(250, Math.max(0, timestamp - state.lastTypingTime)); + state.lastTypingTime = timestamp; + state.typingBudget += elapsed * typingRate(state) / 1000; + + const available = Math.floor(state.typingBudget); + const count = Math.min( + available, + MAX_TYPING_UNITS_PER_FRAME, + state.pendingUnits.length, + ); + if (count > 0) { + state.displayedText += state.pendingUnits.splice(0, count).join(""); + state.typingBudget -= count; + scheduleRender(state); + } + + if (state.pendingUnits.length > 0) { + state.typingFrame = requestAnimationFrame(time => typingTick(state, time)); + } else { + state.lastTypingTime = 0; + state.typingBudget = 0; + finishTypingIfReady(state); + } +} + +function startTyping(state) { + if (state.typingFrame !== null) return; + if (!state.pendingUnits.length) { + finishTypingIfReady(state); + return; + } + state.typingFrame = requestAnimationFrame(time => typingTick(state, time)); +} + +function enqueueDelta(state, value) { + const delta = String(value ?? ""); + if (!delta) return; + state.receivedText += delta; + updateCopyAvailability(state); + if (reducedMotion?.matches) { + state.displayedText = state.receivedText; + state.pendingUnits = []; + scheduleRender(state); + return; + } + state.pendingUnits.push(...splitTextUnits(delta)); + startTyping(state); +} + +function enqueueFinalText(state, value) { + const finalText = String(value ?? state.receivedText); + state.sourceEnded = true; + state.finalText = finalText; + state.receivedText = finalText; + updateCopyAvailability(state); + + if (reducedMotion?.matches) { + state.displayedText = finalText; + state.pendingUnits = []; + finishTypingIfReady(state); + return; + } + + // Rebuild the unplayed tail from the authoritative final response. This + // also prevents duplicate characters when both `end` and `onExecuted` + // deliver the same full text before the animation has caught up. + if (finalText.startsWith(state.displayedText)) { + state.pendingUnits = splitTextUnits(finalText.slice(state.displayedText.length)); + } else { + const shown = splitTextUnits(state.displayedText); + const final = splitTextUnits(finalText); + let common = 0; + while (common < shown.length && common < final.length && shown[common] === final[common]) { + common += 1; + } + state.displayedText = shown.slice(0, common).join(""); + state.pendingUnits = final.slice(common); + } + startTyping(state); +} + +function createPreview(node) { + if (node._ycyyApiResultPreview) return node._ycyyApiResultPreview; + + const root = document.createElement("div"); + root.style.width = "100%"; + root.style.height = "100%"; + root.style.minHeight = "210px"; + root.style.boxSizing = "border-box"; + const shadow = root.attachShadow({ mode: "open" }); + shadow.innerHTML = ` + +
    + +
    + +
    `; + + const state = { + displayedText: "", + receivedText: "", + finalText: "", + pendingUnits: [], + sourceEnded: false, + terminalError: "", + activeRunId: null, + promptId: null, + lastSeq: -1, + streaming: false, + typingFrame: null, + lastTypingTime: 0, + typingBudget: 0, + renderTimer: null, + lastRenderTime: 0, + copyTimer: null, + statusKey: "ready", + statusDetail: "", + root, + host: shadow.querySelector(".preview"), + content: shadow.querySelector(".content"), + status: shadow.querySelector(".status"), + copyButton: shadow.querySelector("button"), + }; + setStatus(state, "ready"); + setCopyState(state, "idle"); + updateCopyAvailability(state); + state.copyButton.addEventListener("click", () => copyRawText(state)); + root.addEventListener("pointerdown", event => event.stopPropagation()); + root.addEventListener("wheel", event => event.stopPropagation(), { passive: true }); + + node.addDOMWidget("preview_api_result", "div", root, { + serialize: false, + hideOnZoom: false, + getMinHeight: () => 210, + }); + node._ycyyApiResultPreview = state; + node.size ||= [MIN_WIDTH, MIN_HEIGHT]; + node.size[0] = Math.max(node.size[0] || 0, MIN_WIDTH); + node.size[1] = Math.max(node.size[1] || 0, MIN_HEIGHT); + scheduleRender(state); + return state; +} + +function disposePreview(node) { + const state = node?._ycyyApiResultPreview; + if (!state) return; + stopTyping(state); + clearTimeout(state.renderTimer); + clearTimeout(state.copyTimer); + node._ycyyApiResultPreview = null; +} + +function setFinalText(node, value) { + const state = createPreview(node); + const text = String(value ?? ""); + if (state.streaming || state.typingFrame !== null || state.pendingUnits.length > 0) { + enqueueFinalText(state, text); + return; + } + const unchanged = state.displayedText === text && !state.streaming; + stopTyping(state); + state.displayedText = text; + state.receivedText = text; + state.finalText = text; + state.pendingUnits = []; + state.sourceEnded = true; + state.terminalError = ""; + state.streaming = false; + state.activeRunId = null; + state.promptId = null; + setStatus(state, "complete"); + updateCopyAvailability(state); + if (!unchanged) scheduleRender(state, true); +} + +function connectedPreviewNodes(sourceNodeId) { + const graph = app.graph; + const source = graph?.getNodeById?.(sourceNodeId) + ?? graph?.getNodeById?.(Number(sourceNodeId)); + if (!source || nodeClass(source) !== OPENAI_NODE_CLASS) return []; + + const resultOutput = source.outputs?.[0]; + const targets = []; + for (const linkId of resultOutput?.links || []) { + const link = getLink(graph, linkId); + if (!link || link.origin_slot !== 0 || link.target_slot !== 0) continue; + const target = graph.getNodeById(link.target_id); + if (target && nodeClass(target) === NODE_CLASS) targets.push(target); + } + return targets; +} + +function receiveStreamEvent(event) { + const data = event.detail || {}; + for (const node of connectedPreviewNodes(data.node_id)) { + const state = createPreview(node); + const sequence = Number(data.seq); + if (data.phase === "start") { + stopTyping(state); + state.activeRunId = data.run_id; + state.promptId = data.prompt_id ?? null; + state.lastSeq = Number.isFinite(sequence) ? sequence : -1; + state.displayedText = ""; + state.receivedText = ""; + state.finalText = ""; + state.pendingUnits = []; + state.sourceEnded = false; + state.terminalError = ""; + state.streaming = true; + setStatus(state, "waiting"); + updateCopyAvailability(state); + scheduleRender(state, true); + continue; + } + if (state.activeRunId !== data.run_id) continue; + if (Number.isFinite(sequence) && sequence <= state.lastSeq) continue; + if (Number.isFinite(sequence)) state.lastSeq = sequence; + + if (data.phase === "activity") { + if (data.activity === "reasoning" && !state.receivedText) { + setStatus(state, "reasoning"); + scheduleRender(state, true); + } + } else if (data.phase === "delta") { + if (!state.receivedText) setStatus(state, "generating"); + enqueueDelta(state, data.delta); + } else if (data.phase === "end") { + if (state.pendingUnits.length || state.displayedText !== String(data.text ?? state.receivedText)) { + setStatus(state, "displaying"); + } + enqueueFinalText(state, data.text ?? state.receivedText); + } else if (data.phase === "error") { + state.sourceEnded = true; + state.finalText = state.receivedText; + state.terminalError = data.message || "unknown"; + setStatus(state, "error", state.terminalError); + updateCopyAvailability(state); + scheduleRender(state, true); + startTyping(state); + } + } +} + +function refreshLocale() { + currentLocale = app.ui?.settings?.getSettingValue?.(LOCALE_SETTING) || "en"; + for (const node of app.graph?._nodes || []) { + const state = node?._ycyyApiResultPreview; + if (!state) continue; + setCopyState(state, state.copyButton.dataset.copyState || "idle"); + setStatus(state, state.statusKey, state.statusDetail); + scheduleRender(state); + } +} + +app.registerExtension({ + name: "YCYY.Preview.API.Result", + + async setup() { + refreshLocale(); + app.ui?.settings?.addEventListener?.(`${LOCALE_SETTING}.change`, refreshLocale); + api.addEventListener(STREAM_EVENT, receiveStreamEvent); + }, + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== NODE_CLASS && nodeType.comfyClass !== NODE_CLASS) return; + + const originalCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const result = originalCreated?.apply(this, arguments); + createPreview(this); + return result; + }; + + const originalConfigured = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const result = originalConfigured?.apply(this, arguments); + setTimeout(() => createPreview(this), 0); + return result; + }; + + const originalExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (executionMessage) { + originalExecuted?.apply(this, arguments); + const value = Array.isArray(executionMessage?.text) + ? executionMessage.text[0] + : (executionMessage?.text ?? ""); + setFinalText(this, value); + }; + + const originalRemoved = nodeType.prototype.onRemoved; + nodeType.prototype.onRemoved = function () { + disposePreview(this); + return originalRemoved?.apply(this, arguments); + }; + }, + + async nodeCreated(node) { + if (nodeClass(node) === NODE_CLASS) createPreview(node); + }, + + async onNodeOutputsUpdated(nodeOutputs) { + for (const [nodeId, output] of Object.entries(nodeOutputs || {})) { + const node = app.graph?.getNodeById?.(nodeId) + ?? app.graph?.getNodeById?.(Number(nodeId)); + if (node && nodeClass(node) === NODE_CLASS) { + const value = Array.isArray(output?.text) + ? output.text[0] + : (output?.text ?? ""); + setFinalText(node, value); + } + } + }, +});