//! markdown_view_leptos — Render Markdown at compile time with inline Leptos components
//!
//! This crate provides two procedural macros:
//!
//! - `markdown_view!`: Converts a string literal or `file = "..."`/`url = "..."` into
//!   a Leptos `view!` tree at compile time. Dynamic strings and computed file paths
//!   render at runtime.
//! - `markdown_anchors!`: Returns `Vec<(String, String)>` of `(title, id)` heading
//!   pairs from the same input sources, matching the IDs generated by
//!   `markdown_view!`.
//!
//! The macro allows embedding
//! real Leptos components inline using a lightweight MDX-like syntax:
//!
//! - Embed components with: `{{ <MyComponent prop=value/> }}`
//! - Include file content: `markdown_view!(file = "README.md")`
//! - Fetch remote content (build-time): `markdown_view!(url = "https://...")`
//!
//! Notes and caveats:
//! - The macro injects generated HTML into the DOM via `inner_html`. Avoid
//!   untrusted Markdown if you need strict sanitization.
//! - Remote `url = "..."` fetch happens at compile time and is disabled under
//!   rust-analyzer to keep IDEs responsive. Prefer `file = "..."` for stability.
//! - Dynamic sources (`markdown_view!(my_string)` or `file = format!(...)`) render
//!   at runtime using a lightweight built-in Markdown renderer (no extra user
//!   dependencies). It supports headings/paragraphs and heading IDs but does not
//!   expand inline `{{ ... }}` components. If the macro can see a
//!   string literal binding in the same file (for example `let body = r#"..."#;`
//!   followed by `markdown_view!(body)`), it treats it like an inline literal so
//!   inline `{{ ... }}` components still expand.
//! - `file = <expr>` paths depend on `std::fs` and therefore are not supported on
//!   wasm32 unless the macro can resolve the path during compilation to embed it.
//! - Component parsing looks for `{{ ... }}` and treats the inner content as
//!   a Rust/RSX snippet. We intentionally keep this flexible and resilient: if
//!   parsing fails, the content is rendered as plain Markdown to avoid breaking
//!   your build.
//!
//! Example
//!
//! ```ignore
//! use leptos::prelude::*;
//! use markdown_view_leptos::markdown_view;
//!
//! #[component]
//! fn Counter() -> impl IntoView { let (n, set_n) = signal(0); view! { <button on:click=move |_| set_n.update(|v| *v+=1)>{n}</button> } }
//!
//! #[component]
//! pub fn App() -> impl IntoView {
//!     view! { <main>{markdown_view!(r#"Hello {{ <Counter/> }}!"#)}</main> }
//! }
//! ```

use proc_macro::TokenStream;
// no extra items from proc_macro needed beyond TokenStream
use proc_macro2::{Group, LineColumn, Span, TokenStream as TokenStream2, TokenTree};
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag, TagEnd};
use quote::quote;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::PathBuf;
use syn::spanned::Spanned;
use syn::visit::Visit;
use syn::{
    parse::Parse, parse::ParseStream, parse_macro_input, Block, Expr, ExprLit, Ident, ImplItemFn,
    ItemConst, ItemFn, ItemImpl, ItemMod, ItemStatic, Lit, LitBool, LitStr, Local, Pat,
    StaticMutability, Token, TraitItemFn,
};

fn is_rust_analyzer() -> bool {
    env::var_os("RUST_ANALYZER_INTERNALS_DO_NOT_USE").is_some()
        || env::var_os("RA_TEST").is_some()
        || env::var_os("RUST_ANALYZER").is_some()
}

const TOC_MARKER: &str = "[[toc]]";

fn has_toc_marker(markdown: &str) -> bool {
    markdown.contains(TOC_MARKER)
}

fn strip_toc_marker<'a>(markdown: &'a str) -> std::borrow::Cow<'a, str> {
    if markdown.contains(TOC_MARKER) {
        std::borrow::Cow::Owned(markdown.replace(TOC_MARKER, ""))
    } else {
        std::borrow::Cow::Borrowed(markdown)
    }
}

#[derive(Default)]
struct AnchorSlugger {
    counts: HashMap<String, usize>,
}

impl AnchorSlugger {
    fn register_custom(&mut self, id: &str) {
        let entry = self.counts.entry(id.to_string()).or_insert(0);
        *entry += 1;
    }

    fn slugify(&mut self, text: &str) -> String {
        let base = slugify_heading(text);
        if base.is_empty() {
            return base;
        }
        let count = self.counts.entry(base.clone()).or_insert(0);
        let slug = if *count == 0 {
            base.clone()
        } else {
            format!("{base}-{}", *count)
        };
        *count += 1;
        slug
    }
}

fn is_combining_mark(ch: char) -> bool {
    matches!(
        ch,
        '\u{0300}'..='\u{036F}'
            | '\u{1AB0}'..='\u{1AFF}'
            | '\u{1DC0}'..='\u{1DFF}'
            | '\u{20D0}'..='\u{20FF}'
            | '\u{FE20}'..='\u{FE2F}'
    )
}

fn fold_diacritic(ch: char) -> Option<&'static str> {
    match ch {
        '\u{C0}' | '\u{C1}' | '\u{C2}' | '\u{C3}' | '\u{C4}' | '\u{C5}' | '\u{E0}' | '\u{E1}'
        | '\u{E2}' | '\u{E3}' | '\u{E4}' | '\u{E5}' | '\u{100}' | '\u{101}' | '\u{102}'
        | '\u{103}' | '\u{104}' | '\u{105}' | '\u{1CD}' | '\u{1CE}' | '\u{1DE}' | '\u{1DF}'
        | '\u{1E0}' | '\u{1E1}' | '\u{1FA}' | '\u{1FB}' | '\u{200}' | '\u{201}' | '\u{202}'
        | '\u{203}' | '\u{226}' | '\u{227}' | '\u{1E00}' | '\u{1E01}' | '\u{1EA0}' | '\u{1EA1}'
        | '\u{1EA2}' | '\u{1EA3}' | '\u{1EA4}' | '\u{1EA5}' | '\u{1EA6}' | '\u{1EA7}'
        | '\u{1EA8}' | '\u{1EA9}' | '\u{1EAA}' | '\u{1EAB}' | '\u{1EAC}' | '\u{1EAD}'
        | '\u{1EAE}' | '\u{1EAF}' | '\u{1EB0}' | '\u{1EB1}' | '\u{1EB2}' | '\u{1EB3}'
        | '\u{1EB4}' | '\u{1EB5}' | '\u{1EB6}' | '\u{1EB7}' | '\u{2090}' | '\u{FF21}'
        | '\u{FF41}' => Some("a"),
        '\u{1E02}' | '\u{1E03}' | '\u{1E04}' | '\u{1E05}' | '\u{1E06}' | '\u{1E07}'
        | '\u{FF22}' | '\u{FF42}' => Some("b"),
        '\u{C7}' | '\u{E7}' | '\u{106}' | '\u{107}' | '\u{108}' | '\u{109}' | '\u{10A}'
        | '\u{10B}' | '\u{10C}' | '\u{10D}' | '\u{1E08}' | '\u{1E09}' | '\u{FF23}' | '\u{FF43}' => {
            Some("c")
        }
        '\u{D0}' | '\u{F0}' | '\u{10E}' | '\u{10F}' | '\u{110}' | '\u{111}' | '\u{1E0A}'
        | '\u{1E0B}' | '\u{1E0C}' | '\u{1E0D}' | '\u{1E0E}' | '\u{1E0F}' | '\u{1E10}'
        | '\u{1E11}' | '\u{1E12}' | '\u{1E13}' | '\u{FF24}' | '\u{FF44}' => Some("d"),
        '\u{C8}' | '\u{C9}' | '\u{CA}' | '\u{CB}' | '\u{E8}' | '\u{E9}' | '\u{EA}' | '\u{EB}'
        | '\u{112}' | '\u{113}' | '\u{114}' | '\u{115}' | '\u{116}' | '\u{117}' | '\u{118}'
        | '\u{119}' | '\u{11A}' | '\u{11B}' | '\u{204}' | '\u{205}' | '\u{206}' | '\u{207}'
        | '\u{228}' | '\u{229}' | '\u{1E14}' | '\u{1E15}' | '\u{1E16}' | '\u{1E17}'
        | '\u{1E18}' | '\u{1E19}' | '\u{1E1A}' | '\u{1E1B}' | '\u{1E1C}' | '\u{1E1D}'
        | '\u{1EB8}' | '\u{1EB9}' | '\u{1EBA}' | '\u{1EBB}' | '\u{1EBC}' | '\u{1EBD}'
        | '\u{1EBE}' | '\u{1EBF}' | '\u{1EC0}' | '\u{1EC1}' | '\u{1EC2}' | '\u{1EC3}'
        | '\u{1EC4}' | '\u{1EC5}' | '\u{1EC6}' | '\u{1EC7}' | '\u{2091}' | '\u{FF25}'
        | '\u{FF45}' => Some("e"),
        '\u{1E1E}' | '\u{1E1F}' | '\u{FF26}' | '\u{FF46}' => Some("f"),
        '\u{11C}' | '\u{11D}' | '\u{11E}' | '\u{11F}' | '\u{120}' | '\u{121}' | '\u{122}'
        | '\u{123}' | '\u{1E6}' | '\u{1E7}' | '\u{1F4}' | '\u{1F5}' | '\u{1E20}' | '\u{1E21}'
        | '\u{FF27}' | '\u{FF47}' => Some("g"),
        '\u{124}' | '\u{125}' | '\u{126}' | '\u{127}' | '\u{21E}' | '\u{21F}' | '\u{1E22}'
        | '\u{1E23}' | '\u{1E24}' | '\u{1E25}' | '\u{1E26}' | '\u{1E27}' | '\u{1E28}'
        | '\u{1E29}' | '\u{1E2A}' | '\u{1E2B}' | '\u{1E96}' | '\u{2095}' | '\u{FF28}'
        | '\u{FF48}' => Some("h"),
        '\u{CC}' | '\u{CD}' | '\u{CE}' | '\u{CF}' | '\u{EC}' | '\u{ED}' | '\u{EE}' | '\u{EF}'
        | '\u{128}' | '\u{129}' | '\u{12A}' | '\u{12B}' | '\u{12C}' | '\u{12D}' | '\u{12E}'
        | '\u{12F}' | '\u{130}' | '\u{131}' | '\u{1CF}' | '\u{1D0}' | '\u{208}' | '\u{209}'
        | '\u{20A}' | '\u{20B}' | '\u{1D62}' | '\u{1E2C}' | '\u{1E2D}' | '\u{1E2E}'
        | '\u{1E2F}' | '\u{1EC8}' | '\u{1EC9}' | '\u{1ECA}' | '\u{1ECB}' | '\u{2071}'
        | '\u{FF29}' | '\u{FF49}' => Some("i"),
        '\u{134}' | '\u{135}' | '\u{1F0}' | '\u{2C7C}' | '\u{FF2A}' | '\u{FF4A}' => Some("j"),
        '\u{136}' | '\u{137}' | '\u{138}' | '\u{1E8}' | '\u{1E9}' | '\u{1E30}' | '\u{1E31}'
        | '\u{1E32}' | '\u{1E33}' | '\u{1E34}' | '\u{1E35}' | '\u{2096}' | '\u{FF2B}'
        | '\u{FF4B}' => Some("k"),
        '\u{139}' | '\u{13A}' | '\u{13B}' | '\u{13C}' | '\u{13D}' | '\u{13E}' | '\u{13F}'
        | '\u{140}' | '\u{141}' | '\u{142}' | '\u{1E36}' | '\u{1E37}' | '\u{1E38}' | '\u{1E39}'
        | '\u{1E3A}' | '\u{1E3B}' | '\u{1E3C}' | '\u{1E3D}' | '\u{2097}' | '\u{FF2C}'
        | '\u{FF4C}' => Some("l"),
        '\u{1E3E}' | '\u{1E3F}' | '\u{1E40}' | '\u{1E41}' | '\u{1E42}' | '\u{1E43}'
        | '\u{2098}' | '\u{FF2D}' | '\u{FF4D}' => Some("m"),
        '\u{D1}' | '\u{F1}' | '\u{143}' | '\u{144}' | '\u{145}' | '\u{146}' | '\u{147}'
        | '\u{148}' | '\u{149}' | '\u{14A}' | '\u{14B}' | '\u{1F8}' | '\u{1F9}' | '\u{1E44}'
        | '\u{1E45}' | '\u{1E46}' | '\u{1E47}' | '\u{1E48}' | '\u{1E49}' | '\u{1E4A}'
        | '\u{1E4B}' | '\u{207F}' | '\u{2099}' | '\u{FF2E}' | '\u{FF4E}' => Some("n"),
        '\u{D2}' | '\u{D3}' | '\u{D4}' | '\u{D5}' | '\u{D6}' | '\u{D8}' | '\u{F2}' | '\u{F3}'
        | '\u{F4}' | '\u{F5}' | '\u{F6}' | '\u{F8}' | '\u{14C}' | '\u{14D}' | '\u{14E}'
        | '\u{14F}' | '\u{150}' | '\u{151}' | '\u{1A0}' | '\u{1A1}' | '\u{1D1}' | '\u{1D2}'
        | '\u{1EA}' | '\u{1EB}' | '\u{1EC}' | '\u{1ED}' | '\u{1FE}' | '\u{1FF}' | '\u{20C}'
        | '\u{20D}' | '\u{20E}' | '\u{20F}' | '\u{22A}' | '\u{22B}' | '\u{22C}' | '\u{22D}'
        | '\u{22E}' | '\u{22F}' | '\u{230}' | '\u{231}' | '\u{1E4C}' | '\u{1E4D}' | '\u{1E4E}'
        | '\u{1E4F}' | '\u{1E50}' | '\u{1E51}' | '\u{1E52}' | '\u{1E53}' | '\u{1ECC}'
        | '\u{1ECD}' | '\u{1ECE}' | '\u{1ECF}' | '\u{1ED0}' | '\u{1ED1}' | '\u{1ED2}'
        | '\u{1ED3}' | '\u{1ED4}' | '\u{1ED5}' | '\u{1ED6}' | '\u{1ED7}' | '\u{1ED8}'
        | '\u{1ED9}' | '\u{1EDA}' | '\u{1EDB}' | '\u{1EDC}' | '\u{1EDD}' | '\u{1EDE}'
        | '\u{1EDF}' | '\u{1EE0}' | '\u{1EE1}' | '\u{1EE2}' | '\u{1EE3}' | '\u{2092}'
        | '\u{FF2F}' | '\u{FF4F}' => Some("o"),
        '\u{1E54}' | '\u{1E55}' | '\u{1E56}' | '\u{1E57}' | '\u{209A}' | '\u{FF30}'
        | '\u{FF50}' => Some("p"),
        '\u{FF31}' | '\u{FF51}' => Some("q"),
        '\u{154}' | '\u{155}' | '\u{156}' | '\u{157}' | '\u{158}' | '\u{159}' | '\u{210}'
        | '\u{211}' | '\u{212}' | '\u{213}' | '\u{1D63}' | '\u{1E58}' | '\u{1E59}' | '\u{1E5A}'
        | '\u{1E5B}' | '\u{1E5C}' | '\u{1E5D}' | '\u{1E5E}' | '\u{1E5F}' | '\u{FF32}'
        | '\u{FF52}' => Some("r"),
        '\u{15A}' | '\u{15B}' | '\u{15C}' | '\u{15D}' | '\u{15E}' | '\u{15F}' | '\u{160}'
        | '\u{161}' | '\u{17F}' | '\u{218}' | '\u{219}' | '\u{1E60}' | '\u{1E61}' | '\u{1E62}'
        | '\u{1E63}' | '\u{1E64}' | '\u{1E65}' | '\u{1E66}' | '\u{1E67}' | '\u{1E68}'
        | '\u{1E69}' | '\u{1E9B}' | '\u{209B}' | '\u{FF33}' | '\u{FF53}' => Some("s"),
        '\u{162}' | '\u{163}' | '\u{164}' | '\u{165}' | '\u{21A}' | '\u{21B}' | '\u{1E6A}'
        | '\u{1E6B}' | '\u{1E6C}' | '\u{1E6D}' | '\u{1E6E}' | '\u{1E6F}' | '\u{1E70}'
        | '\u{1E71}' | '\u{1E97}' | '\u{209C}' | '\u{FF34}' | '\u{FF54}' => Some("t"),
        '\u{D9}' | '\u{DA}' | '\u{DB}' | '\u{DC}' | '\u{F9}' | '\u{FA}' | '\u{FB}' | '\u{FC}'
        | '\u{168}' | '\u{169}' | '\u{16A}' | '\u{16B}' | '\u{16C}' | '\u{16D}' | '\u{16E}'
        | '\u{16F}' | '\u{170}' | '\u{171}' | '\u{172}' | '\u{173}' | '\u{1AF}' | '\u{1B0}'
        | '\u{1D3}' | '\u{1D4}' | '\u{1D5}' | '\u{1D6}' | '\u{1D7}' | '\u{1D8}' | '\u{1D9}'
        | '\u{1DA}' | '\u{1DB}' | '\u{1DC}' | '\u{214}' | '\u{215}' | '\u{216}' | '\u{217}'
        | '\u{1D64}' | '\u{1E72}' | '\u{1E73}' | '\u{1E74}' | '\u{1E75}' | '\u{1E76}'
        | '\u{1E77}' | '\u{1E78}' | '\u{1E79}' | '\u{1E7A}' | '\u{1E7B}' | '\u{1EE4}'
        | '\u{1EE5}' | '\u{1EE6}' | '\u{1EE7}' | '\u{1EE8}' | '\u{1EE9}' | '\u{1EEA}'
        | '\u{1EEB}' | '\u{1EEC}' | '\u{1EED}' | '\u{1EEE}' | '\u{1EEF}' | '\u{1EF0}'
        | '\u{1EF1}' | '\u{FF35}' | '\u{FF55}' => Some("u"),
        '\u{1D65}' | '\u{1E7C}' | '\u{1E7D}' | '\u{1E7E}' | '\u{1E7F}' | '\u{FF36}'
        | '\u{FF56}' => Some("v"),
        '\u{174}' | '\u{175}' | '\u{1E80}' | '\u{1E81}' | '\u{1E82}' | '\u{1E83}' | '\u{1E84}'
        | '\u{1E85}' | '\u{1E86}' | '\u{1E87}' | '\u{1E88}' | '\u{1E89}' | '\u{1E98}'
        | '\u{FF37}' | '\u{FF57}' => Some("w"),
        '\u{1E8A}' | '\u{1E8B}' | '\u{1E8C}' | '\u{1E8D}' | '\u{2093}' | '\u{FF38}'
        | '\u{FF58}' => Some("x"),
        '\u{DD}' | '\u{FD}' | '\u{FF}' | '\u{176}' | '\u{177}' | '\u{178}' | '\u{232}'
        | '\u{233}' | '\u{1E8E}' | '\u{1E8F}' | '\u{1E99}' | '\u{1EF2}' | '\u{1EF3}'
        | '\u{1EF4}' | '\u{1EF5}' | '\u{1EF6}' | '\u{1EF7}' | '\u{1EF8}' | '\u{1EF9}'
        | '\u{FF39}' | '\u{FF59}' => Some("y"),
        '\u{179}' | '\u{17A}' | '\u{17B}' | '\u{17C}' | '\u{17D}' | '\u{17E}' | '\u{1E90}'
        | '\u{1E91}' | '\u{1E92}' | '\u{1E93}' | '\u{1E94}' | '\u{1E95}' | '\u{FF3A}'
        | '\u{FF5A}' => Some("z"),
        '\u{C6}' | '\u{E6}' | '\u{1E2}' | '\u{1E3}' | '\u{1FC}' | '\u{1FD}' => Some("ae"),
        '\u{1C4}' | '\u{1C5}' | '\u{1C6}' | '\u{1F1}' | '\u{1F2}' | '\u{1F3}' => Some("dz"),
        '\u{FB00}' => Some("ff"),
        '\u{FB01}' => Some("fi"),
        '\u{FB02}' => Some("fl"),
        '\u{132}' | '\u{133}' => Some("ij"),
        '\u{1C7}' | '\u{1C8}' | '\u{1C9}' => Some("lj"),
        '\u{1CA}' | '\u{1CB}' | '\u{1CC}' => Some("nj"),
        '\u{152}' | '\u{153}' => Some("oe"),
        '\u{DF}' => Some("ss"),
        '\u{FB05}' | '\u{FB06}' => Some("st"),
        '\u{DE}' | '\u{FE}' => Some("th"),
        '\u{FB03}' => Some("ffi"),
        '\u{FB04}' => Some("ffl"),
        _ => None,
    }
}

