diff --git a/site/doc/pages/markdown_to_html.txt b/site/doc/pages/markdown_to_html.txt
index d60297e..b5c8ef4 100644
--- a/site/doc/pages/markdown_to_html.txt
+++ b/site/doc/pages/markdown_to_html.txt
@@ -45,6 +45,7 @@ Options:
- `options["gfm"]` defaults to `true` and turns on GitHub-style extras such as tables, task lists, autolinks, and strikethrough.
- `options["allow_html"]` defaults to `false`. When true, raw HTML blocks and inline tags may pass through as `raw_html` nodes instead of being escaped as plain text.
+- `options["heading_offset"]` shifts rendered heading levels while leaving AST `level` values unchanged. The default is `0`; final levels clamp to `h1` through `h6`. Use `1` when Markdown is nested below a component-owned page title.
- `options["components"]` declares renderer extension points using normal UCE components.
Directive hook example:
diff --git a/site/tests/cli_runner.uce b/site/tests/cli_runner.uce
index 67ac1ef..b2321b5 100644
--- a/site/tests/cli_runner.uce
+++ b/site/tests/cli_runner.uce
@@ -217,7 +217,9 @@ void cli_run_doc_pages_gate()
CliHttpResponse res;
String summary;
bool ok = false;
- for(u64 attempt = 0; attempt < 3; attempt++)
+ // Frontend GETs may serve the last complete nested component while its
+ // prioritized replacement compiles; cover a normal cold WASI compile.
+ for(u64 attempt = 0; attempt < 12; attempt++)
{
if(attempt > 0)
usleep(500000);
diff --git a/site/tests/components/markdown/heading.uce b/site/tests/components/markdown/heading.uce
new file mode 100644
index 0000000..c5769f7
--- /dev/null
+++ b/site/tests/components/markdown/heading.uce
@@ -0,0 +1,5 @@
+COMPONENT(Request& context)
+{
+ DValue& node = context.props["node"];
+ <>
">
>
+}
diff --git a/site/tests/markdown.uce b/site/tests/markdown.uce
index fedd9cc..d4a7b13 100644
--- a/site/tests/markdown.uce
+++ b/site/tests/markdown.uce
@@ -23,12 +23,21 @@ RENDER(Request& context)
DValue ast = markdown_to_ast(markdown_src, options);
String ast_json = json_encode(ast);
String html = markdown_to_html(markdown_src, options);
+ DValue heading_options;
+ heading_options["heading_offset"] = "1";
+ heading_options["components"]["node.heading"] = "components/markdown/heading";
+ String shifted_headings = markdown_to_html("# Page child\n\n###### Deep child", heading_options);
+ DValue negative_heading_options;
+ negative_heading_options["heading_offset"] = "-2";
+ String clamped_headings = markdown_to_html("# Top\n\n### Third", negative_heading_options);
String ast_excerpt = ast_json.substr(0, ast_json.length() > 220 ? 220 : ast_json.length());
site_tests_page_start("Markdown", "Markdown parser coverage with component-backed directive and node hooks.");
check("markdown_to_ast()", ast_json.find("heading") != String::npos || ast_json.find("code_block") != String::npos, ast_excerpt);
check("markdown_to_html()", html.find("Page child") != String::npos && shifted_headings.find("Deep child
") != String::npos && shifted_headings.find("") == String::npos, shifted_headings);
+ check("negative heading offset clamps at h1", clamped_headings.find("Top
") != String::npos && clamped_headings.find("Third
") != String::npos, clamped_headings);
check("component hook output", html.find("tests-code-block") != String::npos, html);
?>
@@ -38,4 +47,4 @@ RENDER(Request& context)
site_tests_summary(passed, failed, skipped, "The warning directive and code-block renderer both run through local component fixtures under site/tests/components/markdown.");
site_tests_page_end();
-}
\ No newline at end of file
+}
diff --git a/src/lib/markdown.cpp b/src/lib/markdown.cpp
index 30830d1..6bf925d 100644
--- a/src/lib/markdown.cpp
+++ b/src/lib/markdown.cpp
@@ -1218,6 +1218,10 @@ String markdown_render_node(DValue node, DValue& options)
else if(type == "heading")
{
s64 level = int_val(markdown_get_string(node, "level", "1"));
+ s64 heading_offset = int_val(markdown_get_string(options, "heading_offset", "0"));
+ if(heading_offset < -5) heading_offset = -5;
+ if(heading_offset > 5) heading_offset = 5;
+ level += heading_offset;
if(level < 1) level = 1;
if(level > 6) level = 6;
html = String("" + children_html + String("";