From 48394a1dab86824f6a4bcb2f70cadadb531ef24a Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Wed, 21 Jan 2026 23:51:00 +0900 Subject: [PATCH 01/10] Add CJK-friendly Emphasis Extension --- readme.md | 1 + .../Specs/CJKFriendlyEmphasis.md | 37 ++++++ src/Markdig.Tests/Specs/readme.md | 1 + src/Markdig.Tests/TestCjkFriendlyEmphasis.cs | 105 +++++++++++++++++ .../CjkFriendlyEmphasisExtension.cs | 29 +++++ src/Markdig/Helpers/CharHelper.cs | 107 ++++++++++++++++++ src/Markdig/MarkdownExtensions.cs | 7 ++ .../Parsers/Inlines/EmphasisInlineParser.cs | 25 +++- 8 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 src/Markdig.Tests/Specs/CJKFriendlyEmphasis.md create mode 100644 src/Markdig.Tests/TestCjkFriendlyEmphasis.cs create mode 100644 src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs diff --git a/readme.md b/readme.md index 60644a15d..669a6e0c6 100644 --- a/readme.md +++ b/readme.md @@ -51,6 +51,7 @@ You can **try Markdig online** and compare it to other implementations on [babel - [**Diagrams**](src/Markdig.Tests/Specs/DiagramsSpecs.md) extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports [`mermaid`](https://mermaid.js.org) and [`nomnoml`](https://github.com/skanaar/nomnoml) diagrams) - [**YAML Front Matter**](src/Markdig.Tests/Specs/YamlSpecs.md) to parse without evaluating the front matter and to discard it from the HTML output (typically used for previewing without the front matter in MarkdownEditor) - [**JIRA links**](src/Markdig.Tests/Specs/JiraLinks.md) to automatically generate links for JIRA project references (Thanks to @clarkd: https://github.com/clarkd/MarkdigJiraLinker) + - [**CJK-friendly Emphasis**](src/Markdig.Tests/Specs/CJKFriendlyEmphasis.md) to mitigate a CommonMark specification issue in CJK languages (Thanks to @tats-u: https://github.com/tats-u/markdown-cjk-friendly) - Starting with Markdig version `0.20.0+`, Markdig is compatible only with `NETStandard 2.0`, `NETStandard 2.1`, `NETCoreApp 2.1` and `NETCoreApp 3.1`. If you are looking for support for an old .NET Framework 3.5 or 4.0, you can download Markdig `0.18.3`. diff --git a/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.md b/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.md new file mode 100644 index 000000000..fafc53590 --- /dev/null +++ b/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.md @@ -0,0 +1,37 @@ +## CJK-friendly Emphasis Extension + +See https://github.com/tats-u/markdown-cjk-friendly/blob/main/specification.md for details about the spec of this extension. + +This extension drastically mitigates [the long-standing issue (specification flaw)](https://github.com/commonmark/commonmark-spec/issues/650) in CommonMark that emphasis in CJK languages is often not parsed as expected. + +The plain CommonMark cannot recognize even the following emphasis in CJK languages: + +```````````````````````````````` example +**この文を強調できますか(Can I emphasize this sentence)?**残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。 +. +

この文を強調できますか(Can I emphasize this sentence)?残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。

+```````````````````````````````` + +````````````````````````````````` example +我可以强调**这个`code`**吗(Can I emphasize **this `code`**)? +. +

我可以强调这个`code`吗(Can I emphasize this code)?

+````````````````````````````````` + +```````````````````````````````` example +**이 용어(This term)**를 강조해 주세요. (Please emphasize **this term**.) +. +

이 용어(This term)를 강조해 주세요. (Please emphasize this term.)

+```````````````````````````````` + +You can compare the results with and without this extension: https://tats-u.github.io/markdown-cjk-friendly/?sc8=KirjgZPjga7mlofjgpLlvLfoqr_jgafjgY3jgb7jgZnjgYvvvIhDYW4gSSBlbXBoYXNpemUgdGhpcyBzZW50ZW5jZe-8ie-8nyoq5q6L5b-144Gq44GM44KJ44GT44Gu5paH44Gu44Gb44GE44Gn44Gn44GN44G-44Gb44KT77yIVW5mb3J0dW5hdGVseSBub3QgcG9zc2libGUgZHVlIHRvIHRoaXMgc2VudGVuY2XvvInjgIIKCuaIkeWPr-S7peW8uuiwgyoq6L-Z5LiqYGNvZGVgKirlkJfvvIhDYW4gSSBlbXBoYXNpemUgKip0aGlzIGBjb2RlYCoq77yJ77yfCgoqKuydtCDsmqnslrQoVGhpcyB0ZXJtKSoq66W8IOqwleyhsO2VtCDso7zshLjsmpQuIChQbGVhc2UgZW1waGFzaXplICoqdGhpcyB0ZXJtKiouKQo&gfm=1&engine=markdown-it + +You will find how poor the plain CommonMark is for CJK languages. + +To use this extension, configure the pipeline as follows: + +```csharp +var pipeline = new MarkdownPipelineBuilder() + .UseCJKFriendlyEmphasis() // Add this + .Build(); +``` diff --git a/src/Markdig.Tests/Specs/readme.md b/src/Markdig.Tests/Specs/readme.md index 29702bb58..a88f15ec1 100644 --- a/src/Markdig.Tests/Specs/readme.md +++ b/src/Markdig.Tests/Specs/readme.md @@ -32,5 +32,6 @@ You will find from the following links the supported extensions in markdig and t - [**Diagrams**](DiagramsSpecs.md) - [**YAML frontmatter**](YamlSpecs.md) - [**JIRA links**](JiraLinks.md) + - [**CJK-friendly Emphasis**](CJKFriendlyEmphasis.md) > Notice that the links above are not yet the final documentation but are "specification" files used for testing the correctness of markdig for each extension \ No newline at end of file diff --git a/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs b/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs new file mode 100644 index 000000000..e04118364 --- /dev/null +++ b/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs @@ -0,0 +1,105 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Markdig.Tests +{ + [TestFixture] + public class TestCjkFriendlyEmphasis + { + private static MarkdownPipeline GetPipeline() => new MarkdownPipelineBuilder().UseCjkFriendlyEmphasis().Build(); + + private static MarkdownPipeline GetPipelineWithStrikethrough() => new MarkdownPipelineBuilder() + .UseCjkFriendlyEmphasis() + .UseEmphasisExtras() + .Build(); + + [Test] + [TestCase("これは**私のやりたかったこと。**だからするの。", "

これは私のやりたかったこと。だからするの。

\n")] + [TestCase("**[製品ほげ](./product-foo)**と**[製品ふが](./product-bar)**をお試しください", "

製品ほげ製品ふがをお試しください

\n")] + [TestCase("先頭の**`コード`も注意。**", "

先頭のコードも注意。

\n")] + [TestCase("**末尾の`コード`**も注意。", "

末尾のコードも注意。

\n")] + [TestCase("税込**¥10,000**で入手できます。", "

税込¥10,000で入手できます。

\n")] + [TestCase("""太郎は**"こんにちわ"**といった""", "

太郎は"こんにちわ"といった

\n")] + [TestCase("**C#**や**F#**は**「.NET」**というプラットフォーム上で動作します。", "

C#F#「.NET」というプラットフォーム上で動作します。

\n")] + [TestCase(".NET**(.NET Frameworkは不可)**では、", "

.NET(.NET Frameworkは不可)では、

\n")] + [TestCase("大塚︀**(U+585A U+FE00)** 大塚**(U+FA10)**", "

大塚︀(U+585A U+FE00) 大塚(U+FA10)

\n")] + [TestCase("〽︎**(庵点)**は、\n\n","

〽︎(庵点)は、

\n")] + [TestCase("**true。︁**false\n\n", "

true。︁false

\n")] + [TestCase("禰󠄀**(ね)**豆子", "

禰󠄀(ね)豆子

\n")] + public void TestCjkFriendlyEmphasisJapanese(string source, string expected) + { + var pipeline = GetPipeline(); + var actual = Markdown.ToHtml(source, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase("**이 [링크](https://example.kr/)**만을 강조하고 싶다.", "

링크만을 강조하고 싶다.

\n")] + [TestCase("**스크립트(script)**라고", "

스크립트(script)라고

\n")] + [TestCase("패키지를 발행하려면 **`npm publish`**를 실행하십시오.", "

패키지를 발행하려면 npm publish를 실행하십시오.

\n")] + [TestCase("**안녕(hello)**하세요.", "

안녕(hello)하세요.

\n")] + [TestCase("ᅡ**(a)**", "

(a)

\n")] + [TestCase("**(k)**ᄏ", "

(k)

\n")] + public void TestCjkFriendlyEmphasisKorean(string source, string expected) + { + var pipeline = GetPipeline(); + var actual = Markdown.ToHtml(source, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase("__注意__:注意事項", "

注意:注意事項

\n")] + [TestCase("注意:__注意事項__", "

注意:注意事項

\n")] + [TestCase("正體字。︁_Traditional._", "

正體字。︁Traditional.

\n")] + [TestCase("正體字。︁__Hong Kong and Taiwan.__", "

正體字。︁Hong Kong and Taiwan.

\n")] + [TestCase("简体字 / 新字体。︀_Simplified._", "

简体字 / 新字体。︀Simplified.

\n")] + [TestCase("简体字 / 新字体。︀__Mainland China or Japan.__", "

简体字 / 新字体。︀Mainland China or Japan.

\n")] + [TestCase("“︁Git”︁__Hub__", "

“︁Git”︁Hub

\n")] + public void TestCjkFriendlyEmphasisUnderscore(string source, string expected) + { + var pipeline = GetPipeline(); + var actual = Markdown.ToHtml(source, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase("a~~a()~~あ", "

aa()

\n")] + [TestCase("あ~~()a~~a", "

()aa

\n")] + [TestCase("𩸽~~()a~~a", "

𩸽()aa

\n")] + [TestCase("a~~a()~~𩸽", "

aa()𩸽

\n")] + [TestCase("葛󠄀~~()a~~a", "

葛󠄀()aa

\n")] + [TestCase("羽︀~~()a~~a", "

羽︀()aa

\n")] + [TestCase("a~~「a~~」", "

a「a

\n")] + [TestCase("「~~a」~~a", "

a」a

\n")] + [TestCase("~~a~~:~~a~~", "

aa

\n")] + [TestCase("~~日本語。︀~~English.", "

日本語。︀English.

\n")] + [TestCase("~~“︁a”︁~~a", "

“︁a”︁a

\n")] + public void TestCjkFriendlyEmphasisGfmStrikethrough(string source, string expected) + { + var pipeline = GetPipelineWithStrikethrough(); + var actual = Markdown.ToHtml(source, pipeline); + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase("a**〰**a", "

aa

\n")] + [TestCase("a**〽**a", "

aa

\n")] + [TestCase("a**🈂**a", "

a🈂a

\n")] + [TestCase("a**🈷**a", "

a🈷a

\n")] + [TestCase("a**㊗**a", "

aa

\n")] + [TestCase("a**㊙**a", "

aa

\n")] + public void TestCjkFriendlyPseudoEmoji(string source, string expected) + { + var pipeline = GetPipeline(); + var actual = Markdown.ToHtml(source, pipeline); + Assert.AreEqual(expected, actual); + } + } +} diff --git a/src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs b/src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs new file mode 100644 index 000000000..77ed33526 --- /dev/null +++ b/src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs @@ -0,0 +1,29 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using Markdig.Parsers.Inlines; +using Markdig.Renderers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Markdig.Extensions.CjkFriendlyEmphasis +{ + public class CjkFriendlyEmphasisExtension : IMarkdownExtension + { + public void Setup(MarkdownPipelineBuilder pipeline) + { + var parser = pipeline.InlineParsers.FindExact(); + parser?.CjkFriendlyEmphasis = true; + } + + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + var parser = pipeline.InlineParsers.FindExact(); + parser?.CjkFriendlyEmphasis = true; + } + } +} diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index c2db8c4ba..35ac3a0bd 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -150,6 +150,113 @@ private static void CheckOpenCloseDelimiter(bool prevIsWhiteSpace, bool prevIsPu } } +#if NET + public +#else + internal +#endif + static void CheckOpenCloseDelimiterCjkFriendly(Rune pc, Rune c, Rune twoPreviousRune, bool enableWithinWord, out bool canOpen, out bool canClose) + { + pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation); + c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation); + + if (prevIsWhiteSpace || nextIsWhiteSpace) + { + canOpen = !nextIsWhiteSpace; + canClose = !prevIsWhiteSpace; + return; + } + + bool isMainTwoPrevious = false; + Rune mainPreviousRune = pc; + if (IsNonEmojiGeneralUseVariantSelector(pc)) + { + isMainTwoPrevious = true; + mainPreviousRune = twoPreviousRune; + mainPreviousRune.CheckUnicodeCategory(out var _, out prevIsPunctuation); + } + canOpen = prevIsPunctuation; + canClose = nextIsPunctuation; + if (!enableWithinWord) + { + return; + } + bool prevIsCjk = IsCjk(mainPreviousRune) || (isMainTwoPrevious ? IsCjkAmbiousPunctuation(mainPreviousRune, pc) : IsIdeographicVariationSelector(mainPreviousRune)); + bool nextIsCjk = IsCjk(c); + bool eitherIsCjk = prevIsCjk || nextIsCjk; + + canOpen |= eitherIsCjk || !nextIsPunctuation; + canClose |= eitherIsCjk || !prevIsPunctuation; + + static bool IsNonEmojiGeneralUseVariantSelector(Rune r) => r.Value is >= 0xFE00 and <= 0xFE0E; + static bool IsIdeographicVariationSelector(Rune r) => r.Value is >= 0xE0100 and <= 0xE01EF; + static bool IsCjkAmbiousPunctuation(Rune main, Rune vs) => vs.Value is 0xFE01 && main.Value is 0x2018 or 0x2019 or 0x201C or 0x201D; + // As of Unicode 17 + static bool IsCjk(Rune r) => r.Value is + >= 0x1100 and ( // Fast path for most non-CJK characters + <= 0x11ff + or 0x20a9 + or >= 0x2329 and <= 0x232a + or >= 0x2630 and <= 0x2637 + or >= 0x268a and <= 0x268f + or >= 0x2e80 and <= 0x2e99 + or >= 0x2e9b and <= 0x2ef3 + or >= 0x2f00 and <= 0x2fd5 + or >= 0x2ff0 and <= 0x303e + or >= 0x3041 and <= 0x3096 + or >= 0x3099 and <= 0x30ff + or >= 0x3105 and <= 0x312f + or >= 0x3131 and <= 0x318e + or >= 0x3190 and <= 0x31e5 + or >= 0x31ef and <= 0x321e + or >= 0x3220 and <= 0x3247 + or >= 0x3250 and <= 0xa48c + or >= 0xa490 and <= 0xa4c6 + or >= 0xa960 and <= 0xa97c + or >= 0xac00 and <= 0xd7a3 + or >= 0xd7b0 and <= 0xd7c6 + or >= 0xd7cb and <= 0xd7fb + or >= 0xf900 and <= 0xfaff + or >= 0xfe10 and <= 0xfe19 + or >= 0xfe30 and <= 0xfe52 + or >= 0xfe54 and <= 0xfe66 + or >= 0xfe68 and <= 0xfe6b + or >= 0xff01 and <= 0xffbe + or >= 0xffc2 and <= 0xffc7 + or >= 0xffca and <= 0xffcf + or >= 0xffd2 and <= 0xffd7 + or >= 0xffda and <= 0xffdc + or >= 0xffe0 and <= 0xffe6 + or >= 0xffe8 and <= 0xffee + or >= 0x16fe0 and <= 0x16fe4 + or >= 0x16ff0 and <= 0x16ff6 + or >= 0x17000 and <= 0x18cd5 + or >= 0x18cff and <= 0x18d1e + or >= 0x18d80 and <= 0x18df2 + or >= 0x1aff0 and <= 0x1aff3 + or >= 0x1aff5 and <= 0x1affb + or >= 0x1affd and <= 0x1affe + or >= 0x1b000 and <= 0x1b122 + or 0x1b132 + or >= 0x1b150 and <= 0x1b152 + or 0x1b155 + or >= 0x1b164 and <= 0x1b167 + or >= 0x1b170 and <= 0x1b2fb + or >= 0x1d300 and <= 0x1d356 + or >= 0x1d360 and <= 0x1d376 + or 0x1f200 + or 0x1f202 + or >= 0x1f210 and <= 0x1f219 + or >= 0x1f21b and <= 0x1f22e + or >= 0x1f230 and <= 0x1f231 + or 0x1f237 + or 0x1f23b + or >= 0x1f240 and <= 0x1f248 + or >= 0x1f260 and <= 0x1f265 + or >= 0x20000 and <= 0x3fffd + ); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsRomanLetterPartial(char c) { diff --git a/src/Markdig/MarkdownExtensions.cs b/src/Markdig/MarkdownExtensions.cs index 8aa064de8..afcc26605 100644 --- a/src/Markdig/MarkdownExtensions.cs +++ b/src/Markdig/MarkdownExtensions.cs @@ -8,6 +8,7 @@ using Markdig.Extensions.AutoLinks; using Markdig.Extensions.Bootstrap; using Markdig.Extensions.Citations; +using Markdig.Extensions.CjkFriendlyEmphasis; using Markdig.Extensions.CustomContainers; using Markdig.Extensions.DefinitionLists; using Markdig.Extensions.Diagrams; @@ -522,6 +523,12 @@ public static MarkdownPipelineBuilder UseGlobalization(this MarkdownPipelineBuil return pipeline; } + public static MarkdownPipelineBuilder UseCjkFriendlyEmphasis(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } + /// /// This will disable the HTML support in the markdown processor (for constraint/safe parsing). /// diff --git a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs index e2aaada0c..b926d709f 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs @@ -43,6 +43,8 @@ public EmphasisInlineParser() /// public List EmphasisDescriptors { get; } + public bool CjkFriendlyEmphasis { get; set; } = false; + /// /// Determines whether this parser is using the specified character as an emphasis delimiter. /// @@ -151,16 +153,28 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) var emphasisDesc = emphasisMap![delimiterChar]!; Rune pc = (Rune)0; + Rune twoPreviousChar = default; + if (processor.Inline is HtmlEntityInline htmlEntityInline) { if (htmlEntityInline.Transcoded.Length > 0) { pc = htmlEntityInline.Transcoded.RuneAt(htmlEntityInline.Transcoded.End); + + if (CjkFriendlyEmphasis) + { + twoPreviousChar = htmlEntityInline.Transcoded.RuneAt(htmlEntityInline.Transcoded.End - pc.Utf16SequenceLength); + } } } if (pc.Value == 0) { pc = slice.PeekRuneExtra(-1); + if (CjkFriendlyEmphasis) + { + // This cannot be a delegate (Func?) because slice is a reference + twoPreviousChar = slice.PeekRuneExtra(-1 - pc.Utf16SequenceLength); + } // delimiterChar is BMP, so slice.PeekCharExtra(-2) is (a part of) the character two positions back. if (pc == (Rune)delimiterChar && slice.PeekCharExtra(-2) != '\\') { @@ -189,8 +203,17 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) Rune.DecodeFromUtf16(htmlString, out c, out _); } + bool canOpen = false; + bool canClose = false; // Calculate Open-Close for current character - CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out bool canOpen, out bool canClose); + if (CjkFriendlyEmphasis) + { + CharHelper.CheckOpenCloseDelimiterCjkFriendly(pc, c, twoPreviousChar, emphasisDesc.EnableWithinWord, out canOpen, out canClose); + } + else + { + CharHelper.CheckOpenCloseDelimiter(pc, c, emphasisDesc.EnableWithinWord, out canOpen, out canClose); + } // We have potentially an open or close emphasis if (canOpen || canClose) From 03576503665c7a75bccc25e00888ec894702b5c5 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Thu, 22 Jan 2026 23:24:03 +0900 Subject: [PATCH 02/10] Add auto-generated test file --- .../Specs/CJKFriendlyEmphasis.generated.cs | 67 +++++++++++++++++++ src/SpecFileGen/Program.cs | 3 +- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs diff --git a/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs b/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs new file mode 100644 index 000000000..4bdc0ff36 --- /dev/null +++ b/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs @@ -0,0 +1,67 @@ + +// -------------------------------- +// CJK-friendly Emphasis +// -------------------------------- + +using System; +using NUnit.Framework; + +namespace Markdig.Tests.Specs.CJKFriendlyEmphasis +{ + [TestFixture] + public class TestCJKFriendlyEmphasisExtension + { + // ## CJK-friendly Emphasis Extension + // + // See https://github.com/tats-u/markdown-cjk-friendly/blob/main/specification.md for details about the spec of this extension. + // + // This extension drastically mitigates [the long-standing issue (specification flaw)](https://github.com/commonmark/commonmark-spec/issues/650) in CommonMark that emphasis in CJK languages is often not parsed as expected. + // + // The plain CommonMark cannot recognize even the following emphasis in CJK languages: + [Test] + public void CJKFriendlyEmphasisExtension_Example001() + { + // Example 1 + // Section: CJK-friendly Emphasis Extension + // + // The following Markdown: + // **この文を強調できますか(Can I emphasize this sentence)?**残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。 + // + // Should be rendered as: + //

この文を強調できますか(Can I emphasize this sentence)?残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。

+ + TestParser.TestSpec("**この文を強調できますか(Can I emphasize this sentence)?**残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。", "

この文を強調できますか(Can I emphasize this sentence)?残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。

", "emphasisextras|advanced", context: "Example 1\nSection CJK-friendly Emphasis Extension\n"); + } + + // ````````````````````````````````` example + // 我可以强调**这个`code`**吗(Can I emphasize **this `code`**)? + // . + //

我可以强调这个`code`吗(Can I emphasize this code)?

+ // ````````````````````````````````` + [Test] + public void CJKFriendlyEmphasisExtension_Example002() + { + // Example 2 + // Section: CJK-friendly Emphasis Extension + // + // The following Markdown: + // **이 용어(This term)**를 강조해 주세요. (Please emphasize **this term**.) + // + // Should be rendered as: + //

이 용어(This term)를 강조해 주세요. (Please emphasize this term.)

+ + TestParser.TestSpec("**이 용어(This term)**를 강조해 주세요. (Please emphasize **this term**.)", "

이 용어(This term)를 강조해 주세요. (Please emphasize this term.)

", "emphasisextras|advanced", context: "Example 2\nSection CJK-friendly Emphasis Extension\n"); + } + // You can compare the results with and without this extension: https://tats-u.github.io/markdown-cjk-friendly/?sc8=KirjgZPjga7mlofjgpLlvLfoqr_jgafjgY3jgb7jgZnjgYvvvIhDYW4gSSBlbXBoYXNpemUgdGhpcyBzZW50ZW5jZe-8ie-8nyoq5q6L5b-144Gq44GM44KJ44GT44Gu5paH44Gu44Gb44GE44Gn44Gn44GN44G-44Gb44KT77yIVW5mb3J0dW5hdGVseSBub3QgcG9zc2libGUgZHVlIHRvIHRoaXMgc2VudGVuY2XvvInjgIIKCuaIkeWPr-S7peW8uuiwgyoq6L-Z5LiqYGNvZGVgKirlkJfvvIhDYW4gSSBlbXBoYXNpemUgKip0aGlzIGBjb2RlYCoq77yJ77yfCgoqKuydtCDsmqnslrQoVGhpcyB0ZXJtKSoq66W8IOqwleyhsO2VtCDso7zshLjsmpQuIChQbGVhc2UgZW1waGFzaXplICoqdGhpcyB0ZXJtKiouKQo&gfm=1&engine=markdown-it + // + // You will find how poor the plain CommonMark is for CJK languages. + // + // To use this extension, configure the pipeline as follows: + // + // ```csharp + // var pipeline = new MarkdownPipelineBuilder() + // .UseCJKFriendlyEmphasis() // Add this + // .Build(); + // ``` + } +} diff --git a/src/SpecFileGen/Program.cs b/src/SpecFileGen/Program.cs index 3c04fb64a..1ad9754df 100644 --- a/src/SpecFileGen/Program.cs +++ b/src/SpecFileGen/Program.cs @@ -87,6 +87,7 @@ public RoundtripSpec(string name, string fileName, string extensions) new Spec("Jira Links", "JiraLinks.md", "jiralinks"), new Spec("Globalization", "GlobalizationSpecs.md", "globalization+advanced+emojis"), new Spec("Figures, Footers and Cites", "FigureFooterAndCiteSpecs.md", "figures+footers+citations|advanced"), + new Spec("CJK-friendly Emphasis", "CJKFriendlyEmphasis.md", "emphasisextras|advanced"), new NormalizeSpec("Headings", "Headings.md", ""), @@ -358,7 +359,7 @@ static string Escape(string input) static string CompressedName(string name) { string compressedName = ""; - foreach (var part in name.Replace(',', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries)) + foreach (var part in name.Replace(',' , ' ').Replace('-', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries)) { compressedName += char.IsLower(part[0]) ? char.ToUpper(part[0]) + (part.Length > 1 ? part.Substring(1) : "") From 5aad3d92f1aa65b510da8dacc71424638f2180f6 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Fri, 23 Jan 2026 12:39:26 +0900 Subject: [PATCH 03/10] Add name for configuration --- src/Markdig/MarkdownExtensions.cs | 5 ++++- src/SpecFileGen/Program.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Markdig/MarkdownExtensions.cs b/src/Markdig/MarkdownExtensions.cs index afcc26605..ed8485f7a 100644 --- a/src/Markdig/MarkdownExtensions.cs +++ b/src/Markdig/MarkdownExtensions.cs @@ -109,7 +109,7 @@ public static MarkdownPipelineBuilder UseAlertBlocks(this MarkdownPipelineBuilde pipeline.Extensions.ReplaceOrAdd(new AlertExtension() { RenderKind = renderKind }); return pipeline; } - + /// /// Uses this extension to enable autolinks from text `http://`, `https://`, `ftp://`, `mailto:`, `www.xxx.yyy` /// @@ -667,6 +667,9 @@ public static MarkdownPipelineBuilder Configure(this MarkdownPipelineBuilder pip case "globalization": pipeline.UseGlobalization(); break; + case "cjk-friendly-emphasis": + pipeline.UseCjkFriendlyEmphasis(); + break; default: throw new ArgumentException($"Invalid extension `{extension}` from `{extensions}`", nameof(extensions)); } diff --git a/src/SpecFileGen/Program.cs b/src/SpecFileGen/Program.cs index 1ad9754df..c531d45d1 100644 --- a/src/SpecFileGen/Program.cs +++ b/src/SpecFileGen/Program.cs @@ -87,7 +87,7 @@ public RoundtripSpec(string name, string fileName, string extensions) new Spec("Jira Links", "JiraLinks.md", "jiralinks"), new Spec("Globalization", "GlobalizationSpecs.md", "globalization+advanced+emojis"), new Spec("Figures, Footers and Cites", "FigureFooterAndCiteSpecs.md", "figures+footers+citations|advanced"), - new Spec("CJK-friendly Emphasis", "CJKFriendlyEmphasis.md", "emphasisextras|advanced"), + new Spec("CJK-friendly Emphasis", "CJKFriendlyEmphasis.md", "cjk-friendly-emphasis"), new NormalizeSpec("Headings", "Headings.md", ""), From 79f3d690e189f261b92982fae990347dab20772c Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Mon, 26 Jan 2026 08:26:13 +0900 Subject: [PATCH 04/10] Remove useless default value assignments Co-authored-by: Miha Zupan --- src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs index b926d709f..d351abfe3 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs @@ -203,8 +203,8 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) Rune.DecodeFromUtf16(htmlString, out c, out _); } - bool canOpen = false; - bool canClose = false; + bool canOpen; + bool canClose; // Calculate Open-Close for current character if (CjkFriendlyEmphasis) { From 0ba3eaf5a770c33de37de0094d7a4e1de636f45b Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Mon, 26 Jan 2026 19:02:31 +0900 Subject: [PATCH 05/10] Make `CheckOpenCloseDelimiterCjkFriendly` internal only --- src/Markdig/Helpers/CharHelper.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index 35ac3a0bd..39dab0ce2 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -150,12 +150,8 @@ private static void CheckOpenCloseDelimiter(bool prevIsWhiteSpace, bool prevIsPu } } -#if NET - public -#else - internal -#endif - static void CheckOpenCloseDelimiterCjkFriendly(Rune pc, Rune c, Rune twoPreviousRune, bool enableWithinWord, out bool canOpen, out bool canClose) + // The signature of this method is still unstable and can be changed in the future. `internal`-only as for now. + internal static void CheckOpenCloseDelimiterCjkFriendly(Rune pc, Rune c, Rune twoPreviousRune, bool enableWithinWord, out bool canOpen, out bool canClose) { pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation); c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation); From 9fc6972dc8dba88bbb4ba37891ddf77ea0ea8d6f Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Mon, 26 Jan 2026 19:15:49 +0900 Subject: [PATCH 06/10] Remove `CjkFriendlyEmphasisExtension` class --- .../CjkFriendlyEmphasisExtension.cs | 29 ------------------- src/Markdig/MarkdownExtensions.cs | 3 +- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs diff --git a/src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs b/src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs deleted file mode 100644 index 77ed33526..000000000 --- a/src/Markdig/Extensions/CjkFriendlyEmphasis/CjkFriendlyEmphasisExtension.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Alexandre Mutel. All rights reserved. -// This file is licensed under the BSD-Clause 2 license. -// See the license.txt file in the project root for more information. - -using Markdig.Parsers.Inlines; -using Markdig.Renderers; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Markdig.Extensions.CjkFriendlyEmphasis -{ - public class CjkFriendlyEmphasisExtension : IMarkdownExtension - { - public void Setup(MarkdownPipelineBuilder pipeline) - { - var parser = pipeline.InlineParsers.FindExact(); - parser?.CjkFriendlyEmphasis = true; - } - - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - var parser = pipeline.InlineParsers.FindExact(); - parser?.CjkFriendlyEmphasis = true; - } - } -} diff --git a/src/Markdig/MarkdownExtensions.cs b/src/Markdig/MarkdownExtensions.cs index ed8485f7a..7eff1e3b9 100644 --- a/src/Markdig/MarkdownExtensions.cs +++ b/src/Markdig/MarkdownExtensions.cs @@ -8,7 +8,6 @@ using Markdig.Extensions.AutoLinks; using Markdig.Extensions.Bootstrap; using Markdig.Extensions.Citations; -using Markdig.Extensions.CjkFriendlyEmphasis; using Markdig.Extensions.CustomContainers; using Markdig.Extensions.DefinitionLists; using Markdig.Extensions.Diagrams; @@ -525,7 +524,7 @@ public static MarkdownPipelineBuilder UseGlobalization(this MarkdownPipelineBuil public static MarkdownPipelineBuilder UseCjkFriendlyEmphasis(this MarkdownPipelineBuilder pipeline) { - pipeline.Extensions.AddIfNotAlready(); + pipeline.InlineParsers.FindExact()?.CjkFriendlyEmphasis = true; return pipeline; } From b254c6811de9758bf045caecf0dd34e7dc9f7ec1 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Mon, 26 Jan 2026 21:02:12 +0900 Subject: [PATCH 07/10] Add some comments including links --- src/Markdig/Helpers/CharHelper.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index 39dab0ce2..7305c3f23 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -156,8 +156,13 @@ internal static void CheckOpenCloseDelimiterCjkFriendly(Rune pc, Rune c, Rune tw pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation); c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation); + // https://github.com/tats-u/markdown-cjk-friendly/commit/3c4217bea8248e9abc8be4e7c68748a88557662d + // The above flankingness check can be simplified under the following conditions: + // - If the delimiter run is adjacent to a whitespace character, the flankingness does not depend on the existence of a punctuation character (and (in CJK-friendly emphasis) a CJK character). + // - If the delimiter run is `_`, some rules can be simplified. Additionally, in CJK-friendly emphasis, the flankingness does not depend on whether the delimiter run is adjacent to a CJK character. if (prevIsWhiteSpace || nextIsWhiteSpace) { + // Fastest path canOpen = !nextIsWhiteSpace; canClose = !prevIsWhiteSpace; return; @@ -175,6 +180,7 @@ internal static void CheckOpenCloseDelimiterCjkFriendly(Rune pc, Rune c, Rune tw canClose = nextIsPunctuation; if (!enableWithinWord) { + // Fast path for `_` (does not depend on the existence of a CJK character) return; } bool prevIsCjk = IsCjk(mainPreviousRune) || (isMainTwoPrevious ? IsCjkAmbiousPunctuation(mainPreviousRune, pc) : IsIdeographicVariationSelector(mainPreviousRune)); @@ -184,6 +190,8 @@ internal static void CheckOpenCloseDelimiterCjkFriendly(Rune pc, Rune c, Rune tw canOpen |= eitherIsCjk || !nextIsPunctuation; canClose |= eitherIsCjk || !prevIsPunctuation; + // https://github.com/tats-u/markdown-cjk-friendly/blob/main/specification.md + // https://github.com/tats-u/markdown-cjk-friendly/blob/main/ranges.md static bool IsNonEmojiGeneralUseVariantSelector(Rune r) => r.Value is >= 0xFE00 and <= 0xFE0E; static bool IsIdeographicVariationSelector(Rune r) => r.Value is >= 0xE0100 and <= 0xE01EF; static bool IsCjkAmbiousPunctuation(Rune main, Rune vs) => vs.Value is 0xFE01 && main.Value is 0x2018 or 0x2019 or 0x201C or 0x201D; From cf576528931bbc373d11e55e9bf945a076e1a1be Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Wed, 28 Jan 2026 00:31:03 +0900 Subject: [PATCH 08/10] Add direct tests on `CharHelper.CheckOpenCloseDelimiterCjkFriendly` --- src/Markdig.Tests/TestCjkFriendlyEmphasis.cs | 95 ++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs b/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs index e04118364..12d80af7e 100644 --- a/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs +++ b/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs @@ -2,7 +2,9 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using Markdig.Helpers; using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Text; @@ -101,5 +103,98 @@ public void TestCjkFriendlyPseudoEmoji(string source, string expected) var actual = Markdown.ToHtml(source, pipeline); Assert.AreEqual(expected, actual); } + + // delimiter: '*', '_' = each character, '?' = either + // can open/close = whether the places can be in the range of emphasis + // 2 before, previous, can close, delimiter, can open, next + // *****Basic***** + [TestCase("\0", " ", false, '?', false, " ")] + [TestCase("\0", "𰻞", true, '?', false, " ")] + [TestCase("\0", " ", false, '?', true, "𰻞")] + [TestCase("\0", "𝜵", false, '?', true, "A")] + [TestCase("\0", "A", true, '?', false, "𝜵")] + [TestCase("\0", "𝜵", true, '*', true, "𰻞")] + [TestCase("\0", "A", true, '*', true, "𰻞")] + [TestCase("\0", "𰻞", true, '*', true, "𝜵")] + [TestCase("\0", "𰻞", true, '*', true, "A")] + [TestCase("\0", "𰻞", true, '*', true, "」")] + [TestCase("\0", "「", true, '*', true, "𰻞")] + [TestCase("\0", "A", true, '*', true, "」")] + [TestCase("\0", "「", true, '*', true, "A")] + [TestCase("\0", "𝜵", false, '_', true, "𰻞")] + [TestCase("\0", "A", false, '_', false, "𰻞")] + [TestCase("\0", "𰻞", true, '_', false, "𝜵")] + [TestCase("\0", "𰻞", false, '_', false, "A")] + [TestCase("\0", "𰻞", true, '_', false, "」")] + [TestCase("\0", "「", false, '_', true, "𰻞")] + [TestCase("\0", "A", true, '_', false, "」")] + [TestCase("\0", "「", false, '_', true, "A")] + // *****IVS***** + [TestCase("𩸽", "\U000E0101", true, '*', true, "𝜵")] + [TestCase("𩸽", "\U000E0101", true, '_', false, "𝜵")] + [TestCase("𩸽", "\U000E0101", true, '*', true, "𝜵")] + [TestCase("𩸽", "\U000E0101", true, '_', false, "𝜵")] + // Non-Han + U+E01XX does not appear in the wild + [TestCase("\0", "\U000E0101", true, '*', true, "𝜵")] + [TestCase("\0", "\U000E0101", true, '_', false, "𝜵")] + [TestCase("\0", "\U000E0101", true, '*', true, "𝜵")] + [TestCase("\0", "\U000E0101", true, '_', false, "𝜵")] + // *****SVS***** + [TestCase("羽", "\uFE00", true, '*', true, "𝜵")] + [TestCase("羽", "\uFE00", true, '_', false, "𝜵")] + [TestCase("羽", "\uFE00", true, '*', true, "𝜵")] + [TestCase("羽", "\uFE00", true, '_', false, "𝜵")] + // Slashed zero + [TestCase("0", "\uFE00", true, '?', false, "𝜵")] + [TestCase("0", "\uFE00", true, '?', false, "𝜵")] + [TestCase("“", "\uFE00", false, '?', true, "A")] + [TestCase("“", "\uFE01", true, '*', true, "A")] + [TestCase("“", "\uFE01", false, '_', true, "A")] + [TestCase("\0", "“", false, '?', true, "A")] + [TestCase("\0", "A", true, '?', false, "“")] + // *****Emoji***** + // Default text presentation + [TestCase("\0", "㊙", true, '*', true, "A")] + [TestCase("\0", "㊙", false, '_', true, "A")] + [TestCase("\0", "A", true, '*', true, "㊙")] + [TestCase("\0", "A", true, '_', false, "㊙")] + // Default emoji presentation + [TestCase("\0", "🈯", false, '?', true, "A")] + [TestCase("\0", "A", true, '?', false, "🈯")] + // EAW = Ambiguous (not CJK) + [TestCase("\0", "☎", false, '?', true, "A")] + // Text presentation sequences + [TestCase("㊙", "\uFE0E", true, '*', true, "A")] + [TestCase("㊙", "\uFE0E", false, '_', true, "A")] + // Caution: default emoji presentation character + text presentation selector has not been supported yet + [TestCase("🈯", "\uFE0E", false, '?', true, "A")] + // Emoji presentation sequences + [TestCase("㊙", "\uFE0F", true, '*', true, "A")] + [TestCase("㊙", "\uFE0F", false, '_', false, "A")] + [TestCase("🈯", "\uFE0F", true, '*', true, "A")] + [TestCase("🈯", "\uFE0F", false, '_', false, "A")] + // *****Korean***** + [TestCase("\0", "한", true, '*', true, "𝜵")] + [TestCase("\0", "𝜵", true, '*', true, "한")] + // A part of NFD form + [TestCase("\0", "ᆫ", true, '*', true, "𝜵")] + [TestCase("\0", "𝜵", true, '*', true, "ᆫ")] + [Test] + public void TestCheckOpenCloseDelimiterCjkFriendly(string twoPrevStr, string prevStr, bool shouldBeClosable, char delim, bool shouldBeOpenable, string nextStr) + { + Assert.AreEqual(OperationStatus.Done, Rune.DecodeFromUtf16(twoPrevStr, out var twoPrev, out _)); + Assert.AreEqual(OperationStatus.Done, Rune.DecodeFromUtf16(prevStr, out var prev, out _)); + Assert.AreEqual(OperationStatus.Done, Rune.DecodeFromUtf16(nextStr, out var next, out _)); + + CharHelper.CheckOpenCloseDelimiterCjkFriendly(prev, next, twoPrev, delim == '*', out bool isOpen, out bool isClose); + Assert.AreEqual(shouldBeOpenable, isOpen, "isOpen"); + Assert.AreEqual(shouldBeClosable, isClose, "isClose"); + if (delim == '?') + { + CharHelper.CheckOpenCloseDelimiterCjkFriendly(prev, next, twoPrev, true, out isOpen, out isClose); + Assert.AreEqual(shouldBeOpenable, isOpen, "isOpen (*)"); + Assert.AreEqual(shouldBeClosable, isClose, "isClose (*)"); + } + } } } From fdefca04d6ab89d9c75ba2b5e4e7371c36b37427 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Wed, 28 Jan 2026 00:31:11 +0900 Subject: [PATCH 09/10] Fix generated tests --- src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs b/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs index 4bdc0ff36..3aa690010 100644 --- a/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs +++ b/src/Markdig.Tests/Specs/CJKFriendlyEmphasis.generated.cs @@ -30,7 +30,7 @@ public void CJKFriendlyEmphasisExtension_Example001() // Should be rendered as: //

この文を強調できますか(Can I emphasize this sentence)?残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。

- TestParser.TestSpec("**この文を強調できますか(Can I emphasize this sentence)?**残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。", "

この文を強調できますか(Can I emphasize this sentence)?残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。

", "emphasisextras|advanced", context: "Example 1\nSection CJK-friendly Emphasis Extension\n"); + TestParser.TestSpec("**この文を強調できますか(Can I emphasize this sentence)?**残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。", "

この文を強調できますか(Can I emphasize this sentence)?残念ながらこの文のせいでできません(Unfortunately not possible due to this sentence)。

", "cjk-friendly-emphasis", context: "Example 1\nSection CJK-friendly Emphasis Extension\n"); } // ````````````````````````````````` example @@ -50,7 +50,7 @@ public void CJKFriendlyEmphasisExtension_Example002() // Should be rendered as: //

이 용어(This term)를 강조해 주세요. (Please emphasize this term.)

- TestParser.TestSpec("**이 용어(This term)**를 강조해 주세요. (Please emphasize **this term**.)", "

이 용어(This term)를 강조해 주세요. (Please emphasize this term.)

", "emphasisextras|advanced", context: "Example 2\nSection CJK-friendly Emphasis Extension\n"); + TestParser.TestSpec("**이 용어(This term)**를 강조해 주세요. (Please emphasize **this term**.)", "

이 용어(This term)를 강조해 주세요. (Please emphasize this term.)

", "cjk-friendly-emphasis", context: "Example 2\nSection CJK-friendly Emphasis Extension\n"); } // You can compare the results with and without this extension: https://tats-u.github.io/markdown-cjk-friendly/?sc8=KirjgZPjga7mlofjgpLlvLfoqr_jgafjgY3jgb7jgZnjgYvvvIhDYW4gSSBlbXBoYXNpemUgdGhpcyBzZW50ZW5jZe-8ie-8nyoq5q6L5b-144Gq44GM44KJ44GT44Gu5paH44Gu44Gb44GE44Gn44Gn44GN44G-44Gb44KT77yIVW5mb3J0dW5hdGVseSBub3QgcG9zc2libGUgZHVlIHRvIHRoaXMgc2VudGVuY2XvvInjgIIKCuaIkeWPr-S7peW8uuiwgyoq6L-Z5LiqYGNvZGVgKirlkJfvvIhDYW4gSSBlbXBoYXNpemUgKip0aGlzIGBjb2RlYCoq77yJ77yfCgoqKuydtCDsmqnslrQoVGhpcyB0ZXJtKSoq66W8IOqwleyhsO2VtCDso7zshLjsmpQuIChQbGVhc2UgZW1waGFzaXplICoqdGhpcyB0ZXJtKiouKQo&gfm=1&engine=markdown-it // From 5b7519254a0bcb1e84494de1c4de6275d0af6c57 Mon Sep 17 00:00:00 2001 From: Tatsunori Uchino Date: Wed, 28 Jan 2026 20:08:27 +0900 Subject: [PATCH 10/10] Add `#if NET` --- src/Markdig.Tests/TestCjkFriendlyEmphasis.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs b/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs index 12d80af7e..59e6211c5 100644 --- a/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs +++ b/src/Markdig.Tests/TestCjkFriendlyEmphasis.cs @@ -7,7 +7,9 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; +#if NET using System.Text; +#endif using System.Threading.Tasks; namespace Markdig.Tests @@ -104,6 +106,7 @@ public void TestCjkFriendlyPseudoEmoji(string source, string expected) Assert.AreEqual(expected, actual); } +#if NET // delimiter: '*', '_' = each character, '?' = either // can open/close = whether the places can be in the range of emphasis // 2 before, previous, can close, delimiter, can open, next @@ -196,5 +199,6 @@ public void TestCheckOpenCloseDelimiterCjkFriendly(string twoPrevStr, string pre Assert.AreEqual(shouldBeClosable, isClose, "isClose (*)"); } } +#endif } }