fn slugify_heading(text: &str) -> String {
    let mut out = String::new();
    let mut last_dash = false;

    for ch in text.chars() {
        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
            out.push(ch.to_ascii_lowercase());
            last_dash = false;
        } else if let Some(folded) = fold_diacritic(ch) {
            out.push_str(folded);
            last_dash = false;
        } else if is_combining_mark(ch) {
            continue;
        } else if ch.is_alphanumeric() {
            for lower in ch.to_lowercase() {
                out.push(lower);
            }
            last_dash = false;
        } else if !last_dash && !out.is_empty() {
            out.push('-');
            last_dash = true;
        }
    }

    while out.ends_with('-') {
        out.pop();
    }

    out
}

#[derive(Clone, Debug)]
struct AnchorRenderOptions {
    enabled: bool,
    class: Option<String>,
    style: Option<String>,
    wrapper_class: Option<String>,
    wrapper_style: Option<String>,
    symbol: String,
}

impl Default for AnchorRenderOptions {
    fn default() -> Self {
        Self {
            enabled: true,
            class: Some("header-anchor".to_string()),
            style: None,
            wrapper_class: None,
            wrapper_style: None,
            symbol: "#".to_string(),
        }
    }
}

impl AnchorRenderOptions {
    fn anchor_html(&self, id: &str) -> Option<String> {
        if !self.enabled || id.is_empty() {
            return None;
        }
        let mut html = String::new();
        html.push_str("<a");
        if let Some(class) = &self.class {
            if !class.is_empty() {
                html.push_str(" class=\"");
                html.push_str(&escape_html_attr(class));
                html.push('"');
            }
        }
        if let Some(style) = &self.style {
            if !style.is_empty() {
                html.push_str(" style=\"");
                html.push_str(&escape_html_attr(style));
                html.push('"');
            }
        }
        html.push_str(" href=\"#");
        html.push_str(&escape_html_attr(id));
        html.push_str("\" aria-hidden=\"true\">");
        html.push_str(&escape_html_attr(&self.symbol));
        html.push_str("</a>");
        Some(html)
    }
}

fn extract_heading_text(events: &[Event<'_>]) -> String {
    let mut text = String::new();
    for event in events {
        match event {
            Event::Text(value) | Event::Code(value) => {
                text.push_str(value.as_ref());
            }
            Event::SoftBreak | Event::HardBreak => {
                text.push(' ');
            }
            Event::InlineMath(value) | Event::DisplayMath(value) => {
                text.push_str(value.as_ref());
            }
            _ => {}
        }
    }
    text
}

fn escape_html_attr(input: &str) -> String {
    let mut escaped = String::with_capacity(input.len());
    for ch in input.chars() {
        match ch {
            '&' => escaped.push_str("&amp;"),
            '<' => escaped.push_str("&lt;"),
            '>' => escaped.push_str("&gt;"),
            '"' => escaped.push_str("&quot;"),
            '\'' => escaped.push_str("&#39;"),
            _ => escaped.push(ch),
        }
    }
    escaped
}

#[cfg(test)]
fn collect_markdown_anchors(markdown: impl AsRef<str>) -> Vec<(String, String)> {
    let mut slugger = AnchorSlugger::default();
    collect_markdown_anchors_with_slugger(markdown, &mut slugger)
}

fn collect_markdown_anchors_with_slugger(
    markdown: impl AsRef<str>,
    slugger: &mut AnchorSlugger,
) -> Vec<(String, String)> {
    let markdown = markdown.as_ref();
    if markdown.trim().is_empty() {
        return Vec::new();
    }
    let parser = Parser::new_ext(markdown, Options::all());
    let mut anchors: Vec<(String, String)> = Vec::new();
    let mut iter = parser.into_iter();
    while let Some(event) = iter.next() {
        if let Event::Start(Tag::Heading { id, .. }) = event {
            let mut inner_events: Vec<Event<'_>> = Vec::new();
            while let Some(inner) = iter.next() {
                if matches!(inner, Event::End(TagEnd::Heading(_))) {
                    break;
                }
                inner_events.push(inner);
            }
            let heading_text = extract_heading_text(&inner_events);
            let anchor_id = if let Some(id) = id {
                let id_value = id.to_string();
                slugger.register_custom(&id_value);
                Some(id_value)
            } else {
                let slug = slugger.slugify(&heading_text);
                if slug.is_empty() {
                    None
                } else {
                    Some(slug)
                }
            };
            if let Some(id) = anchor_id {
                anchors.push((heading_text.trim().to_string(), id));
            }
        }
    }
    anchors
}

#[cfg(test)]
fn convert_markdown_to_html(markdown: impl AsRef<str>) -> String {
    let mut slugger = AnchorSlugger::default();
    let anchor_options = AnchorRenderOptions::default();
    convert_markdown_to_html_with_slugger(markdown, &mut slugger, &anchor_options)
}

fn convert_markdown_to_html_with_slugger(
    markdown: impl AsRef<str>,
    slugger: &mut AnchorSlugger,
    anchor_options: &AnchorRenderOptions,
) -> String {
    let markdown = markdown.as_ref();
    if markdown.trim().is_empty() {
        return String::new();
    }
    let parser = Parser::new_ext(markdown, Options::all());
    let mut events: Vec<Event<'_>> = Vec::new();
    let mut iter = parser.into_iter();
    while let Some(event) = iter.next() {
        match event {
            Event::Start(Tag::Heading {
                level,
                id,
                mut classes,
                mut attrs,
            }) => {
                if let Some(wrapper_class) = anchor_options
                    .wrapper_class
                    .as_ref()
                    .filter(|class| !class.is_empty())
                {
                    classes.push(CowStr::from(wrapper_class.clone()));
                }
                if let Some(wrapper_style) = anchor_options
                    .wrapper_style
                    .as_ref()
                    .filter(|style| !style.is_empty())
                {
                    if let Some((_, value)) = attrs
                        .iter_mut()
                        .find(|(name, _)| name.as_ref() == "style")
                    {
                        let mut merged = value
                            .as_ref()
                            .map(|val| val.to_string())
                            .unwrap_or_default();
                        if !merged.trim().is_empty() {
                            if !merged.trim_end().ends_with(';') {
                                merged.push(';');
                            }
                            merged.push(' ');
                        }
                        merged.push_str(wrapper_style);
                        *value = Some(CowStr::from(merged));
                    } else {
                        attrs.push((
                            CowStr::from("style"),
                            Some(CowStr::from(wrapper_style.clone())),
                        ));
                    }
                }
                let mut inner_events: Vec<Event<'_>> = Vec::new();
                while let Some(inner) = iter.next() {
                    if matches!(inner, Event::End(TagEnd::Heading(_))) {
                        break;
                    }
                    inner_events.push(inner);
                }

                let heading_text = extract_heading_text(&inner_events);
                let anchor_id = if let Some(id) = id {
                    let id_value = id.to_string();
                    slugger.register_custom(&id_value);
                    Some(id_value)
                } else {
                    let slug = slugger.slugify(&heading_text);
                    if slug.is_empty() {
                        None
                    } else {
                        Some(slug)
                    }
                };

                let heading_id = anchor_id.as_ref().map(|id| CowStr::from(id.clone()));
                let anchor_html = anchor_id
                    .as_ref()
                    .and_then(|id| anchor_options.anchor_html(id));
                events.push(Event::Start(Tag::Heading {
                    level,
                    id: heading_id,
                    classes,
                    attrs,
                }));
                if let Some(anchor_html) = anchor_html {
                    events.push(Event::Html(CowStr::from(anchor_html)));
                }
                events.extend(inner_events);
                events.push(Event::End(TagEnd::Heading(level)));
            }
            _ => events.push(event),
        }
    }
    let mut html_output = String::new();
    html::push_html(&mut html_output, events.into_iter());
    html_output
}

/// A parsed piece of the input stream.
enum Segment {
    /// Plain Markdown, to be converted to HTML and injected as `inner_html`.
    Markdown(String),
    /// Inline Leptos component snippet inside `{{ ... }}`.
    Component(TokenStream2),
}

/// Splits the input into Markdown and inline-component segments.
///
/// - Inline components are enclosed by `{{` and `}}` and can span multiple
///   lines. They are parsed as Rust/RSX tokens.
/// - Content between triple backtick fences (```lang) is treated as plain
///   Markdown; any `{{`/`}}` inside code fences are ignored.
fn split_markdown_with_components(input: impl AsRef<str>) -> Vec<Segment> {
    let input = input.as_ref();
    let mut segments: Vec<Segment> = Vec::new();
    let bytes = input.as_bytes();

    let mut i: usize = 0; // current cursor
    let mut last: usize = 0; // start of the current Markdown span
    let mut in_fence = false; // inside ``` fenced block

    while i < bytes.len() {
        // Toggle code fence state when encountering "```" at any position.
        if !in_fence
            && i + 2 < bytes.len()
            && bytes[i] == b'`'
            && bytes[i + 1] == b'`'
            && bytes[i + 2] == b'`'
        {
            i += 3;
            in_fence = true;
            continue;
        } else if in_fence
            && i + 2 < bytes.len()
            && bytes[i] == b'`'
            && bytes[i + 1] == b'`'
            && bytes[i + 2] == b'`'
        {
            i += 3;
            in_fence = false;
            continue;
        }

        // Only consider component delimiters outside code fences.
        if !in_fence && i + 1 < bytes.len() && bytes[i] == b'{' && bytes[i + 1] == b'{' {
            let comp_start = i + 2; // skip opening "{{"
            let mut j = comp_start;
            let mut found = None;
            while j + 1 < bytes.len() {
                // Don't toggle code fences while inside component scanning.
                if bytes[j] == b'}' && bytes[j + 1] == b'}' {
                    found = Some(j);
                    break;
                }
                j += 1;
            }
            if let Some(comp_end) = found {
                // push preceding markdown
                if last < i {
                    let md = &input[last..i];
                    if !md.trim().is_empty() {
                        segments.push(Segment::Markdown(md.to_string()));
                    }
                }
                // parse component snippet
                let inner_raw = &input[comp_start..comp_end];
                let inner = inner_raw.trim();
                if !inner.is_empty() {
                    let sanitized = sanitize_component_snippet(inner);
                    let parsed = std::panic::catch_unwind(|| sanitized.parse::<TokenStream2>());
                    match parsed {
                        Ok(Ok(ts)) => {
                            let normalized = normalize_component_tokens(ts);
                            segments.push(Segment::Component(normalized));
                        }
                        _ => {
                            // Gracefully fall back to rendering literally.
                            segments.push(Segment::Markdown(format!("{{{{{inner}}}}}")));
                        }
                    }
                }
                i = comp_end + 2; // skip closing "}}"
                last = i;
                continue;
            } else {
                // No closing delimiter; treat remainder as Markdown.
                break;
            }
        }

        i += 1;
    }

    // Tail
    if last < input.len() {
        let tail = &input[last..];
        if !tail.trim().is_empty() {
            segments.push(Segment::Markdown(tail.to_string()));
        }
    }

    if segments.is_empty() {
        segments.push(Segment::Markdown(input.to_string()));
    }

    segments
}

fn raw_string_literal_sharp_count(value: &str) -> usize {
    let mut max_sharps = 0usize;
    let mut saw_quote = false;
    let bytes = value.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'"' {
            saw_quote = true;
            let mut count = 0;
            let mut j = i + 1;
            while j < bytes.len() && bytes[j] == b'#' {
                count += 1;
                j += 1;
            }
            if count > max_sharps {
                max_sharps = count;
            }
        }
        i += 1;
    }

    if saw_quote {
        max_sharps + 1
    } else {
        0
    }
}

