diff --git a/.github/workflows/publish-wiki.yml b/.github/workflows/publish-wiki.yml new file mode 100644 index 0000000..b25c00c --- /dev/null +++ b/.github/workflows/publish-wiki.yml @@ -0,0 +1,71 @@ +name: Publish Wiki + +on: + push: + branches: + - main + - master + paths: + - "docs/wiki/**" + - ".github/workflows/publish-wiki.yml" + workflow_dispatch: {} + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Sync docs/wiki -> GitHub Wiki + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + if [ ! -d "docs/wiki" ]; then + echo "::error::docs/wiki does not exist" + exit 1 + fi + + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + wiki_repo="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.wiki.git" + + rm -rf wiki + if ! git ls-remote "$wiki_repo" > /dev/null 2>&1; then + echo "::error::Unable to access wiki repo. Ensure Wiki is enabled in repository Settings -> Features -> Wiki." + exit 1 + fi + + mkdir -p wiki + cd wiki + git init + git remote add origin "$wiki_repo" + git fetch origin || true + + if git show-ref --quiet refs/remotes/origin/master; then + git checkout -B master origin/master + elif git show-ref --quiet refs/remotes/origin/main; then + git checkout -B main origin/main + else + git checkout -B master + fi + cd .. + + rsync -av --delete --exclude ".git" docs/wiki/ wiki/ + + cd wiki + git add -A + if git diff --cached --quiet; then + echo "No wiki changes to publish." + exit 0 + fi + + git commit -m "Sync wiki from docs/wiki (${GITHUB_SHA})" + git push origin HEAD + diff --git a/.gitignore b/.gitignore index 33ee743..304e390 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,7 @@ comfyworkflows_sharekey github-stats-cache.json pip_overrides.json *.json +# Allow version-controlled workflow examples / assets +!assets/*.json check2.sh -/venv/ \ No newline at end of file +/venv/ diff --git a/README.md b/README.md index af1fd97..70781a7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,15 @@ Simple, no-nonsense LLM chat nodes for ComfyUI. Connect to OpenAI, Claude, Gemin - **Multimodal Support**: Chat with images using vision-capable models. - **NoASS Roleplay Mode (Exp)**: Experimental Hardcore roleplay node with Assistant Prefill support. - **Gemini Native Features**: Dedicated nodes for Gemini's image generation and editing capabilities. +- **Template Variables (Mustache)**: Write `{{var}}` in any prompt/system field and inject values via a node. +- **JSON Field Extractor**: Parse JSON and split values into multiple outputs (fixed 8 ports). +- **Markdown Preview**: Render markdown output in a safe, sanitized popup. + +## Wiki (GitHub Native) + +- **Wiki Home**: `https://github.com/Moeblack/ComfyUI-SimpleChat/wiki` +- **Source of truth**: `docs/wiki/` (auto-published by GitHub Actions) +- **Note**: Ensure Wiki is enabled in your repo Settings -> Features -> Wiki ## Installation @@ -56,6 +65,7 @@ Use this for standard text generation or Q&A. * **Connection**: Connect `config` from **API Config** to `config` on **Chat**. * **Input**: Enter your prompt in `text`. * **Output**: Returns the AI's response as a string. +* **Template vars (optional)**: Connect `Mustache Var` output `vars` to `Chat` input `vars`, then you can use `{{var}}` in `prompt/system` and it will be replaced. ### 3. Chat with Images Use this to have the AI analyze or describe an image. @@ -89,6 +99,43 @@ Modify existing images using text instructions. * **Node**: `Gemini Image Edit` * **Input**: Source image + Mask image + Prompt. +### 7. JSON Parse +Parse an LLM-produced JSON string and extract up to 8 fields. + +* **Node**: `JSON Parse` +* **Inputs**: + * `json_text`: JSON string (can auto-extract from ```json ... ``` fences) + * `path1..path8`: dot/bracket paths like `a.b.c` or `items[0].name` +* **Outputs**: `out1..out8` + `obj` (the full parsed object) + +### 7.1 Prompt JSON Unpack (recommended) +If your LLM outputs a fixed JSON schema (positive/negative/width/height/steps/cfg/sampler/seed/notes), use this node to unpack everything with **proper typed outputs** (INT/FLOAT/SAMPLER) so you can wire directly into ComfyUI nodes without manual paths or casting. + +* **Node**: `Prompt JSON Unpack` +* **Input**: `json_text` (can auto-extract from ```json ... ``` fences) +* **Outputs**: + * `positive` / `negative` (STRING) + * `width` / `height` / `steps` / `seed` (INT) + * `cfg` (FLOAT) + * `sampler` (SAMPLER) + * `notes` (STRING) + * `vars` (SIMPLECHAT_VARS mapping for Mustache: `{{positive}}`, `{{width}}`, etc.) + +### 8. Markdown Preview +Preview markdown content in a popup (sanitized). + +* **Node**: `Markdown Preview` +* **Usage**: Connect any text output to `text`, execute to open the preview popup. + +### 9. Anima prompt template (docs) +If you use SimpleChat to have an LLM generate Anima prompts, see: + +* `docs/anima_prompt.md` +* `docs/anima_scheme.md` (JSON fields + built-in Mustache vars scheme) + +(Optional) Example workflow: +* `assets/AnimaPromptScheme.json` + ## FAQ **Q: I don't see my model in the list.** diff --git a/README_CN.md b/README_CN.md index 9f8a23e..b93d92e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -16,6 +16,15 @@ ComfyUI 的极简 LLM 聊天节点。轻松连接 OpenAI, Claude, Gemini 以及 - **多模态支持**: 使用具备视觉能力的模型与图片进行对话。 - **NoASS 角色扮演模式 (实验性)**: 专为硬核角色扮演设计的节点,支持 Assistant Prefill (预填) 技术。 - **Gemini 原生功能**: 专为 Gemini 设计的文生图和图片编辑节点。 +- **模板变量 (Mustache)**: 在 prompt/system 等任意文本里写 `{{变量名}}`,通过节点统一注入替换。 +- **JSON 解析拆字段**: 将 JSON 自动解析并按 path 输出到多个端口(固定 8 路)。 +- **Markdown 渲染预览**: 将文本以 Markdown 方式弹窗渲染展示(已做净化,避免脚本注入)。 + +## Wiki(GitHub 原生) + +- **Wiki 首页**:`https://github.com/Moeblack/ComfyUI-SimpleChat/wiki` +- **仓库内维护源**:`docs/wiki/`(由 GitHub Actions 自动同步发布) +- **注意**:需要在 GitHub 仓库 Settings -> Features -> 勾选 Wiki ## 安装 (Installation) @@ -56,6 +65,7 @@ git clone https://github.com/Moeblack/ComfyUI-SimpleChat.git * **连接**: 将 **API Config** 的 `config` 输出连接到 **Chat** 的 `config` 输入。 * **输入**: 在 `text` 中输入您的提示词。 * **输出**: 返回 AI 的回复字符串。 +* **模板变量(可选)**: 将 `Mustache Var` 的 `vars` 输出接到 `Chat` 的 `vars` 输入后,`prompt/system` 中支持写 `{{变量名}}` 并自动替换。 ### 3. 图片对话 (Chat with Images) 让 AI 分析或描述图片。 @@ -89,6 +99,43 @@ git clone https://github.com/Moeblack/ComfyUI-SimpleChat.git * **节点**: `Gemini Image Edit` * **输入**: 原图 (Source image) + 遮罩 (Mask image) + 提示词 (Prompt)。 +### 7. JSON 解析拆字段 (JSON Parse) +把 LLM 输出的 JSON 自动解析成多个字段,便于接入后续节点。 + +* **节点**: `JSON Parse` +* **输入**: + * `json_text`: JSON 字符串(支持从 ```json ... ``` 代码块中自动提取) + * `path1..path8`: 取值路径(支持 `a.b.c` 与 `items[0].name`) +* **输出**: `out1..out8` + `obj`(完整解析后的对象) + +### 7.1 Prompt JSON 拆包(强烈推荐) +如果你的 LLM 输出遵循固定 JSON 格式(positive/negative/width/height/steps/cfg/sampler/seed/notes),推荐用这个节点“一键拆包”,并且 **width/height/steps/cfg/seed/sampler 都是带类型输出**,可直接接到 ComfyUI 的对应输入口(不用手动填 path,也不用字符串转数字)。 + +* **节点**: `Prompt JSON Unpack` +* **输入**: `json_text`(支持从 ```json ... ``` 代码块中自动提取) +* **输出**: + * `positive` / `negative`(STRING) + * `width` / `height` / `steps` / `seed`(INT) + * `cfg`(FLOAT) + * `sampler`(SAMPLER) + * `notes`(STRING) + * `vars`(SIMPLECHAT_VARS:自动把这些字段映射成 `{{positive}}`/`{{width}}` 等胡子变量,另含中文别名) + +### 8. Markdown 渲染预览 (Markdown Preview) +把文本以 Markdown 渲染的方式展示(弹窗),适合预览 LLM 的结构化输出/说明文档。 + +* **节点**: `Markdown Preview` +* **用法**: 把任意文本输出接到 `text`,执行后自动弹窗渲染。 + +### 9. Anima 提示词模板(文档) +如果你用 SimpleChat 让 LLM 帮你写 Anima 的提示词,推荐直接用这份 System Prompt: + +* `docs/anima_prompt.md` +* `docs/anima_scheme.md`(JSON 字段 + 内置胡子变量命名方案) + +(可选)示例工作流: +* `assets/AnimaPromptScheme.json` + ## 常见问题 (FAQ) **Q: 列表里找不到我要的模型。** diff --git a/__init__.py b/__init__.py index 7ce1eb3..53e59ef 100644 --- a/__init__.py +++ b/__init__.py @@ -11,6 +11,12 @@ from .nodes import ( SimpleChatNoASS, GeminiImageGen, GeminiImageEdit, + SimpleChatMustacheVar, + SimpleChatMustacheRender, + SimpleChatTextInput, + SimpleChatJsonParse, + SimpleChatMarkdownPreview, + SimpleChatPromptJsonUnpack, ) # Register API routes @@ -25,6 +31,12 @@ NODE_CLASS_MAPPINGS = { "SimpleChatNoASS": SimpleChatNoASS, "GeminiImageGen": GeminiImageGen, "GeminiImageEdit": GeminiImageEdit, + "SimpleChatMustacheVar": SimpleChatMustacheVar, + "SimpleChatMustacheRender": SimpleChatMustacheRender, + "SimpleChatTextInput": SimpleChatTextInput, + "SimpleChatJsonParse": SimpleChatJsonParse, + "SimpleChatMarkdownPreview": SimpleChatMarkdownPreview, + "SimpleChatPromptJsonUnpack": SimpleChatPromptJsonUnpack, } # Display names for the UI @@ -35,6 +47,12 @@ NODE_DISPLAY_NAME_MAPPINGS = { "SimpleChatNoASS": "Chat NoASS", "GeminiImageGen": "Gemini Image Gen", "GeminiImageEdit": "Gemini Image Edit", + "SimpleChatMustacheVar": "Mustache Var", + "SimpleChatMustacheRender": "Mustache Render", + "SimpleChatTextInput": "Text Input", + "SimpleChatJsonParse": "JSON Parse", + "SimpleChatMarkdownPreview": "Markdown Preview", + "SimpleChatPromptJsonUnpack": "Prompt JSON Unpack", } # Expose web directory for JS extensions diff --git a/assets/AnimaPromptScheme.json b/assets/AnimaPromptScheme.json new file mode 100644 index 0000000..37de247 --- /dev/null +++ b/assets/AnimaPromptScheme.json @@ -0,0 +1,319 @@ +{ + "id": "a6f6a4f0-9e1a-4c34-bcf9-8b2d9bb78c2a", + "revision": 0, + "last_node_id": 6, + "last_link_id": 5, + "nodes": [ + { + "id": 1, + "type": "SimpleChatConfig", + "pos": [ + -1248.0, + 360.0 + ], + "size": [ + 270, + 178 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "config", + "type": "SIMPLECHAT_CONFIG", + "links": [ + 1 + ] + } + ], + "properties": { + "aux_id": "Moeblack/ComfyUI-SimpleChat", + "Node name for S&R": "SimpleChatConfig" + }, + "widgets_values": [ + "openai", + "", + "", + "refresh", + "" + ] + }, + { + "id": 2, + "type": "SimpleChatText", + "pos": [ + -820.0, + 360.0 + ], + "size": [ + 430, + 240 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "config", + "type": "SIMPLECHAT_CONFIG", + "link": 1 + } + ], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": [ + 2 + ] + } + ], + "properties": { + "aux_id": "Moeblack/ComfyUI-SimpleChat", + "Node name for S&R": "SimpleChatText" + }, + "widgets_values": [ + "在这里写你的绘画需求(建议包含:人数/角色外观/构图/光照/背景/画风/安全标签/画师@):\n\n示例:\n1girl, black long hair, red eyes, black trench coat, holding umbrella, rainy night neon street, cel shading + slight painterly, half body, cold mood. safe. artist ref: @xxx", + "你是“Anima(circlestone-labs/Anima)专用提示词工程师”,目标是在 ComfyUI 里用 Anima 生成高质量二次元/插画向图像(非写实、非摄影)。\n\n### 模型规则(必须遵守)\n1) 你必须输出“可直接粘贴到 ComfyUI”的提示词与参数建议(通过 JSON 字段给出)。\n2) Anima 支持 Danbooru 风格 tags + 自然语言混合输入;输出时以 tags 为主,自然语言必须至少 2 句(多人/复杂构图时更要写清楚)。\n3) Anima 标签顺序固定为:\n [质量/元数据/年份/安全标签] [人物数量] [角色名] [作品名] [画师] [一般描述标签]\n4) 画师标签必须以 @ 开头(例如 @xxx),否则影响很弱。\n5) 默认生成参数(可按用户需求微调):\n - 分辨率:约 1MP(如 1024x1024 / 896x1152 / 1152x896)\n - Steps:30–50\n - CFG:4–5\n - Sampler:优先 er_sde;也可 euler_a;想更“发散/创意”可 dpmpp_2m_sde_gpu\n6) 安全标签必须明确:safe / sensitive / nsfw / explicit;并在 negative 里加入相反约束(例如 positive 里是 safe,则 negative 要包含 nsfw, explicit)。\n7) 不要追求写实:避免 photo, realistic, 真实摄影、皮肤毛孔等措辞;应强调 illustration, anime style, painterly, lineart 等。\n8) 若用户想要非二次元风格:positive 第一行写 dataset tag(ye-pop 或 deviantart),换行后再给标题/描述,再给正常标签行。\n\n### 输出格式(必须是 JSON,且不要夹带额外解释文字)\n输出一个 JSON 对象,键固定如下:\n{\n \"positive\": \"...\",\n \"negative\": \"...\",\n \"width\": 1024,\n \"height\": 1024,\n \"steps\": 40,\n \"cfg\": 4.5,\n \"sampler\": \"er_sde\",\n \"seed\": -1,\n \"notes\": \"1-3条简短技巧/提醒\"\n}\n\n### notes 里要写的技巧(1-3 条,短句)\n- 用年份/时期控制画风:year 2025 / newest / recent / mid / early / old\n- 用质量标签:masterpiece / best quality 或 score_9..score_1(可混用)\n- 角色外观要说清:发色发型、眼睛、服装、表情、姿态、镜头、光照、背景元素\n- 多人时逐个描述,避免只堆角色名\n- Tag dropout 存在:不必塞满所有标签,但关键标签必须有\n\n现在开始:根据用户输入生成 Anima 提示词 JSON。", + 1, + 2048 + ] + }, + { + "id": 3, + "type": "SimpleChatPromptJsonUnpack", + "pos": [ + -330.0, + 360.0 + ], + "size": [ + 420, + 300 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "json_text", + "type": "STRING", + "link": 2 + } + ], + "outputs": [ + { + "name": "positive", + "type": "STRING", + "links": [ + 3 + ] + }, + { + "name": "negative", + "type": "STRING", + "links": [ + 4 + ] + }, + { + "name": "width", + "type": "INT", + "links": null + }, + { + "name": "height", + "type": "INT", + "links": null + }, + { + "name": "steps", + "type": "INT", + "links": null + }, + { + "name": "cfg", + "type": "FLOAT", + "links": null + }, + { + "name": "sampler", + "type": "SAMPLER", + "links": null + }, + { + "name": "seed", + "type": "INT", + "links": null + }, + { + "name": "notes", + "type": "STRING", + "links": [ + 5 + ] + }, + { + "name": "vars", + "type": "SIMPLECHAT_VARS", + "links": null + }, + { + "name": "obj", + "type": "SIMPLECHAT_JSON", + "links": null + }, + { + "name": "sampler_name", + "type": "STRING", + "links": null + } + ], + "properties": { + "aux_id": "Moeblack/ComfyUI-SimpleChat", + "Node name for S&R": "SimpleChatPromptJsonUnpack" + }, + "widgets_values": [ + "", + true + ] + }, + { + "id": 4, + "type": "PreviewAny", + "pos": [ + 160.0, + 240.0 + ], + "size": [ + 300, + 230 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 3 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 5, + "type": "PreviewAny", + "pos": [ + 160.0, + 500.0 + ], + "size": [ + 300, + 230 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 4 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 6, + "type": "SimpleChatMarkdownPreview", + "pos": [ + 160.0, + 770.0 + ], + "size": [ + 320, + 210 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 5 + } + ], + "outputs": [], + "properties": { + "aux_id": "Moeblack/ComfyUI-SimpleChat", + "Node name for S&R": "SimpleChatMarkdownPreview" + }, + "widgets_values": [ + "" + ] + } + ], + "links": [ + [ + 1, + 1, + 0, + 2, + 0, + "SIMPLECHAT_CONFIG" + ], + [ + 2, + 2, + 0, + 3, + 0, + "STRING" + ], + [ + 3, + 3, + 0, + 4, + 0, + "*" + ], + [ + 4, + 3, + 1, + 5, + 0, + "*" + ], + [ + 5, + 3, + 8, + 6, + 0, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": { + "workflowRendererVersion": "LG", + "frontendVersion": "1.33.10" + }, + "version": 0.4 +} + diff --git a/core/template.py b/core/template.py new file mode 100644 index 0000000..8c62f1d --- /dev/null +++ b/core/template.py @@ -0,0 +1,47 @@ +""" +Template utilities for SimpleChat. + +Currently provides a lightweight Mustache-style replacement: + - Replace {{ key }} with vars[key] (stringified). + - Unmatched placeholders are left as-is by default (easier debugging). +""" + +from __future__ import annotations + +import re +from typing import Any, Mapping + + +_MUSTACHE_RE = re.compile(r"\{\{\s*([^\{\}\n]+?)\s*\}\}") + + +def render_mustache(text: str, vars: Mapping[str, Any] | None, *, keep_unmatched: bool = True) -> str: + """ + Render {{var}} placeholders in text using vars mapping. + + Args: + text: Input text (any string field). + vars: Mapping of variable name -> value. Values will be stringified. + keep_unmatched: If True, keep unknown placeholders like {{foo}} unchanged. + """ + if not isinstance(text, str) or not text: + return text + + # Fast path: avoid regex unless needed + if "{{" not in text or "}}" not in text: + return text + + if not vars or not isinstance(vars, Mapping): + return text + + def _repl(match: re.Match) -> str: + key = match.group(1).strip() + if not key: + return match.group(0) if keep_unmatched else "" + if key in vars: + value = vars.get(key) + return "" if value is None else str(value) + return match.group(0) if keep_unmatched else "" + + return _MUSTACHE_RE.sub(_repl, text) + diff --git a/docs/anima_prompt.md b/docs/anima_prompt.md new file mode 100644 index 0000000..bfb66ef --- /dev/null +++ b/docs/anima_prompt.md @@ -0,0 +1,101 @@ +# Anima(CircleStone Labs × Comfy Org)提示词生成模板(给 SimpleChat 用) + +> 目标:让 LLM 帮你把“想画什么”转换成 **Anima 可用的 Danbooru 标签 + 自然语言混合提示词**,并附带推荐生成参数。 +> +> 模型特点:偏二次元/插画,不擅长写真。建议 1MP 分辨率附近(如 1024×1024)。 + +--- + +## 1) 直接可用的 System Prompt(强制:只输出一个 JSON,便于工作流“拆字段”) + +把下面内容粘贴到 `SimpleChat -> Chat` 节点的 `system` 输入里即可: + +```text +你是“Anima(circlestone-labs/Anima)专用提示词工程师”,目标是在 ComfyUI 里用 Anima 生成高质量二次元/插画向图像(非写实、非摄影)。 + +### 模型规则(必须遵守) +1) 你必须输出“可直接粘贴到 ComfyUI”的提示词与参数建议(通过 JSON 字段给出)。 +2) Anima 支持 Danbooru 风格 tags + 自然语言混合输入;输出时以 tags 为主,自然语言必须至少 2 句(多人/复杂构图时更要写清楚)。 +3) Anima 标签顺序固定为: + [质量/元数据/年份/安全标签] [人物数量] [角色名] [作品名] [画师] [一般描述标签] +4) 画师标签必须以 @ 开头(例如 @xxx),否则影响很弱。 +5) 默认生成参数(可按用户需求微调): + - 分辨率:约 1MP(如 1024x1024 / 896x1152 / 1152x896) + - Steps:30–50 + - CFG:4–5 + - Sampler:优先 er_sde;也可 euler_a;想更“发散/创意”可 dpmpp_2m_sde_gpu +6) 安全标签必须明确:safe / sensitive / nsfw / explicit;并在 negative 里加入相反约束(例如 positive 里是 safe,则 negative 要包含 nsfw, explicit)。 +7) 不要追求写实:避免 photo, realistic, 真实摄影、皮肤毛孔等措辞;应强调 illustration, anime style, painterly, lineart 等。 +8) 若用户想要非二次元风格:positive 第一行写 dataset tag(ye-pop 或 deviantart),换行后再给标题/描述,再给正常标签行。 + +### 输出格式(必须是 JSON,且不要夹带额外解释文字) +输出一个 JSON 对象,键固定如下: +{ + "positive": "...", + "negative": "...", + "width": 1024, + "height": 1024, + "steps": 40, + "cfg": 4.5, + "sampler": "er_sde", + "seed": -1, + "notes": "1-3条简短技巧/提醒" +} + +### 质量/年份/安全标签建议 +- 质量:masterpiece / best quality 或 score_9~score_1(二选一或都用) +- 年份:year 2025 / newest / recent / mid / early / old +- 安全:safe / sensitive / nsfw / explicit(按用户要求) + +### 负面提示词建议(按需挑选,不要堆太多) +worst quality, low quality, blurry, bad anatomy, bad hands, extra fingers, missing fingers, text, watermark, logo, jpeg artifacts + +### notes 里要写的技巧(1-3 条,短句) +- 用年份/时期控制画风:year 2025 / newest / recent / mid / early / old +- 用质量标签:masterpiece / best quality 或 score_9..score_1(可混用) +- 角色外观要说清:发色发型、眼睛、服装、表情、姿态、镜头、光照、背景元素 +- 多人时逐个描述,避免只堆角色名 +- Tag dropout 存在:不必塞满所有标签,但关键标签必须有 + +现在开始:根据用户输入生成 Anima 提示词 JSON。 +``` + +--- + +## 2) 用户输入(User Prompt)怎么写更出片 + +给 LLM 的用户输入建议包含这些维度(越具体越稳定): +- **题材**:角色设计/概念图/封面插画/立绘/表情包风格等 +- **人物**:人数、性别、年龄段、发色/发型、服饰、表情、动作 +- **构图**:半身/全身、仰视/俯视、景别、镜头焦段感(如“广角夸张透视”) +- **场景**:室内/室外、季节、时间、光照(柔光/逆光/霓虹) +- **风格**:赛璐璐/厚涂/水彩/线稿/2.5D 等 +- **安全**:safe/nsfw/explicit(明确写) +- **可选:画师**:必须带 `@` + +示例用户输入: + +```text +画一张二次元角色设定插画:1girl,黑长直,红瞳,黑色风衣,手持雨伞,雨夜街头霓虹灯,赛璐璐+少量厚涂,情绪冷淡,半身构图。safe。画师参考:@xxx +``` + +--- + +## 3) 生成“非二次元艺术风格”的特殊用法 + +Anima 也支持额外数据集标签(放在第一行,换行后写标题/描述): + +```text +ye-pop +For Sale: Others by Arun Prem +Abstract, oil painting of ... +``` + +或: + +```text +deviantart +Flame +Digital painting of a fiery dragon ... +``` + diff --git a/docs/anima_scheme.md b/docs/anima_scheme.md new file mode 100644 index 0000000..32749f2 --- /dev/null +++ b/docs/anima_scheme.md @@ -0,0 +1,87 @@ +# Anima Prompt Scheme(JSON + 内置胡子变量) + +本方案用于:**LLM 只输出一个 JSON** → ComfyUI 里用 `Prompt JSON Unpack` **一键拆字段** → 直接接入你的图像工作流(positive/negative/参数),同时自动得到一套可复用的 Mustache 变量。 + +--- + +## 1) LLM 必须输出的 JSON Schema(固定) + +> 你在 `docs/anima_prompt.md` 的 System Prompt 已经强制要求输出这个结构。 + +```json +{ + "positive": "...", + "negative": "...", + "width": 1024, + "height": 1024, + "steps": 40, + "cfg": 4.5, + "sampler": "er_sde", + "seed": -1, + "notes": "1-3条简短技巧/提醒" +} +``` + +--- + +## 2) `Prompt JSON Unpack` 的输出(端口) + +- **positive/negative**: `STRING` +- **width/height/steps/seed**: `INT` +- **cfg**: `FLOAT` +- **sampler**: `SAMPLER`(对象,适配 custom-sampling 工作流) +- **sampler_name**: `STRING`(文本形式,便于展示/拼接/日志) +- **notes**: `STRING` +- **vars**: `SIMPLECHAT_VARS`(下文“内置胡子变量”) +- **obj**: `SIMPLECHAT_JSON`(完整 dict) + +--- + +## 3) 内置胡子变量(Mustache Vars) + +把 `Prompt JSON Unpack.vars` 接到任何 SimpleChat 节点的 `vars` 输入后,就能在 `prompt/system` 里直接使用这些变量: + +### 3.1 推荐使用(带前缀,避免变量冲突) + +- `{{anima.positive}}` +- `{{anima.negative}}` +- `{{anima.width}}` +- `{{anima.height}}` +- `{{anima.steps}}` +- `{{anima.cfg}}` +- `{{anima.sampler}}` / `{{anima.sampler_name}}` +- `{{anima.seed}}` +- `{{anima.notes}}` + +### 3.2 简写(不带前缀) + +- `{{positive}}` +- `{{negative}}` +- `{{width}}` +- `{{height}}` +- `{{steps}}` +- `{{cfg}}` +- `{{sampler}}` / `{{sampler_name}}` +- `{{seed}}` +- `{{notes}}` + +### 3.3 中文别名(可选) + +- `{{正面提示词}}` / `{{anima.正面提示词}}` +- `{{负面提示词}}` / `{{anima.负面提示词}}` +- `{{宽}}` / `{{anima.宽}}` +- `{{高}}` / `{{anima.高}}` +- `{{步数}}` / `{{anima.步数}}` +- `{{采样器}}` / `{{anima.采样器}}` +- `{{种子}}` / `{{anima.种子}}` +- `{{备注}}` / `{{anima.备注}}` + +--- + +## 4) 最小工作流接线(推荐) + +1) `SimpleChatText`(system 用 `docs/anima_prompt.md`,prompt 写你的需求) +2) 输出接 `Prompt JSON Unpack.json_text` +3) 把 `positive/negative/width/...` 分别接到你现有出图工作流对应口 +4) 如需二次加工:把 `Prompt JSON Unpack.vars` 接到下一个 `SimpleChatText.vars`,就能用上面的胡子变量复用字段 + diff --git a/docs/wiki/Anima.md b/docs/wiki/Anima.md new file mode 100644 index 0000000..e305d6d --- /dev/null +++ b/docs/wiki/Anima.md @@ -0,0 +1,73 @@ +# Anima(circlestone-labs/Anima)专用方案 + +目标:在 ComfyUI 里用 Anima 生成高质量二次元/插画向图像(非写实、非摄影)。 + +--- + +## 1) 强制 JSON 输出(推荐) + +建议你在 `Chat.system` 中使用仓库提供的模板(已强制“只输出一个 JSON”): +- `docs/anima_prompt.md` + +LLM 输出 JSON schema(固定): + +```json +{ + "positive": "...", + "negative": "...", + "width": 1024, + "height": 1024, + "steps": 40, + "cfg": 4.5, + "sampler": "er_sde", + "seed": -1, + "notes": "1-3条简短技巧/提醒" +} +``` + +--- + +## 2) 一键拆字段(避免“只能接 positive 不能接 json”) + +把 LLM 的输出接到: +- `Prompt JSON Unpack.json_text` + +你会得到强类型输出口(可直接接到 ComfyUI 节点): +- `positive/negative`(STRING) +- `width/height/steps/seed`(INT) +- `cfg`(FLOAT) +- `sampler`(SAMPLER 对象,用于 custom-sampling 图) +- `sampler_name`(STRING,便于展示/拼接) +- `notes`(STRING) + +--- + +## 3) 内置胡子变量(vars) + +`Prompt JSON Unpack` 同时输出 `vars`(`SIMPLECHAT_VARS`),可直接用于 Mustache 模板渲染: + +推荐用带前缀(防冲突): +- `{{anima.positive}}` / `{{anima.negative}}` +- `{{anima.width}}` / `{{anima.height}}` +- `{{anima.steps}}` / `{{anima.cfg}}` +- `{{anima.sampler}}` / `{{anima.sampler_name}}` +- `{{anima.seed}}` / `{{anima.notes}}` + +也支持不带前缀的简写:`{{positive}}`、`{{width}}` 等;并提供中文别名(如 `{{正面提示词}}`)。 + +更完整的变量清单见: +- `docs/anima_scheme.md` + +--- + +## 4) 规则要点(给提示词工程) + +- **标签顺序固定**: + `[质量/元数据/年份/安全] [人数] [角色名] [作品名] [画师] [通用标签]` +- **画师必须 @**:例如 `@xxx` +- **混合输入**:tags + 自然语言(自然语言建议至少 2 句,复杂构图更要写清楚) +- **默认参数**:约 1MP;steps 30–50;cfg 4–5;sampler 优先 `er_sde` +- **安全标签**:safe/sensitive/nsfw/explicit 必须明确;negative 里写相反约束 +- **避免写实**:不要用 photo/realistic/皮肤毛孔等措辞 +- **非二次元风格**:第一行写 `ye-pop` 或 `deviantart`,换行后再给标题/描述与正常标签行 + diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..b4af0da --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,27 @@ +# ComfyUI-SimpleChat Wiki + +这份 Wiki 用于集中放置插件的使用文档与工作流方案。 + +> 维护方式:本仓库的 `docs/wiki/` 会通过 GitHub Actions **自动同步**到 GitHub Wiki(原生 Wiki)。 + +--- + +## 快速入口 + +- **节点清单与用途**:见 `Nodes.md` +- **Anima(circlestone-labs/Anima)专用方案**:见 `Anima.md` +- **Wiki 自动发布(配置/排错)**:见 `Wiki-Sync.md` + +--- + +## 推荐工作流(Anima:LLM→JSON→拆字段→接入出图) + +核心思路: +1) LLM **只输出一个固定 JSON**(包含 positive/negative/width/height/steps/cfg/sampler/seed/notes) +2) 用 `Prompt JSON Unpack` 把 JSON **拆成多个强类型端口**(STRING/INT/FLOAT/SAMPLER) +3) 把各字段直接接到 ComfyUI 的对应输入口(避免“只接收 positive 不接收 json”的问题) +4) 同时得到 `vars`(内置胡子变量)用于复用/二次加工 + +示例工作流文件: +- `assets/AnimaPromptScheme.json` + diff --git a/docs/wiki/Nodes.md b/docs/wiki/Nodes.md new file mode 100644 index 0000000..445eb70 --- /dev/null +++ b/docs/wiki/Nodes.md @@ -0,0 +1,27 @@ +# Nodes(节点清单) + +分类路径均为 `SimpleChat` 或 `SimpleChat/Utils`(右键菜单里可搜到)。 + +--- + +## SimpleChat(主节点) + +- **API Config**:统一配置 OpenAI / Claude / Gemini(支持刷新模型列表) +- **Chat**:文本对话(支持 `system`) +- **Chat with Image**:图文对话 +- **Chat NoASS**:NoASS 角色扮演模式(实验性) +- **Gemini Image Gen / Edit**:Gemini 原生文生图/图片编辑 + +> 以上节点均支持可选输入 `vars`:用于把 `{{变量}}` 模板渲染进 prompt/system 等文本字段。 + +--- + +## SimpleChat/Utils(工具节点) + +- **Text Input**:纯文本输入框(输出 STRING) +- **Mustache Var**:定义变量(输出 `SIMPLECHAT_VARS` 字典,支持菊花链合并) +- **Mustache Render**:不经过 Chat,也能把 `{{变量}}` 渲染成纯文本(输出 STRING) +- **JSON Parse**:解析 JSON 并按 path 拆 8 路输出(支持 `a.b[0].c`、支持 ```json 代码块提取) +- **Prompt JSON Unpack**:固定 schema 的“一键拆包”(强类型输出 + 自动生成 `vars`) +- **Markdown Preview**:把文本以 Markdown 方式弹窗预览(已净化) + diff --git a/docs/wiki/Wiki-Sync.md b/docs/wiki/Wiki-Sync.md new file mode 100644 index 0000000..f8ed5ce --- /dev/null +++ b/docs/wiki/Wiki-Sync.md @@ -0,0 +1,33 @@ +# Wiki Sync(GitHub 原生 Wiki 自动发布) + +本仓库采用 **GitHub 原生 Wiki**,并使用 GitHub Actions 把 `docs/wiki/` 自动同步到 Wiki 仓库。 + +--- + +## 1) 需要在 GitHub 仓库里打开 Wiki 功能 + +进入 GitHub 仓库: +- Settings -> Features -> 勾选 **Wiki** + +若未启用,Actions 会在克隆 `${repo}.wiki.git` 时失败。 + +--- + +## 2) 文档如何更新 + +请直接改动仓库内: +- `docs/wiki/*.md` + +推送到 `main/master` 后会自动发布到 Wiki。 + +--- + +## 3) 同步工作流文件在哪里 + +自动发布配置位于: +- `.github/workflows/publish-wiki.yml` + +同步逻辑: +- 把 `docs/wiki/` rsync 到 Wiki 仓库根目录 +- 自动提交并 push + diff --git a/nodes/__init__.py b/nodes/__init__.py index 9b6e614..892b683 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -7,6 +7,12 @@ from .chat_image import SimpleChatImage from .chat_noass import SimpleChatNoASS from .gemini_gen import GeminiImageGen from .gemini_edit import GeminiImageEdit +from .mustache_var import SimpleChatMustacheVar +from .mustache_render import SimpleChatMustacheRender +from .text_input import SimpleChatTextInput +from .json_parse import SimpleChatJsonParse +from .markdown_preview import SimpleChatMarkdownPreview +from .prompt_json_unpack import SimpleChatPromptJsonUnpack __all__ = [ "SimpleChatConfig", @@ -15,4 +21,10 @@ __all__ = [ "SimpleChatNoASS", "GeminiImageGen", "GeminiImageEdit", + "SimpleChatMustacheVar", + "SimpleChatMustacheRender", + "SimpleChatTextInput", + "SimpleChatJsonParse", + "SimpleChatMarkdownPreview", + "SimpleChatPromptJsonUnpack", ] diff --git a/nodes/chat.py b/nodes/chat.py index 8f0ca4d..2cb5e92 100644 --- a/nodes/chat.py +++ b/nodes/chat.py @@ -2,6 +2,7 @@ Chat node - Basic text conversation. """ from ..core import get_provider, ChatConfig +from ..core.template import render_mustache class SimpleChatText: @@ -16,6 +17,7 @@ class SimpleChatText: }, "optional": { "system": ("STRING", {"multiline": True, "default": ""}), + "vars": ("SIMPLECHAT_VARS",), "temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}), "max_tokens": ("INT", {"default": 2048, "min": 1, "max": 128000}), } @@ -32,9 +34,14 @@ class SimpleChatText: config: ChatConfig, prompt: str, system: str = "", + vars=None, temperature: float = 1.0, max_tokens: int = 2048, ): + # Template rendering ({{var}}) for prompt/system + prompt = render_mustache(prompt, vars) + system = render_mustache(system, vars) + # Build messages messages = [] if system.strip(): diff --git a/nodes/chat_image.py b/nodes/chat_image.py index 5dbcaa0..cad9cdc 100644 --- a/nodes/chat_image.py +++ b/nodes/chat_image.py @@ -3,6 +3,7 @@ Chat with Image node - Send image to LLM for analysis. """ import torch from ..core import get_provider, ChatConfig +from ..core.template import render_mustache class SimpleChatImage: @@ -18,6 +19,7 @@ class SimpleChatImage: }, "optional": { "system": ("STRING", {"multiline": True, "default": ""}), + "vars": ("SIMPLECHAT_VARS",), "temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}), "max_tokens": ("INT", {"default": 2048, "min": 1, "max": 128000}), } @@ -35,9 +37,14 @@ class SimpleChatImage: image: torch.Tensor, prompt: str, system: str = "", + vars=None, temperature: float = 1.0, max_tokens: int = 2048, ): + # Template rendering ({{var}}) for prompt/system + prompt = render_mustache(prompt, vars) + system = render_mustache(system, vars) + # Build messages messages = [] if system.strip(): diff --git a/nodes/chat_noass.py b/nodes/chat_noass.py index d6415ee..f9de18f 100644 --- a/nodes/chat_noass.py +++ b/nodes/chat_noass.py @@ -11,6 +11,7 @@ from ..core import ( build_full_history, get_stop_sequences, ) +from ..core.template import render_mustache class SimpleChatNoASS: @@ -39,6 +40,7 @@ class SimpleChatNoASS: "placeholder": "Force the start of the assistant's response (Prefill)." }), "history": ("STRING", {"multiline": True, "default": "", "forceInput": True}), + "vars": ("SIMPLECHAT_VARS",), "image": ("IMAGE",), "user_name": ("STRING", {"default": "User"}), "char_name": ("STRING", {"default": "Assistant"}), @@ -60,12 +62,20 @@ class SimpleChatNoASS: user_action: str, prefill_start: str = "", history: str = "", + vars=None, image: torch.Tensor = None, user_name: str = "User", char_name: str = "Assistant", temperature: float = 1.0, max_tokens: int = 2048, ): + # Template rendering ({{var}}) for scenario/user/prefill + scenario_instructions = render_mustache(scenario_instructions, vars) + user_action = render_mustache(user_action, vars) + prefill_start = render_mustache(prefill_start, vars) + user_name = render_mustache(user_name, vars) + char_name = render_mustache(char_name, vars) + # Format in NoASS style system_prompt, prefilled = format_noass_prompt( system=scenario_instructions, diff --git a/nodes/gemini_edit.py b/nodes/gemini_edit.py index 42ff0b1..983952b 100644 --- a/nodes/gemini_edit.py +++ b/nodes/gemini_edit.py @@ -3,6 +3,7 @@ Gemini Image Edit node - Edit images using Gemini. """ import torch from ..core import get_provider, ChatConfig +from ..core.template import render_mustache class GeminiImageEdit: @@ -17,6 +18,7 @@ class GeminiImageEdit: "prompt": ("STRING", {"multiline": True, "default": ""}), }, "optional": { + "vars": ("SIMPLECHAT_VARS",), "size": (["1K", "2K", "4K"], {"default": "1K"}), } } @@ -32,8 +34,12 @@ class GeminiImageEdit: config: ChatConfig, image: torch.Tensor, prompt: str, + vars=None, size: str = "1K", ): + # Template rendering ({{var}}) for prompt + prompt = render_mustache(prompt, vars) + # Validate provider if config.provider != "gemini": raise ValueError("Gemini Image Edit requires Gemini provider. Please use a Gemini config.") diff --git a/nodes/gemini_gen.py b/nodes/gemini_gen.py index e8b7dac..97e3b8f 100644 --- a/nodes/gemini_gen.py +++ b/nodes/gemini_gen.py @@ -3,6 +3,7 @@ Gemini Image Gen node - Text-to-image generation with Gemini. """ import torch from ..core import get_provider, ChatConfig +from ..core.template import render_mustache class GeminiImageGen: @@ -16,6 +17,7 @@ class GeminiImageGen: "prompt": ("STRING", {"multiline": True, "default": ""}), }, "optional": { + "vars": ("SIMPLECHAT_VARS",), "aspect_ratio": (["1:1", "16:9", "9:16", "4:3", "3:4", "5:4", "4:5"], {"default": "1:1"}), "size": (["1K", "2K", "4K"], {"default": "1K"}), } @@ -31,9 +33,13 @@ class GeminiImageGen: self, config: ChatConfig, prompt: str, + vars=None, aspect_ratio: str = "1:1", size: str = "1K", ): + # Template rendering ({{var}}) for prompt + prompt = render_mustache(prompt, vars) + # Validate provider if config.provider != "gemini": raise ValueError("Gemini Image Gen requires Gemini provider. Please use a Gemini config.") diff --git a/nodes/json_parse.py b/nodes/json_parse.py new file mode 100644 index 0000000..9fa373a --- /dev/null +++ b/nodes/json_parse.py @@ -0,0 +1,169 @@ +""" +JSON parsing node for SimpleChat. + +Parses a JSON string (optionally wrapped in ```json code fences) and extracts +up to 8 fields via simple dot/bracket paths. +""" + +from __future__ import annotations + +import json +import re +from typing import Any + + +_CODE_FENCE_RE = re.compile(r"```(?:json)?\s*([\s\S]*?)\s*```", re.IGNORECASE) + + +def _strip_code_fence(text: str) -> str: + if not isinstance(text, str): + return text + m = _CODE_FENCE_RE.search(text) + return m.group(1) if m else text + + +def _tokenize_path(path: str) -> list[Any]: + """ + Tokenize a simple path like: + a.b[0].c + items[0]['name'] + items[0][\"name\"] + + Returns tokens: ["a","b",0,"c"] ... + """ + if not isinstance(path, str): + return [] + s = path.strip() + if not s: + return [] + + tokens: list[Any] = [] + i = 0 + n = len(s) + while i < n: + ch = s[i] + if ch == ".": + i += 1 + continue + + if ch == "[": + j = s.find("]", i + 1) + if j == -1: + # malformed, treat rest as a single token + tokens.append(s[i:]) + break + inner = s[i + 1 : j].strip() + # quoted key + if (inner.startswith('"') and inner.endswith('"')) or (inner.startswith("'") and inner.endswith("'")): + tokens.append(inner[1:-1]) + else: + try: + tokens.append(int(inner)) + except Exception: + tokens.append(inner) + i = j + 1 + continue + + # normal key token until '.' or '[' + j = i + while j < n and s[j] not in ".[": + j += 1 + key = s[i:j].strip() + if key: + tokens.append(key) + i = j + + return tokens + + +_MISSING = object() + + +def _get_by_tokens(obj: Any, tokens: list[Any]) -> Any: + cur = obj + for tok in tokens: + if isinstance(tok, int): + if isinstance(cur, (list, tuple)) and -len(cur) <= tok < len(cur): + cur = cur[tok] + else: + return _MISSING + else: + if isinstance(cur, dict) and tok in cur: + cur = cur[tok] + else: + return _MISSING + return cur + + +def _stringify(value: Any) -> str: + if value is None: + return "" + if isinstance(value, str): + return value + if isinstance(value, (int, float, bool)): + return str(value) + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) + + +class SimpleChatJsonParse: + """ + Parse a JSON string and split into multiple outputs (fixed 8 ports). + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_text": ("STRING", {"multiline": True, "default": "", "forceInput": True}), + "strip_code_fence": ("BOOLEAN", {"default": True}), + "default": ("STRING", {"default": ""}), + "path1": ("STRING", {"default": ""}), + "path2": ("STRING", {"default": ""}), + "path3": ("STRING", {"default": ""}), + "path4": ("STRING", {"default": ""}), + "path5": ("STRING", {"default": ""}), + "path6": ("STRING", {"default": ""}), + "path7": ("STRING", {"default": ""}), + "path8": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "SIMPLECHAT_JSON") + RETURN_NAMES = ("out1", "out2", "out3", "out4", "out5", "out6", "out7", "out8", "obj") + FUNCTION = "parse" + CATEGORY = "SimpleChat/Utils" + DESCRIPTION = "Parse JSON and extract up to 8 values by dot/bracket paths." + + def parse( + self, + json_text: str, + strip_code_fence: bool = True, + default: str = "", + path1: str = "", + path2: str = "", + path3: str = "", + path4: str = "", + path5: str = "", + path6: str = "", + path7: str = "", + path8: str = "", + ): + raw = _strip_code_fence(json_text) if strip_code_fence else json_text + data = json.loads(raw) if isinstance(raw, str) else raw + + paths = [path1, path2, path3, path4, path5, path6, path7, path8] + outs: list[str] = [] + for p in paths: + p = (p or "").strip() + if not p: + outs.append(default) + continue + tokens = _tokenize_path(p) + val = _get_by_tokens(data, tokens) + outs.append(default if val is _MISSING else _stringify(val)) + + return (*outs, data) + diff --git a/nodes/markdown_preview.py b/nodes/markdown_preview.py new file mode 100644 index 0000000..3da19ee --- /dev/null +++ b/nodes/markdown_preview.py @@ -0,0 +1,28 @@ +""" +Markdown preview node. + +Backend returns UI text; frontend JS renders markdown to HTML safely. +""" + +from __future__ import annotations + + +class SimpleChatMarkdownPreview: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"multiline": True, "default": "", "forceInput": True}), + } + } + + RETURN_TYPES = () + FUNCTION = "preview" + OUTPUT_NODE = True + CATEGORY = "SimpleChat/Utils" + DESCRIPTION = "Render input text as Markdown (frontend-rendered)." + + def preview(self, text: str): + # Use the standard 'ui.text' channel (same shape as PreviewAny) + return {"ui": {"text": (text or "",)}} + diff --git a/nodes/mustache_render.py b/nodes/mustache_render.py new file mode 100644 index 0000000..b1579cb --- /dev/null +++ b/nodes/mustache_render.py @@ -0,0 +1,41 @@ +""" +Mustache text render node. + +This node renders {{var}} placeholders in arbitrary text using SIMPLECHAT_VARS +without requiring any SimpleChat "main" chat/image nodes. +""" + +from __future__ import annotations + +from typing import Any + +from ..core.template import render_mustache + + +class SimpleChatMustacheRender: + """ + Render a text template with {{var}} placeholders. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"multiline": True, "default": "", "forceInput": True}), + }, + "optional": { + # Accept either vars dict (SIMPLECHAT_VARS) or a parsed JSON object (SIMPLECHAT_JSON) + "vars": ("SIMPLECHAT_VARS,SIMPLECHAT_JSON",), + "keep_unmatched": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "render" + CATEGORY = "SimpleChat/Utils" + DESCRIPTION = "Render {{var}} placeholders in text using SIMPLECHAT_VARS." + + def render(self, text: str, vars: Any = None, keep_unmatched: bool = True): + return (render_mustache(text, vars, keep_unmatched=keep_unmatched),) + diff --git a/nodes/mustache_var.py b/nodes/mustache_var.py new file mode 100644 index 0000000..55cd709 --- /dev/null +++ b/nodes/mustache_var.py @@ -0,0 +1,48 @@ +""" +Mustache variables node - define {{var}} values. +""" + +from __future__ import annotations + +from typing import Any + + +class SimpleChatMustacheVar: + """ + Define a single Mustache variable and output a merged vars dict. + + Usage: + - Set name/value in this node + - Connect output to other SimpleChat nodes via optional `vars` input + - Use {{name}} anywhere in prompts to inject the value + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "name": ("STRING", {"default": "自定义胡子变量"}), + "value": ("STRING", {"multiline": True, "default": ""}), + }, + "optional": { + "vars_in": ("SIMPLECHAT_VARS",), + }, + } + + RETURN_TYPES = ("SIMPLECHAT_VARS",) + RETURN_NAMES = ("vars",) + FUNCTION = "set_var" + CATEGORY = "SimpleChat/Utils" + DESCRIPTION = "Define a {{var}} value for prompt templating." + + def set_var(self, name: str, value: str, vars_in: Any = None): + merged = {} + if isinstance(vars_in, dict): + merged.update(vars_in) + + key = (name or "").strip() + if key: + merged[key] = value + + return (merged,) + diff --git a/nodes/prompt_json_unpack.py b/nodes/prompt_json_unpack.py new file mode 100644 index 0000000..0d2cbdb --- /dev/null +++ b/nodes/prompt_json_unpack.py @@ -0,0 +1,247 @@ +""" +Prompt JSON unpacker (Anima-oriented schema). + +This node is designed to take an LLM output that is *one JSON object only*: +{ + "positive": "...", + "negative": "...", + "width": 1024, + "height": 1024, + "steps": 40, + "cfg": 4.5, + "sampler": "er_sde", + "seed": -1, + "notes": "..." +} + +And split it into typed ComfyUI outputs, plus a SIMPLECHAT_VARS dict for +Mustache templating in other SimpleChat nodes. +""" + +from __future__ import annotations + +import json +import re +from typing import Any + + +_CODE_FENCE_RE = re.compile(r"```(?:json)?\s*([\s\S]*?)\s*```", re.IGNORECASE) + + +def _strip_code_fence(text: str) -> str: + if not isinstance(text, str): + return text + m = _CODE_FENCE_RE.search(text) + return m.group(1) if m else text + + +def _try_parse_json(text: str) -> Any: + """ + Try strict JSON first; if it fails, apply a minimal cleanup and retry. + """ + try: + return json.loads(text) + except Exception: + # Common LLM mistake: trailing commas + cleaned = re.sub(r",\s*([}\]])", r"\1", text) + return json.loads(cleaned) + + +def _as_int(value: Any, default: int) -> int: + if value is None: + return default + if isinstance(value, bool): + return int(value) + if isinstance(value, int): + return value + if isinstance(value, float): + return int(round(value)) + if isinstance(value, str): + s = value.strip() + if not s: + return default + try: + return int(s) + except Exception: + try: + return int(float(s)) + except Exception: + return default + return default + + +def _as_float(value: Any, default: float) -> float: + if value is None: + return default + if isinstance(value, bool): + return float(int(value)) + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + s = value.strip() + if not s: + return default + try: + return float(s) + except Exception: + return default + return default + + +def _as_str(value: Any, default: str = "") -> str: + if value is None: + return default + if isinstance(value, str): + return value + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) + + +_SAMPLER_ALIASES = { + # community shorthand -> ComfyUI names + "euler_a": "euler_ancestral", + "euler-ancestral": "euler_ancestral", + "euler ancestral": "euler_ancestral", +} + + +def _normalize_sampler(name: str, default: str) -> str: + if not isinstance(name, str): + return default + s = name.strip() + if not s: + return default + low = s.lower() + return _SAMPLER_ALIASES.get(low, low) + + +class SimpleChatPromptJsonUnpack: + """ + Unpack a fixed-schema JSON (positive/negative/params/notes) into typed outputs. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "json_text": ("STRING", {"multiline": True, "default": "", "forceInput": True}), + }, + "optional": { + "strip_code_fence": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ( + "STRING", # positive + "STRING", # negative + "INT", # width + "INT", # height + "INT", # steps + "FLOAT", # cfg + "SAMPLER", # sampler + "INT", # seed + "STRING", # notes + "SIMPLECHAT_VARS", # vars + "SIMPLECHAT_JSON", # obj + "STRING", # sampler_name (string) + ) + RETURN_NAMES = ( + "positive", + "negative", + "width", + "height", + "steps", + "cfg", + "sampler", + "seed", + "notes", + "vars", + "obj", + "sampler_name", + ) + FUNCTION = "unpack" + CATEGORY = "SimpleChat/Utils" + DESCRIPTION = "Unpack an LLM-produced JSON into ComfyUI-typed fields + Mustache vars." + + def unpack(self, json_text: str, strip_code_fence: bool = True): + raw = _strip_code_fence(json_text) if strip_code_fence else json_text + data = _try_parse_json(raw) if isinstance(raw, str) else raw + if not isinstance(data, dict): + raise ValueError("Expected a JSON object (dict) at top-level.") + + positive = _as_str(data.get("positive", ""), "") + negative = _as_str(data.get("negative", ""), "") + + width = _as_int(data.get("width"), 1024) + height = _as_int(data.get("height"), 1024) + steps = _as_int(data.get("steps"), 40) + cfg = _as_float(data.get("cfg"), 4.5) + + sampler_name = _normalize_sampler(_as_str(data.get("sampler", ""), ""), "er_sde") + seed = _as_int(data.get("seed"), -1) + notes = _as_str(data.get("notes", ""), "") + + vars_dict = { + # English keys (match JSON schema) + "positive": positive, + "negative": negative, + "width": width, + "height": height, + "steps": steps, + "cfg": cfg, + "sampler": sampler_name, + "sampler_name": sampler_name, + "seed": seed, + "notes": notes, + # Prefixed scheme keys (avoid collisions) + "anima.positive": positive, + "anima.negative": negative, + "anima.width": width, + "anima.height": height, + "anima.steps": steps, + "anima.cfg": cfg, + "anima.sampler": sampler_name, + "anima.sampler_name": sampler_name, + "anima.seed": seed, + "anima.notes": notes, + # Chinese aliases (optional convenience) + "正面提示词": positive, + "负面提示词": negative, + "宽": width, + "高": height, + "步数": steps, + "采样器": sampler_name, + "种子": seed, + "备注": notes, + # Chinese aliases (prefixed) + "anima.正面提示词": positive, + "anima.负面提示词": negative, + "anima.宽": width, + "anima.高": height, + "anima.步数": steps, + "anima.采样器": sampler_name, + "anima.种子": seed, + "anima.备注": notes, + } + + # Build an actual sampler object for SAMPLER output (for custom-sampling graphs) + import comfy.samplers + sampler_obj = comfy.samplers.sampler_object(sampler_name) + + return ( + positive, + negative, + width, + height, + steps, + cfg, + sampler_obj, + seed, + notes, + vars_dict, + data, + sampler_name, + ) + diff --git a/nodes/text_input.py b/nodes/text_input.py new file mode 100644 index 0000000..5ebd7e8 --- /dev/null +++ b/nodes/text_input.py @@ -0,0 +1,27 @@ +""" +Simple text input node. + +Provides a standalone multiline text box and outputs a STRING. +""" + +from __future__ import annotations + + +class SimpleChatTextInput: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"multiline": True, "default": ""}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "output" + CATEGORY = "SimpleChat/Utils" + DESCRIPTION = "Standalone multiline text input." + + def output(self, text: str): + return (text,) + diff --git a/web/markdown_preview.js b/web/markdown_preview.js new file mode 100644 index 0000000..466b0e7 --- /dev/null +++ b/web/markdown_preview.js @@ -0,0 +1,187 @@ +import { app } from "../../scripts/app.js"; + +const EXT_NAME = "ComfyUI.SimpleChat.MarkdownPreview"; +const STYLE_ID = "simplechat-markdown-preview-style"; + +function chainCallback(object, property, callback) { + if (!object) return; + if (property in object && object[property]) { + const callback_orig = object[property]; + object[property] = function () { + const r = callback_orig.apply(this, arguments); + return callback.apply(this, arguments) ?? r; + }; + } else { + object[property] = callback; + } +} + +function ensureStyles() { + if (document.getElementById(STYLE_ID)) return; + const style = document.createElement("style"); + style.id = STYLE_ID; + style.type = "text/css"; + style.innerHTML = ` + .simplechat-md-modal { + position: fixed; + right: 18px; + top: 18px; + width: min(720px, calc(100vw - 36px)); + height: min(70vh, 760px); + background: var(--comfy-menu-bg); + color: var(--fg-color); + border: 1px solid var(--border-color); + border-radius: 12px; + z-index: 9999; + box-shadow: 0 16px 40px rgba(0,0,0,0.35); + overflow: hidden; + display: flex; + flex-direction: column; + } + .simplechat-md-modal header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border-bottom: 1px solid var(--border-color); + background: rgba(0,0,0,0.08); + font: 12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + } + .simplechat-md-modal header .title { + opacity: 0.9; + } + .simplechat-md-modal header .actions { + display: flex; + gap: 8px; + align-items: center; + } + .simplechat-md-modal header button { + border: 1px solid var(--border-color); + background: transparent; + color: var(--fg-color); + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + font-size: 12px; + } + .simplechat-md-modal header button:hover { + background: rgba(255,255,255,0.06); + } + .simplechat-md-body { + padding: 12px; + overflow: auto; + } + .simplechat-md-body pre, .simplechat-md-body code { + background: rgba(0,0,0,0.16); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 8px; + padding: 8px 10px; + overflow: auto; + } + .simplechat-md-body a { + color: #ffd166; + } + .simplechat-md-body a:visited { + color: #f4a261; + } + `; + document.head.appendChild(style); +} + +function loadScript(url) { + return new Promise((resolve, reject) => { + const existing = document.querySelector(`script[src="${url}"]`); + if (existing) return resolve(true); + const el = document.createElement("script"); + el.async = true; + el.type = "text/javascript"; + el.src = url; + el.addEventListener("load", () => resolve(true)); + el.addEventListener("error", () => reject(new Error(`Failed to load ${url}`))); + document.body.appendChild(el); + }); +} + +let _libsPromise = null; +async function ensureMarkdownLibs() { + if (window.marked && window.DOMPurify) return; + if (_libsPromise) return _libsPromise; + const markedUrl = new URL("./vendor/marked.min.js", import.meta.url).href; + const purifyUrl = new URL("./vendor/purify.min.js", import.meta.url).href; + _libsPromise = (async () => { + await loadScript(markedUrl); + await loadScript(purifyUrl); + })(); + return _libsPromise; +} + +function escapeHtml(text) { + return (text || "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +let modalEl = null; +function showMarkdown(rawText, title = "Markdown Preview") { + ensureStyles(); + + if (!modalEl) { + modalEl = document.createElement("div"); + modalEl.className = "simplechat-md-modal"; + modalEl.innerHTML = ` +
+
+
+ + +
+
+
+ `; + document.body.appendChild(modalEl); + modalEl.querySelector('button[data-action="close"]').addEventListener("click", () => { + modalEl.remove(); + modalEl = null; + }); + modalEl.querySelector('button[data-action="copy"]').addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(modalEl.__rawText || ""); + } catch (e) { + console.warn("Clipboard copy failed", e); + } + }); + } + + modalEl.__rawText = rawText || ""; + modalEl.querySelector(".title").textContent = title; + + const body = modalEl.querySelector(".simplechat-md-body"); + const hasLibs = window.marked && window.DOMPurify; + if (hasLibs) { + const html = window.DOMPurify.sanitize(window.marked.parse(rawText || "")); + body.innerHTML = html; + } else { + body.innerHTML = `
${escapeHtml(rawText || "")}
`; + } +} + +app.registerExtension({ + name: EXT_NAME, + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== "SimpleChatMarkdownPreview") return; + + // Ensure libraries are loaded once; if it fails, we fall back to plain text + ensureMarkdownLibs().catch((e) => console.warn("[SimpleChat] markdown libs load failed:", e)); + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, [message]); + const text = (message?.text && message.text.length) ? message.text[0] : ""; + showMarkdown(text, "SimpleChat Markdown Preview"); + }; + }, +}); + diff --git a/web/vendor/marked.min.js b/web/vendor/marked.min.js new file mode 100644 index 0000000..cdd1109 --- /dev/null +++ b/web/vendor/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v12.0.1 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=x(t[0].replace(/^ *>[ \t]?/gm,""),"\n"),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,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:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const 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){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.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){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="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",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\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",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).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:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=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(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
'+(n?e:c(e,!0))+"
\n":"
"+(n?e:c(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); diff --git a/web/vendor/purify.min.js b/web/vendor/purify.min.js new file mode 100644 index 0000000..c2f5164 --- /dev/null +++ b/web/vendor/purify.min.js @@ -0,0 +1,3 @@ +/*! @license DOMPurify 3.0.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.11/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),A=b(RegExp.prototype.test),_=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=function(){return"undefined"==typeof window?null:window},Z=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var J=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:V();const o=e=>t(e);if(o.version="3.0.11",o.removed=[],!n||!n.document||9!==n.document.nodeType)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=L(j,"cloneNode"),$=L(j,"nextSibling"),J=L(j,"childNodes"),Q=L(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ee,te="";const{implementation:ne,createNodeIterator:oe,createDocumentFragment:re,getElementsByTagName:ie}=r,{importNode:ae}=a;let le={};o.isSupported="function"==typeof e&&"function"==typeof Q&&ne&&void 0!==ne.createHTMLDocument;const{MUSTACHE_EXPR:ce,ERB_EXPR:se,TMPLIT_EXPR:ue,DATA_ATTR:me,ARIA_ATTR:pe,IS_SCRIPT_OR_DATA:fe,ATTR_WHITESPACE:de,CUSTOM_ELEMENT:he}=K;let{IS_ALLOWED_URI:ge}=K,Te=null;const ye=S({},[...D,...C,...O,...v,...M]);let Ee=null;const Ae=S({},[...I,...U,...P,...F]);let _e=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ne=null,be=null,Se=!0,Re=!0,we=!1,Le=!0,De=!1,Ce=!0,Oe=!1,xe=!1,ve=!1,ke=!1,Me=!1,Ie=!1,Ue=!0,Pe=!1;const Fe="user-content-";let He=!0,ze=!1,Be={},We=null;const Ge=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ye=null;const je=S({},["audio","video","img","source","image","track"]);let Xe=null;const qe=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),$e="http://www.w3.org/1998/Math/MathML",Ke="http://www.w3.org/2000/svg",Ve="http://www.w3.org/1999/xhtml";let Ze=Ve,Je=!1,Qe=null;const et=S({},[$e,Ke,Ve],d);let tt=null;const nt=["application/xhtml+xml","text/html"],ot="text/html";let rt=null,it=null;const at=r.createElement("form"),lt=function(e){return e instanceof RegExp||e instanceof Function},ct=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!it||it!==e){if(e&&"object"==typeof e||(e={}),e=w(e),tt=-1===nt.indexOf(e.PARSER_MEDIA_TYPE)?ot:e.PARSER_MEDIA_TYPE,rt="application/xhtml+xml"===tt?d:f,Te=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,rt):ye,Ee=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,rt):Ae,Qe=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):et,Xe=E(e,"ADD_URI_SAFE_ATTR")?S(w(qe),e.ADD_URI_SAFE_ATTR,rt):qe,Ye=E(e,"ADD_DATA_URI_TAGS")?S(w(je),e.ADD_DATA_URI_TAGS,rt):je,We=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,rt):Ge,Ne=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,rt):{},be=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,rt):{},Be=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,Se=!1!==e.ALLOW_ARIA_ATTR,Re=!1!==e.ALLOW_DATA_ATTR,we=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Le=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,De=e.SAFE_FOR_TEMPLATES||!1,Ce=!1!==e.SAFE_FOR_XML,Oe=e.WHOLE_DOCUMENT||!1,ke=e.RETURN_DOM||!1,Me=e.RETURN_DOM_FRAGMENT||!1,Ie=e.RETURN_TRUSTED_TYPE||!1,ve=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,Pe=e.SANITIZE_NAMED_PROPS||!1,He=!1!==e.KEEP_CONTENT,ze=e.IN_PLACE||!1,ge=e.ALLOWED_URI_REGEXP||Y,Ze=e.NAMESPACE||Ve,_e=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(_e.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(_e.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(_e.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),De&&(Re=!1),Me&&(ke=!0),Be&&(Te=S({},M),Ee=[],!0===Be.html&&(S(Te,D),S(Ee,I)),!0===Be.svg&&(S(Te,C),S(Ee,U),S(Ee,F)),!0===Be.svgFilters&&(S(Te,O),S(Ee,U),S(Ee,F)),!0===Be.mathMl&&(S(Te,v),S(Ee,P),S(Ee,F))),e.ADD_TAGS&&(Te===ye&&(Te=w(Te)),S(Te,e.ADD_TAGS,rt)),e.ADD_ATTR&&(Ee===Ae&&(Ee=w(Ee)),S(Ee,e.ADD_ATTR,rt)),e.ADD_URI_SAFE_ATTR&&S(Xe,e.ADD_URI_SAFE_ATTR,rt),e.FORBID_CONTENTS&&(We===Ge&&(We=w(We)),S(We,e.FORBID_CONTENTS,rt)),He&&(Te["#text"]=!0),Oe&&S(Te,["html","head","body"]),Te.table&&(S(Te,["tbody"]),delete Ne.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ee=e.TRUSTED_TYPES_POLICY,te=ee.createHTML("")}else void 0===ee&&(ee=Z(G,c)),null!==ee&&"string"==typeof te&&(te=ee.createHTML(""));i&&i(e),it=e}},st=S({},["mi","mo","mn","ms","mtext"]),ut=S({},["foreignobject","desc","title","annotation-xml"]),mt=S({},["title","style","font","a","script"]),pt=S({},[...C,...O,...x]),ft=S({},[...v,...k]),dt=function(e){let t=Q(e);t&&t.tagName||(t={namespaceURI:Ze,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!Qe[e.namespaceURI]&&(e.namespaceURI===Ke?t.namespaceURI===Ve?"svg"===n:t.namespaceURI===$e?"svg"===n&&("annotation-xml"===o||st[o]):Boolean(pt[n]):e.namespaceURI===$e?t.namespaceURI===Ve?"math"===n:t.namespaceURI===Ke?"math"===n&&ut[o]:Boolean(ft[n]):e.namespaceURI===Ve?!(t.namespaceURI===Ke&&!ut[o])&&(!(t.namespaceURI===$e&&!st[o])&&(!ft[n]&&(mt[n]||!pt[n]))):!("application/xhtml+xml"!==tt||!Qe[e.namespaceURI]))},ht=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},gt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(ke||Me)try{ht(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Tt=function(e){let t=null,n=null;if(ve)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===tt&&Ze===Ve&&(e=''+e+"");const o=ee?ee.createHTML(e):e;if(Ze===Ve)try{t=(new W).parseFromString(o,tt)}catch(e){}if(!t||!t.documentElement){t=ne.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?te:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Ze===Ve?ie.call(t,Oe?"html":"body")[0]:Oe?t.documentElement:i},yt=function(e){return oe.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},Et=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},At=function(e){return"function"==typeof b&&e instanceof b},_t=function(e,t,n){le[e]&&u(le[e],(e=>{e.call(o,t,n,it)}))},Nt=function(e){let t=null;if(_t("beforeSanitizeElements",e,null),Et(e))return ht(e),!0;const n=rt(e.nodeName);if(_t("uponSanitizeElement",e,{tagName:n,allowedTags:Te}),e.hasChildNodes()&&!At(e.firstElementChild)&&A(/<[/\w]/g,e.innerHTML)&&A(/<[/\w]/g,e.textContent))return ht(e),!0;if(7===e.nodeType)return ht(e),!0;if(Ce&&8===e.nodeType&&A(/<[/\w]/g,e.data))return ht(e),!0;if(!Te[n]||Ne[n]){if(!Ne[n]&&St(n)){if(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n))return!1;if(_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))return!1}if(He&&!We[n]){const t=Q(e)||e.parentNode,n=J(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o)t.insertBefore(X(n[o],!0),$(e))}}return ht(e),!0}return e instanceof R&&!dt(e)?(ht(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!A(/<\/no(script|embed|frames)/i,e.innerHTML)?(De&&3===e.nodeType&&(t=e.textContent,u([ce,se,ue],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),_t("afterSanitizeElements",e,null),!1):(ht(e),!0)},bt=function(e,t,n){if(Ue&&("id"===t||"name"===t)&&(n in r||n in at))return!1;if(Re&&!be[t]&&A(me,t));else if(Se&&A(pe,t));else if(!Ee[t]||be[t]){if(!(St(e)&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,e)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(e))&&(_e.attributeNameCheck instanceof RegExp&&A(_e.attributeNameCheck,t)||_e.attributeNameCheck instanceof Function&&_e.attributeNameCheck(t))||"is"===t&&_e.allowCustomizedBuiltInElements&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))))return!1}else if(Xe[t]);else if(A(ge,g(n,de,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ye[e]){if(we&&!A(fe,g(n,de,"")));else if(n)return!1}else;return!0},St=function(e){return"annotation-xml"!==e&&h(e,he)},Rt=function(e){_t("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=rt(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,_t("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(gt(a,e),!n.keepAttr)continue;if(!Le&&A(/\/>/i,p)){gt(a,e);continue}De&&u([ce,se,ue],(e=>{p=g(p,e," ")}));const f=rt(e.nodeName);if(bt(f,s,p)){if(!Pe||"id"!==s&&"name"!==s||(gt(a,e),p=Fe+p),ee&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ee.createHTML(p);break;case"TrustedScriptURL":p=ee.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),m(o.removed)}catch(e){}}}_t("afterSanitizeAttributes",e,null)},wt=function e(t){let n=null;const o=yt(t);for(_t("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)_t("uponSanitizeShadowNode",n,null),Nt(n)||(n.content instanceof s&&e(n.content),Rt(n));_t("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(Je=!e,Je&&(e="\x3c!--\x3e"),"string"!=typeof e&&!At(e)){if("function"!=typeof e.toString)throw _("toString is not a function");if("string"!=typeof(e=e.toString()))throw _("dirty is not a string, aborting")}if(!o.isSupported)return e;if(xe||ct(t),o.removed=[],"string"==typeof e&&(ze=!1),ze){if(e.nodeName){const t=rt(e.nodeName);if(!Te[t]||Ne[t])throw _("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Tt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!ke&&!De&&!Oe&&-1===e.indexOf("<"))return ee&&Ie?ee.createHTML(e):e;if(n=Tt(e),!n)return ke?null:Ie?te:""}n&&ve&&ht(n.firstChild);const c=yt(ze?e:n);for(;i=c.nextNode();)Nt(i)||(i.content instanceof s&&wt(i.content),Rt(i));if(ze)return e;if(ke){if(Me)for(l=re.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Ee.shadowroot||Ee.shadowrootmode)&&(l=ae.call(a,l,!0)),l}let m=Oe?n.outerHTML:n.innerHTML;return Oe&&Te["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&A(q,n.ownerDocument.doctype.name)&&(m="\n"+m),De&&u([ce,se,ue],(e=>{m=g(m,e," ")})),ee&&Ie?ee.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ct(e),xe=!0},o.clearConfig=function(){it=null,xe=!1},o.isValidAttribute=function(e,t,n){it||ct({});const o=rt(e),r=rt(t);return bt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(le[e]=le[e]||[],p(le[e],t))},o.removeHook=function(e){if(le[e])return m(le[e])},o.removeHooks=function(e){le[e]&&(le[e]=[])},o.removeAllHooks=function(){le={}},o}();return J})); +//# sourceMappingURL=purify.min.js.map