Skip to content

feat(markdown-normalizer): add intra-word emphasis fix for issue #97#100

Merged
Fu-Jie merged 2 commits into
mainfrom
feat/intra-word-emphasis-fix
Jun 30, 2026
Merged

feat(markdown-normalizer): add intra-word emphasis fix for issue #97#100
Fu-Jie merged 2 commits into
mainfrom
feat/intra-word-emphasis-fix

Conversation

@Fu-Jie

@Fu-Jie Fu-Jie commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new opt-in enable_intra_word_emphasis_fix Valve to the Markdown Normalizer filter that fixes a long-standing rendering issue when LLMs mark up punctuation-only additions inside a word boundary (e.g. for grammar/spelling diff outputs). Closes #97.

Background — Issue #97

The reporter uses Open WebUI for grammar and spelling checks and prefers the output to show deletions with ~~strikethrough~~ and additions with **bold**. The problem: when an LLM marks up a punctuation-only addition inside a word boundary — e.g. series**,** to indicate the comma was added — most Markdown parsers refuse to render the inline emphasis because the opening delimiter is intra-word. The reporter asked for a toggle that moves the opening tag to the beginning of the word so the added punctuation renders together with the word.

Changes

New Features

  • Intra-word Emphasis Repair (enable_intra_word_emphasis_fix, default False): moves the opening emphasis marker to the start of a word when the wrapped content is pure punctuation:

    Before After
    series**,** **series,**
    word~~,~~ ~~word,~~
    word__,__ __word,__
    word***,*** ***word,***

    Supports **, __, ~~, ***, ___. Only transforms cases where the wrapped content is pure punctuation (no whitespace, no word characters), so legitimate **bold**, word**bold**, math-like a**2**, and snake_case identifiers (my_var__bold__) are left untouched. Code blocks and inline code spans are also protected via the standard split("```") / split("")` partitioning.

Safety / UX

  • Off by default for backward compatibility (consistent with enable_emphasis_spacing_fix).
  • Added i18n key fix_intra_word to all 12 supported languages.
  • New fix is wired into fix_key_map so the status notification is translated correctly.

Plugin Version

  • markdown_normalizer bumped to v1.2.9.

Documentation

  • Bilingual READMEs (README.md, README_CN.md) updated with new "What's New in v1.2.9" section, feature description, and Valve table entry.
  • Docs mirror synced: docs/plugins/filters/markdown_normalizer.md, docs/plugins/filters/markdown_normalizer.zh.md, plus version badges in index.md / index.zh.md.
  • FEATURES_CN.md extended with a new "11. 词内强调修复" section.
  • New release notes v1.2.9.md and v1.2.9_CN.md.

Testing

  • New plugins/filters/markdown_normalizer/tests/test_intra_word_emphasis.py covering: bold/strikethrough/underscore/triple markers, multiple punctuation chars, multiple occurrences in a sentence, CJK word prefix, safe cases (legitimate emphasis, math, snake_case, HRs), code block + inline code protection, interaction with enable_emphasis_spacing_fix, and disabled-default behavior.
  • New regression test tests/plugins/filters/markdown_normalizer/test_issue_97_regression.py (mirrors the test_issue_57_regression.py pattern).
  • Updated tests/conftest.py with an intra_word_only_normalizer fixture.
  • Verified the existing pre-existing test failures (test_emphasis_spacing.py::test_safeguard_and_correct_emphasis_unchanged[* italic *], test_reliability.py::test_code_block_escape_control) are unchanged on main and unrelated to this PR.
  • python3 scripts/check_version_consistency.py passes.

Testing Checklist

  • Unit tests pass (pytest plugins/filters/markdown_normalizer/tests/ tests/plugins/filters/markdown_normalizer/)
  • End-to-end Filter.outlet integration test verified
  • i18n keys present in all 12 languages
  • Version consistency check passed

变更摘要(中文)

为 Markdown Normalizer 过滤器新增可选启用的 enable_intra_word_emphasis_fix 配置项,修复 LLM 在词边界内标记仅含标点的添加时(如语法/拼写差异输出场景)的渲染问题。Closes #97

新功能

  • 词内强调修复enable_intra_word_emphasis_fix,默认 False):将开头的强调标记移动到词的开头,让添加的标点能与单词一起渲染。支持 **__~~***___。仅对包含纯标点内容的情况生效,合法的 **bold** / word**bold** / 数学算式 a**2** / snake_case 标识符均不受影响;代码块与行内代码也受到保护。

默认关闭

  • 默认关闭以保证向后兼容(与 enable_emphasis_spacing_fix 一致)。

插件版本

  • markdown_normalizer 升级到 v1.2.9

文档

  • 双语 README、docs 镜像、FEATURES_CN.mdv1.2.9.md / v1.2.9_CN.md 发行说明均已同步。

测试

  • 新增 tests/test_intra_word_emphasis.pytests/plugins/filters/markdown_normalizer/test_issue_97_regression.py,覆盖:各类标记、纯标点内容、CJK、安全用例(合法强调/数学/snake_case/水平线)、代码块与行内代码保护、与 enable_emphasis_spacing_fix 的交互、默认关闭行为。
  • check_version_consistency.py 通过。

- New `enable_intra_word_emphasis_fix` Valve moves the opening emphasis
  marker to the start of a word when the wrapped content is pure
  punctuation, e.g. `series**,**` -> `**series,**` so added punctuation
  renders with the word (common in grammar/spelling diff outputs)
- Supports **, __, ~~, ***, ___; code blocks and inline code are skipped
- Bump version to 1.2.9; sync bilingual READMEs, docs mirror, and
  FEATURES_CN; add v1.2.9 release notes
- Add tests under tests/ and plugins/.../tests/ covering the fix, code
  protection, and disabled-default behavior

Closes #97
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Plugin Version Check / 插件版本检查

版本更新检测通过!PR 包含版本变化和更新说明。

Version check passed! PR contains version changes and update description.


版本变化 / Version Changes

Plugin Updates


This comment was generated automatically. / 此评论由自动生成。

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本拉取请求为 markdown_normalizer 过滤器插件引入了新功能“词内强调修复”(版本 v1.2.9),并新增了可选配置项 enable_intra_word_emphasis_fix,用于解决 LLM 在词边界内标记纯标点符号时(如 series**,**)Markdown 解析器无法正常渲染的问题。对此,评审人员提出了三项改进意见:首先,指出当前正则表达式中的 [^\\W_]+ 会排除下划线,导致 snake_case 变量名(如 my_var__,__)被错误拆分,建议将其替换为 \\w+;其次,指出 _fix_intra_word_emphasis 方法中的 while True 循环是多余的,因为该模式不会嵌套,单次 re.sub 即可,建议简化以节省资源;最后,建议在测试用例中增加对 snake_case 变量名的单元测试。

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +353 to +358
"intra_word_emphasis": re.compile(
r"(?P<word>[^\W_]+)"
r"(?P<marker>\*\*\*|___|\*\*|__|~~)"
r"(?P<content>[^\s\w]+?)"
r"(?P=marker)"
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

问题分析
在当前实现中,intra_word_emphasis 正则表达式使用了 [^\W_]+ 来匹配单词(word),这故意排除了下划线 _
然而,在技术文本或代码中,下划线是 snake_case 变量名(如 my_var)的常见组成部分。
如果 LLM 输出形如 my_var__,__ 的内容(表示在变量名后添加了逗号),由于 _ 被排除,[^\W_]+ 只能匹配到 var,导致该正则匹配了 var__,__,最终将其错误地转换为 my___var,__
这会把变量名拆开,导致渲染出来的格式极其混乱(my_ 正常,而 var, 被加粗),破坏了代码标识符的完整性。

解决方案
建议将 [^\W_]+ 替换为 \w+。由于 content 部分已经通过 [^\s\w]+? 严格限制为纯标点符号(不含任何字母、数字或下划线),因此即使 word 匹配了包含下划线的完整变量名,也绝不会误匹配合法的 my_var__bold__(因为 bold 包含字母,无法匹配 content)。
使用 \w+ 后,my_var__,__ 将被完美修复为 __my_var,__,保持了变量名的完整性。

Suggested change
"intra_word_emphasis": re.compile(
r"(?P<word>[^\W_]+)"
r"(?P<marker>\*\*\*|___|\*\*|__|~~)"
r"(?P<content>[^\s\w]+?)"
r"(?P=marker)"
),
"intra_word_emphasis": re.compile(
r"(?P<word>\w+)"
r"(?P<marker>\*\*\*|___|\*\*|__|~~)"
r"(?P<content>[^\s\w]+?)"
r"(?P=marker)"
),

Comment on lines +786 to +792
while True:
new_segment = self._PATTERNS["intra_word_emphasis"].sub(
replacer, inline_parts[k]
)
if new_segment == inline_parts[k]:
break
inline_parts[k] = new_segment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

问题分析
_fix_intra_word_emphasis 方法中,使用了 while True 循环来反复执行 re.sub 直到字符串不再发生变化。
然而,由于 intra_word_emphasis 的匹配条件要求中间的 content 必须是纯标点符号([^\s\w]+?),这意味着该模式绝不可能发生嵌套(因为标点符号内部不可能再包含单词和强调标记)。
因此,单次 re.sub 即可完全替换所有符合条件的匹配项,无需进行多次迭代。
使用 while True 不仅多余,还会导致每次处理时都至少额外执行一次无用的正则搜索(用于确认没有更多匹配),在高并发或长文本场景下会浪费 CPU 资源,甚至在极端情况下存在死循环的风险。

解决方案
建议移除 while True 循环,直接进行单次替换。

                inline_parts[k] = self._PATTERNS["intra_word_emphasis"].sub(
                    replacer, inline_parts[k]
                )

Comment on lines +50 to +51
# CJK word prefix
("中文**,**", "**中文,**"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

建议
为了验证我们对 snake_case 变量名(如 my_var__,__)修复逻辑的正确性,建议在此处增加对应的单元测试用例。

            # CJK word prefix
            ("中文**,**", "**中文,**"),
            # snake_case variable name with punctuation addition
            ("my_var__,__", "__my_var,__"),

@Fu-Jie

Fu-Jie commented Jun 29, 2026

Copy link
Copy Markdown
Owner Author

Hi @dchurch315 👋

Thanks for the detailed feature request! I've opened PR #100 to address this exact use case.

What's been implemented

A new opt-in Valve enable_intra_word_emphasis_fix has been added to the Markdown Normalizer filter (defaults to False, so existing behavior is unchanged). When enabled, it detects emphasis markers (**, __, ~~, ***, ___) wrapping pure-punctuation content inside a word boundary and moves the opening marker to the start of the word so the punctuation renders.

Before → After

Input Output
series**,** **series,**
word~~,~~ ~~word,~~
word__.__ __word,__
word***,*** ***word,***

Code blocks and inline code are protected, and legitimate emphasis (**bold**, word**bold**, math like a**2**) is left untouched.

How to test

  1. Pull the branch feat/intra-word-emphasis-fix from this PR
  2. In the Markdown Normalizer filter settings, enable "Fix intra-word emphasis" (enable_intra_word_emphasis_fix)
  3. Try a prompt that produces output like series**,** and verify the comma now renders in bold

Known limitation (edge cases)

During testing I noticed that when the punctuation marker sits inside an identifier-like token (e.g. my_var**,**, 3.14**,**, example.com**#**), the fix currently moves the marker to the start of the last word fragment rather than the whole token — so my_var**,** becomes my_**var,** instead of staying put. In your grammar-check scenario this is unlikely to matter (you're mostly dealing with natural-language words), but I want to be transparent about it. I'm working on a lookbehind refinement to avoid splitting on ., _, -, @.

Asking for your help

Before I merge this, could you:

  1. Pull the branch and try it against your actual grammar-check prompts?
  2. Confirm whether the series**,****series,** behavior matches what you had in mind?
  3. Share any real-world inputs from your LLM output where it doesn't behave as expected, so I can cover those cases too?

I'd rather get your real-world validation first than merge blindly. Happy to iterate based on your feedback! 🙏

— Fu-Jie

@dchurch315

Copy link
Copy Markdown

It works but I notice that it only does so after all the content has been displayed, i.e. it doesn't change during the streaming. Not an issue for me but I wanted to mention it in case that wasn't expected behavior. I believe that you can handle stream events by switching outlet() to stream(), but I am a bit rusty.

@Fu-Jie

Fu-Jie commented Jun 30, 2026

Copy link
Copy Markdown
Owner Author

Hi @dchurch315,

Glad to hear it works for your use case! 🎉

Regarding the streaming behavior you observed — that's actually expected and by design, not a bug. The Markdown Normalizer filter is intentionally implemented as a non-streaming post-processing filter via outlet(), not stream().

Why it's designed this way

This plugin contains 9+ different fix passes (emphasis spacing, escape control, smart punctuation, HTML entity cleanup, intra-word emphasis, etc.), and almost all of them need to operate on the complete, final text to work correctly:

  1. Context-dependent fixes — Many patterns (e.g., balancing emphasis markers, repairing unmatched delimiters, escape sequences) require looking at the full surrounding text. Applying them to a partial streaming chunk would produce wrong results because the "rest of the text" hasn't arrived yet.
  2. Avoiding false positives on partial chunks — During streaming, a chunk might end mid-token (e.g., ser + ies**,**). Running the regex on ser alone could match patterns that wouldn't match in the final text, causing flicker or wrong transformations that get undone on the next chunk.
  3. Consistency — Every existing fix in this plugin already runs through outlet(). The new enable_intra_word_emphasis_fix deliberately follows the same path so it behaves consistently with the rest of the plugin and doesn't introduce a separate code path that could drift.

Why not switch to stream()

Switching to stream() would mean rewriting all the existing fixes, not just this one — otherwise the new fix would be the only one applied mid-stream while the others still wait for completion, which would be confusing and inconsistent. It would also risk regressions in the 8 already-shipped, stable fixes. So I'd prefer to keep the whole plugin as a post-processing filter.

In short: the "applies after streaming completes" behavior is intentional for this plugin's design philosophy, not something I plan to change. Thanks for flagging it though — it's a fair question and a good heads-up! 👍

— Fu-Jie

1. Refine word pattern: `[^\W_]+` -> `[^\W_]+(?:_[^\W_]+)*`
   - Snake_case identifiers (`my_var**,**`) now stay intact instead of
     being split into `my_` + `var` (Gemini review, high priority).
   - Trailing underscores are left for the marker alternation to consume,
     so triple-underscore markers (`word___,___`) keep working correctly.
   - Legitimate `my_var__bold__` is still protected (content excludes
     word chars, so the pattern never matches).

2. Remove redundant `while True` loop in `_fix_intra_word_emphasis`.
   - The pattern cannot nest (content is restricted to pure punctuation),
     so a single `re.sub` pass is sufficient (Gemini review, medium).

3. Add snake_case test cases covering `my_var**,**`, `my_var__,__`,
   `my_var~~,~~`, and `snake_case_var**.**`.

Test results: 110 passed, 3 failed (all 3 failures are pre-existing on
`main` and unrelated to this change).
@Fu-Jie

Fu-Jie commented Jun 30, 2026

Copy link
Copy Markdown
Owner Author

Update for @dchurch315 and reviewers: I've pushed a follow-up commit (5a28740) addressing the Gemini Code Assist review and the edge-case limitation I mentioned earlier.

What changed

1. Snake_case identifiers now stay intact (Gemini review, high priority)

The word pattern was refined from [^\W_]+[^\W_]+(?:_[^\W_]+)* so that underscores sitting between word-char runs are kept as part of the word, while trailing underscores are still left for the marker alternation. This means:

Input Before After
my_var**,** my_**var,** **my_var,**
my_var__,__ my___var,__ __my_var,__
my_var~~,~~ my_~~var,~~ ~~my_var,~~
snake_case_var**.** snake_case_**var.** **snake_case_var.**

Crucially, this is done without breaking triple-underscore markers — word___,___ still correctly becomes ___word,___, because the word pattern stops before a trailing run of underscores, letting the marker alternation consume ___ as a whole.

Legitimate my_var__bold__ markup remains untouched (the content group is restricted to pure punctuation, so it never matches).

2. Removed redundant while True loop (Gemini review, medium)

Since the pattern cannot nest (content is pure punctuation), a single re.sub pass is sufficient. The loop was dead code and has been removed.

3. Added snake_case test cases

test_intra_word_emphasis.py now covers my_var**,**, my_var__,__, my_var~~,~~, and snake_case_var**.**.

Test results

Full plugin test suite: 110 passed, 3 failed

The 3 failures (* italic *, _ italic _, test_code_block_escape_control) are pre-existing on main and unrelated to this PR — I verified by checking out main and running the same suite (3 failed, 58 passed there).

@dchurch315 — the streaming behavior you observed is expected (see my previous comment), but the core fix itself now handles your snake_case/identifier cases too. If you pull the latest from feat/intra-word-emphasis-fix, those cases should now render correctly. Feedback still welcome before merge!

@Fu-Jie
Fu-Jie merged commit aac1b21 into main Jun 30, 2026
1 check passed
@Fu-Jie
Fu-Jie deleted the feat/intra-word-emphasis-fix branch June 30, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Ability to repair issues with intra-word emphasis

2 participants