@@ -5,26 +5,83 @@ import 'prismjs/themes/prism-tomorrow.css';
55import ' prismjs/components/prism-javascript' ;
66import ' prismjs/components/prism-python' ;
77
8- const { textBlock, className = " " } = Astro .props ;
9-
10- // Configure marked to use Prism for code blocks
11- marked .use ({
12- renderer: {
13- code(token : any ) {
14- // Debug logs
15- console .log (' Token:' , token );
16- let lang = token .lang || ' ' ;
17- if (lang === ' js' ) lang = ' javascript' ;
18- if (Prism .languages [lang ]) {
19- const highlighted = Prism .highlight (token .text , Prism .languages [lang ], lang );
20- console .log (' Highlighted result:' , highlighted );
21- return ` <pre class="language-${lang }"><code>${highlighted }</code></pre> ` ;
22- }
23- return ` <pre><code>${token .text }</code></pre> ` ;
24- }
8+ const { textBlock = ' ' , className = ' ' } = Astro .props ;
9+
10+ let __codeBlockCounter = 0 ;
11+
12+ const renderer = new marked .Renderer ();
13+ renderer .code = (code : any , infostring ? : string ) => {
14+ const rawLang = (infostring || ' ' ).trim ().split (/ \s + / )[0 ] || ' ' ;
15+ let lang = rawLang === ' js' ? ' javascript' : rawLang ;
16+ const safeLang = lang || ' text' ;
17+
18+ function escapeHtmlString(s : any ) {
19+ const str = String (s ?? ' ' );
20+ return str .replace (/ &/ g , ' &' ).replace (/ </ g , ' <' ).replace (/ >/ g , ' >' );
2521 }
26- });
2722
28- const htmlContent = marked .parse (textBlock );
23+ // `code` can sometimes be an object depending on marked version; ensure we have a string
24+ const rawCode = typeof code === ' string' ? code : (code && typeof (code as any ).text === ' string' ? (code as any ).text : String (code ?? ' ' ));
25+
26+ const highlighted = Prism .languages [lang ]
27+ ? Prism .highlight (rawCode , Prism .languages [lang ], lang )
28+ : escapeHtmlString (rawCode );
29+
30+ const id = ` code-${++ __codeBlockCounter } ` ;
31+
32+ return `
33+ <div class="code-terminal" role="group" aria-label="Code example" style="background:#0b1220;border:1px solid #2b3844;border-radius:8px;margin:1rem 0;overflow:hidden;">
34+ <div class="code-header" style="display:flex;justify-content:space-between;align-items:center;padding:0.4rem 0.6rem;background:linear-gradient(90deg, rgba(255,255,255,0.015), rgba(255,255,255,0.01));color:#9aa6b2;font-size:0.85rem;">
35+ <span class="code-lang" style="font-weight:600;text-transform:uppercase;font-size:0.75rem;color:#7f8b95;">${safeLang }</span>
36+ <button class="copy-btn" data-target="${id }" aria-label="Copy code" style="background:transparent;border:1px solid rgba(255,255,255,0.03);color:#9aa6b2;padding:0.25rem 0.45rem;border-radius:6px;cursor:pointer;font-size:0.9rem;display:inline-flex;align-items:center;gap:0.35rem;">
37+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
38+ <path d="M16 1H4C2.89543 1 2 1.89543 2 3V17H4V3H16V1Z" fill="#9aa6b2"/>
39+ <path d="M20 5H8C6.89543 5 6 5.89543 6 7V21C6 22.1046 6.89543 23 8 23H20C21.1046 23 22 22.1046 22 21V7C22 5.89543 21.1046 5 20 5ZM20 21H8V7H20V21Z" fill="#9aa6b2"/>
40+ </svg>
41+ </button>
42+ </div>
43+ <pre class="language-${safeLang }" style="margin:0;padding:0.75rem;overflow:auto;background:transparent;color:#cbd5d8;"><code id="${id }" style="font-family: 'Cascadia Mono PL', ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', 'Courier New', monospace;font-size:0.875rem;line-height:1.4;">${highlighted }</code></pre>
44+ </div>
45+ ` ;
46+ };
47+
48+ marked .use ({ renderer });
49+
50+ const htmlContent = marked .parse (textBlock || ' ' );
2951---
30- <div class ={ className } set:html ={ htmlContent } ></div >
52+ <section class ={ className } set:html ={ htmlContent } ></section >
53+
54+ <style >
55+ .code-terminal{ background:#0b1220; border:1px solid #2b3844; border-radius:8px; margin:1rem 0; overflow:hidden; }
56+ .code-header{ display:flex; justify-content:space-between; align-items:center; padding:0.4rem 0.6rem; background:linear-gradient(90deg, rgba(255,255,255,0.015), rgba(255,255,255,0.01)); color:#9aa6b2; font-size:0.85rem; }
57+ .code-lang{ font-weight:600; text-transform:uppercase; font-size:0.75rem; color:#7f8b95; }
58+ .copy-btn{ background:transparent; border:1px solid rgba(255,255,255,0.03); color:#9aa6b2; padding:0.25rem 0.45rem; border-radius:6px; cursor:pointer; font-size:0.9rem; display:inline-flex; align-items:center; gap:0.35rem; }
59+ .copy-btn:active{ transform:translateY(1px); }
60+ pre{ margin:0; padding:0.75rem; overflow:auto; background:transparent; color:#cbd5d8; }
61+ code{ font-family: 'Cascadia Mono PL', ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Courier New", monospace; font-size:0.875rem; line-height:1.4; }
62+ @media (prefers-color-scheme: light){ .code-terminal{ border-color:#e6e6e6 } }
63+ </style >
64+
65+ <script type =" module" >
66+ const successSVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15l-5-5 1.41-1.41L11 14.17l6.59-6.59L19 9l-8 8z" fill="#7ee787"/></svg >`;
67+ const failSVG = `<svg width =" 16" height =" 16" viewBox =" 0 0 24 24" fill =" none" xmlns =" http://www.w3.org/2000/svg" aria-hidden =" true" focusable =" false" ><path d =" M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4 13.59L15.59 16 12 12.41 8.41 16 8 15.59 11.59 12 8 8.41 8.41 8 12 11.59 15.59 8 16 8.41 12.41 12 16 15.59z" fill =" #ff7b7b" /></svg >`;
68+
69+ window.addEventListener('click', async (e) => {
70+ const btn = (e .target .closest && e .target .closest (' .copy-btn' )) || (e .target .matches && e .target .matches (' .copy-btn' ) && e .target );
71+ if (! btn ) return ;
72+ const id = btn .getAttribute (' data-target' );
73+ const codeEl = document .getElementById (id );
74+ if (! codeEl ) return ;
75+ const text = codeEl .textContent || codeEl .innerText || ' ' ;
76+ const origHTML = btn .innerHTML ;
77+ try {
78+ await navigator .clipboard .writeText (text );
79+ btn .innerHTML = successSVG ;
80+ btn .setAttribute (' aria-label' , ' Copied' );
81+ } catch (err ) {
82+ btn .innerHTML = failSVG ;
83+ btn .setAttribute (' aria-label' , ' Copy failed' );
84+ }
85+ setTimeout (() => { btn .innerHTML = origHTML ; btn .setAttribute (' aria-label' , ' Copy code' ); }, 1500 );
86+ } );
87+ </script >
0 commit comments