From 7ef9689e087409e00dbb362ff8bba35989261399 Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Thu, 5 Nov 2026 17:26:47 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix(triage):=20=E5=BF=BD=E7=95=A5=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=A4=BA=E4=BE=8B=E4=B8=AD=E7=9A=84=E5=88=86=E8=AF=8A?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #343 --- .../test_prepare_issue_triage_context.py | 56 ++++++++ .../test_review_workflow_dispatch.py | 11 ++ .../scripts/prepare_issue_triage_context.py | 132 ++++++++++++++++-- .github/workflows/triage-issue.yml | 12 ++ 4 files changed, 197 insertions(+), 14 deletions(-) diff --git a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py index 4f2a24b..7cd701a 100644 --- a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py +++ b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py @@ -157,6 +157,62 @@ def test_should_not_run_for_triage_command_inside_fenced_code(self) -> None: (False, "issue comment did not contain @codex /triage command"), ) + def test_triage_command_ignores_code_examples(self) -> None: + code_only = ( + "~~~\n@codex /triage\n~~~", + "~~~~log\n@codex /triage\n~~~~", + "````\n@codex /triage\n```\n@codex /triage\n````", + "~~~\n```\n@codex /triage\n```\n~~~", + " @codex /triage", + "\t@codex /triage", + "Intro\n\n @codex /triage", + " ~~~\n @codex /triage\n ~~~", + "- ```\n @codex /triage\n ```", + "1. ~~~\n @codex /triage\n ~~~", + "- item\n\n @codex /triage", + "`@codex /triage`", + "\\@codex /triage", + "
\n
\n@codex /triage\n
\n
", + "
\n\n
", + "
\n```\n@codex /triage\n```\n
", + "
\n
\n@codex /triage\n
\n
", + "
\n@codex /triage\n
", + "
\n
\n\n@codex /triage\n
\n
", + "
\n\n\n@codex /triage\n\n
", + "- - ```\n @codex /triage\n ```", + "`example\n@codex /triage\ncontinued`", + ) + for comment in code_only: + with self.subTest(comment=comment): + self.assertFalse(prepare.comment_has_triage_command(comment, "codex")) + + def test_triage_command_runs_after_code_examples(self) -> None: + for comment in ( + "~~~\n@codex /triage example\n~~~\n@codex /triage real request", + "````\n@codex /triage example\n````\n@codex /triage", + " @codex /triage example\n@codex /triage", + "Please\n @codex /triage", + "- item\n @codex /triage", + "- item\n\n @codex /triage", + "- @codex /triage", + "1. @codex /triage", + "- ```\n @codex /triage example\n ```\n@codex /triage", + "
\n@codex /triage\n
", + "
\n
\n@codex /triage example\n
\n@codex /triage\n
", + "
\n```\n@codex /triage example\n```\n@codex /triage\n
", + "
\n@codex /triage please now\n
", + "- - @codex /triage", + "1. - @codex /triage", + "@codex /triage\n`example\ncontinued`", + "`example\ncontinued`\n@codex /triage", + "prefix `example\ncontinued`\n@codex /triage", + "[link](https://e.test/`a`)\n`code\nend`\n@codex /triage", + "[link](https://e.test/`code end`)\n`code\nend`\n@codex /triage", + "
\n\n\n@codex /triage example\n\n@codex /triage\n
", + ): + with self.subTest(comment=comment): + self.assertTrue(prepare.comment_has_triage_command(comment, "codex")) + def test_should_not_run_for_pull_request_comment(self) -> None: event = { "action": "created", diff --git a/.github/aicodingflow-tests/test_review_workflow_dispatch.py b/.github/aicodingflow-tests/test_review_workflow_dispatch.py index 6e9e713..bbaa019 100644 --- a/.github/aicodingflow-tests/test_review_workflow_dispatch.py +++ b/.github/aicodingflow-tests/test_review_workflow_dispatch.py @@ -347,6 +347,17 @@ def test_triage_workflow_allows_regular_issue_authors_through_preflight(self) -> data = workflow(".github/workflows/triage-issue.yml") triggers = data[True] + triage_steps = steps(data, "generate") + parser_setup = next(step for step in triage_steps if step.get("name") == "set-up-python-for-markdown-parser") + parser_install = next(step for step in triage_steps if step.get("name") == "install-markdown-parser") + self.assertEqual(parser_setup["uses"], "actions/setup-python@v6") + self.assertIn("-r .github/scripts/requirements-spec-context.txt", parser_install["run"]) + self.assertLess(triage_steps.index(parser_setup), triage_steps.index(parser_install)) + self.assertLess( + triage_steps.index(parser_install), + next(index for index, step in enumerate(triage_steps) if step.get("name") == "prepare-triage-context"), + ) + self.assertEqual(triggers["issues"]["types"], ["opened", "reopened"]) self.assertEqual(triggers["issue_comment"]["types"], ["created"]) diff --git a/.github/scripts/prepare_issue_triage_context.py b/.github/scripts/prepare_issue_triage_context.py index 357a2a0..ff257ff 100644 --- a/.github/scripts/prepare_issue_triage_context.py +++ b/.github/scripts/prepare_issue_triage_context.py @@ -5,7 +5,9 @@ import argparse import os +import re from datetime import datetime, timedelta, timezone +from html.parser import HTMLParser from pathlib import Path from typing import Any @@ -27,6 +29,55 @@ NEEDS_INFO_LABEL = "needs-info" +class _VisibleTriageHtml(HTMLParser): + HIDDEN_TAGS = {"code", "pre", "blockquote", "script", "style"} + + def __init__(self) -> None: + super().__init__() + self.hidden_tags: list[str] = [] + self.text: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag in self.HIDDEN_TAGS: + self.hidden_tags.append(tag) + + def handle_endtag(self, tag: str) -> None: + if self.hidden_tags and self.hidden_tags[-1] == tag: + self.hidden_tags.pop() + + def handle_data(self, data: str) -> None: + if not self.hidden_tags: + self.text.append(data) + + +def _html_block_has_triage_command(content: str, expected: str, html: _VisibleTriageHtml) -> bool: + fence_char = "" + fence_length = 0 + for line in content.splitlines(keepends=True): + previous_text_count = len(html.text) + html.feed(line) + stripped = line.strip() + visible = "".join(html.text[previous_text_count:]).strip() + if not visible: + continue + if fence_char: + if visible == stripped and re.fullmatch( + re.escape(fence_char) + "{" + str(fence_length) + r",}", stripped + ): + fence_char = "" + continue + opening = re.match(r"(`{3,}|~{3,})(.*)$", stripped) + if visible == stripped and opening and (opening.group(1)[0] == "~" or "`" not in opening.group(2)): + fence_char = opening.group(1)[0] + fence_length = len(opening.group(1)) + continue + if (stripped == expected or stripped.startswith(f"{expected} ")) and ( + visible == expected or visible.startswith(f"{expected} ") + ): + return True + return False + + def load_event(path: str | None) -> dict[str, Any]: return load_json(path, default={}) @@ -59,21 +110,74 @@ def comment_has_triage_command(comment: object, login: str) -> bool: if not isinstance(comment, str) or not comment.strip() or not login.strip(): return False - expected = f"@{login.strip()} /triage" - in_fenced_code = False - for line in comment.splitlines(): - stripped_left = line.lstrip() - if stripped_left.startswith(">"): - continue - if stripped_left.startswith("```"): - in_fenced_code = not in_fenced_code - continue - if in_fenced_code: - continue + from markdown_it import MarkdownIt + from markdown_it.rules_inline.backticks import backtick - stripped = line.strip() - if stripped == expected or stripped.startswith(f"{expected} "): - return True + markdown = MarkdownIt("commonmark") + + def backtick_with_source_lines(state: Any, silent: bool) -> bool: + start = state.pos + previous_tokens = len(state.tokens) + parsed = backtick(state, silent) + if not silent and len(state.tokens) > previous_tokens and state.tokens[-1].type == "code_inline": + state.tokens[-1].meta["source_newlines"] = state.src[start : state.pos].count("\n") + return parsed + + markdown.inline.ruler.at("backticks", backtick_with_source_lines) + + expected = f"@{login.strip()} /triage" + source_lines = comment.splitlines() + quote_depth = 0 + list_depth = 0 + html = _VisibleTriageHtml() + for block in markdown.parse(comment): + if block.type == "blockquote_open": + quote_depth += 1 + elif block.type == "blockquote_close": + quote_depth -= 1 + elif block.type in {"bullet_list_open", "ordered_list_open"}: + list_depth += 1 + elif block.type in {"bullet_list_close", "ordered_list_close"}: + list_depth -= 1 + elif block.type == "html_block" and not quote_depth: + if _html_block_has_triage_command(block.content, expected, html): + return True + elif block.type == "inline" and not quote_depth and block.map: + visible_lines = [""] + safe_line_count = None + for inline in block.children or []: + if inline.type == "text": + parts = inline.content.split("\n") + visible_lines[-1] += parts[0] if not html.hidden_tags else "\0" + visible_lines.extend(parts[1:] if not html.hidden_tags else ["\0"] * (len(parts) - 1)) + elif inline.type in {"softbreak", "hardbreak"}: + visible_lines.append("") + elif inline.type == "html_inline": + html.feed(inline.content) + visible_lines[-1] += "\0" + visible_lines.extend(["\0"] * inline.content.count("\n")) + elif inline.type == "code_inline": + newlines = inline.meta.get("source_newlines") + if newlines is None and safe_line_count is None: + safe_line_count = len(visible_lines) + visible_lines[-1] += "\0" + if newlines is not None: + visible_lines.extend(["\0"] * newlines) + else: + visible_lines[-1] += "\0" + + paragraph_lines = source_lines[block.map[0] : block.map[1]] + line_count = min(len(visible_lines), len(paragraph_lines)) + if len(visible_lines) != len(paragraph_lines): + line_count = min(line_count, safe_line_count or 0) + for source, visible in zip(paragraph_lines[:line_count], visible_lines[:line_count]): + source = source.strip() + for _ in range(list_depth): + source = re.sub(r"^(?:[-+*]|[0-9]{1,9}[.)])\s+", "", source, count=1) + if (source == expected or source.startswith(f"{expected} ")) and ( + visible.strip() == expected or visible.strip().startswith(f"{expected} ") + ): + return True return False diff --git a/.github/workflows/triage-issue.yml b/.github/workflows/triage-issue.yml index caa1f71..69cfe2c 100644 --- a/.github/workflows/triage-issue.yml +++ b/.github/workflows/triage-issue.yml @@ -67,6 +67,18 @@ jobs: ref: ${{ github.event.repository.default_branch }} persist-credentials: false + - name: set-up-python-for-markdown-parser + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: install-markdown-parser + run: | + python3 -m venv "$RUNNER_TEMP/aicodingflow-triage-venv" + "$RUNNER_TEMP/aicodingflow-triage-venv/bin/python" -m pip install \ + --disable-pip-version-check -r .github/scripts/requirements-spec-context.txt + echo "$RUNNER_TEMP/aicodingflow-triage-venv/bin" >> "$GITHUB_PATH" + - name: prepare-triage-context id: issue env: From e7bbe9c0b35d14cf7bad5392b4537d79384b77ec Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 6 Nov 2026 01:50:20 +0800 Subject: [PATCH 2/7] =?UTF-8?q?test(triage):=20=E8=A6=86=E7=9B=96=E8=B7=A8?= =?UTF-8?q?=20HTML=20=E5=9D=97=E7=9A=84=E8=84=9A=E6=9C=AC=E7=BB=93?= =?UTF-8?q?=E6=9D=9F=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #343 --- .../test_prepare_issue_triage_context.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py index 7cd701a..b6c27c1 100644 --- a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py +++ b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py @@ -213,6 +213,18 @@ def test_triage_command_runs_after_code_examples(self) -> None: with self.subTest(comment=comment): self.assertTrue(prepare.comment_has_triage_command(comment, "codex")) + def test_triage_command_tracks_script_and_style_across_html_blocks(self) -> None: + for tag in ("script", "style"): + example = f"
\n<{tag}>\n@codex /triage example\n\n" + with self.subTest(tag=tag, command_after=False): + self.assertFalse(prepare.comment_has_triage_command(f"{example}\n
", "codex")) + with self.subTest(tag=tag, command_after=True): + self.assertTrue( + prepare.comment_has_triage_command( + f"{example}\n\n@codex /triage\n", "codex" + ) + ) + def test_should_not_run_for_pull_request_comment(self) -> None: event = { "action": "created", From a2f08243b31e7657cf83c236b420e4f13b15ec1c Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 6 Nov 2026 01:56:26 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix(triage):=20=E5=8F=8A=E6=97=B6=E5=A4=84?= =?UTF-8?q?=E7=90=86=20HTML=20=E8=84=9A=E6=9C=AC=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #343 --- .github/scripts/prepare_issue_triage_context.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/scripts/prepare_issue_triage_context.py b/.github/scripts/prepare_issue_triage_context.py index ff257ff..7bee132 100644 --- a/.github/scripts/prepare_issue_triage_context.py +++ b/.github/scripts/prepare_issue_triage_context.py @@ -56,6 +56,12 @@ def _html_block_has_triage_command(content: str, expected: str, html: _VisibleTr for line in content.splitlines(keepends=True): previous_text_count = len(html.text) html.feed(line) + # Some Python versions defer parsing short feed() calls. Flush after a + # complete script/style close, preserving incomplete HTML across blocks. + if html.cdata_elem and re.search( + r"", line, re.IGNORECASE + ): + html.close() stripped = line.strip() visible = "".join(html.text[previous_text_count:]).strip() if not visible: From b13497b333cefe946c893a5a921a235f140bc160 Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 6 Nov 2026 02:09:48 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(triage):=20=E6=AD=A3=E7=A1=AE=E5=A4=84?= =?UTF-8?q?=E7=90=86=E8=B7=A8=E5=9D=97=20HTML=20=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #343 --- .../test_prepare_issue_triage_context.py | 23 +++++++ .../scripts/prepare_issue_triage_context.py | 61 ++++++++++++++++--- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py index b6c27c1..d4a6ea8 100644 --- a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py +++ b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py @@ -225,6 +225,29 @@ def test_triage_command_tracks_script_and_style_across_html_blocks(self) -> None ) ) + def test_triage_command_tracks_html_comments_across_blocks(self) -> None: + self.assertFalse( + prepare.comment_has_triage_command("
\n\n@codex /triage\n
", "codex" + ) + ) + self.assertTrue( + prepare.comment_has_triage_command( + "
\n\n\n@codex /triage\n
", "codex" + ) + ) + example_then_code = ( + "
\n \n" + "@codex /triage example\n\n" + ) + self.assertFalse(prepare.comment_has_triage_command(f"{example_then_code}
", "codex")) + self.assertTrue( + prepare.comment_has_triage_command(f"{example_then_code}@codex /triage\n", "codex") + ) + def test_should_not_run_for_pull_request_comment(self) -> None: event = { "action": "created", diff --git a/.github/scripts/prepare_issue_triage_context.py b/.github/scripts/prepare_issue_triage_context.py index 7bee132..567573c 100644 --- a/.github/scripts/prepare_issue_triage_context.py +++ b/.github/scripts/prepare_issue_triage_context.py @@ -35,8 +35,28 @@ class _VisibleTriageHtml(HTMLParser): def __init__(self) -> None: super().__init__() self.hidden_tags: list[str] = [] + self.comment_open = False self.text: list[str] = [] + def track_comments(self, source: str) -> bool: + position = 0 + closed = False + while position < len(source): + if self.comment_open: + end = source.find("-->", position) + if end < 0: + break + self.comment_open = False + closed = True + position = end + 3 + else: + start = source.find("") + html.feed(source_line[: end + 3] if end >= 0 else source_line + "\n") + comment_closed = html.track_comments(source_line) + if comment_closed: + html.close() + comment_hidden_lines.append(was_in_comment) + visible_lines = [""] safe_line_count = None for inline in block.children or []: if inline.type == "text": parts = inline.content.split("\n") - visible_lines[-1] += parts[0] if not html.hidden_tags else "\0" - visible_lines.extend(parts[1:] if not html.hidden_tags else ["\0"] * (len(parts) - 1)) + visible = not html.hidden_tags + visible_lines[-1] += parts[0] if visible else "\0" + visible_lines.extend(parts[1:] if visible else ["\0"] * (len(parts) - 1)) elif inline.type in {"softbreak", "hardbreak"}: visible_lines.append("") elif inline.type == "html_inline": html.feed(inline.content) + if not html.comment_open or "-->" in inline.content: + html.close() visible_lines[-1] += "\0" visible_lines.extend(["\0"] * inline.content.count("\n")) elif inline.type == "code_inline": @@ -172,11 +212,14 @@ def backtick_with_source_lines(state: Any, silent: bool) -> bool: else: visible_lines[-1] += "\0" - paragraph_lines = source_lines[block.map[0] : block.map[1]] line_count = min(len(visible_lines), len(paragraph_lines)) if len(visible_lines) != len(paragraph_lines): line_count = min(line_count, safe_line_count or 0) - for source, visible in zip(paragraph_lines[:line_count], visible_lines[:line_count]): + for source, visible, in_comment in zip( + paragraph_lines[:line_count], visible_lines[:line_count], comment_hidden_lines[:line_count] + ): + if in_comment: + continue source = source.strip() for _ in range(list_depth): source = re.sub(r"^(?:[-+*]|[0-9]{1,9}[.)])\s+", "", source, count=1) From 6602228f6556b67864589ace8601708fda03eac0 Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 6 Nov 2026 02:19:51 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix(triage):=20=E8=B7=B3=E8=BF=87=20HTML=20?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E6=A1=86=E4=B8=8E=E5=88=97=E8=A1=A8=E5=9B=B4?= =?UTF-8?q?=E6=A0=8F=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #343 --- .../test_prepare_issue_triage_context.py | 6 ++++++ .../scripts/prepare_issue_triage_context.py | 21 ++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py index d4a6ea8..52769cf 100644 --- a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py +++ b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py @@ -179,6 +179,10 @@ def test_triage_command_ignores_code_examples(self) -> None: "
\n@codex /triage\n
", "
\n
\n\n@codex /triage\n
\n
", "
\n\n\n@codex /triage\n\n
", + "
\n\n
", + "
\n\n
", + "
\n- ```\n @codex /triage\n ```\n
", + "
\n- - ~~~\n @codex /triage\n ~~~\n
", "- - ```\n @codex /triage\n ```", "`example\n@codex /triage\ncontinued`", ) @@ -209,6 +213,8 @@ def test_triage_command_runs_after_code_examples(self) -> None: "[link](https://e.test/`a`)\n`code\nend`\n@codex /triage", "[link](https://e.test/`code end`)\n`code\nend`\n@codex /triage", "
\n\n\n@codex /triage example\n\n@codex /triage\n
", + "
\n\n@codex /triage\n
", + "
\n- ```\n @codex /triage example\n ```\n@codex /triage\n
", ): with self.subTest(comment=comment): self.assertTrue(prepare.comment_has_triage_command(comment, "codex")) diff --git a/.github/scripts/prepare_issue_triage_context.py b/.github/scripts/prepare_issue_triage_context.py index 567573c..25d300e 100644 --- a/.github/scripts/prepare_issue_triage_context.py +++ b/.github/scripts/prepare_issue_triage_context.py @@ -30,7 +30,7 @@ class _VisibleTriageHtml(HTMLParser): - HIDDEN_TAGS = {"code", "pre", "blockquote", "script", "style"} + HIDDEN_TAGS = {"code", "pre", "blockquote", "script", "style", "textarea"} def __init__(self) -> None: super().__init__() @@ -79,25 +79,32 @@ def _html_block_has_triage_command(content: str, expected: str, html: _VisibleTr html.feed(line) # Some Python versions defer short feed() calls. Flush complete close # markers while preserving incomplete comments and tags across blocks. - cdata_closed = html.cdata_elem and re.search( - r"", line, re.IGNORECASE + hidden_tag_closed = html.hidden_tags and re.search( + r"", line, re.IGNORECASE ) - if comment_closed or cdata_closed: + if comment_closed or hidden_tag_closed: html.close() stripped = line.strip() + fence_source = stripped + while True: + remainder = re.sub(r"^(?:[-+*]|[0-9]{1,9}[.)])\s+", "", fence_source, count=1) + if remainder == fence_source: + break + fence_source = remainder + fence_marker = re.match(r"(`{3,}|~{3,})(.*)$", fence_source) candidate = stripped == expected or stripped.startswith(f"{expected} ") - if candidate and not html.comment_open: + if (candidate or fence_marker) and not html.comment_open: html.close() visible = "".join(html.text[previous_text_count:]).strip() if not visible: continue if fence_char: if visible == stripped and re.fullmatch( - re.escape(fence_char) + "{" + str(fence_length) + r",}", stripped + re.escape(fence_char) + "{" + str(fence_length) + r",}", fence_source ): fence_char = "" continue - opening = re.match(r"(`{3,}|~{3,})(.*)$", stripped) + opening = fence_marker if visible == stripped and opening and (opening.group(1)[0] == "~" or "`" not in opening.group(2)): fence_char = opening.group(1)[0] fence_length = len(opening.group(1)) From f02d6f22d5fc628ee035854c0c6cdb76e5591984 Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 6 Nov 2026 11:06:21 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix(triage):=20=E8=B7=A8=20HTML=20=E5=9D=97?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E5=9B=B4=E6=A0=8F=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #343 --- .../test_prepare_issue_triage_context.py | 4 ++ .../scripts/prepare_issue_triage_context.py | 55 +++++++++++++------ 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py index 52769cf..19c7a92 100644 --- a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py +++ b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py @@ -183,6 +183,8 @@ def test_triage_command_ignores_code_examples(self) -> None: "
\n\n
", "
\n- ```\n @codex /triage\n ```\n
", "
\n- - ~~~\n @codex /triage\n ~~~\n
", + "
\n```\n@codex /triage example\n\n@codex /triage\n```\n
", + "
\n- ~~~\n @codex /triage example\n\n @codex /triage\n ~~~\n
", "- - ```\n @codex /triage\n ```", "`example\n@codex /triage\ncontinued`", ) @@ -215,6 +217,8 @@ def test_triage_command_runs_after_code_examples(self) -> None: "
\n\n\n@codex /triage example\n\n@codex /triage\n
", "
\n\n@codex /triage\n
", "
\n- ```\n @codex /triage example\n ```\n@codex /triage\n
", + "
\n```\n@codex /triage example\n\n@codex /triage example\n```\n@codex /triage\n
", + "
\n```\n\n\n@codex /triage example\n```\n@codex /triage\n
", ): with self.subTest(comment=comment): self.assertTrue(prepare.comment_has_triage_command(comment, "codex")) diff --git a/.github/scripts/prepare_issue_triage_context.py b/.github/scripts/prepare_issue_triage_context.py index 25d300e..a71e36e 100644 --- a/.github/scripts/prepare_issue_triage_context.py +++ b/.github/scripts/prepare_issue_triage_context.py @@ -36,6 +36,8 @@ def __init__(self) -> None: super().__init__() self.hidden_tags: list[str] = [] self.comment_open = False + self.fence_char = "" + self.fence_length = 0 self.text: list[str] = [] def track_comments(self, source: str) -> bool: @@ -70,10 +72,31 @@ def handle_data(self, data: str) -> None: self.text.append(data) +def _html_fence_source(line: str) -> str: + source = line.strip() + while True: + remainder = re.sub(r"^(?:[-+*]|[0-9]{1,9}[.)])\s+", "", source, count=1) + if remainder == source: + return source + source = remainder + + +def _html_fence_closes(line: str, html: _VisibleTriageHtml) -> bool: + return bool( + html.fence_char + and re.fullmatch( + re.escape(html.fence_char) + "{" + str(html.fence_length) + r",}", + _html_fence_source(line), + ) + ) + + def _html_block_has_triage_command(content: str, expected: str, html: _VisibleTriageHtml) -> bool: - fence_char = "" - fence_length = 0 for line in content.splitlines(keepends=True): + if html.fence_char: + if _html_fence_closes(line, html): + html.fence_char = "" + continue previous_text_count = len(html.text) comment_closed = html.track_comments(line) if not html.cdata_elem else False html.feed(line) @@ -85,12 +108,7 @@ def _html_block_has_triage_command(content: str, expected: str, html: _VisibleTr if comment_closed or hidden_tag_closed: html.close() stripped = line.strip() - fence_source = stripped - while True: - remainder = re.sub(r"^(?:[-+*]|[0-9]{1,9}[.)])\s+", "", fence_source, count=1) - if remainder == fence_source: - break - fence_source = remainder + fence_source = _html_fence_source(stripped) fence_marker = re.match(r"(`{3,}|~{3,})(.*)$", fence_source) candidate = stripped == expected or stripped.startswith(f"{expected} ") if (candidate or fence_marker) and not html.comment_open: @@ -98,16 +116,10 @@ def _html_block_has_triage_command(content: str, expected: str, html: _VisibleTr visible = "".join(html.text[previous_text_count:]).strip() if not visible: continue - if fence_char: - if visible == stripped and re.fullmatch( - re.escape(fence_char) + "{" + str(fence_length) + r",}", fence_source - ): - fence_char = "" - continue opening = fence_marker if visible == stripped and opening and (opening.group(1)[0] == "~" or "`" not in opening.group(2)): - fence_char = opening.group(1)[0] - fence_length = len(opening.group(1)) + html.fence_char = opening.group(1)[0] + html.fence_length = len(opening.group(1)) continue if candidate and ( visible == expected or visible.startswith(f"{expected} ") @@ -169,6 +181,17 @@ def backtick_with_source_lines(state: Any, silent: bool) -> bool: list_depth = 0 html = _VisibleTriageHtml() for block in markdown.parse(comment): + if html.fence_char and block.type in {"inline", "fence", "code_block"} and block.map: + block_lines = source_lines[block.map[0] : block.map[1]] + for index, source_line in enumerate(block_lines): + if _html_fence_closes(source_line, html): + html.fence_char = "" + if _html_block_has_triage_command( + "\n".join(block_lines[index + 1 :]), expected, html + ): + return True + break + continue if block.type == "blockquote_open": quote_depth += 1 elif block.type == "blockquote_close": From d18110f3f62d0940d7aa3312f118c67435f2b0da Mon Sep 17 00:00:00 2001 From: "Terry.Mao" Date: Fri, 6 Nov 2026 11:12:46 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix(triage):=20=E9=81=B5=E5=BE=AA=20Markdow?= =?UTF-8?q?n=20=E5=9B=B4=E6=A0=8F=20token=20=E9=9A=90=E8=97=8F=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs #343 --- .../test_prepare_issue_triage_context.py | 7 +++++-- .github/scripts/prepare_issue_triage_context.py | 6 +----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py index 19c7a92..1583bbe 100644 --- a/.github/aicodingflow-tests/test_prepare_issue_triage_context.py +++ b/.github/aicodingflow-tests/test_prepare_issue_triage_context.py @@ -185,6 +185,9 @@ def test_triage_command_ignores_code_examples(self) -> None: "
\n- - ~~~\n @codex /triage\n ~~~\n
", "
\n```\n@codex /triage example\n\n@codex /triage\n```\n
", "
\n- ~~~\n @codex /triage example\n\n @codex /triage\n ~~~\n
", + "
\n```\n@codex /triage example\n\n```\n@codex /triage\n```\n
", + "
\n```\n@codex /triage example\n\n@codex /triage example\n```\n@codex /triage\n
", + "
\n- ~~~\n @codex /triage example\n\n ~~~\n@codex /triage\n~~~\n
", "- - ```\n @codex /triage\n ```", "`example\n@codex /triage\ncontinued`", ) @@ -217,8 +220,8 @@ def test_triage_command_runs_after_code_examples(self) -> None: "
\n\n\n@codex /triage example\n\n@codex /triage\n
", "
\n\n@codex /triage\n
", "
\n- ```\n @codex /triage example\n ```\n@codex /triage\n
", - "
\n```\n@codex /triage example\n\n@codex /triage example\n```\n@codex /triage\n
", - "
\n```\n\n\n@codex /triage example\n```\n@codex /triage\n
", + "
\n```\n@codex /triage example\n\n```\n@codex /triage example\n```\n@codex /triage\n
", + "
\n```\n\n\n@codex /triage example\n```\n@codex /triage example\n```\n@codex /triage\n
", ): with self.subTest(comment=comment): self.assertTrue(prepare.comment_has_triage_command(comment, "codex")) diff --git a/.github/scripts/prepare_issue_triage_context.py b/.github/scripts/prepare_issue_triage_context.py index a71e36e..16860ac 100644 --- a/.github/scripts/prepare_issue_triage_context.py +++ b/.github/scripts/prepare_issue_triage_context.py @@ -183,13 +183,9 @@ def backtick_with_source_lines(state: Any, silent: bool) -> bool: for block in markdown.parse(comment): if html.fence_char and block.type in {"inline", "fence", "code_block"} and block.map: block_lines = source_lines[block.map[0] : block.map[1]] - for index, source_line in enumerate(block_lines): + for source_line in block_lines: if _html_fence_closes(source_line, html): html.fence_char = "" - if _html_block_has_triage_command( - "\n".join(block_lines[index + 1 :]), expected, html - ): - return True break continue if block.type == "blockquote_open":