fn raw_string_literal_tokens(value: &str) -> TokenStream2 {
    let hashes = "#".repeat(raw_string_literal_sharp_count(value));
    let literal = format!("r{hashes}\"{value}\"{hashes}");
    literal
        .parse::<TokenStream2>()
        .expect("raw string literal should always parse")
}

fn normalize_component_tokens(stream: TokenStream2) -> TokenStream2 {
    fn normalize_stream(stream: TokenStream2) -> TokenStream2 {
        stream
            .into_iter()
            .map(|tt| match tt {
                TokenTree::Group(g) => {
                    let inner = normalize_stream(g.stream());
                    let mut new_group = Group::new(g.delimiter(), inner);
                    new_group.set_span(Span::call_site());
                    TokenTree::Group(new_group)
                }
                TokenTree::Ident(mut ident) => {
                    ident.set_span(Span::call_site());
                    TokenTree::Ident(ident)
                }
                TokenTree::Punct(mut punct) => {
                    punct.set_span(Span::call_site());
                    TokenTree::Punct(punct)
                }
                TokenTree::Literal(mut lit) => {
                    lit.set_span(Span::call_site());
                    TokenTree::Literal(lit)
                }
            })
            .collect()
    }

    normalize_stream(stream)
}

fn sanitize_component_snippet(inner: &str) -> String {
    let mut out = String::new();
    let mut text_buf = String::new();
    let mut in_tag = false;

    let mut chars = inner.chars().peekable();
    while let Some(ch) = chars.next() {
        match ch {
            '<' => {
                if !text_buf.is_empty() {
                    if text_buf.chars().any(|c| !c.is_whitespace()) {
                        if !text_buf.contains('"') {
                            let mut escaped = String::new();
                            for t in text_buf.chars() {
                                match t {
                                    '\\' => escaped.push_str("\\\\"),
                                    '"' => escaped.push_str("\\\""),
                                    _ => escaped.push(t),
                                }
                            }
                            out.push('"');
                            out.push_str(&escaped);
                            out.push('"');
                        } else {
                            out.push_str(&text_buf);
                        }
                    } else {
                        out.push_str(&text_buf);
                    }
                    text_buf.clear();
                }
                in_tag = true;
                out.push('<');
            }
            '>' => {
                in_tag = false;
                out.push('>');
            }
            _ => {
                if in_tag {
                    out.push(ch);
                } else {
                    text_buf.push(ch);
                }
            }
        }
    }

    if !text_buf.is_empty() {
        if text_buf.chars().any(|c| !c.is_whitespace()) {
            if !text_buf.contains('"') {
                let mut escaped = String::new();
                for t in text_buf.chars() {
                    match t {
                        '\\' => escaped.push_str("\\\\"),
                        '"' => escaped.push_str("\\\""),
                        _ => escaped.push(t),
                    }
                }
                out.push('"');
                out.push_str(&escaped);
                out.push('"');
            } else {
                out.push_str(&text_buf);
            }
        } else {
            out.push_str(&text_buf);
        }
    }

    out
}

#[derive(Debug)]
enum Source {
    Inline(LitStr),
    File {
        lit: LitStr,
        used_expr: Option<Expr>,
    },
    FileExpr(Expr),
    Url(LitStr),
    /// Any other expression; rendered at runtime with the built-in lightweight
    /// renderer without inline component expansion.
    Dynamic(Expr),
}

fn normalize_url_for_fetch(url: &str) -> String {
    rewrite_github_blob_url(url).unwrap_or_else(|| url.to_string())
}

fn rewrite_github_blob_url(url: &str) -> Option<String> {
    let prefixes = [
        "https://github.com/",
        "http://github.com/",
        "https://www.github.com/",
        "http://www.github.com/",
    ];
    let stripped = prefixes.iter().find_map(|p| url.strip_prefix(p))?;
    let mut parts: Vec<&str> = stripped.split('/').collect();
    let blob_idx = parts.iter().position(|part| *part == "blob")?;
    if blob_idx + 1 >= parts.len() {
        return None;
    }
    parts.remove(blob_idx);
    Some(format!(
        "https://raw.githubusercontent.com/{}",
        parts.join("/")
    ))
}

fn resolve_expr_to_path_lit(expr: &Expr) -> Option<LitStr> {
    if let Some(lit) = resolve_expr_to_lit(expr) {
        return Some(lit);
    }

    // Heuristic: if the expression is a bare identifier and a file with that
    // stem exists in the manifest directory, treat it as a compile-time path.
    if let Expr::Path(path) = expr {
        if path.qself.is_none() && path.path.segments.len() == 1 {
            let ident = path.path.segments.first().unwrap().ident.to_string();
            let manifest_dir =
                std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| String::from("."));
            let mut candidates = vec![ident.clone()];
            if !ident.ends_with(".md") {
                candidates.push(format!("{ident}.md"));
            }
            for cand in candidates {
                let mut full = PathBuf::from(&manifest_dir);
                full.push(&cand);
                if full.is_file() {
                    return Some(LitStr::new(&cand, expr.span()));
                }
            }
        }
    }

    None
}

impl Parse for Source {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.peek(Ident) && input.peek2(Token![=]) {
            let ident: Ident = input.parse()?;
            input.parse::<Token![=]>()?;
            let expr: Expr = input.parse()?;
            return match ident.to_string().as_str() {
                "file" | "path" => {
                    if let Some(lit) = resolve_expr_to_path_lit(&expr) {
                        let used_expr = match expr {
                            Expr::Lit(_) => None,
                            _ => Some(expr),
                        };
                        Ok(Source::File { lit, used_expr })
                    } else {
                        Ok(Source::FileExpr(expr))
                    }
                }
                "url" => {
                    // Prefer compile-time resolution so remote content can be fetched
                    // during macro expansion. If that fails, gracefully fall back to
                    // treating the expression as a dynamic Markdown string source,
                    // equivalent to calling `markdown_view!(expr)` without `url =`.
                    match resolve_expr_to_lit(&expr) {
                        Some(lit) => Ok(Source::Url(lit)),
                        None => Ok(Source::Dynamic(expr)),
                    }
                }
                _ => {
                    Err(input.error("markdown_view!: expected `file`, `path`, or `url` before `=`"))
                }
            };
        }

        let expr: Expr = input.parse()?;
        Ok(match resolve_expr_to_lit(&expr) {
            Some(lit) => Source::Inline(lit),
            None => Source::Dynamic(expr),
        })
    }
}

#[derive(Debug)]
struct MacroArgs {
    source: Source,
    anchor: AnchorRenderOptions,
}

fn extract_str_literal(expr: &Expr) -> Option<LitStr> {
    match expr {
        Expr::Lit(ExprLit {
            lit: Lit::Str(s), ..
        }) => Some(s.clone()),
        Expr::Call(call) => {
            if call.args.len() != 1 {
                return None;
            }
            // Allow String::from("...") and std::string::String::from("...").
            let is_string_from = match call.func.as_ref() {
                Expr::Path(path) => {
                    let mut segs = path.path.segments.iter().rev();
                    matches!(
                        (segs.next(), segs.next()),
                        (Some(last), Some(prev)) if last.ident == "from" && prev.ident == "String"
                    )
                }
                _ => false,
            };
            if !is_string_from {
                return None;
            }
            match call.args.first() {
                Some(Expr::Lit(ExprLit {
                    lit: Lit::Str(s), ..
                })) => Some(s.clone()),
                _ => None,
            }
        }
        Expr::MethodCall(method) => {
            if method.args.is_empty() {
                // Treat zero-arg method calls as transparent wrappers around the
                // receiver when resolving literals. This lets patterns like
                // `"url".to_string()`, `"url".clone()`, or `URL_CONST.clone()`
                // still be recognised as literal-like at compile time.
                resolve_expr_to_lit(method.receiver.as_ref())
            } else {
                None
            }
        }
        Expr::Macro(mac) => {
            if mac.mac.path.is_ident("format") {
                if let Ok(fmt) = syn::parse2::<FormatMacro>(mac.mac.tokens.clone()) {
                    // Simple cases: `format!("literal")` and `format!(IDENT)`.
                    if fmt.args.is_empty() {
                        match fmt.fmt {
                            Expr::Lit(ExprLit {
                                lit: Lit::Str(s), ..
                            }) => return Some(s),
                            Expr::Path(path)
                                if path.qself.is_none() && path.path.segments.len() == 1 =>
                            {
                                let ident = &path.path.segments[0].ident;
                                if let Some(lit) =
                                    resolve_ident_literal_from_source(ident, expr.span())
                                {
                                    return Some(lit);
                                }
                            }
                            _ => {}
                        }
                    } else {
                        // Heuristic: if the format string is a simple literal with `{}` placeholders
                        // and all arguments resolve to string literals, build a combined literal.
                        if let Expr::Lit(ExprLit {
                            lit: Lit::Str(fmt_lit),
                            ..
                        }) = &fmt.fmt
                        {
                            let fmt_value = fmt_lit.value();
                            let mut pieces: Vec<String> = Vec::new();
                            let mut placeholders = 0usize;
                            let mut current = String::new();
                            let mut chars = fmt_value.chars().peekable();
                            while let Some(ch) = chars.next() {
                                if ch == '{' {
                                    // Only accept a bare `{}` placeholder; anything else (named
                                    // arguments, formatting flags, or escaped braces) is treated as
                                    // too complex for literal resolution.
                                    if chars.peek() == Some(&'}') {
                                        chars.next();
                                        pieces.push(std::mem::take(&mut current));
                                        placeholders += 1;
                                    } else {
                                        // Unsupported formatting pattern.
                                        return None;
                                    }
                                } else if ch == '}' {
                                    // Unmatched closing brace.
                                    return None;
                                } else {
                                    current.push(ch);
                                }
                            }
                            pieces.push(current);

                            if placeholders != fmt.args.len() {
                                return None;
                            }

                            let mut out = String::new();
                            for (idx, piece) in pieces.iter().enumerate() {
                                out.push_str(piece);
                                if idx < fmt.args.len() {
                                    let arg_lit = resolve_expr_to_lit(&fmt.args[idx])?;
                                    out.push_str(&arg_lit.value());
                                }
                            }
                            return Some(LitStr::new(&out, fmt_lit.span()));
                        }
                    }
                }
            }
            None
        }
        _ => None,
    }
}

fn is_before_or_equal(a: LineColumn, b: LineColumn) -> bool {
    a.line < b.line || (a.line == b.line && a.column <= b.column)
}

fn span_contains(haystack: proc_macro2::Span, needle: proc_macro2::Span) -> bool {
    let h_start = haystack.start();
    let h_end = haystack.end();
    let n_start = needle.start();
    let n_end = needle.end();
    is_before_or_equal(h_start, n_start) && is_before_or_equal(n_end, h_end)
}

struct BindingFinder {
    target: String,
    usage_span: proc_macro2::Span,
    usage_start: LineColumn,
    best: Option<(LineColumn, LitStr)>,
}

impl BindingFinder {
    fn consider(&mut self, span: proc_macro2::Span, lit: LitStr) {
        let start = span.start();
        if !is_before_or_equal(start, self.usage_start) {
            return;
        }
        let replace = match &self.best {
            None => true,
            Some((prev, _)) => {
                start.line > prev.line || (start.line == prev.line && start.column >= prev.column)
            }
        };
        if replace {
            self.best = Some((start, lit));
        }
    }
}

impl<'ast> Visit<'ast> for BindingFinder {
    fn visit_item_fn(&mut self, i: &'ast ItemFn) {
        if span_contains(i.block.span(), self.usage_span) {
            syn::visit::visit_item_fn(self, i);
        }
    }

    fn visit_impl_item_fn(&mut self, i: &'ast ImplItemFn) {
        if span_contains(i.block.span(), self.usage_span) {
            syn::visit::visit_impl_item_fn(self, i);
        }
    }

    fn visit_trait_item_fn(&mut self, i: &'ast TraitItemFn) {
        if let Some(block) = &i.default {
            if span_contains(block.span(), self.usage_span) {
                syn::visit::visit_trait_item_fn(self, i);
            }
        }
    }

