From 7e7fecc3a89347de1dd1a373cc093c8ac72975f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enoc=20Falc=C3=B3n?= Date: Thu, 14 May 2026 03:32:41 -0400 Subject: [PATCH 1/2] Emit slug-based heading IDs for anchor link navigation Headings now receive text-derived id attributes so standard CommonMark/GFM anchor links like [text](#section) navigate to the corresponding heading in the preview. The visible [TOC] renderer uses the same slugs for its links, keeping them consistent with the new heading ids. Existing golden fixtures and a few inline assertions that assumed without attributes are updated. Four new tests cover slug generation (basic ASCII, UTF-8 preservation, punctuation handling, and emission independent of the TOC preference). Related to #429 --- MacDown/Code/Document/MPRenderer.m | 1 + MacDown/Code/Extension/hoedown_html_patch.c | 89 ++++++++++++++++++- MacDown/Code/Extension/hoedown_html_patch.h | 4 + MacDownTests/Fixtures/autolinks.html | 14 +-- MacDownTests/Fixtures/basic.html | 12 +-- MacDownTests/Fixtures/blockquotes.html | 22 ++--- MacDownTests/Fixtures/code-fenced.html | 12 +-- MacDownTests/Fixtures/code-inline.html | 10 +-- MacDownTests/Fixtures/code-languages.html | 18 ++-- MacDownTests/Fixtures/edge-cases.html | 44 ++++----- MacDownTests/Fixtures/emphasis.html | 10 +-- MacDownTests/Fixtures/horizontal-rules.html | 18 ++-- MacDownTests/Fixtures/images.html | 10 +-- MacDownTests/Fixtures/links.html | 8 +- MacDownTests/Fixtures/lists-nested.html | 12 +-- MacDownTests/Fixtures/lists-ordered.html | 14 +-- MacDownTests/Fixtures/lists-unordered.html | 14 +-- MacDownTests/Fixtures/mathjax-in-code.html | 10 +-- MacDownTests/Fixtures/mathjax-syntax.html | 16 ++-- MacDownTests/Fixtures/mixed-complex.html | 20 ++--- MacDownTests/Fixtures/regression-issue25.html | 2 +- MacDownTests/Fixtures/regression-issue34.html | 10 +-- MacDownTests/Fixtures/regression-issue36.html | 10 +-- MacDownTests/Fixtures/regression-issue37.html | 12 +-- MacDownTests/Fixtures/strikethrough.html | 12 +-- .../Fixtures/syntax-highlighting-aliases.html | 12 +-- .../syntax-highlighting-languages.html | 18 ++-- .../Fixtures/syntax-highlighting-mixed.html | 12 +-- MacDownTests/Fixtures/tables.html | 14 +-- MacDownTests/Fixtures/task-lists.html | 14 +-- MacDownTests/Fixtures/unicode.html | 42 ++++----- MacDownTests/MPHTMLExportTests.m | 8 +- MacDownTests/MPImageExportTests.m | 2 +- MacDownTests/MPMarkdownRenderingTests.m | 50 ++++++++++- MacDownTests/MPRendererEdgeCaseTests.m | 4 +- MacDownTests/MPRendererStateTests.m | 4 +- 36 files changed, 361 insertions(+), 223 deletions(-) diff --git a/MacDown/Code/Document/MPRenderer.m b/MacDown/Code/Document/MPRenderer.m index 9aec3617..c4021bef 100644 --- a/MacDown/Code/Document/MPRenderer.m +++ b/MacDown/Code/Document/MPRenderer.m @@ -423,6 +423,7 @@ NS_INLINE void add_to_languages( flags, tocLevel); htmlRenderer->blockcode = hoedown_patch_render_blockcode; htmlRenderer->listitem = hoedown_patch_render_listitem; + htmlRenderer->header = hoedown_patch_render_header; hoedown_html_renderer_state_extra *extra = hoedown_malloc(sizeof(hoedown_html_renderer_state_extra)); diff --git a/MacDown/Code/Extension/hoedown_html_patch.c b/MacDown/Code/Extension/hoedown_html_patch.c index 8cd5cec4..d2959b47 100644 --- a/MacDown/Code/Extension/hoedown_html_patch.c +++ b/MacDown/Code/Extension/hoedown_html_patch.c @@ -161,6 +161,85 @@ void hoedown_patch_render_listitem( HOEDOWN_BUFPUTSL(ob, "\n"); } +// Build a stable text-derived slug from a heading's HTML content. +// Strips HTML tags, lowercases ASCII, converts spaces to hyphens, +// drops ASCII punctuation, preserves UTF-8 multi-byte sequences so +// accented characters survive (e.g. "Introducción" -> "introducción"). +static void slugify(hoedown_buffer *out, const hoedown_buffer *content) +{ + if (!content || !content->size) + return; + + int in_tag = 0; + int last_was_dash = 1; + + for (size_t i = 0; i < content->size; i++) + { + uint8_t c = content->data[i]; + + if (in_tag) + { + if (c == '>') in_tag = 0; + continue; + } + if (c == '<') + { + in_tag = 1; + continue; + } + + if (c >= 0x80) + { + hoedown_buffer_putc(out, c); + last_was_dash = 0; + continue; + } + + if (c >= 'A' && c <= 'Z') + c += 32; + + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') + { + hoedown_buffer_putc(out, c); + last_was_dash = 0; + } + else if (c == ' ' || c == '\t' || c == '-') + { + if (!last_was_dash) + { + hoedown_buffer_putc(out, '-'); + last_was_dash = 1; + } + } + } + + while (out->size > 0 && out->data[out->size - 1] == '-') + out->size--; +} + +// rndr_header replacement that always emits a text-derived id, independent +// of the TOC nesting level. Enables [link](#section-name) navigation. +void hoedown_patch_render_header( + hoedown_buffer *ob, const hoedown_buffer *content, int level, + const hoedown_renderer_data *data) +{ + (void)data; + if (ob->size) hoedown_buffer_putc(ob, '\n'); + + hoedown_buffer *slug = hoedown_buffer_new(content ? content->size : 16); + slugify(slug, content); + if (slug->size == 0) + HOEDOWN_BUFPUTSL(slug, "section"); + + hoedown_buffer_printf(ob, "data, slug->size); + HOEDOWN_BUFPUTSL(ob, "\">"); + if (content) hoedown_buffer_put(ob, content->data, content->size); + hoedown_buffer_printf(ob, "\n", level); + + hoedown_buffer_free(slug); +} + // Adds a "toc" class to the outmost UL element to support TOC styling. void hoedown_patch_render_toc_header( hoedown_buffer *ob, const hoedown_buffer *content, int level, @@ -195,7 +274,15 @@ void hoedown_patch_render_toc_header( HOEDOWN_BUFPUTSL(ob,"\n
  • \n"); } - hoedown_buffer_printf(ob, "", state->toc_data.header_count++); + hoedown_buffer *slug = hoedown_buffer_new(content ? content->size : 16); + slugify(slug, content); + if (slug->size == 0) + HOEDOWN_BUFPUTSL(slug, "section"); + HOEDOWN_BUFPUTSL(ob, "data, slug->size); + HOEDOWN_BUFPUTSL(ob, "\">"); + hoedown_buffer_free(slug); + state->toc_data.header_count++; if (content) hoedown_buffer_put(ob, content->data, content->size); HOEDOWN_BUFPUTSL(ob, "\n"); } diff --git a/MacDown/Code/Extension/hoedown_html_patch.h b/MacDown/Code/Extension/hoedown_html_patch.h index fed3ceec..0b85c8db 100644 --- a/MacDown/Code/Extension/hoedown_html_patch.h +++ b/MacDown/Code/Extension/hoedown_html_patch.h @@ -38,6 +38,10 @@ void hoedown_patch_render_listitem( hoedown_buffer *ob, const hoedown_buffer *text, hoedown_list_flags flags, const hoedown_renderer_data *data); +void hoedown_patch_render_header( + hoedown_buffer *ob, const hoedown_buffer *content, int level, + const hoedown_renderer_data *data); + void hoedown_patch_render_toc_header( hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data); diff --git a/MacDownTests/Fixtures/autolinks.html b/MacDownTests/Fixtures/autolinks.html index c615e0d6..938f303c 100644 --- a/MacDownTests/Fixtures/autolinks.html +++ b/MacDownTests/Fixtures/autolinks.html @@ -1,6 +1,6 @@ -

    Autolink Tests

    +

    Autolink Tests

    -

    URL Autolinks

    +

    https://example.com

    @@ -10,7 +10,7 @@

    URL Autolinks

    https://example.com/path?query=param&other=value

    -

    Email Autolinks

    +

    user@example.com

    @@ -18,17 +18,17 @@

    Email Autolinks

    contact+tag@example.com

    -

    Plain URLs (may or may not auto-link depending on parser)

    +

    Visit https://example.com for more.

    Check out http://www.example.org too.

    -

    Mixed Content

    +

    Mixed Content

    Email me at user@example.com or visit https://example.com.

    -

    Edge Cases

    +

    Edge Cases

    Multiple autolinks: https://first.com and https://second.com.

    @@ -36,7 +36,7 @@

    Edge Cases

    End with autolink: https://end.com

    -

    In Lists

    +

    In Lists

    • https://example.com
    • diff --git a/MacDownTests/Fixtures/basic.html b/MacDownTests/Fixtures/basic.html index 057524b4..d5312a55 100644 --- a/MacDownTests/Fixtures/basic.html +++ b/MacDownTests/Fixtures/basic.html @@ -1,24 +1,24 @@ -

      Heading 1

      +

      Heading 1

      This is a paragraph under heading 1. It contains some text to test basic paragraph rendering.

      -

      Heading 2

      +

      Heading 2

      Another paragraph here. This one is under heading 2.

      -

      Heading 3

      +

      Heading 3

      A third paragraph for heading 3.

      -

      Heading 4

      +

      Heading 4

      Fourth level heading with text.

      -
      Heading 5
      +
      Heading 5

      Fifth level heading.

      -
      Heading 6
      +
      Heading 6

      The deepest heading level in Markdown.

      diff --git a/MacDownTests/Fixtures/blockquotes.html b/MacDownTests/Fixtures/blockquotes.html index a01c9ebd..6907b662 100644 --- a/MacDownTests/Fixtures/blockquotes.html +++ b/MacDownTests/Fixtures/blockquotes.html @@ -1,19 +1,19 @@ -

      Blockquote Tests

      +

      Blockquote Tests

      -

      Basic Blockquote

      +

      Basic Blockquote

      This is a blockquote. It can span multiple lines.

      -

      Single Line Blockquote

      +

      Single Line Blockquote

      Single line blockquote.

      -

      Nested Blockquotes

      +

      Nested Blockquotes

      First level quote

      @@ -27,7 +27,7 @@

      Nested Blockquotes

      -

      Blockquote with Multiple Paragraphs

      +

      Blockquote with Multiple Paragraphs

      First paragraph in blockquote.

      @@ -35,7 +35,7 @@

      Blockquote with Multiple Paragraphs

      Second paragraph in blockquote.

      -

      Blockquote with Formatting

      +

      Blockquote with Formatting

      This has bold text.

      @@ -45,7 +45,7 @@

      Blockquote with Formatting

      This has code.

      -

      Blockquote with Lists

      +

      Blockquote with Lists

        @@ -61,22 +61,22 @@

        Blockquote with Lists

      -

      Blockquote with Headers

      +

      Blockquote with Headers

      -

      Header in Blockquote

      +

      Header in Blockquote

      Some text under the header.

      -

      Lazy Blockquotes

      +

      Lazy Blockquotes

      This is lazy continuation without the > character.

      -

      Complex Nesting

      +

      Complex Nesting

      Top level

      diff --git a/MacDownTests/Fixtures/code-fenced.html b/MacDownTests/Fixtures/code-fenced.html index 56e8264c..4035f876 100644 --- a/MacDownTests/Fixtures/code-fenced.html +++ b/MacDownTests/Fixtures/code-fenced.html @@ -1,18 +1,18 @@ -

      Fenced Code Block Tests

      +

      Fenced Code Block Tests

      -

      Basic Fenced Block

      +

      Basic Fenced Block

      This is a basic code block.
       No language specified.
       Multiple lines.
      -

      Code Block with Content

      +

      Code Block with Content

      function example() {
           return true;
       }
      -

      Multiple Blocks

      +

      Multiple Blocks

      First block:

      @@ -22,11 +22,11 @@

      Multiple Blocks

      Block two
      -

      Empty Block

      +

      Empty Block

      -

      Block with Special Characters

      +

      Block with Special Characters

      <html>
       **Not bold**
      diff --git a/MacDownTests/Fixtures/code-inline.html b/MacDownTests/Fixtures/code-inline.html
      index 3144def7..18dd9478 100644
      --- a/MacDownTests/Fixtures/code-inline.html
      +++ b/MacDownTests/Fixtures/code-inline.html
      @@ -1,16 +1,16 @@
      -

      Inline Code Tests

      +

      Inline Code Tests

      -

      Basic Inline Code

      +

      Basic Inline Code

      Use the printf() function to print output.

      The variable userName stores the user's name.

      -

      Multiple Code Spans

      +

      Multiple Code Spans

      Use git add and then git commit to save changes.

      -

      Code with Special Characters

      +

      Code with Special Characters

      This is code with spaces.

      @@ -20,7 +20,7 @@

      Code with Special Characters

      Use **not bold** inside code.

      -

      Edge Cases

      +

      Edge Cases

      Code at start of line.

      diff --git a/MacDownTests/Fixtures/code-languages.html b/MacDownTests/Fixtures/code-languages.html index e49d9bcf..0f3229c7 100644 --- a/MacDownTests/Fixtures/code-languages.html +++ b/MacDownTests/Fixtures/code-languages.html @@ -1,49 +1,49 @@ -

      Code Blocks with Language Tags

      +

      Code Blocks with Language Tags

      -

      JavaScript

      +

      JavaScript

      function greet(name) {
           console.log("Hello, " + name);
       }
      -

      Python

      +

      Python

      def greet(name):
           print(f"Hello, {name}")
      -

      Ruby

      +

      Ruby

      def greet(name)
         puts "Hello, #{name}"
       end
      -

      HTML

      +

      HTML

      <div class="container">
           <h1>Hello World</h1>
       </div>
      -

      CSS

      +

      CSS

      .container {
           max-width: 1200px;
           margin: 0 auto;
       }
      -

      Objective-C

      +

      Objective-C

      
       - (void)greetWithName:(NSString *)name {
           NSLog(@"Hello, %@", name);
       }
      -

      Swift

      +

      Swift

      func greet(name: String) {
           print("Hello, \(name)")
       }
      -

      Bash

      +

      Bash

      #!/bin/bash
       echo "Hello, World"
      diff --git a/MacDownTests/Fixtures/edge-cases.html b/MacDownTests/Fixtures/edge-cases.html index 22655924..514ea273 100644 --- a/MacDownTests/Fixtures/edge-cases.html +++ b/MacDownTests/Fixtures/edge-cases.html @@ -1,12 +1,12 @@ -

      Edge Cases and Special Characters

      +

      Edge Cases and Special Characters

      -

      Empty Lines

      +

      Empty Lines

      This paragraph has an empty line after it.

      This paragraph has two empty lines before it.

      -

      Special Characters

      +

      Special Characters

      Ampersand: & &

      @@ -16,7 +16,7 @@

      Special Characters

      Quotes: "double" and 'single'

      -

      Escaping

      +

      Escaping

      *Not italic*

      @@ -28,7 +28,7 @@

      Escaping

      ~~Not strikethrough~~

      -

      HTML Entities

      +

      HTML Entities

      © ® ™

      @@ -36,7 +36,7 @@

      HTML Entities

        (non-breaking space)

      -

      Unicode

      +

      Unicode

      Emoji: 😀 🚀 ⭐

      @@ -46,15 +46,15 @@

      Unicode

      Arrows: → ← ↑ ↓

      -

      Edge Case Headers

      +

      Edge Case Headers

      -

      No space after hash

      +

      No space after hash

      -

      Trailing hash

      +

      Trailing hash

      -

      Multiple##Hashes

      +

      Multiple##Hashes

      -

      Consecutive Formatting

      +

      Consecutive Formatting

      BoldMore bold

      @@ -62,7 +62,7 @@

      Consecutive Formatting

      All together

      -

      Malformed Markdown

      +

      Malformed Markdown

      [Link without URL]

      @@ -74,7 +74,7 @@

      Malformed Markdown

      ~~Unclosed strikethrough

      -

      Line Breaks

      +

      Line Breaks

      Line with two spaces at end Should create line break.

      @@ -82,19 +82,19 @@

      Line Breaks

      Line without spaces Should not break.

      -

      Tabs vs Spaces

      +

      Tabs vs Spaces

      Tab-indented text
       
       Four-space indented text
      -

      Mixed Whitespace

      +

      Mixed Whitespace

      Text with multiple spaces.

      Text with tabs.

      -

      URL Edge Cases

      +

      URL Edge Cases

      https://example.com/path?query=1&other=2

      @@ -102,7 +102,7 @@

      URL Edge Cases

      ftp://files.example.com

      -

      Numbers and Punctuation

      +

      Numbers and Punctuation

      1. Not a list (no space).
      2. @@ -110,9 +110,9 @@

        Numbers and Punctuation

        100.5 decimal number

        -

        1 hashtag number

        +

        1 hashtag number

        -

        Backslashes

        +

        Backslashes

        Backslash: \

        @@ -120,7 +120,7 @@

        Backslashes

        Network: \\server\share

        -

        Parentheses and Brackets

        +

        Parentheses and Brackets

        (Text in parentheses)

        @@ -130,7 +130,7 @@

        Parentheses and Brackets

        -

        Consecutive Punctuation

        +

        Consecutive Punctuation

        Multiple periods...

        @@ -140,7 +140,7 @@

        Consecutive Punctuation

        Mixed?!?!

        -

        Empty Elements

        +

        Empty Elements

        []

        diff --git a/MacDownTests/Fixtures/emphasis.html b/MacDownTests/Fixtures/emphasis.html index d8c5dc3a..e6405865 100644 --- a/MacDownTests/Fixtures/emphasis.html +++ b/MacDownTests/Fixtures/emphasis.html @@ -1,18 +1,18 @@ -

        Emphasis Tests

        +

        Emphasis Tests

        -

        Italic

        +

        Italic

        This is italic text with asterisks.

        This is italic text with underscores.

        -

        Bold

        +

        Bold

        This is bold text with asterisks.

        This is bold text with underscores.

        -

        Combined Emphasis

        +

        Combined Emphasis

        This is bold and italic with asterisks.

        @@ -22,7 +22,7 @@

        Combined Emphasis

        This is *italic with bold inside*.

        -

        Edge Cases

        +

        Edge Cases

        Notactuallyitalic because no spaces.

        diff --git a/MacDownTests/Fixtures/horizontal-rules.html b/MacDownTests/Fixtures/horizontal-rules.html index 60eafad0..b97e4a5a 100644 --- a/MacDownTests/Fixtures/horizontal-rules.html +++ b/MacDownTests/Fixtures/horizontal-rules.html @@ -1,6 +1,6 @@ -

        Horizontal Rule Tests

        +

        Horizontal Rule Tests

        -

        Three Hyphens

        +

        Three Hyphens

        Text before rule.

        @@ -8,7 +8,7 @@

        Three Hyphens

        Text after rule.

        -

        Three Asterisks

        +

        Three Asterisks

        Text before rule.

        @@ -16,7 +16,7 @@

        Three Asterisks

        Text after rule.

        -

        Three Underscores

        +

        Three Underscores

        Text before rule.

        @@ -24,7 +24,7 @@

        Three Underscores

        Text after rule.

        -

        More Than Three

        +

        More Than Three


        @@ -32,7 +32,7 @@

        More Than Three


        -

        With Spaces

        +

        With Spaces


        @@ -40,7 +40,7 @@

        With Spaces


        -

        Multiple Rules

        +

        Multiple Rules

        First section.

        @@ -56,9 +56,9 @@

        Multiple Rules

        Fourth section.

        -

        Rules in Context

        +

        Rules in Context

        -

        Heading

        +

        Heading

        Some text here.

        diff --git a/MacDownTests/Fixtures/images.html b/MacDownTests/Fixtures/images.html index 80d2faea..bb9aa1af 100644 --- a/MacDownTests/Fixtures/images.html +++ b/MacDownTests/Fixtures/images.html @@ -1,6 +1,6 @@ -

        Image Tests

        +

        Image Tests

        -

        Inline Images

        +

        Inline Images

        Alt text

        @@ -8,17 +8,17 @@

        Inline Images

        -

        Reference Images

        +

        Reference Images

        Reference image

        Another reference

        -

        Images in Links

        +

        Linked image

        -

        Edge Cases

        +

        Edge Cases

        Alt text with **bold**

        diff --git a/MacDownTests/Fixtures/links.html b/MacDownTests/Fixtures/links.html index 3874cbc8..8ec12520 100644 --- a/MacDownTests/Fixtures/links.html +++ b/MacDownTests/Fixtures/links.html @@ -1,6 +1,6 @@ -

        Link Tests

        +

        Link Tests

        -

        Inline Links

        +

        This is an inline link.

        @@ -8,7 +8,7 @@

        Inline Links

        This is a link to a path.

        -

        Reference Links

        +

        This is a reference link.

        @@ -16,7 +16,7 @@

        Reference Links

        This uses implicit reference.

        -

        Edge Cases

        +

        Edge Cases

        Link at start of line.

        diff --git a/MacDownTests/Fixtures/lists-nested.html b/MacDownTests/Fixtures/lists-nested.html index c126a442..b543fbf0 100644 --- a/MacDownTests/Fixtures/lists-nested.html +++ b/MacDownTests/Fixtures/lists-nested.html @@ -1,6 +1,6 @@ -

        Nested List Tests

        +

        Nested List Tests

        -

        Nested Unordered Lists

        +

        Nested Unordered Lists

        • First level @@ -13,7 +13,7 @@

          Nested Unordered Lists

        • Back to first level
        -

        Nested Ordered Lists

        +

        Nested Ordered Lists

        1. First level @@ -30,7 +30,7 @@

          Nested Ordered Lists

        2. Back to first level
        -

        Mixed Nested Lists

        +

        Mixed Nested Lists

        • Unordered first @@ -48,7 +48,7 @@

          Mixed Nested Lists

        • Unordered first again
        -

        Nested with Content

        +

        Nested with Content

        1. First item

          @@ -66,7 +66,7 @@

          Nested with Content

        2. Second item

        -

        Deep Nesting

        +

        Deep Nesting

        • Level 1 diff --git a/MacDownTests/Fixtures/lists-ordered.html b/MacDownTests/Fixtures/lists-ordered.html index 98953fc2..5c3d947b 100644 --- a/MacDownTests/Fixtures/lists-ordered.html +++ b/MacDownTests/Fixtures/lists-ordered.html @@ -1,6 +1,6 @@ -

          Ordered List Tests

          +

          Ordered List Tests

          -

          Basic Ordered List

          +

          Basic Ordered List

          1. First item
          2. @@ -8,7 +8,7 @@

            Basic Ordered List

          3. Third item
          -

          List Starting with Different Number

          +

          List Starting with Different Number

          1. Fifth item
          2. @@ -16,7 +16,7 @@

            List Starting with Different Number

          3. Seventh item
          -

          List with All Ones

          +

          List with All Ones

          1. First item
          2. @@ -24,7 +24,7 @@

            List with All Ones

          3. Third item (still numbered 3)
          -

          List with Paragraphs

          +

          List with Paragraphs

          1. First item with text

          2. @@ -32,7 +32,7 @@

            List with Paragraphs

          3. Third item

          -

          List with Inline Formatting

          +

          List with Inline Formatting

          1. Bold item
          2. @@ -41,7 +41,7 @@

            List with Inline Formatting

          3. Link item
          -

          Tight vs Loose Lists

          +

          Tight vs Loose Lists

          Tight list:

          diff --git a/MacDownTests/Fixtures/lists-unordered.html b/MacDownTests/Fixtures/lists-unordered.html index 03661cc5..8ecfc94d 100644 --- a/MacDownTests/Fixtures/lists-unordered.html +++ b/MacDownTests/Fixtures/lists-unordered.html @@ -1,6 +1,6 @@ -

          Unordered List Tests

          +

          Unordered List Tests

          -

          Basic List with Asterisks

          +

          Basic List with Asterisks

          • First item
          • @@ -8,7 +8,7 @@

            Basic List with Asterisks

          • Third item
          -

          Basic List with Hyphens

          +

          Basic List with Hyphens

          • First item
          • @@ -16,7 +16,7 @@

            Basic List with Hyphens

          • Third item
          -

          Basic List with Plus

          +

          Basic List with Plus

          • First item
          • @@ -24,7 +24,7 @@

            Basic List with Plus

          • Third item
          -

          List with Paragraphs

          +

          List with Paragraphs

          • First item with text

          • @@ -32,7 +32,7 @@

            List with Paragraphs

          • Third item

          -

          List with Inline Formatting

          +

          List with Inline Formatting

          • Bold item
          • @@ -41,7 +41,7 @@

            List with Inline Formatting

          • Link item
          -

          Tight vs Loose Lists

          +

          Tight vs Loose Lists

          Tight list:

          diff --git a/MacDownTests/Fixtures/mathjax-in-code.html b/MacDownTests/Fixtures/mathjax-in-code.html index 61df5c3f..59ae6366 100644 --- a/MacDownTests/Fixtures/mathjax-in-code.html +++ b/MacDownTests/Fixtures/mathjax-in-code.html @@ -1,19 +1,19 @@ -

          MathJax in Code Blocks

          +

          MathJax in Code Blocks

          -

          Regular Math

          +

          Regular Math

          This math should be processed: ( x^2 + y^2 = z^2 )

          -

          Math in Code Block

          +

          Math in Code Block

          This math should NOT be processed: \( x^2 + y^2 = z^2 \)
           And display math: $$ E = mc^2 $$
          -

          Math in Inline Code

          +

          Math in Inline Code

          This math in inline code should be literal: \( x^2 \) and $$ E = mc^2 $$

          -

          More Regular Math

          +

          More Regular Math

          Display math should be processed:

          diff --git a/MacDownTests/Fixtures/mathjax-syntax.html b/MacDownTests/Fixtures/mathjax-syntax.html index a2e0169e..fe3188e3 100644 --- a/MacDownTests/Fixtures/mathjax-syntax.html +++ b/MacDownTests/Fixtures/mathjax-syntax.html @@ -1,12 +1,12 @@ -

          MathJax Syntax Tests

          +

          MathJax Syntax Tests

          -

          Inline Math

          +

          Inline Math

          The Pythagorean theorem states that ( a^2 + b^2 = c^2 ) for right triangles.

          Einstein's famous equation is ( E = mc^2 ).

          -

          Display Math

          +

          Display Math

          The quadratic formula is:

          @@ -14,13 +14,13 @@

          Display Math

          x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} $$

          -

          Integral

          +

          Integral

          $$ \int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2} $$

          -

          Matrix

          +

          Matrix

          $$ \begin{bmatrix} @@ -30,11 +30,11 @@

          Matrix

          \end{bmatrix} $$

          -

          Greek Letters

          +

          Greek Letters

          Symbols like ( \alpha ), ( \beta ), ( \gamma ), and ( \Delta ) are common in mathematics.

          -

          Sum and Product

          +

          Sum and Product

          $$ \sum_{i=1}^{n} i = \frac{n(n+1)}{2} @@ -44,6 +44,6 @@

          Sum and Product

          \prod_{i=1}^{n} i = n! $$

          -

          Fractions and Subscripts

          +

          Fractions and Subscripts

          The formula ( \frac{x1 + x2}{2} ) calculates the average.

          diff --git a/MacDownTests/Fixtures/mixed-complex.html b/MacDownTests/Fixtures/mixed-complex.html index f8b66cca..009471ca 100644 --- a/MacDownTests/Fixtures/mixed-complex.html +++ b/MacDownTests/Fixtures/mixed-complex.html @@ -1,8 +1,8 @@ -

          MacDown Feature Guide

          +

          MacDown Feature Guide

          Welcome to MacDown, a powerful Markdown editor for macOS.

          -

          Introduction

          +

          Introduction

          MacDown is an open source Markdown editor that supports:

          @@ -15,7 +15,7 @@

          Introduction


          -

          Code Examples

          +

          Code Examples

          Here's a simple JavaScript function:

          @@ -30,7 +30,7 @@

          Code Examples

          Inline code like const x = 42; is also supported.

          -

          Feature Comparison

          +

          Feature Comparison

          @@ -65,7 +65,7 @@

          Feature Comparison

          -

          Task List

          +

          Task List

          Development progress:

          @@ -89,9 +89,9 @@

          Task List

        -

        Advanced Features

        +

        Advanced Features

        -

        Nested Lists and Quotes

        +

        Nested Lists and Quotes

        Here's what users are saying:

        @@ -108,13 +108,13 @@

        Nested Lists and Quotes

      -

      Images and Links

      +

      Check out the logo: MacDown Logo

      Visit our website at https://macdown.example.com.

      -

      Complex Nesting

      +

      Complex Nesting

      1. Installation Steps

        @@ -153,7 +153,7 @@

        Complex Nesting


        -

        Contact

        +

        Contact

        Email us at support@macdown.example.com or open an issue on GitHub.

        diff --git a/MacDownTests/Fixtures/regression-issue25.html b/MacDownTests/Fixtures/regression-issue25.html index 0770c2d2..c6d8ca3a 100644 --- a/MacDownTests/Fixtures/regression-issue25.html +++ b/MacDownTests/Fixtures/regression-issue25.html @@ -1,4 +1,4 @@ -

        Adjacent Shortcut Links (Issue #25)

        +

        Adjacent Shortcut Links (Issue #25)

        Two shortcut-style links side by side should both render:

        diff --git a/MacDownTests/Fixtures/regression-issue34.html b/MacDownTests/Fixtures/regression-issue34.html index 2a2f499a..c75555fb 100644 --- a/MacDownTests/Fixtures/regression-issue34.html +++ b/MacDownTests/Fixtures/regression-issue34.html @@ -1,6 +1,6 @@ -

        Lists After Colons (Issue #34)

        +

        Lists After Colons (Issue #34)

        -

        Test Case 1: Basic list after colon

        +

        Test Case 1: Basic list after colon

        Here is my list:

        @@ -10,7 +10,7 @@

        Test Case 1: Basic list after colon

      2. Item 3
    -

    Test Case 2: Ordered list after colon

    +

    Test Case 2: Ordered list after colon

    My grocery list:

    @@ -20,7 +20,7 @@

    Test Case 2: Ordered list after colon

  • Bread
  • -

    Test Case 3: Nested list after colon

    +

    Test Case 3: Nested list after colon

    My tasks:

    @@ -39,7 +39,7 @@

    Test Case 3: Nested list after colon

    -

    Test Case 4: List with proper blank line (works correctly)

    +

    Test Case 4: List with proper blank line (works correctly)

    This should work:

    diff --git a/MacDownTests/Fixtures/regression-issue36.html b/MacDownTests/Fixtures/regression-issue36.html index 01a69e15..6f06e48d 100644 --- a/MacDownTests/Fixtures/regression-issue36.html +++ b/MacDownTests/Fixtures/regression-issue36.html @@ -1,6 +1,6 @@ -

    Code Blocks Without Blank Lines (Issue #36)

    +

    Code Blocks Without Blank Lines (Issue #36)

    -

    Test Case 1: Fenced code block immediately after text

    +

    Test Case 1: Fenced code block immediately after text

    Here is some code:

    @@ -8,7 +8,7 @@

    Test Case 1: Fenced code block immediately after text

    return true; }
    -

    Test Case 2: Multiple code blocks in sequence

    +

    Test Case 2: Multiple code blocks in sequence

    First block:

    @@ -19,7 +19,7 @@

    Test Case 2: Multiple code blocks in sequence

    y = 100
    -

    Test Case 3: Code block with proper blank line (works correctly)

    +

    Test Case 3: Code block with proper blank line (works correctly)

    This should work:

    @@ -27,7 +27,7 @@

    Test Case 3: Code block with proper blank line (works correctly)

    return 'yes'; } -

    Test Case 4: Code block in list without blank line

    +

    Test Case 4: Code block in list without blank line

    Steps to follow:

    diff --git a/MacDownTests/Fixtures/regression-issue37.html b/MacDownTests/Fixtures/regression-issue37.html index 07e9e6f1..f3ae8cf1 100644 --- a/MacDownTests/Fixtures/regression-issue37.html +++ b/MacDownTests/Fixtures/regression-issue37.html @@ -1,19 +1,19 @@ -

    Square Brackets in Code (Issue #37)

    +

    Square Brackets in Code (Issue #37)

    -

    Test Case 1: TypeScript index signature

    +

    Test Case 1: TypeScript index signature

    interface MyType {
       [key: string]​: any;
     }
    -

    Test Case 2: JavaScript array access

    +

    Test Case 2: JavaScript array access

    const value = array[index];
     const obj = {
       [computed]​: 'value'
     };
    -

    Test Case 3: Multiple bracket patterns

    +

    Test Case 3: Multiple bracket patterns

    type Dict = {
       [id: number]​: string;
    @@ -25,11 +25,11 @@ 

    Test Case 3: Multiple bracket patterns

    }; }
    -

    Test Case 4: Inline code with brackets

    +

    Test Case 4: Inline code with brackets

    Here's an example: array[0] and obj[key] should work.

    -

    Test Case 5: Python dictionary syntax

    +

    Test Case 5: Python dictionary syntax

    my_dict = {
         "key": "value"
    diff --git a/MacDownTests/Fixtures/strikethrough.html b/MacDownTests/Fixtures/strikethrough.html
    index f1baac64..a6323467 100644
    --- a/MacDownTests/Fixtures/strikethrough.html
    +++ b/MacDownTests/Fixtures/strikethrough.html
    @@ -1,12 +1,12 @@
    -

    Strikethrough Tests

    +

    Strikethrough Tests

    -

    Basic Strikethrough

    +

    Basic Strikethrough

    This is strikethrough text.

    Entire line strikethrough

    -

    Combined with Other Formatting

    +

    Combined with Other Formatting

    This is strikethrough with bold.

    @@ -16,11 +16,11 @@

    Combined with Other Formatting

    This is italic with strikethrough.

    -

    Multiple Strikethroughs

    +

    Multiple Strikethroughs

    First and second strikethrough.

    -

    Edge Cases

    +

    Edge Cases

    Not~strikethrough (single tilde).

    @@ -30,7 +30,7 @@

    Edge Cases

    End of line has strikethrough.

    -

    In Lists

    +

    In Lists

    • Strikethrough list item
    • diff --git a/MacDownTests/Fixtures/syntax-highlighting-aliases.html b/MacDownTests/Fixtures/syntax-highlighting-aliases.html index e1341c77..b724674c 100644 --- a/MacDownTests/Fixtures/syntax-highlighting-aliases.html +++ b/MacDownTests/Fixtures/syntax-highlighting-aliases.html @@ -1,16 +1,16 @@ -

      Language Aliases Test

      +

      Language Aliases Test

      -

      JavaScript (js alias)

      +

      JavaScript (js alias)

      const greeting = "Hello, World!";
       console.log(greeting);
      -

      Objective-C (objc alias)

      +

      Objective-C (objc alias)

      NSString *greeting = @"Hello, World!";
       NSLog(@"%@", greeting);
      -

      HTML (maps to markup)

      +

      HTML (maps to markup)

      <!DOCTYPE html>
       <html>
      @@ -19,12 +19,12 @@ 

      HTML (maps to markup)

      </body> </html>
      -

      Shell (sh alias for bash)

      +

      Shell (sh alias for bash)

      #!/bin/sh
       echo "Hello, World!"
      -

      C++ (c++ and cpp aliases)

      +

      C++ (c++ and cpp aliases)

      #include <iostream>
       int main() {
      diff --git a/MacDownTests/Fixtures/syntax-highlighting-languages.html b/MacDownTests/Fixtures/syntax-highlighting-languages.html
      index e947631a..454bba11 100644
      --- a/MacDownTests/Fixtures/syntax-highlighting-languages.html
      +++ b/MacDownTests/Fixtures/syntax-highlighting-languages.html
      @@ -1,41 +1,41 @@
      -

      Common Programming Languages

      +

      Common Programming Languages

      -

      JavaScript

      +

      JavaScript

      function fibonacci(n) {
           if (n <= 1) return n;
           return fibonacci(n - 1) + fibonacci(n - 2);
       }
      -

      Python

      +

      Python

      def fibonacci(n):
           if n <= 1:
               return n
           return fibonacci(n - 1) + fibonacci(n - 2)
      -

      C

      +

      C

      int fibonacci(int n) {
           if (n <= 1) return n;
           return fibonacci(n - 1) + fibonacci(n - 2);
       }
      -

      C++

      +

      C++

      int fibonacci(int n) {
           if (n <= 1) return n;
           return fibonacci(n - 1) + fibonacci(n - 2);
       }
      -

      Java

      +

      Java

      public int fibonacci(int n) {
           if (n <= 1) return n;
           return fibonacci(n - 1) + fibonacci(n - 2);
       }
      -

      Go

      +

      Go

      func fibonacci(n int) int {
           if n <= 1 {
      @@ -44,7 +44,7 @@ 

      Go

      return fibonacci(n-1) + fibonacci(n-2) }
      -

      Rust

      +

      Rust

      fn fibonacci(n: u32) -> u32 {
           match n {
      @@ -54,7 +54,7 @@ 

      Rust

      } }
      -

      TypeScript

      +

      TypeScript

      function fibonacci(n: number): number {
           if (n <= 1) return n;
      diff --git a/MacDownTests/Fixtures/syntax-highlighting-mixed.html b/MacDownTests/Fixtures/syntax-highlighting-mixed.html
      index d6aafc0f..4b1dc860 100644
      --- a/MacDownTests/Fixtures/syntax-highlighting-mixed.html
      +++ b/MacDownTests/Fixtures/syntax-highlighting-mixed.html
      @@ -1,24 +1,24 @@
      -

      Mixed Code Blocks

      +

      Mixed Code Blocks

      -

      Labeled Code Block

      +

      Labeled Code Block

      def hello():
           print("Hello from Python")
      -

      Unlabeled Code Block

      +

      Unlabeled Code Block

      This code has no language specified.
       It should get the "language-none" class.
      -

      Another Labeled Block

      +

      Another Labeled Block

      console.log("Hello from JavaScript");
      -

      Indented Code Block

      +

      Indented Code Block

      This is indented code.
       It should not have language classes.
      -

      Inline Code

      +

      Inline Code

      Here is some inline code that should not have language classes.

      diff --git a/MacDownTests/Fixtures/tables.html b/MacDownTests/Fixtures/tables.html index ffc2b0b5..c96e4978 100644 --- a/MacDownTests/Fixtures/tables.html +++ b/MacDownTests/Fixtures/tables.html @@ -1,6 +1,6 @@ -

      Table Tests

      +

      Table Tests

      -

      Basic Table

      +

      Basic Table

      @@ -25,7 +25,7 @@

      Basic Table

      -

      Table with Alignment

      +

      Table with Alignment

      @@ -55,14 +55,14 @@

      Table with Alignment

      -

      Minimal Table

      +

      Minimal Table

      | A | B | | - | - | | 1 | 2 | | 3 | 4 |

      -

      Table with Inline Formatting

      +

      Table with Inline Formatting

      @@ -87,7 +87,7 @@

      Table with Inline Formatting

      -

      Table with Different Column Widths

      +

      Table with Different Column Widths

      @@ -112,7 +112,7 @@

      Table with Different Column Widths

      -

      Empty Cells

      +

      Empty Cells

      diff --git a/MacDownTests/Fixtures/task-lists.html b/MacDownTests/Fixtures/task-lists.html index e197a15d..b3b3f267 100644 --- a/MacDownTests/Fixtures/task-lists.html +++ b/MacDownTests/Fixtures/task-lists.html @@ -1,6 +1,6 @@ -

      Task List Tests

      +

      Task List Tests

      -

      Basic Task Lists

      +

      Basic Task Lists

      • Unchecked task @@ -12,7 +12,7 @@

        Basic Task Lists

      • [X] Checked with capital X
      -

      Nested Task Lists

      +

      Nested Task Lists

      • Parent task @@ -30,7 +30,7 @@

        Nested Task Lists

      -

      Mixed with Regular Lists

      +

      Mixed with Regular Lists

      • Regular list item
      • @@ -42,7 +42,7 @@

        Mixed with Regular Lists

      • Final regular item
      -

      Tasks with Inline Formatting

      +

      Tasks with Inline Formatting

      • Task with bold text @@ -55,7 +55,7 @@

        Tasks with Inline Formatting

      -

      Numbered Task Lists

      +

      Numbered Task Lists

      1. First task @@ -66,7 +66,7 @@

        Numbered Task Lists

      -

      Edge Cases

      +

      Edge Cases

      • [] Invalid (no space)
      • diff --git a/MacDownTests/Fixtures/unicode.html b/MacDownTests/Fixtures/unicode.html index 70cb12a7..81672610 100644 --- a/MacDownTests/Fixtures/unicode.html +++ b/MacDownTests/Fixtures/unicode.html @@ -1,82 +1,82 @@ -

        Unicode Test

        +

        Unicode Test

        -

        Basic Latin and Symbols

        +

        Basic Latin and Symbols

        Regular text with special characters: © ® ™ § ¶

        -

        Accented Characters

        +

        Accented Characters

        Café, naïve, résumé, piñata, Zürich

        -

        Currency Symbols

        +

        Currency Symbols

        Dollar: $ Euro: € Pound: £ Yen: ¥ Bitcoin: ₿

        -

        Mathematical Symbols

        +

        Mathematical Symbols

        ∀ ∃ ∅ ∞ ∫ ∑ ∏ ≈ ≠ ≤ ≥ ± × ÷

        -

        Arrows

        +

        Arrows

        ← → ↑ ↓ ↔ ⇐ ⇒ ⇔

        -

        Chinese Characters

        +

        Chinese Characters

        你好世界 (Hello World)

        中文字符测试

        -

        Japanese Characters

        +

        Japanese Characters

        こんにちは世界 (Hello World)

        ひらがな、カタカナ、漢字

        -

        Korean Characters

        +

        Korean Characters

        안녕하세요 세계 (Hello World)

        한글 테스트

        -

        Emoji

        +

        Emoji

        😀 😃 😄 😁 🚀 🌟 ⭐ 💻 📱 🎉 ✨

        -

        Arabic

        +

        Arabic

        مرحبا بالعالم (Hello World)

        -

        Hebrew

        +

        Hebrew

        שלום עולם (Hello World)

        -

        Cyrillic

        +

        Cyrillic

        Привет мир (Hello World)

        -

        Greek

        +

        Greek

        Γεια σου κόσμε (Hello World)

        -

        Special Punctuation

        +

        Special Punctuation

        Em dash: — En dash: – Ellipsis: … Quotes: " " ' '

        -

        Combining Characters

        +

        Combining Characters

        Café (with combining accent: Café)

        -

        Zero-Width Characters

        +

        Zero-Width Characters

        Zero-width space: ​ (invisible)

        -

        Code Block with Unicode

        +

        Code Block with Unicode

        def greet():
             print("Hello 世界 🌍")
             # Comment with unicode: ñ á é í ó ú
        -

        Bullet List with Unicode

        +

        Bullet List with Unicode

        • Item with emoji 🎯
        • @@ -85,10 +85,10 @@

          Bullet List with Unicode

        • Item with special char ©
        -

        Links with Unicode

        +

        Unicode Test 测试

        -

        Emphasis with Unicode

        +

        Emphasis with Unicode

        Bold 粗体 and italic 斜体

        diff --git a/MacDownTests/MPHTMLExportTests.m b/MacDownTests/MPHTMLExportTests.m index 7344b518..9ab68f9b 100644 --- a/MacDownTests/MPHTMLExportTests.m +++ b/MacDownTests/MPHTMLExportTests.m @@ -401,7 +401,7 @@ - (void)testHTMLExportWritesToFile error:&error]; XCTAssertNil(error, @"Should read file without error"); XCTAssertEqualObjects(readBack, html, @"Content should match"); - XCTAssertTrue([readBack containsString:@"

        Test Document

        "], @"Should contain heading"); + XCTAssertTrue([readBack containsString:@"

        Test Document

        "], @"Should contain heading"); XCTAssertTrue([readBack containsString:@"paragraph"], @"Should contain bold text"); } @@ -431,7 +431,7 @@ - (void)testHTMLExportOverwritesExistingFile NSString *readBack = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:&error]; - XCTAssertTrue([readBack containsString:@"

        New Content

        "], @"Should have new content"); + XCTAssertTrue([readBack containsString:@"

        New Content

        "], @"Should have new content"); XCTAssertFalse([readBack containsString:@"Initial"], @"Should not have old content"); } @@ -460,8 +460,8 @@ - (void)testHTMLExportCreatesValidStructure @"Should be complete HTML document"); XCTAssertTrue([content containsString:@""], @"Should have head"); XCTAssertTrue([content containsString:@""], @"Should have h1"); - XCTAssertTrue([content containsString:@"

        "], @"Should have h2"); + XCTAssertTrue([content containsString:@"

        "], @"Should have list"); XCTAssertTrue([content containsString:@"
      • "], @"Should have list items"); XCTAssertTrue([content containsString:@"
        "] || [content containsString:@""],
        diff --git a/MacDownTests/MPImageExportTests.m b/MacDownTests/MPImageExportTests.m
        index 64a48244..9c297443 100644
        --- a/MacDownTests/MPImageExportTests.m
        +++ b/MacDownTests/MPImageExportTests.m
        @@ -405,7 +405,7 @@ - (void)testExportWithImagesAndText
             [self.renderer parseMarkdown:self.dataSource.markdown];
             NSString *html = [self.renderer HTMLForExportWithStyles:NO highlighting:NO];
         
        -    XCTAssertTrue([html containsString:@"

        "], @"Should have heading"); + XCTAssertTrue([html containsString:@"

        "], - @"CRLF heading should render as

        "); + XCTAssertTrue([crlfHtml containsString:@"

        element"); XCTAssertTrue([crlfHtml containsString:@"Heading"], @"CRLF heading text should appear in output"); XCTAssertTrue([crlfHtml containsString:@"

        "], @@ -753,4 +753,50 @@ - (void)testFencedCodeAfterTextWithCRLFLineEndings @"CRLF and LF fenced-code-after-text should produce identical HTML"); } + +#pragma mark - Heading Anchor ID Tests + +// Headings should always receive a text-derived id attribute so that +// CommonMark/GFM-style anchor links like [text](#section) navigate to +// the corresponding heading in the preview. + +- (void)testHeadingHasSlugBasedAnchorId +{ + NSString *html = [self renderMarkdown:@"## Foo Bar" + withExtensions:0 + rendererFlags:0]; + XCTAssertTrue([html containsString:@"id=\"foo-bar\""], + @"Heading should have a slug-based id derived from its text. Got: %@", html); +} + +- (void)testHeadingAnchorIdPreservesUTF8 +{ + NSString *html = [self renderMarkdown:@"## Introducción" + withExtensions:0 + rendererFlags:0]; + XCTAssertTrue([html containsString:@"id=\"introducción\""], + @"Heading id should preserve UTF-8 multi-byte characters. Got: %@", html); +} + +- (void)testHeadingAnchorIdStripsPunctuation +{ + NSString *html = [self renderMarkdown:@"# Hello, World!" + withExtensions:0 + rendererFlags:0]; + XCTAssertTrue([html containsString:@"id=\"hello-world\""], + @"Heading id should drop ASCII punctuation. Got: %@", html); +} + +- (void)testHeadingAnchorIdEmittedWithoutTOCPreference +{ + // Regression guard: ids must be emitted regardless of the + // "Detect TOC token" preference state. + self.delegate.renderTOC = NO; + NSString *html = [self renderMarkdown:@"### Some Section" + withExtensions:0 + rendererFlags:0]; + XCTAssertTrue([html containsString:@"id=\"some-section\""], + @"Heading id must be emitted independent of TOC preference. Got: %@", html); +} + @end diff --git a/MacDownTests/MPRendererEdgeCaseTests.m b/MacDownTests/MPRendererEdgeCaseTests.m index 63a2909f..64039d9c 100644 --- a/MacDownTests/MPRendererEdgeCaseTests.m +++ b/MacDownTests/MPRendererEdgeCaseTests.m @@ -534,8 +534,8 @@ - (void)testRendererWithManyHeadings NSString *html = [self.renderer HTMLForExportWithStyles:NO highlighting:NO]; XCTAssertNotNil(html, @"Should handle many headings"); - XCTAssertTrue([html containsString:@"

        "], @"Should have h1"); - XCTAssertTrue([html containsString:@"

        "], @"Should have h6"); + XCTAssertTrue([html containsString:@"

        "], + XCTAssertTrue([html containsString:@"

        "], + XCTAssertTrue([html containsString:@"

        Date: Thu, 14 May 2026 03:32:49 -0400 Subject: [PATCH 2/2] Apply heading anchor IDs to Quick Look renderer for parity MPQuickLookRenderer uses its own parallel Hoedown setup, so without this the anchor link fix would only apply to the main preview pane and not to Finder Quick Look previews of the same file. The slugify helper and header renderer are duplicated rather than shared because MacDownCore is a separate Xcode target without access to MacDown's hoedown_html_patch.c. Sharing them would require restructuring the project, which is out of scope here. Related to #429 --- MacDownCore/MPQuickLookRenderer.m | 79 +++++++++++++++++++++++++ MacDownTests/MPQuickLookRendererTests.m | 8 +-- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/MacDownCore/MPQuickLookRenderer.m b/MacDownCore/MPQuickLookRenderer.m index 212f2fe4..4c434dd2 100644 --- a/MacDownCore/MPQuickLookRenderer.m +++ b/MacDownCore/MPQuickLookRenderer.m @@ -180,6 +180,84 @@ #pragma mark - Hoedown Renderer Callbacks +// Build a stable text-derived slug from a heading's HTML content. +// Mirrors slugify() in MacDown/Code/Extension/hoedown_html_patch.c so the +// preview and Quick Look render identical heading ids. +static void mp_quicklook_slugify(hoedown_buffer *out, const hoedown_buffer *content) +{ + if (!content || !content->size) + return; + + int in_tag = 0; + int last_was_dash = 1; + + for (size_t i = 0; i < content->size; i++) + { + uint8_t c = content->data[i]; + + if (in_tag) + { + if (c == '>') in_tag = 0; + continue; + } + if (c == '<') + { + in_tag = 1; + continue; + } + + if (c >= 0x80) + { + hoedown_buffer_putc(out, c); + last_was_dash = 0; + continue; + } + + if (c >= 'A' && c <= 'Z') + c += 32; + + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') + { + hoedown_buffer_putc(out, c); + last_was_dash = 0; + } + else if (c == ' ' || c == '\t' || c == '-') + { + if (!last_was_dash) + { + hoedown_buffer_putc(out, '-'); + last_was_dash = 1; + } + } + } + + while (out->size > 0 && out->data[out->size - 1] == '-') + out->size--; +} + +// Emit headings with text-derived id attributes so anchor links work in +// Quick Look previews, matching the main MacDown preview behavior. +static void mp_quicklook_render_header( + hoedown_buffer *ob, const hoedown_buffer *content, int level, + const hoedown_renderer_data *data) +{ + (void)data; + if (ob->size) hoedown_buffer_putc(ob, '\n'); + + hoedown_buffer *slug = hoedown_buffer_new(content ? content->size : 16); + mp_quicklook_slugify(slug, content); + if (slug->size == 0) + HOEDOWN_BUFPUTSL(slug, "section"); + + hoedown_buffer_printf(ob, "data, slug->size); + HOEDOWN_BUFPUTSL(ob, "\">"); + if (content) hoedown_buffer_put(ob, content->data, content->size); + hoedown_buffer_printf(ob, "\n", level); + + hoedown_buffer_free(slug); +} + /** * Custom blockcode renderer that adds Prism language classes without scripts. */ @@ -310,6 +388,7 @@ - (NSString *)parseMarkdownToHTML:(NSString *)markdown // Preserve Prism language classes, but Quick Look never executes Prism JS. renderer->blockcode = mp_quicklook_render_blockcode; + renderer->header = mp_quicklook_render_header; // Create document hoedown_document *document = hoedown_document_new( diff --git a/MacDownTests/MPQuickLookRendererTests.m b/MacDownTests/MPQuickLookRendererTests.m index 90146a02..075dee4e 100644 --- a/MacDownTests/MPQuickLookRendererTests.m +++ b/MacDownTests/MPQuickLookRendererTests.m @@ -124,8 +124,8 @@ - (void)testRenderSimpleMarkdown NSString *html = [self.renderer renderMarkdown:markdown]; XCTAssertNotNil(html, @"Should return non-nil HTML"); - XCTAssertTrue([html containsString:@"

        "], - @"Should render heading as

        "); + XCTAssertTrue([html containsString:@"

        element"); XCTAssertTrue([html containsString:@"Hello World"], @"Should include heading text"); XCTAssertTrue([html containsString:@"

        "], @@ -640,8 +640,8 @@ - (void)testRenderMarkdownFromURLWithCRLFLineEndings XCTAssertNil(error, @"Should render CRLF file without error"); XCTAssertNotNil(html, @"Should produce HTML from CRLF file"); - XCTAssertTrue([html containsString:@"

        "], - @"CRLF heading should render as

        "); + XCTAssertTrue([html containsString:@"

        element"); XCTAssertTrue([html containsString:@"Heading"], @"Heading text should appear in output"); XCTAssertTrue([html containsString:@"

        "],