Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MacDown/Code/Document/MPRenderer.m
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
89 changes: 88 additions & 1 deletion MacDown/Code/Extension/hoedown_html_patch.c
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,85 @@ void hoedown_patch_render_listitem(
HOEDOWN_BUFPUTSL(ob, "</li>\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, "<h%d id=\"", level);
hoedown_buffer_put(ob, slug->data, slug->size);
HOEDOWN_BUFPUTSL(ob, "\">");
if (content) hoedown_buffer_put(ob, content->data, content->size);
hoedown_buffer_printf(ob, "</h%d>\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,
Expand Down Expand Up @@ -195,7 +274,15 @@ void hoedown_patch_render_toc_header(
HOEDOWN_BUFPUTSL(ob,"</li>\n<li>\n");
}

hoedown_buffer_printf(ob, "<a href=\"#toc_%d\">", 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, "<a href=\"#");
hoedown_buffer_put(ob, slug->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, "</a>\n");
}
Expand Down
4 changes: 4 additions & 0 deletions MacDown/Code/Extension/hoedown_html_patch.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
79 changes: 79 additions & 0 deletions MacDownCore/MPQuickLookRenderer.m
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<h%d id=\"", level);
hoedown_buffer_put(ob, slug->data, slug->size);
HOEDOWN_BUFPUTSL(ob, "\">");
if (content) hoedown_buffer_put(ob, content->data, content->size);
hoedown_buffer_printf(ob, "</h%d>\n", level);

hoedown_buffer_free(slug);
}

/**
* Custom blockcode renderer that adds Prism language classes without scripts.
*/
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 7 additions & 7 deletions MacDownTests/Fixtures/autolinks.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1>Autolink Tests</h1>
<h1 id="autolink-tests">Autolink Tests</h1>

<h2>URL Autolinks</h2>
<h2 id="url-autolinks">URL Autolinks</h2>

<p><a href="https://example.com">https://example.com</a></p>

Expand All @@ -10,33 +10,33 @@ <h2>URL Autolinks</h2>

<p><a href="https://example.com/path?query=param&amp;other=value">https://example.com/path?query=param&amp;other=value</a></p>

<h2>Email Autolinks</h2>
<h2 id="email-autolinks">Email Autolinks</h2>

<p><a href="mailto:user@example.com">user@example.com</a></p>

<p><a href="mailto:john.doe@example.org">john.doe@example.org</a></p>

<p><a href="mailto:contact+tag@example.com">contact+tag@example.com</a></p>

<h2>Plain URLs (may or may not auto-link depending on parser)</h2>
<h2 id="plain-urls-may-or-may-not-auto-link-depending-on-parser">Plain URLs (may or may not auto-link depending on parser)</h2>

<p>Visit <a href="https://example.com">https://example.com</a> for more.</p>

<p>Check out <a href="http://www.example.org">http://www.example.org</a> too.</p>

<h2>Mixed Content</h2>
<h2 id="mixed-content">Mixed Content</h2>

<p>Email me at <a href="mailto:user@example.com">user@example.com</a> or visit <a href="https://example.com">https://example.com</a>.</p>

<h2>Edge Cases</h2>
<h2 id="edge-cases">Edge Cases</h2>

<p>Multiple autolinks: <a href="https://first.com">https://first.com</a> and <a href="https://second.com">https://second.com</a>.</p>

<p>Autolink at start: <a href="https://start.com">https://start.com</a></p>

<p>End with autolink: <a href="https://end.com">https://end.com</a></p>

<h2>In Lists</h2>
<h2 id="in-lists">In Lists</h2>

<ul>
<li><a href="https://example.com">https://example.com</a></li>
Expand Down
12 changes: 6 additions & 6 deletions MacDownTests/Fixtures/basic.html
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
<h1>Heading 1</h1>
<h1 id="heading-1">Heading 1</h1>

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

<h2>Heading 2</h2>
<h2 id="heading-2">Heading 2</h2>

<p>Another paragraph here. This one is under heading 2.</p>

<h3>Heading 3</h3>
<h3 id="heading-3">Heading 3</h3>

<p>A third paragraph for heading 3.</p>

<h4>Heading 4</h4>
<h4 id="heading-4">Heading 4</h4>

<p>Fourth level heading with text.</p>

<h5>Heading 5</h5>
<h5 id="heading-5">Heading 5</h5>

<p>Fifth level heading.</p>

<h6>Heading 6</h6>
<h6 id="heading-6">Heading 6</h6>

<p>The deepest heading level in Markdown.</p>

Expand Down
22 changes: 11 additions & 11 deletions MacDownTests/Fixtures/blockquotes.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<h1>Blockquote Tests</h1>
<h1 id="blockquote-tests">Blockquote Tests</h1>

<h2>Basic Blockquote</h2>
<h2 id="basic-blockquote">Basic Blockquote</h2>

<blockquote>
<p>This is a blockquote.
It can span multiple lines.</p>
</blockquote>

<h2>Single Line Blockquote</h2>
<h2 id="single-line-blockquote">Single Line Blockquote</h2>

<blockquote>
<p>Single line blockquote.</p>
</blockquote>

<h2>Nested Blockquotes</h2>
<h2 id="nested-blockquotes">Nested Blockquotes</h2>

<blockquote>
<p>First level quote</p>
Expand All @@ -27,15 +27,15 @@ <h2>Nested Blockquotes</h2>
</blockquote>
</blockquote>

<h2>Blockquote with Multiple Paragraphs</h2>
<h2 id="blockquote-with-multiple-paragraphs">Blockquote with Multiple Paragraphs</h2>

<blockquote>
<p>First paragraph in blockquote.</p>

<p>Second paragraph in blockquote.</p>
</blockquote>

<h2>Blockquote with Formatting</h2>
<h2 id="blockquote-with-formatting">Blockquote with Formatting</h2>

<blockquote>
<p>This has <strong>bold text</strong>.</p>
Expand All @@ -45,7 +45,7 @@ <h2>Blockquote with Formatting</h2>
<p>This has <code>code</code>.</p>
</blockquote>

<h2>Blockquote with Lists</h2>
<h2 id="blockquote-with-lists">Blockquote with Lists</h2>

<blockquote>
<ul>
Expand All @@ -61,22 +61,22 @@ <h2>Blockquote with Lists</h2>
</ol>
</blockquote>

<h2>Blockquote with Headers</h2>
<h2 id="blockquote-with-headers">Blockquote with Headers</h2>

<blockquote>
<h2>Header in Blockquote</h2>
<h2 id="header-in-blockquote">Header in Blockquote</h2>

<p>Some text under the header.</p>
</blockquote>

<h2>Lazy Blockquotes</h2>
<h2 id="lazy-blockquotes">Lazy Blockquotes</h2>

<blockquote>
<p>This is lazy continuation
without the &gt; character.</p>
</blockquote>

<h2>Complex Nesting</h2>
<h2 id="complex-nesting">Complex Nesting</h2>

<blockquote>
<p>Top level</p>
Expand Down
Loading