    fn visit_item_mod(&mut self, m: &'ast ItemMod) {
        if let Some((_, items)) = &m.content {
            if span_contains(m.span(), self.usage_span) {
                for item in items {
                    self.visit_item(item);
                }
            }
        }
    }

    fn visit_item_impl(&mut self, i: &'ast ItemImpl) {
        if span_contains(i.span(), self.usage_span) {
            syn::visit::visit_item_impl(self, i);
        }
    }

    fn visit_block(&mut self, b: &'ast Block) {
        if span_contains(b.span(), self.usage_span) {
            syn::visit::visit_block(self, b);
        }
    }

    fn visit_local(&mut self, local: &'ast Local) {
        if let Pat::Ident(pat_ident) = &local.pat {
            if pat_ident.mutability.is_none() && pat_ident.ident == self.target {
                if let Some(init) = &local.init {
                    if let Some(lit) = extract_str_literal(&init.expr) {
                        self.consider(local.span(), lit);
                    }
                }
            }
        }
        syn::visit::visit_local(self, local);
    }

    fn visit_item_const(&mut self, item: &'ast ItemConst) {
        if item.ident == self.target {
            if let Some(lit) = extract_str_literal(&item.expr) {
                self.consider(item.span(), lit);
            }
        }
        syn::visit::visit_item_const(self, item);
    }

    fn visit_item_static(&mut self, item: &'ast ItemStatic) {
        if matches!(item.mutability, StaticMutability::None) && item.ident == self.target {
            if let Some(lit) = extract_str_literal(&item.expr) {
                self.consider(item.span(), lit);
            }
        }
        syn::visit::visit_item_static(self, item);
    }
}

fn resolve_ident_literal_from_source(
    ident: &Ident,
    usage_span: proc_macro2::Span,
) -> Option<LitStr> {
    // Best-effort: scan the same source file for the nearest non-mut string
    // binding of this identifier that appears before the macro invocation.
    let path = usage_span.local_file()?;
    let source = fs::read_to_string(&path).ok()?;

    // First, try the AST-based heuristic used originally (covers `const`/`static`
    // and some simple locals when spans line up).
    if let Ok(file) = syn::parse_file(&source) {
        let mut finder = BindingFinder {
            target: ident.to_string(),
            usage_span,
            usage_start: usage_span.start(),
            best: None,
        };
        finder.visit_file(&file);
        if let Some((_, lit)) = finder.best {
            return Some(LitStr::new(&lit.value(), usage_span));
        }
    }

    // Fallback: a lightweight textual scan that looks only at lines before the
    // macro usage, so that common patterns like
    //
    //   let url: String = String::from("https://...");
    //
    // are recognised even when span information from `syn::parse_file` does not
    // correspond to the original file layout.
    let ident_name = ident.to_string();
    let usage_line = usage_span.start().line as usize;
    if usage_line == 0 {
        return None;
    }

    let mut last_value: Option<String> = None;
    for (idx, line) in source.lines().enumerate() {
        let line_no = idx + 1;
        if line_no >= usage_line {
            break;
        }

        // Skip lines that obviously don't mention the identifier.
        if !line.contains(&ident_name) {
            continue;
        }

        let trimmed = line.trim_start();
        let eq_idx = match trimmed.find('=') {
            Some(i) => i,
            None => continue,
        };
        let (lhs, rhs_with_eq) = trimmed.split_at(eq_idx);
        let lhs = lhs.trim_end();
        let rhs = rhs_with_eq[1..].trim_start(); // skip '='

        // Extract the binding name from the left-hand side. We support common
        // patterns such as:
        //   let name: Type = ...;
        //   const NAME: &str = ...;
        //   static NAME: &str = ...;
        //   pub const NAME: &str = ...;
        //   pub static NAME: &str = ...;
        let mut words = lhs
            .split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
            .filter(|w| !w.is_empty());

        let first = match words.next() {
            Some(w) => w,
            None => continue,
        };

        let binding_name = match first {
            "let" | "const" | "static" => {
                let mut name = match words.next() {
                    Some(w) => w,
                    None => continue,
                };
                if name == "mut" {
                    name = match words.next() {
                        Some(w) => w,
                        None => continue,
                    };
                }
                name
            }
            "pub" => {
                let kw = match words.next() {
                    Some(w) => w,
                    None => continue,
                };
                if kw != "let" && kw != "const" && kw != "static" {
                    continue;
                }
                let mut name = match words.next() {
                    Some(w) => w,
                    None => continue,
                };
                if name == "mut" {
                    name = match words.next() {
                        Some(w) => w,
                        None => continue,
                    };
                }
                name
            }
            _ => continue,
        };

        if binding_name != ident_name {
            continue;
        }

        let rhs_trimmed = rhs.trim_end();
        if rhs_trimmed.is_empty() {
            continue;
        }

        if let Some(value) = extract_simple_string_literal(rhs_trimmed) {
            last_value = Some(value);
        }
    }

    last_value.map(|val| LitStr::new(&val, usage_span))
}

fn extract_simple_string_literal(rhs: &str) -> Option<String> {
    let rhs = rhs.trim();

    // Handle `String::from("...")` and `std::string::String::from("...")`-like patterns.
    if let Some(idx) = rhs.find("String::from(\"") {
        let start = idx + "String::from(\"".len();
        let rest = &rhs[start..];
        if let Some(end_rel) = rest.find('"') {
            let value = &rest[..end_rel];
            return Some(value.to_string());
        }
    }

    // Fallback: a bare `"..."` string literal on the line.
    if let Some(start) = rhs.find('"') {
        let rest = &rhs[start + 1..];
        if let Some(end_rel) = rest.find('"') {
            let value = &rest[..end_rel];
            return Some(value.to_string());
        }
    }

    // As a final heuristic, treat a bare, URL‑like value with no quotes or
    // whitespace as a simple literal. This mainly covers cases where the caller
    // already passes the concrete string value (for example, `"https://..."`).
    if !rhs.contains('"') && rhs.contains("://") && rhs.split_whitespace().count() == 1 {
        return Some(rhs.to_string());
    }

    None
}

fn resolve_expr_to_lit(expr: &Expr) -> Option<LitStr> {
    if let Some(lit) = extract_str_literal(expr) {
        return Some(lit);
    }
    match expr {
        Expr::Group(group) => resolve_expr_to_lit(&group.expr),
        Expr::Reference(r) => resolve_expr_to_lit(&r.expr),
        Expr::Paren(p) => resolve_expr_to_lit(&p.expr),
        Expr::Path(path) => {
            if path.qself.is_none() && path.path.segments.len() == 1 {
                let ident = &path.path.segments[0].ident;
                return resolve_ident_literal_from_source(ident, expr.span());
            }
            None
        }
        _ => None,
    }
}

struct FormatMacro {
    fmt: Expr,
    args: Vec<Expr>,
}

impl Parse for FormatMacro {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let fmt: Expr = input.parse()?;
        let mut args = Vec::new();
        if input.peek(Token![,]) {
            let _: Token![,] = input.parse()?;
            while !input.is_empty() {
                let arg: Expr = input.parse()?;
                args.push(arg);
                if input.peek(Token![,]) {
                    let _: Token![,] = input.parse()?;
                } else {
                    break;
                }
            }
        }
        Ok(Self { fmt, args })
    }
}

impl Parse for MacroArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut anchor_enabled: Option<bool> = None;
        let mut anchor_class: Option<String> = None;
        let mut anchor_style: Option<String> = None;
        let mut anchor_wrapper_class: Option<String> = None;
        let mut anchor_wrapper_style: Option<String> = None;
        let mut anchor_symbol: Option<String> = None;

        if input.is_empty() {
            return Err(input.error("markdown_view!: expected a source argument"));
        }

        let source: Source = input.parse()?;

        while input.peek(Token![,]) {
            let _ = input.parse::<Token![,]>();
            if input.is_empty() {
                break;
            }
            if !(input.peek(Ident) && input.peek2(Token![=])) {
                return Err(
                    input.error("markdown_view!: expected `key = value` option after source")
                );
            }
            let ident: Ident = input.parse()?;
            input.parse::<Token![=]>()?;
            match ident.to_string().as_str() {
                "anchor" => {
                    let flag: LitBool = input.parse()?;
                    anchor_enabled = Some(flag.value());
                }
                "anchor_class" => {
                    let lit: LitStr = input.parse()?;
                    anchor_class = Some(lit.value());
                }
                "anchor_style" => {
                    let lit: LitStr = input.parse()?;
                    anchor_style = Some(lit.value());
                }
                "anchor_wrapper_class" => {
                    let lit: LitStr = input.parse()?;
                    anchor_wrapper_class = Some(lit.value());
                }
                "anchor_wrapper_style" => {
                    let lit: LitStr = input.parse()?;
                    anchor_wrapper_style = Some(lit.value());
                }
                "anchor_symbol" => {
                    let lit: LitStr = input.parse()?;
                    anchor_symbol = Some(lit.value());
                }
                _ => {
                    return Err(input.error(
                        "markdown_view!: expected `anchor`, `anchor_class`, `anchor_style`, `anchor_wrapper_class`, `anchor_wrapper_style`, or `anchor_symbol`",
                    ))
                }
            }
        }

        if !input.is_empty() {
            return Err(input.error("markdown_view!: unexpected tokens after source"));
        }

        let mut anchor = AnchorRenderOptions::default();
        if let Some(enabled) = anchor_enabled {
            anchor.enabled = enabled;
        }
        if let Some(class) = anchor_class {
            anchor.class = if class.is_empty() { None } else { Some(class) };
        }
        if let Some(style) = anchor_style {
            anchor.style = if style.is_empty() { None } else { Some(style) };
        }
        if let Some(wrapper_class) = anchor_wrapper_class {
            anchor.wrapper_class = if wrapper_class.is_empty() {
                None
            } else {
                Some(wrapper_class)
            };
        }
        if let Some(wrapper_style) = anchor_wrapper_style {
            anchor.wrapper_style = if wrapper_style.is_empty() {
                None
            } else {
                Some(wrapper_style)
            };
        }
        if let Some(symbol) = anchor_symbol {
            anchor.symbol = symbol;
        }

        Ok(MacroArgs { source, anchor })
    }
}

fn runtime_helpers_tokens() -> TokenStream2 {
    quote! {
        #[derive(Clone, Copy)]
        struct __MdvAnchorOptions {
            enabled: bool,
            class: Option<&'static str>,
            style: Option<&'static str>,
            wrapper_class: Option<&'static str>,
            wrapper_style: Option<&'static str>,
            symbol: &'static str,
        }
        const __MDV_TOC_MARKER: &str = "[[toc]]";
        fn __mdv_has_toc_marker(markdown: &str) -> bool {
            markdown.contains(__MDV_TOC_MARKER)
        }
        fn __mdv_strip_toc_marker<'a>(markdown: &'a str) -> ::std::borrow::Cow<'a, str> {
            if markdown.contains(__MDV_TOC_MARKER) {
                ::std::borrow::Cow::Owned(markdown.replace(__MDV_TOC_MARKER, ""))
            } else {
                ::std::borrow::Cow::Borrowed(markdown)
            }
        }
        fn __mdv_escape_html(input: &str) -> String {
            let mut escaped = String::with_capacity(input.len());
            for ch in input.chars() {
                match ch {
                    '&' => escaped.push_str("&amp;"),
                    '<' => escaped.push_str("&lt;"),
                    '>' => escaped.push_str("&gt;"),
                    '"' => escaped.push_str("&quot;"),
                    '\'' => escaped.push_str("&#39;"),
                    _ => escaped.push(ch),
                }
            }
            escaped
        }
        fn __mdv_render_anchor_html(id: &str, options: __MdvAnchorOptions) -> Option<String> {
            if !options.enabled || id.is_empty() {
                return None;
            }
            let mut html = String::new();
            html.push_str("<a");
            if let Some(class) = options.class {
                if !class.is_empty() {
                    html.push_str(" class=\"");
                    html.push_str(&__mdv_escape_html(class));
                    html.push('"');
                }
            }
            if let Some(style) = options.style {
                if !style.is_empty() {
                    html.push_str(" style=\"");
                    html.push_str(&__mdv_escape_html(style));
                    html.push('"');
                }
            }
            html.push_str(" href=\"#");
            html.push_str(&__mdv_escape_html(id));
            html.push_str("\" aria-hidden=\"true\">");
            html.push_str(&__mdv_escape_html(options.symbol));
            html.push_str("</a>");
            Some(html)
        }
        fn __mdv_is_combining_mark(ch: char) -> bool {
            matches!(
                ch,
                '\u{0300}'..='\u{036F}'
                    | '\u{1AB0}'..='\u{1AFF}'
                    | '\u{1DC0}'..='\u{1DFF}'
                    | '\u{20D0}'..='\u{20FF}'
                    | '\u{FE20}'..='\u{FE2F}'
            )
        }
        fn __mdv_fold_diacritic(ch: char) -> Option<&'static str> {
            match ch {
                '\u{C0}' | '\u{C1}' | '\u{C2}' | '\u{C3}' | '\u{C4}' | '\u{C5}' | '\u{E0}' | '\u{E1}'
                | '\u{E2}' | '\u{E3}' | '\u{E4}' | '\u{E5}' | '\u{100}' | '\u{101}' | '\u{102}' | '\u{103}'
                | '\u{104}' | '\u{105}' | '\u{1CD}' | '\u{1CE}' | '\u{1DE}' | '\u{1DF}' | '\u{1E0}' | '\u{1E1}'
                | '\u{1FA}' | '\u{1FB}' | '\u{200}' | '\u{201}' | '\u{202}' | '\u{203}' | '\u{226}' | '\u{227}'
                | '\u{1E00}' | '\u{1E01}' | '\u{1EA0}' | '\u{1EA1}' | '\u{1EA2}' | '\u{1EA3}' | '\u{1EA4}'
                | '\u{1EA5}' | '\u{1EA6}' | '\u{1EA7}' | '\u{1EA8}' | '\u{1EA9}' | '\u{1EAA}' | '\u{1EAB}'
                | '\u{1EAC}' | '\u{1EAD}' | '\u{1EAE}' | '\u{1EAF}' | '\u{1EB0}' | '\u{1EB1}' | '\u{1EB2}'
                | '\u{1EB3}' | '\u{1EB4}' | '\u{1EB5}' | '\u{1EB6}' | '\u{1EB7}' | '\u{2090}' | '\u{FF21}'
                | '\u{FF41}' => Some("a"),
                '\u{1E02}' | '\u{1E03}' | '\u{1E04}' | '\u{1E05}' | '\u{1E06}' | '\u{1E07}' | '\u{FF22}'
                | '\u{FF42}' => Some("b"),
                '\u{C7}' | '\u{E7}' | '\u{106}' | '\u{107}' | '\u{108}' | '\u{109}' | '\u{10A}' | '\u{10B}'
                | '\u{10C}' | '\u{10D}' | '\u{1E08}' | '\u{1E09}' | '\u{FF23}' | '\u{FF43}' => Some("c"),
                '\u{D0}' | '\u{F0}' | '\u{10E}' | '\u{10F}' | '\u{110}' | '\u{111}' | '\u{1E0A}' | '\u{1E0B}'
                | '\u{1E0C}' | '\u{1E0D}' | '\u{1E0E}' | '\u{1E0F}' | '\u{1E10}' | '\u{1E11}' | '\u{1E12}'
                | '\u{1E13}' | '\u{FF24}' | '\u{FF44}' => Some("d"),
                '\u{C8}' | '\u{C9}' | '\u{CA}' | '\u{CB}' | '\u{E8}' | '\u{E9}' | '\u{EA}' | '\u{EB}' | '\u{112}'
                | '\u{113}' | '\u{114}' | '\u{115}' | '\u{116}' | '\u{117}' | '\u{118}' | '\u{119}' | '\u{11A}'
                | '\u{11B}' | '\u{204}' | '\u{205}' | '\u{206}' | '\u{207}' | '\u{228}' | '\u{229}' | '\u{1E14}'
                | '\u{1E15}' | '\u{1E16}' | '\u{1E17}' | '\u{1E18}' | '\u{1E19}' | '\u{1E1A}' | '\u{1E1B}'
                | '\u{1E1C}' | '\u{1E1D}' | '\u{1EB8}' | '\u{1EB9}' | '\u{1EBA}' | '\u{1EBB}' | '\u{1EBC}'
                | '\u{1EBD}' | '\u{1EBE}' | '\u{1EBF}' | '\u{1EC0}' | '\u{1EC1}' | '\u{1EC2}' | '\u{1EC3}'
                | '\u{1EC4}' | '\u{1EC5}' | '\u{1EC6}' | '\u{1EC7}' | '\u{2091}' | '\u{FF25}' | '\u{FF45}'
                => Some("e"),
                '\u{1E1E}' | '\u{1E1F}' | '\u{FF26}' | '\u{FF46}' => Some("f"),
                '\u{11C}' | '\u{11D}' | '\u{11E}' | '\u{11F}' | '\u{120}' | '\u{121}' | '\u{122}' | '\u{123}'
                | '\u{1E6}' | '\u{1E7}' | '\u{1F4}' | '\u{1F5}' | '\u{1E20}' | '\u{1E21}' | '\u{FF27}'
                | '\u{FF47}' => Some("g"),
                '\u{124}' | '\u{125}' | '\u{126}' | '\u{127}' | '\u{21E}' | '\u{21F}' | '\u{1E22}' | '\u{1E23}'
                | '\u{1E24}' | '\u{1E25}' | '\u{1E26}' | '\u{1E27}' | '\u{1E28}' | '\u{1E29}' | '\u{1E2A}'
                | '\u{1E2B}' | '\u{1E96}' | '\u{2095}' | '\u{FF28}' | '\u{FF48}' => Some("h"),
                '\u{CC}' | '\u{CD}' | '\u{CE}' | '\u{CF}' | '\u{EC}' | '\u{ED}' | '\u{EE}' | '\u{EF}' | '\u{128}'
                | '\u{129}' | '\u{12A}' | '\u{12B}' | '\u{12C}' | '\u{12D}' | '\u{12E}' | '\u{12F}' | '\u{130}'
                | '\u{131}' | '\u{1CF}' | '\u{1D0}' | '\u{208}' | '\u{209}' | '\u{20A}' | '\u{20B}' | '\u{1D62}'
                | '\u{1E2C}' | '\u{1E2D}' | '\u{1E2E}' | '\u{1E2F}' | '\u{1EC8}' | '\u{1EC9}' | '\u{1ECA}'
                | '\u{1ECB}' | '\u{2071}' | '\u{FF29}' | '\u{FF49}' => Some("i"),
                '\u{134}' | '\u{135}' | '\u{1F0}' | '\u{2C7C}' | '\u{FF2A}' | '\u{FF4A}' => Some("j"),
                '\u{136}' | '\u{137}' | '\u{138}' | '\u{1E8}' | '\u{1E9}' | '\u{1E30}' | '\u{1E31}'
                | '\u{1E32}' | '\u{1E33}' | '\u{1E34}' | '\u{1E35}' | '\u{2096}' | '\u{FF2B}' | '\u{FF4B}'
                => Some("k"),
                '\u{139}' | '\u{13A}' | '\u{13B}' | '\u{13C}' | '\u{13D}' | '\u{13E}' | '\u{13F}' | '\u{140}'
                | '\u{141}' | '\u{142}'
                | '\u{1E36}' | '\u{1E37}' | '\u{1E38}' | '\u{1E39}' | '\u{1E3A}' | '\u{1E3B}' | '\u{1E3C}'
                | '\u{1E3D}' | '\u{2097}' | '\u{FF2C}' | '\u{FF4C}' => Some("l"),
                '\u{1E3E}' | '\u{1E3F}' | '\u{1E40}' | '\u{1E41}' | '\u{1E42}' | '\u{1E43}' | '\u{2098}'
                | '\u{FF2D}' | '\u{FF4D}' => Some("m"),
                '\u{D1}' | '\u{F1}' | '\u{143}' | '\u{144}' | '\u{145}' | '\u{146}' | '\u{147}' | '\u{148}'
                | '\u{149}' | '\u{14A}' | '\u{14B}' | '\u{1F8}' | '\u{1F9}' | '\u{1E44}' | '\u{1E45}'
                | '\u{1E46}'
                | '\u{1E47}' | '\u{1E48}' | '\u{1E49}' | '\u{1E4A}' | '\u{1E4B}' | '\u{207F}' | '\u{2099}'
                | '\u{FF2E}' | '\u{FF4E}' => Some("n"),
                '\u{D2}' | '\u{D3}' | '\u{D4}' | '\u{D5}' | '\u{D6}' | '\u{D8}' | '\u{F2}' | '\u{F3}'
                | '\u{F4}' | '\u{F5}' | '\u{F6}' | '\u{F8}' | '\u{14C}' | '\u{14D}' | '\u{14E}' | '\u{14F}'
                | '\u{150}' | '\u{151}' | '\u{1A0}' | '\u{1A1}' | '\u{1D1}' | '\u{1D2}' | '\u{1EA}' | '\u{1EB}'
                | '\u{1EC}' | '\u{1ED}' | '\u{1FE}' | '\u{1FF}' | '\u{20C}' | '\u{20D}' | '\u{20E}' | '\u{20F}'
                | '\u{22A}' | '\u{22B}' | '\u{22C}' | '\u{22D}' | '\u{22E}' | '\u{22F}' | '\u{230}' | '\u{231}'
                | '\u{1E4C}' | '\u{1E4D}' | '\u{1E4E}' | '\u{1E4F}' | '\u{1E50}' | '\u{1E51}' | '\u{1E52}'
                | '\u{1E53}' | '\u{1ECC}' | '\u{1ECD}' | '\u{1ECE}' | '\u{1ECF}' | '\u{1ED0}' | '\u{1ED1}'
                | '\u{1ED2}' | '\u{1ED3}' | '\u{1ED4}' | '\u{1ED5}' | '\u{1ED6}' | '\u{1ED7}' | '\u{1ED8}'
                | '\u{1ED9}' | '\u{1EDA}' | '\u{1EDB}' | '\u{1EDC}' | '\u{1EDD}' | '\u{1EDE}' | '\u{1EDF}'
                | '\u{1EE0}' | '\u{1EE1}' | '\u{1EE2}' | '\u{1EE3}' | '\u{2092}' | '\u{FF2F}' | '\u{FF4F}'
                => Some("o"),
                '\u{1E54}' | '\u{1E55}' | '\u{1E56}' | '\u{1E57}' | '\u{209A}' | '\u{FF30}' | '\u{FF50}'
                => Some("p"),
                '\u{FF31}' | '\u{FF51}' => Some("q"),
                '\u{154}' | '\u{155}' | '\u{156}' | '\u{157}' | '\u{158}' | '\u{159}' | '\u{210}' | '\u{211}'
                | '\u{212}' | '\u{213}' | '\u{1D63}' | '\u{1E58}' | '\u{1E59}' | '\u{1E5A}' | '\u{1E5B}'
                | '\u{1E5C}' | '\u{1E5D}' | '\u{1E5E}' | '\u{1E5F}' | '\u{FF32}' | '\u{FF52}' => Some("r"),
                '\u{15A}' | '\u{15B}' | '\u{15C}' | '\u{15D}' | '\u{15E}' | '\u{15F}' | '\u{160}' | '\u{161}'
                | '\u{17F}' | '\u{218}' | '\u{219}' | '\u{1E60}' | '\u{1E61}' | '\u{1E62}' | '\u{1E63}'
                | '\u{1E64}' | '\u{1E65}' | '\u{1E66}' | '\u{1E67}' | '\u{1E68}' | '\u{1E69}' | '\u{1E9B}'
                | '\u{209B}' | '\u{FF33}' | '\u{FF53}' => Some("s"),
                '\u{162}' | '\u{163}' | '\u{164}' | '\u{165}' | '\u{21A}' | '\u{21B}' | '\u{1E6A}' | '\u{1E6B}'
                | '\u{1E6C}' | '\u{1E6D}' | '\u{1E6E}' | '\u{1E6F}' | '\u{1E70}' | '\u{1E71}' | '\u{1E97}'
                | '\u{209C}' | '\u{FF34}' | '\u{FF54}' => Some("t"),
                '\u{D9}' | '\u{DA}' | '\u{DB}' | '\u{DC}' | '\u{F9}' | '\u{FA}' | '\u{FB}' | '\u{FC}' | '\u{168}'
                | '\u{169}' | '\u{16A}' | '\u{16B}' | '\u{16C}' | '\u{16D}' | '\u{16E}' | '\u{16F}' | '\u{170}'
                | '\u{171}' | '\u{172}' | '\u{173}' | '\u{1AF}' | '\u{1B0}' | '\u{1D3}' | '\u{1D4}' | '\u{1D5}'
                | '\u{1D6}' | '\u{1D7}' | '\u{1D8}' | '\u{1D9}' | '\u{1DA}' | '\u{1DB}' | '\u{1DC}' | '\u{214}'
                | '\u{215}' | '\u{216}' | '\u{217}' | '\u{1D64}' | '\u{1E72}' | '\u{1E73}' | '\u{1E74}'
                | '\u{1E75}' | '\u{1E76}' | '\u{1E77}' | '\u{1E78}' | '\u{1E79}' | '\u{1E7A}' | '\u{1E7B}'
                | '\u{1EE4}' | '\u{1EE5}' | '\u{1EE6}' | '\u{1EE7}' | '\u{1EE8}' | '\u{1EE9}' | '\u{1EEA}'
                | '\u{1EEB}' | '\u{1EEC}' | '\u{1EED}' | '\u{1EEE}' | '\u{1EEF}' | '\u{1EF0}' | '\u{1EF1}'
                | '\u{FF35}' | '\u{FF55}' => Some("u"),
                '\u{1D65}' | '\u{1E7C}' | '\u{1E7D}' | '\u{1E7E}' | '\u{1E7F}' | '\u{FF36}' | '\u{FF56}'
                => Some("v"),
                '\u{174}' | '\u{175}' | '\u{1E80}' | '\u{1E81}' | '\u{1E82}' | '\u{1E83}' | '\u{1E84}'
                | '\u{1E85}' | '\u{1E86}' | '\u{1E87}' | '\u{1E88}' | '\u{1E89}' | '\u{1E98}' | '\u{FF37}'
                | '\u{FF57}' => Some("w"),
                '\u{1E8A}' | '\u{1E8B}' | '\u{1E8C}' | '\u{1E8D}' | '\u{2093}' | '\u{FF38}' | '\u{FF58}'
                => Some("x"),
                '\u{DD}' | '\u{FD}' | '\u{FF}' | '\u{176}' | '\u{177}' | '\u{178}' | '\u{232}' | '\u{233}'
                | '\u{1E8E}' | '\u{1E8F}' | '\u{1E99}' | '\u{1EF2}' | '\u{1EF3}' | '\u{1EF4}' | '\u{1EF5}'
                | '\u{1EF6}' | '\u{1EF7}' | '\u{1EF8}' | '\u{1EF9}' | '\u{FF39}' | '\u{FF59}' => Some("y"),
                '\u{179}' | '\u{17A}' | '\u{17B}' | '\u{17C}' | '\u{17D}' | '\u{17E}' | '\u{1E90}' | '\u{1E91}'
                | '\u{1E92}' | '\u{1E93}' | '\u{1E94}' | '\u{1E95}' | '\u{FF3A}' | '\u{FF5A}' => Some("z"),
                '\u{C6}' | '\u{E6}' | '\u{1E2}' | '\u{1E3}' | '\u{1FC}' | '\u{1FD}' => Some("ae"),
                '\u{1C4}' | '\u{1C5}' | '\u{1C6}' | '\u{1F1}' | '\u{1F2}' | '\u{1F3}' => Some("dz"),
                '\u{FB00}' => Some("ff"),
                '\u{FB01}' => Some("fi"),
                '\u{FB02}' => Some("fl"),
                '\u{132}' | '\u{133}' => Some("ij"),
                '\u{1C7}' | '\u{1C8}' | '\u{1C9}' => Some("lj"),
                '\u{1CA}' | '\u{1CB}' | '\u{1CC}' => Some("nj"),
                '\u{152}' | '\u{153}' => Some("oe"),
                '\u{DF}' => Some("ss"),
                '\u{FB05}' | '\u{FB06}' => Some("st"),
                '\u{DE}' | '\u{FE}' => Some("th"),
                '\u{FB03}' => Some("ffi"),
                '\u{FB04}' => Some("ffl"),
                _ => None,
            }
        }
        fn __mdv_slugify_heading(text: &str) -> String {
            let mut out = String::new();
            let mut last_dash = false;

            for ch in text.chars() {
                if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
                    out.push(ch.to_ascii_lowercase());
                    last_dash = false;
                } else if let Some(folded) = __mdv_fold_diacritic(ch) {
                    out.push_str(folded);
                    last_dash = false;
                } else if __mdv_is_combining_mark(ch) {
                    continue;
                } else if ch.is_alphanumeric() {
                    for lower in ch.to_lowercase() {
                        out.push(lower);
                    }
                    last_dash = false;
                } else if !last_dash && !out.is_empty() {
                    out.push('-');
                    last_dash = true;
                }
            }

            while out.ends_with('-') {
                out.pop();
            }

            out
        }
        struct __MdvAnchorSlugger {
            counts: ::std::collections::HashMap<String, usize>,
        }
        impl __MdvAnchorSlugger {
            fn new() -> Self {
                Self {
                    counts: ::std::collections::HashMap::new(),
                }
            }
            fn register_custom(&mut self, id: &str) {
                let entry = self.counts.entry(id.to_string()).or_insert(0);
                *entry += 1;
            }
            fn slugify(&mut self, text: &str) -> String {
                let base = __mdv_slugify_heading(text);
                if base.is_empty() {
                    return base;
                }
                let count = self.counts.entry(base.clone()).or_insert(0);
                let slug = if *count == 0 {
                    base.clone()
                } else {
                    format!("{base}-{}", *count)
                };
                *count += 1;
                slug
            }
        }
        fn __mdv_parse_heading_attrs(text: &str) -> (::std::string::String, Option<String>) {
            let trimmed = text.trim_end();
            if let Some(start) = trimmed.rfind('{') {
                if trimmed.ends_with('}') {
                    let attrs = &trimmed[start + 1..trimmed.len() - 1];
                    let mut custom_id: Option<String> = None;
                    let mut has_attr = false;
                    for part in attrs.split_whitespace() {
                        if let Some(id) = part.strip_prefix('#') {
                            if !id.is_empty() {
                                custom_id = Some(id.to_string());
                                has_attr = true;
                                break;
                            }
                        } else if part.starts_with('.') || part.contains('=') {
                            has_attr = true;
                        }
                    }
                    if has_attr {
                        let before = trimmed[..start].trim_end();
                        return (before.to_string(), custom_id);
                    }
                }
            }
            (trimmed.to_string(), None)
        }
        fn __mdv_parse_atx_heading(line: &str) -> Option<(u8, ::std::string::String, Option<String>)> {
            let trimmed = line.trim_start();
            let mut level = 0usize;
            for ch in trimmed.chars() {
                if ch == '#' {
                    level += 1;
                } else {
                    break;
                }
            }
            if level == 0 || level > 6 {
                return None;
            }
            let rest = trimmed[level..].trim_start();
            let mut text = rest.trim_end();
            if text.ends_with('#') {
                let mut end = text.len();
                while end > 0 && text.as_bytes()[end - 1] == b'#' {
                    end -= 1;
                }
                text = text[..end].trim_end();
            }
            let (clean_text, custom_id) = __mdv_parse_heading_attrs(text);
            Some((level as u8, clean_text, custom_id))
        }
        fn __mdv_setext_level(line: &str) -> Option<u8> {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                return None;
            }
            let mut chars = trimmed.chars();
            let first = chars.next()?;
            if first != '=' && first != '-' {
                return None;
            }
            if chars.any(|c| c != first) {
                return None;
            }
            Some(if first == '=' { 1 } else { 2 })
        }
        enum __MdvBlock {
            Heading {
                level: u8,
                text: ::std::string::String,
                custom_id: Option<String>,
            },
            Paragraph(::std::string::String),
        }
        fn __mdv_push_paragraph(
            blocks: &mut ::std::vec::Vec<__MdvBlock>,
            lines: &mut ::std::vec::Vec<::std::string::String>,
        ) {
            if lines.is_empty() {
                return;
            }
            let joined = lines.join(" ");
            let text = joined.trim().to_string();
            if !text.is_empty() {
                blocks.push(__MdvBlock::Paragraph(text));
            }
            lines.clear();
        }
        fn __mdv_parse_blocks(markdown: &str) -> ::std::vec::Vec<__MdvBlock> {
            let mut blocks = ::std::vec::Vec::new();
            let mut para_lines: ::std::vec::Vec<::std::string::String> = ::std::vec::Vec::new();
            let lines: ::std::vec::Vec<&str> = markdown
                .lines()
                .map(|line| line.trim_end_matches('\r'))
                .collect();
            let mut idx: usize = 0;

            if let Some(first) = lines.first() {
                let first_trimmed = first.trim();
                if first_trimmed == "---" || first_trimmed == "+++" {
                    let mut end_idx = None;
                    for i in 1..lines.len() {
                        if lines[i].trim() == first_trimmed {
                            end_idx = Some(i);
                            break;
                        }
                    }
                    if let Some(end_idx) = end_idx {
                        idx = end_idx + 1;
                    }
                }
            }

            while idx < lines.len() {
                let line = lines[idx];
                if let Some((level, text, custom_id)) = __mdv_parse_atx_heading(line) {
                    __mdv_push_paragraph(&mut blocks, &mut para_lines);
                    if !text.trim().is_empty() {
                        blocks.push(__MdvBlock::Heading {
                            level,
                            text,
                            custom_id,
                        });
                    }
                    idx += 1;
                    continue;
                }
                if !line.trim().is_empty() && idx + 1 < lines.len() {
                    if let Some(level) = __mdv_setext_level(lines[idx + 1]) {
                        let (text, custom_id) = __mdv_parse_heading_attrs(line);
                        __mdv_push_paragraph(&mut blocks, &mut para_lines);
                        if !text.trim().is_empty() {
                            blocks.push(__MdvBlock::Heading {
                                level,
                                text,
                                custom_id,
                            });
                        }
                        idx += 2;
                        continue;
                    }
                }
                if line.trim().is_empty() {
                    __mdv_push_paragraph(&mut blocks, &mut para_lines);
                    idx += 1;
                    continue;
                }
                para_lines.push(line.to_string());
                idx += 1;
            }

            __mdv_push_paragraph(&mut blocks, &mut para_lines);
            blocks
        }
        fn __mdv_render_markdown_to_html(
            markdown: impl AsRef<str>,
            anchor_options: __MdvAnchorOptions,
        ) -> String {
            let markdown = markdown.as_ref();
            let cleaned = __mdv_strip_toc_marker(markdown);
            let markdown = cleaned.as_ref();
            if markdown.trim().is_empty() {
                return String::new();
            }
            let mut html_output = String::new();
            let mut anchor_slugger = __MdvAnchorSlugger::new();
            for block in __mdv_parse_blocks(markdown) {
                match block {
                    __MdvBlock::Heading {
                        level,
                        text,
                        custom_id,
                    } => {
                        let anchor_id = if let Some(id) = custom_id {
                            anchor_slugger.register_custom(&id);
                            Some(id)
                        } else {
                            let slug = anchor_slugger.slugify(&text);
                            if slug.is_empty() { None } else { Some(slug) }
                        };
                        html_output.push_str("<h");
                        html_output.push_str(&level.to_string());
                        if let Some(id) = anchor_id.as_ref() {
                            html_output.push_str(" id=\"");
                            html_output.push_str(&__mdv_escape_html(id));
                            html_output.push('"');
                        }
                        if let Some(class) = anchor_options.wrapper_class {
                            if !class.is_empty() {
                                html_output.push_str(" class=\"");
                                html_output.push_str(&__mdv_escape_html(class));
                                html_output.push('"');
                            }
                        }
                        if let Some(style) = anchor_options.wrapper_style {
                            if !style.is_empty() {
                                html_output.push_str(" style=\"");
                                html_output.push_str(&__mdv_escape_html(style));
                                html_output.push('"');
                            }
                        }
                        html_output.push('>');
                        if let Some(id) = anchor_id.as_ref() {
                            if let Some(anchor_html) = __mdv_render_anchor_html(id, anchor_options) {
                                html_output.push_str(&anchor_html);
                            }
                        }
                        html_output.push_str(&__mdv_escape_html(&text));
                        html_output.push_str("</h");
                        html_output.push_str(&level.to_string());
                        html_output.push('>');
                    }
                    __MdvBlock::Paragraph(text) => {
                        if text.trim().is_empty() {
                            continue;
                        }
                        html_output.push_str("<p>");
                        html_output.push_str(&__mdv_escape_html(&text));
                        html_output.push_str("</p>");
                    }
                }
            }
            html_output
        }
        fn __mdv_extract_anchors(
            markdown: impl AsRef<str>,
        ) -> ::std::vec::Vec<(::std::string::String, ::std::string::String)> {
            let markdown = markdown.as_ref();
            if markdown.trim().is_empty() {
                return ::std::vec::Vec::new();
            }
            if !__mdv_has_toc_marker(markdown) {
                return ::std::vec::Vec::new();
            }
            let mut anchors: ::std::vec::Vec<(::std::string::String, ::std::string::String)> =
                ::std::vec::Vec::new();
            let mut slugger = __MdvAnchorSlugger::new();
            for block in __mdv_parse_blocks(markdown) {
                if let __MdvBlock::Heading {
                    text,
                    custom_id,
                    ..
                } = block
                {
                    let anchor_id = if let Some(id) = custom_id {
                        slugger.register_custom(&id);
                        Some(id)
                    } else {
                        let slug = slugger.slugify(&text);
                        if slug.is_empty() { None } else { Some(slug) }
                    };
                    if let Some(id) = anchor_id {
                        anchors.push((text.trim().to_string(), id));
                    }
                }
            }
            anchors
        }
    }
}

/// Compile-time Markdown to a Leptos `view!` tree with optional inline components.
///
/// What it accepts:
/// - Inline strings (`"..."`, `r#"..."#`, `String::from("...")`, `"...".to_string()`).
/// - `file = "path.md"`: read at compile time (relative to `CARGO_MANIFEST_DIR`) and
///   recompiled on change.
/// - `file = some_var`: if the macro can see a real file at that path while compiling
///   (e.g., `let content = "content.md";` in the same module), it embeds the file like
///   the literal form. Otherwise it falls back to reading at runtime (non-wasm only).
/// - `url = "https://..."`: fetched at compile time (disabled in rust-analyzer). Literal-like
///   wrappers such as `url = String::from("https://...")`, raw strings
///   (`url = r#"https://..."#`), or simple `format!` calls with literal parts
///   (including `format!("https://host/{}", slug)` where `slug` itself comes from a
///   literal binding) are also resolved when possible. If the value cannot be resolved
///   to a literal at compile time, the macro falls back to treating the expression as a
///   dynamic Markdown string, equivalent to calling `markdown_view!(expr)` without `url =`.
/// - Any other expression: rendered at runtime using a lightweight built-in parser
///   that supports headings/paragraphs and heading IDs. Inline `{{ ... }}` component
///   syntax is **not** expanded in this mode; it is rendered as literal text. To use
///   `{{ <MyComponent/> }}` you must pass a source that can be resolved to a string
///   literal or file path at compile time so the macro can split the Markdown and
///   inject real Leptos components.
///
/// Inline Leptos components:
/// - Use `{{ ... }}` inside the Markdown: `{{ <MyComponent prop=value/> }}`.
/// - Component expansion only happens for sources that are resolved at compile time
///   (inline literals, `file = "..."`, or identifiers that the macro can trace back to
///   a literal binding in the same file). Dynamic `String` inputs are rendered purely
///   as Markdown/HTML without interpreting `{{ ... }}`.
///
/// Options:
/// - `anchor = true/false,`: enable/disable heading anchor links (IDs are still generated).
/// - `anchor_class = "...",`: override the CSS class for heading anchors.
/// - `anchor_style = "...",`: set inline styles for heading anchors.
/// - `anchor_wrapper_class = "...",`: add a CSS class on the heading wrapper element.
/// - `anchor_wrapper_style = "...",`: add inline styles on the heading wrapper element.
/// - `anchor_symbol = "...",`: set the text shown for the anchor link.
///
/// Minimal examples:
/// ```ignore
/// markdown_view!("# Title\n\nSome text.");
/// markdown_view!(r#"Hello {{ <Hello/> }}!"#);
/// markdown_view!(file = "content.md");                // compile-time include
/// let content = format!("content.md");                
/// let view = markdown_view!(file = content);          // resolves if file exists at build time
/// let body = r#"Inline {{ <MyComp/> }} via variable."#;
/// let inline_view = markdown_view!(body);             // still compile-time, components work
/// let runtime_body: String = load_somehow();          // rendered at runtime
/// let view_runtime = markdown_view!(runtime_body);    // `{{ ... }}` stays literal
/// ```
#[proc_macro]
pub fn markdown_view(input: TokenStream) -> TokenStream {
    let MacroArgs {
        source: parsed,
        anchor,
    } = parse_macro_input!(input as MacroArgs);

    let anchor_enabled = anchor.enabled;
    let anchor_symbol_lit = LitStr::new(&anchor.symbol, Span::call_site());
    let anchor_class_tokens = match anchor.class.as_ref() {
        Some(class) => {
            let lit = LitStr::new(class, Span::call_site());
            quote! { Some(#lit) }
        }
        None => quote! { None },
    };
    let anchor_style_tokens = match anchor.style.as_ref() {
        Some(style) => {
            let lit = LitStr::new(style, Span::call_site());
            quote! { Some(#lit) }
        }
        None => quote! { None },
    };
    let anchor_wrapper_class_tokens = match anchor.wrapper_class.as_ref() {
        Some(class) => {
            let lit = LitStr::new(class, Span::call_site());
            quote! { Some(#lit) }
        }
        None => quote! { None },
    };
    let anchor_wrapper_style_tokens = match anchor.wrapper_style.as_ref() {
        Some(style) => {
            let lit = LitStr::new(style, Span::call_site());
            quote! { Some(#lit) }
        }
        None => quote! { None },
    };
    let anchor_options_tokens = quote! {
        __MdvAnchorOptions {
            enabled: #anchor_enabled,
            class: #anchor_class_tokens,
            style: #anchor_style_tokens,
            wrapper_class: #anchor_wrapper_class_tokens,
            wrapper_style: #anchor_wrapper_style_tokens,
            symbol: #anchor_symbol_lit,
        }
    };

    let runtime_helpers = runtime_helpers_tokens();

    // Keep lit for include_str! emission
    let mut file_path_lit: Option<LitStr> = None;
    let mut url_lit: Option<LitStr> = None;
    let mut usage_hint: Option<TokenStream2> = None;

    let markdown_source: String = match parsed {
        Source::Dynamic(expr) => {
            let expanded = quote! {{
                #runtime_helpers
                let __md_source = #expr;
                let __mdv_anchor_options = #anchor_options_tokens;
                let __html = __mdv_render_markdown_to_html(
                    __md_source,
                    __mdv_anchor_options,
                );
                ::leptos::view! { <div inner_html={__html}></div> }
            }};
            return expanded.into();
        }
        Source::FileExpr(path_expr) => {
            let expanded = quote! {{
                #runtime_helpers
                #[cfg(target_arch = "wasm32")]
                {
                    ::leptos::view! { <div>"(markdown_view!: `file = <expr>` requires a literal path on wasm)"</div> }
                }
                #[cfg(not(target_arch = "wasm32"))]
                {
                    let __path_val = #path_expr;
                    let __markdown = ::std::fs::read_to_string(&__path_val).unwrap_or_else(|err| {
                        panic!(
                            "markdown_view!: failed to read file at '{}': {}",
                            __path_val, err
                        )
                    });
                    let __mdv_anchor_options = #anchor_options_tokens;
                    let __html = __mdv_render_markdown_to_html(
                        __markdown,
                        __mdv_anchor_options,
                    );
                    ::leptos::view! { <div inner_html={__html}></div> }
                }
            }};
            return expanded.into();
        }
        Source::Inline(lit) => lit.value(),
        Source::File { lit, used_expr } => {
            file_path_lit = Some(lit.clone());
            let manifest_dir =
                std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| String::from("."));
            let mut full_path = PathBuf::from(manifest_dir);
            full_path.push(lit.value());
            let content = match fs::read_to_string(&full_path) {
                Ok(content) => content,
                Err(err) => {
                    let msg = format!(
                        "markdown_view!: failed to read file at '{}': {}",
                        full_path.display(),
                        err
                    );
                    return syn::Error::new(proc_macro2::Span::call_site(), msg)
                        .to_compile_error()
                        .into();
                }
            };
            if let Some(expr) = used_expr {
                // Keep the caller expression "used" to avoid unused variable warnings when
                // we resolved the path during macro expansion.
                usage_hint = Some(quote! { let _ = #expr; });
            }
            content
        }
        Source::Url(lit) => {
            let span = lit.span();
            url_lit = Some(lit.clone());
            let normalized_url = normalize_url_for_fetch(&lit.value());
            if is_rust_analyzer() {
                let msg = "(remote Markdown preview disabled in rust-analyzer)";
                let expanded = quote! {{
                    ::leptos::view! { <div>{#msg}</div> }
                }};
                return expanded.into();
            }
            let fetch_result = (|| {
                let client = reqwest::blocking::Client::builder()
                    .user_agent(concat!(
                        env!("CARGO_PKG_NAME"),
                        "/",
                        env!("CARGO_PKG_VERSION"),
                        " (+",
                        env!("CARGO_PKG_REPOSITORY"),
                        ")"
                    ))
                    .build()?;
                let resp = client.get(&normalized_url).send()?.error_for_status()?;
                resp.text()
            })();

            match fetch_result {
                Ok(body) => body,
                Err(err) => {
                    let msg = format!(
                        "markdown_view!: HTTP GET '{}' failed: {}",
                        normalized_url, err
                    );
                    return syn::Error::new(span, msg).to_compile_error().into();
                }
            }
        }
    };

    let segments = split_markdown_with_components(&markdown_source);
    let mut slugger = AnchorSlugger::default();

    let parts: Vec<TokenStream2> = segments
        .into_iter()
        .filter_map(|seg| match seg {
            Segment::Markdown(md) => {
                let cleaned = strip_toc_marker(&md);
                let cleaned_md = cleaned.as_ref();
                if cleaned_md.trim().is_empty() {
                    None
                } else {
                    let html_output =
                        convert_markdown_to_html_with_slugger(cleaned_md, &mut slugger, &anchor);
                    let literal = raw_string_literal_tokens(&html_output);
                    Some(quote! { <div inner_html={#literal}></div> })
                }
            }
            Segment::Component(ts) => Some(quote! { #ts }),
        })
        .collect();

    let include_stmt = if let Some(lit) = file_path_lit {
        // Turn file into an input dependency of the build to trigger recompiles.
        quote! { let _ = include_str!(::core::concat!(env!("CARGO_MANIFEST_DIR"), "/", #lit)); }
    } else if let Some(lit) = url_lit {
        let _ = lit; // keep future-proofed: url literal still available for diagnostics
        quote! {}
    } else {
        quote! {}
    };

    let expanded = quote! {{
        #include_stmt
        #usage_hint
        ::leptos::view! {
            <div>
                #(#parts)*
            </div>
        }
    }};

    expanded.into()
}

/// Collect all heading anchors from Markdown sources at compile time or runtime.
///
/// Usage mirrors `markdown_view!` for `file =`, `url =`, inline strings, and
/// dynamic expressions. Returns `Vec<(String, String)>` of `(title, id)` pairs in
/// document order.
#[proc_macro]
pub fn markdown_anchors(input: TokenStream) -> TokenStream {
    let MacroArgs { source: parsed, .. } = parse_macro_input!(input as MacroArgs);

    let runtime_helpers = runtime_helpers_tokens();

    // Keep lit for include_str! emission
    let mut file_path_lit: Option<LitStr> = None;
    let mut url_lit: Option<LitStr> = None;
    let mut usage_hint: Option<TokenStream2> = None;

    let markdown_source: String = match parsed {
        Source::Dynamic(expr) => {
            let expanded = quote! {{
                #runtime_helpers
                let __md_source = #expr;
                __mdv_extract_anchors(__md_source)
            }};
            return expanded.into();
        }
        Source::FileExpr(path_expr) => {
            let expanded = quote! {{
                #runtime_helpers
                #[cfg(target_arch = "wasm32")]
                {
                    ::std::vec::Vec::<(::std::string::String, ::std::string::String)>::new()
                }
                #[cfg(not(target_arch = "wasm32"))]
                {
                    let __path_val = #path_expr;
                    let __markdown = ::std::fs::read_to_string(&__path_val).unwrap_or_else(|err| {
                        panic!(
                            "markdown_anchors!: failed to read file at '{}': {}",
                            __path_val, err
                        )
                    });
                    __mdv_extract_anchors(__markdown)
                }
            }};
            return expanded.into();
        }
        Source::Inline(lit) => lit.value(),
        Source::File { lit, used_expr } => {
            file_path_lit = Some(lit.clone());
            let manifest_dir =
                std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| String::from("."));
            let mut full_path = PathBuf::from(manifest_dir);
            full_path.push(lit.value());
            let content = match fs::read_to_string(&full_path) {
                Ok(content) => content,
                Err(err) => {
                    let msg = format!(
                        "markdown_anchors!: failed to read file at '{}': {}",
                        full_path.display(),
                        err
                    );
                    return syn::Error::new(proc_macro2::Span::call_site(), msg)
                        .to_compile_error()
                        .into();
                }
            };
            if let Some(expr) = used_expr {
                usage_hint = Some(quote! { let _ = #expr; });
            }
            content
        }
        Source::Url(lit) => {
            let span = lit.span();
            url_lit = Some(lit.clone());
            let normalized_url = normalize_url_for_fetch(&lit.value());
            if is_rust_analyzer() {
                return quote! {
                    ::std::vec::Vec::<(::std::string::String, ::std::string::String)>::new()
                }
                .into();
            }
            let fetch_result = (|| {
                let client = reqwest::blocking::Client::builder()
                    .user_agent(concat!(
                        env!("CARGO_PKG_NAME"),
                        "/",
                        env!("CARGO_PKG_VERSION"),
                        " (+",
                        env!("CARGO_PKG_REPOSITORY"),
                        ")"
                    ))
                    .build()?;
                let resp = client.get(&normalized_url).send()?.error_for_status()?;
                resp.text()
            })();

            match fetch_result {
                Ok(body) => body,
                Err(err) => {
                    let msg = format!(
                        "markdown_anchors!: HTTP GET '{}' failed: {}",
                        normalized_url, err
                    );
                    return syn::Error::new(span, msg).to_compile_error().into();
                }
            }
        }
    };

    let mut anchors: Vec<(String, String)> = Vec::new();
    if has_toc_marker(&markdown_source) {
        let segments = split_markdown_with_components(&markdown_source);
        let mut slugger = AnchorSlugger::default();
        for seg in segments {
            if let Segment::Markdown(md) = seg {
                anchors.extend(collect_markdown_anchors_with_slugger(&md, &mut slugger));
            }
        }
    }

    let anchor_tokens: Vec<TokenStream2> = anchors
        .iter()
        .map(|(title, id)| {
            let title_lit = LitStr::new(title, Span::call_site());
            let id_lit = LitStr::new(id, Span::call_site());
            quote! {
                (
                    ::std::string::String::from(#title_lit),
                    ::std::string::String::from(#id_lit)
                )
            }
        })
        .collect();

    let include_stmt = if let Some(lit) = file_path_lit {
        quote! { let _ = include_str!(::core::concat!(env!("CARGO_MANIFEST_DIR"), "/", #lit)); }
    } else if let Some(lit) = url_lit {
        let _ = lit;
        quote! {}
    } else {
        quote! {}
    };

    let expanded = quote! {{
        #include_stmt
        #usage_hint
        ::std::vec![#(#anchor_tokens),*]
    }};

    expanded.into()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn runtime_parse_heading_attrs(text: &str) -> (String, Option<String>) {
        let trimmed = text.trim_end();
        if let Some(start) = trimmed.rfind('{') {
            if trimmed.ends_with('}') {
                let attrs = &trimmed[start + 1..trimmed.len() - 1];
                let mut custom_id: Option<String> = None;
                let mut has_attr = false;
                for part in attrs.split_whitespace() {
                    if let Some(id) = part.strip_prefix('#') {
                        if !id.is_empty() {
                            custom_id = Some(id.to_string());
                            has_attr = true;
                            break;
                        }
                    } else if part.starts_with('.') || part.contains('=') {
                        has_attr = true;
                    }
                }
                if has_attr {
                    let before = trimmed[..start].trim_end();
                    return (before.to_string(), custom_id);
                }
            }
        }
        (trimmed.to_string(), None)
    }

    fn runtime_parse_atx_heading(line: &str) -> Option<(u8, String, Option<String>)> {
        let trimmed = line.trim_start();
        let mut level = 0usize;
        for ch in trimmed.chars() {
            if ch == '#' {
                level += 1;
            } else {
                break;
            }
        }
        if level == 0 || level > 6 {
            return None;
        }
        let rest = trimmed[level..].trim_start();
        let mut text = rest.trim_end();
        if text.ends_with('#') {
            let mut end = text.len();
            while end > 0 && text.as_bytes()[end - 1] == b'#' {
                end -= 1;
            }
            text = text[..end].trim_end();
        }
        let (clean_text, custom_id) = runtime_parse_heading_attrs(text);
        Some((level as u8, clean_text, custom_id))
    }

    fn runtime_setext_level(line: &str) -> Option<u8> {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            return None;
        }
        let mut chars = trimmed.chars();
        let first = chars.next()?;
        if first != '=' && first != '-' {
            return None;
        }
        if chars.any(|c| c != first) {
            return None;
        }
        Some(if first == '=' { 1 } else { 2 })
    }

    fn runtime_extract_anchors_for_test(markdown: &str) -> Vec<(String, String)> {
        let lines: Vec<&str> = markdown
            .lines()
            .map(|line| line.trim_end_matches('\r'))
            .collect();
        let mut idx: usize = 0;

        if let Some(first) = lines.first() {
            let first_trimmed = first.trim();
            if first_trimmed == "---" || first_trimmed == "+++" {
                let mut end_idx = None;
                for i in 1..lines.len() {
                    if lines[i].trim() == first_trimmed {
                        end_idx = Some(i);
                        break;
                    }
                }
                if let Some(end_idx) = end_idx {
                    idx = end_idx + 1;
                }
            }
        }

        let mut anchors = Vec::new();
        let mut slugger = AnchorSlugger::default();
        while idx < lines.len() {
            let line = lines[idx];
            if let Some((_level, text, custom_id)) = runtime_parse_atx_heading(line) {
                if !text.trim().is_empty() {
                    let anchor_id = if let Some(id) = custom_id {
                        slugger.register_custom(&id);
                        Some(id)
                    } else {
                        let slug = slugger.slugify(&text);
                        if slug.is_empty() {
                            None
                        } else {
                            Some(slug)
                        }
                    };
                    if let Some(id) = anchor_id {
                        anchors.push((text.trim().to_string(), id));
                    }
                }
                idx += 1;
                continue;
            }
            if !line.trim().is_empty() && idx + 1 < lines.len() {
                if runtime_setext_level(lines[idx + 1]).is_some() {
                    let (text, custom_id) = runtime_parse_heading_attrs(line);
                    if !text.trim().is_empty() {
                        let anchor_id = if let Some(id) = custom_id {
                            slugger.register_custom(&id);
                            Some(id)
                        } else {
                            let slug = slugger.slugify(&text);
                            if slug.is_empty() {
                                None
                            } else {
                                Some(slug)
                            }
                        };
                        if let Some(id) = anchor_id {
                            anchors.push((text.trim().to_string(), id));
                        }
                    }
                    idx += 2;
                    continue;
                }
            }
            idx += 1;
        }

        anchors
    }

    #[test]
    fn convert_markdown_to_html_basic() {
        let html = convert_markdown_to_html("# Title\n\nSome **bold** text.");
        assert!(html.contains("<h1 id=\"title\"><a class=\"header-anchor\" href=\"#title\""));
        assert!(html.contains(">#</a>Title</h1>"));
        assert!(html.contains("<strong>bold</strong>"));
        assert!(html.contains("<p>"));
    }

    #[test]
    fn convert_markdown_to_html_accepts_string() {
        let html = convert_markdown_to_html(String::from("just **bold** text"));
        assert!(html.contains("<strong>bold</strong>"));
    }

    #[test]
    fn convert_markdown_to_html_empty() {
        let html = convert_markdown_to_html("");
        assert_eq!(html, "");
        let html_ws = convert_markdown_to_html("   \n\t\n");
        assert_eq!(html_ws, "");
    }

    #[test]
    fn convert_markdown_skips_metadata_blocks() {
        let yaml_html = convert_markdown_to_html("---\ntitle: Example\n---\n\n# Title");
        assert!(yaml_html.contains("<h1 id=\"title\">"));
        assert!(!yaml_html.contains("title: Example"));

        let plus_html = convert_markdown_to_html("+++\nfoo = \"bar\"\n+++\n\n# Title");
        assert!(plus_html.contains("<h1 id=\"title\">"));
        assert!(!plus_html.contains("foo = \"bar\""));
    }

    #[test]
    fn convert_markdown_custom_heading_anchor() {
        let html = convert_markdown_to_html("# Using custom anchors {#my-anchor}");
        assert!(
            html.contains("<h1 id=\"my-anchor\"><a class=\"header-anchor\" href=\"#my-anchor\"")
        );
        assert!(html.contains(">#</a>Using custom anchors</h1>"));
        assert!(!html.contains("{#my-anchor}"));
    }

    #[test]
    fn convert_markdown_preserves_heading_attributes() {
        let html = convert_markdown_to_html("# Heading {#custom .fancy data-foo=bar}");
        assert!(html.contains("id=\"custom\""));
        assert!(html.contains("class=\"fancy\""));
        assert!(html.contains("data-foo=\"bar\""));
    }

    #[test]
    fn convert_markdown_custom_anchor_rendering() {
        let mut slugger = AnchorSlugger::default();
        let anchor_options = AnchorRenderOptions {
            enabled: true,
            class: Some("my-anchor".to_string()),
            style: Some("color: #f40;".to_string()),
            wrapper_class: Some("anchor-wrap".to_string()),
            wrapper_style: Some("display: inline-block;".to_string()),
            symbol: "§".to_string(),
        };
        let html = convert_markdown_to_html_with_slugger("# Title", &mut slugger, &anchor_options);
        assert!(html.contains("class=\"anchor-wrap\""));
        assert!(html.contains("style=\"display: inline-block;\""));
        assert!(html.contains("class=\"my-anchor\""));
        assert!(html.contains("style=\"color: #f40;\""));
        assert!(html.contains(">§</a>Title</h1>"));
    }

    #[test]
    fn markdown_view_strips_toc_marker() {
        let input = "[[toc]]\n# Title";
        let cleaned = strip_toc_marker(input);
        let mut slugger = AnchorSlugger::default();
        let anchor_options = AnchorRenderOptions::default();
        let html = convert_markdown_to_html_with_slugger(cleaned.as_ref(), &mut slugger, &anchor_options);
        assert!(!html.contains("[[toc]]"));
        assert!(html.contains("Title"));
    }

    fn collect_anchors_for_toc(markdown: &str) -> Vec<(String, String)> {
        if !has_toc_marker(markdown) {
            return Vec::new();
        }
        let segments = split_markdown_with_components(markdown);
        let mut slugger = AnchorSlugger::default();
        let mut anchors = Vec::new();
        for seg in segments {
            if let Segment::Markdown(md) = seg {
                anchors.extend(collect_markdown_anchors_with_slugger(&md, &mut slugger));
            }
        }
        anchors
    }

    #[test]
    fn markdown_anchors_returns_when_toc_present() {
        let anchors = collect_anchors_for_toc("[[toc]]\n# Title");
        assert_eq!(
            anchors,
            vec![("Title".to_string(), "title".to_string())]
        );
    }

    #[test]
    fn markdown_anchors_empty_without_toc() {
        let anchors = collect_anchors_for_toc("# Title");
        assert!(anchors.is_empty());
    }

    #[test]
    fn collect_markdown_anchors_basic() {
        let anchors = collect_markdown_anchors("# Title\n\n## Title");
        assert_eq!(
            anchors,
            vec![
                ("Title".to_string(), "title".to_string()),
                ("Title".to_string(), "title-1".to_string())
            ]
        );
    }

    #[test]
    fn collect_markdown_anchors_custom_id() {
        let anchors = collect_markdown_anchors("# Custom {#custom-id}");
        assert_eq!(
            anchors,
            vec![("Custom".to_string(), "custom-id".to_string())]
        );
    }

    #[test]
    fn collect_markdown_anchors_heading_attributes() {
        let anchors = collect_markdown_anchors("# Title {#custom .fancy data-foo=bar}");
        assert_eq!(anchors, vec![("Title".to_string(), "custom".to_string())]);
    }

    #[test]
    fn collect_markdown_anchors_strips_accents() {
        let input = "# \u{00DA}ltimo par\u{00E1}grafo";
        let anchors = collect_markdown_anchors(input);
        assert_eq!(
            anchors,
            vec![(
                "\u{00DA}ltimo par\u{00E1}grafo".to_string(),
                "ultimo-paragrafo".to_string()
            )]
        );
    }

    #[test]
    fn runtime_anchors_ignore_yaml_front_matter() {
        let md = [
            "---",
            "title: Um título imponente",
            "date: 2025-03-13T04:00:00Z",
            "lang: pt-br",
            "duration: 1min",
            "---",
            "",
            "No principio...",
        ]
        .join("\n");
        let anchors = runtime_extract_anchors_for_test(&md);
        assert!(anchors.is_empty(), "anchors: {:?}", anchors);
    }

    #[test]
    fn runtime_anchors_ignore_toml_front_matter() {
        let md = [
            "+++",
            "title = \"Um título imponente\"",
            "date = 2025-03-13T04:00:00Z",
            "lang = \"pt-br\"",
            "duration = \"1min\"",
            "+++",
            "",
            "No principio...",
        ]
        .join("\n");
        let anchors = runtime_extract_anchors_for_test(&md);
        assert!(anchors.is_empty(), "anchors: {:?}", anchors);
    }

    #[test]
    fn split_markdown_with_components_simple() {
        let input = "Hello {{ <MyComp/> }} world";
        let segments = split_markdown_with_components(input);
        assert_eq!(segments.len(), 3);
        match &segments[0] {
            Segment::Markdown(s) => assert_eq!(s, "Hello "),
            _ => panic!("expected markdown"),
        }
        match &segments[1] {
            Segment::Component(_) => {}
            _ => panic!("expected component"),
        }
        match &segments[2] {
            Segment::Markdown(s) => assert_eq!(s, " world"),
            _ => panic!("expected markdown"),
        }
    }

    #[test]
    fn split_markdown_with_components_string_input() {
        let input = String::from("Owned {{ <MyComp/> }} value");
        let segments = split_markdown_with_components(input);
        assert_eq!(segments.len(), 3);
        assert!(matches!(segments[0], Segment::Markdown(_)));
        assert!(matches!(segments[1], Segment::Component(_)));
        assert!(matches!(segments[2], Segment::Markdown(_)));
    }

    #[test]
    fn macro_input_accepts_dynamic_file_path_expr() {
        let parsed: Source = syn::parse_str(r#"file = format!("location/{}", path)"#).unwrap();
        assert!(matches!(parsed, Source::FileExpr(_)));
    }

    #[test]
    fn macro_input_accepts_format_without_args_as_literal_path() {
        let parsed: Source = syn::parse_str(r#"file = format!("content.md")"#).unwrap();
        match parsed {
            Source::File { lit, .. } => assert_eq!(lit.value(), "content.md"),
            _ => panic!("expected literal file path from format!"),
        }
    }

    #[test]
    fn macro_input_accepts_literal_file_path() {
        let parsed: Source = syn::parse_str(r#"file = "content.md""#).unwrap();
        match parsed {
            Source::File { lit, .. } => assert_eq!(lit.value(), "content.md"),
            _ => panic!("expected literal file path"),
        }
    }

    #[test]
    fn macro_accepts_url_literal_source() {
        let parsed: Source = syn::parse_str(r#"url = "https://example.com/readme.md""#).unwrap();
        match parsed {
            Source::Url(lit) => assert!(lit.value().contains("https://example.com")),
            _ => panic!("expected url literal"),
        }
    }

    #[test]
    fn macro_accepts_url_clone_on_literal() {
        let parsed: Source =
            syn::parse_str(r#"url = "https://example.com/readme.md".clone()"#).unwrap();
        match parsed {
            Source::Url(lit) => assert!(lit.value().contains("https://example.com")),
            _ => panic!("expected url literal via clone()"),
        }
    }

    #[test]
    fn macro_accepts_url_format_with_literal_arg() {
        let parsed: Source =
            syn::parse_str(r#"url = format!("https://example.com/posts/{}", "slug")"#).unwrap();
        match parsed {
            Source::Url(lit) => {
                assert_eq!(lit.value(), "https://example.com/posts/slug");
            }
            _ => panic!("expected url literal from format! with arg"),
        }
    }

    #[test]
    fn macro_accepts_url_string_from_literal() {
        let parsed: Source =
            syn::parse_str(r#"url = String::from("https://example.com/readme.md")"#).unwrap();
        match parsed {
            Source::Url(lit) => {
                assert_eq!(lit.value(), "https://example.com/readme.md");
            }
            _ => panic!("expected url literal from String::from"),
        }
    }

    #[test]
    fn macro_accepts_url_raw_string_literal() {
        let parsed: Source =
            syn::parse_str(r##"url = r#"https://example.com/readme.md"#"##).unwrap();
        match parsed {
            Source::Url(lit) => {
                assert_eq!(lit.value(), "https://example.com/readme.md");
            }
            _ => panic!("expected url literal from raw string"),
        }
    }

    #[test]
    fn macro_url_falls_back_to_dynamic_when_not_literal_like() {
        // `slug` is unresolved in this standalone parse, so the expression cannot
        // be reduced to a literal; we should treat it as a dynamic expression
        // rather than erroring.
        let parsed: Source =
            syn::parse_str(r#"url = format!("https://example.com/posts/{}", slug)"#).unwrap();
        assert!(matches!(parsed, Source::Dynamic(_)));
    }

    #[test]
    fn macro_input_accepts_string_from_literal() {
        let parsed: Source = syn::parse_str(r#"String::from("Owned")"#).unwrap();
        match parsed {
            Source::Inline(lit) => assert_eq!(lit.value(), "Owned"),
            _ => panic!("expected literal variant"),
        }
    }

    #[test]
    fn extract_simple_string_literal_parses_string_from() {
        let rhs = r#"String::from("http://example.com/path")"#;
        let extracted = extract_simple_string_literal(rhs).expect("should extract");
        assert_eq!(extracted, "http://example.com/path");
    }

    #[test]
    fn extract_simple_string_literal_parses_example_url() {
        let rhs = String::from("https://example.com/abc");
        let extracted = extract_simple_string_literal(rhs.as_str()).expect("should extract");
        assert_eq!(extracted, "https://example.com/abc");
    }

    #[test]
    fn macro_input_allows_dynamic_expression() {
        let parsed: Source = syn::parse_str("some_variable").unwrap();
        assert!(matches!(parsed, Source::Dynamic(_)));
    }

    #[test]
    fn split_markdown_with_components_unclosed() {
        let input = "Hello {{";
        let segments = split_markdown_with_components(input);
        assert_eq!(segments.len(), 1);
        match &segments[0] {
            Segment::Markdown(s) => assert_eq!(s, "Hello {{"),
            _ => panic!("expected markdown"),
        }
    }

    #[test]
    fn split_markdown_with_components_only_text() {
        let input = "Just text";
        let segments = split_markdown_with_components(input);
        assert_eq!(segments.len(), 1);
        match &segments[0] {
            Segment::Markdown(s) => assert_eq!(s, "Just text"),
            _ => panic!("expected markdown"),
        }
    }

    #[test]
    fn split_ignores_embeds_inside_code_fences() {
        let input =
            "````\ncode with {{ <Nope/> }} inside\n````\nAfter {{ <Yes/> }}".replace("````", "```");
        let segments = split_markdown_with_components(&input);
        // Expect two segments: code fence (markdown), then a component and trailing markdown.
        assert!(segments.len() >= 2);
        match &segments[0] {
            Segment::Markdown(s) => assert!(s.contains("code with {{ <Nope/> }} inside")),
            _ => panic!("expected markdown for code fence block"),
        }
        assert!(segments.iter().any(|s| matches!(s, Segment::Component(_))));
    }

    #[test]
    fn split_handles_multiline_component() {
        let input =
            "Before\n\n{{\n    <MyComp\n        foo=123\n        bar=\"baz\"\n    />\n}}\n\nAfter";
        let segments = split_markdown_with_components(input);
        assert_eq!(segments.len(), 3);
        assert!(matches!(segments[0], Segment::Markdown(_)));
        assert!(matches!(segments[1], Segment::Component(_)));
        assert!(matches!(segments[2], Segment::Markdown(_)));
    }

    #[test]
    fn split_handles_hebrew_block_multiline_with_nested_tags() {
        let input = r#"
Before

{{
 <HebrewBlock> 
    <ruby>ישב<rp>(</rp><rt>yoshev</rt><rp>)</rp></ruby>
    <ruby>בסתר<rp>(</rp><rt>beseter</rt><rp>)</rp></ruby>
    <ruby>עליון<rp>(</rp><rt>elyon</rt><rp>)</rp></ruby>
    <ruby>בצל<rp>(</rp><rt>betzel</rt><rp>)</rp></ruby>
    <ruby>שדי<rp>(</rp><rt>shaddai</rt><rp>)</rp></ruby>
    <ruby>יתלונן<rp>(</rp><rt>yitlonan</rt><rp>)</rp></ruby>
    </HebrewBlock> 
}}

After
"#;
        let segments = split_markdown_with_components(input);
        assert!(segments.iter().any(|s| matches!(s, Segment::Component(_))));
    }

    #[test]
    fn parses_multiline_hebrew_block_tokens() {
        let inner = r#"
 <HebrewBlock> 
    <ruby>ישב<rp>(</rp><rt>yoshev</rt><rp>)</rp></ruby>
    <ruby>בסתר<rp>(</rp><rt>beseter</rt><rp>)</rp></ruby>
    <ruby>עליון<rp>(</rp><rt>elyon</rt><rp>)</rp></ruby>
    <ruby>בצל<rp>(</rp><rt>betzel</rt><rp>)</rp></ruby>
    <ruby>שדי<rp>(</rp><rt>shaddai</rt><rp>)</rp></ruby>
    <ruby>יתלונן<rp>(</rp><rt>yitlonan</rt><rp>)</rp></ruby>
 </HebrewBlock> 
"#;
        let _ts: TokenStream2 = inner.parse().expect("should parse multiline HebrewBlock");
    }

    #[test]
    fn split_handles_hebrew_block_inline_with_nested_tags() {
        let input = r#"Text before {{ <HebrewBlock> <ruby>ישב<rp>(</rp><rt>yoshev</rt><rp>)</rp></ruby> </HebrewBlock> }} text after"#;
        let segments = split_markdown_with_components(input);
        assert!(segments.iter().any(|s| matches!(s, Segment::Component(_))));
    }

    #[test]
    fn split_handles_hebrew_block_with_inline_text() {
        let input = r#"{{ <HebrewBlock> בְּרֵאשִׁית בָּרָא אֱלֹהִים אֵת הַשָּׁמַיִם וְאֵת הָאָרֶץ </HebrewBlock> }}"#;
        let segments = split_markdown_with_components(input);
        assert_eq!(segments.len(), 1);
        assert!(matches!(segments[0], Segment::Component(_)));
    }

    #[test]
    fn rewrites_github_blob_urls_to_raw() {
        let raw = rewrite_github_blob_url(
            "https://github.com/leptos-rs/awesome-leptos/blob/main/README.md",
        )
        .expect("should rewrite blob url");
        assert_eq!(
            raw,
            "https://raw.githubusercontent.com/leptos-rs/awesome-leptos/main/README.md"
        );
    }

    #[test]
    fn leaves_non_github_urls() {
        let url = "https://example.com/docs/readme.md";
        assert_eq!(normalize_url_for_fetch(url), url);
    }

    #[test]
    fn split_multiple_components() {
        let input = "A {{ <One/> }} B {{ <Two/> }} C";
        let segments = split_markdown_with_components(input);
        assert_eq!(segments.len(), 5);
        assert!(matches!(segments[0], Segment::Markdown(_)));
        assert!(matches!(segments[1], Segment::Component(_)));
        assert!(matches!(segments[2], Segment::Markdown(_)));
        assert!(matches!(segments[3], Segment::Component(_)));
        assert!(matches!(segments[4], Segment::Markdown(_)));
    }
}
