Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

url not being parsed correctly and not opening in new tab in broswer #3628

Open
deepsky1d opened this issue Feb 27, 2025 · 2 comments
Open

Comments

@deepsky1d
Copy link

Marked version:
marked@15.0.7

Describe the bug
I have a url links in my blog .md file, example:
$ cat notes.md | grep https
🔗 Learn more about GPT-5
🎨 Try MidJourney

And they are supposed to be correctly parsed and should open in new browser tab on users click on webpage
however this is not happening, links are showing as undefined on webpage and i am seeing this error in console > F12

Error
paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:!0}]}):n+=a;continue}default:{const o='Token with "'+c.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){var r,s;let n="";for(let l=0;l<e.length;l++){const i=e[l];if((s=(r=this.options.extensions)==null?void 0:r.renderers)!=null&&s[i.type]){const o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}const c=i;switch(c.type){case"escape":{n+=t.text(c);break}case"html":{n+=t.html(c);break}case"link":{n+=t.link(c);break}case"image":{n+=t.image(c);break}case"strong":{n+=t.strong(c);break}case"em":{n+=t.em(c);break}case"codespan":{n+=t.codespan(c);break}case"br":{n+=t.br(c);break}case"del":{n+=t.del(c);break}case"text":{n+=t.text(c);break}default:{const o='Token with "'+c.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}}class L{constructor(e){b(this,"options");b(this,"block");this.options=e||z}preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}provideLexer(){return this.block?$.lex:$.lexInline}provideParser(){return this.block?R.parse:R.parseInline}}b(L,"passThroughHooks",new Set(["preprocess","postprocess","processAllTokens"]));class it{constructor(...e){b(this,"defaults",H());b(this,"options",this.setOptions);b(this,"parse",this.parseMarkdown(!0));b(this,"parseInline",this.parseMarkdown(!1));b(this,"Parser",R);b(this,"Renderer",v);b(this,"TextRenderer",V);b(this,"Lexer",$);b(this,"Tokenizer",E);b(this,"Hooks",L);this.use(...e)}walkTokens(e,t){var r,s;let n=[];for(const l of e)switch(n=n.concat(t.call(this,l)),l.type){case"table":{const i=l;for(const c of i.header)n=n.concat(this.walkTokens(c.tokens,t));for(const c of i.rows)for(const o of c)n=n.concat(this.walkTokens(o.tokens,t));break}case"list":{const i=l;n=n.concat(this.walkTokens(i.items,t));break}default:{const i=l;(s=(r=this.defaults.extensions)==null?void 0:r.childTokens)!=null&&s[i.type]?this.defaults.extensions.childTokens[i.type].forEach(c=>{const o=i[c].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{const r={...n};if(r.async=this.defaults.async||r.async||!1,n.extensions&&(n.extensions.forEach(s=>{if(!s.name)throw new Error("extension name required");if("renderer"in s){const l=t.renderers[s.name];l?t.renderers[s.name]=function(...i){let c=s.renderer.apply(this,i);return c===!1&&(c=l.apply(this,i)),c}:t.renderers[s.name]=s.renderer}if("tokenizer"in s){if(!s.level||s.level!=="block"&&s.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");const l=t[s.level];l?l.unshift(s.tokenizer):t[s.level]=[s.tokenizer],s.start&&(s.level==="block"?t.startBlock?t.startBlock.push(s.start):t.startBlock=[s.start]:s.level==="inline"&&(t.startInline?t.startInline.push(s.start):t.startInline=[s.start]))}"childTokens"in s&&s.childTokens&&(t.childTokens[s.name]=s.childTokens)}),r.extensions=t),n.renderer){const s=this.defaults.renderer||new v(this.defaults);for(const l in n.renderer){if(!(l in s))throw new Error(renderer '${l}' does not exist);if(["options","parser"].includes(l))continue;const i=l,c=n.renderer[i],o=s[i];s[i]=(...a)=>{let u=c.apply(s,a);return u===!1&&(u=o.apply(s,a)),u||""}}r.renderer=s}if(n.tokenizer){const s=this.defaults.tokenizer||new E(this.defaults);for(const l in n.tokenizer){if(!(l in s))throw new Error(tokenizer '${l}' does not exist);if(["options","rules","lexer"].includes(l))continue;const i=l,c=n.tokenizer[i],o=s[i];s[i]=(...a)=>{let u=c.apply(s,a);return u===!1&&(u=o.apply(s,a)),u}}r.tokenizer=s}if(n.hooks){const s=this.defaults.hooks||new L;for(const l in n.hooks){if(!(l in s))throw new Error(hook '${l}' does not exist);if(["options","block"].includes(l))continue;const i=l,c=n.hooks[i],o=s[i];L.passThroughHooks.has(l)?s[i]=a=>{if(this.defaults.async)return Promise.resolve(c.call(s,a)).then(p=>o.call(s,p));const u=c.call(s,a);return o.call(s,u)}:s[i]=(...a)=>{let u=c.apply(s,a);return u===!1&&(u=o.apply(s,a)),u}}r.hooks=s}if(n.walkTokens){const s=this.defaults.walkTokens,l=n.walkTokens;r.walkTokens=function(i){let c=[];return c.push(l.call(this,i)),s&&(c=c.concat(s.call(this,i))),c}}this.defaults={...this.defaults,...r}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return $.lex(e,t??this.defaults)}parser(e,t){return R.parse(e,t??this.defaults)}parseMarkdown(e){return(n,r)=>{const s={...r},l={...this.defaults,...s},i=this.onError(!!l.silent,!!l.async);if(this.defaults.async===!0&&s.async===!1)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return i(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));l.hooks&&(l.hooks.options=l,l.hooks.block=e);const c=l.hooks?l.hooks.provideLexer():e?$.lex:$.lexInline,o=l.hooks?l.hooks.provideParser():e?R.parse:R.parseInline;if(l.async)return Promise.resolve(l.hooks?l.hooks.preprocess(n):n).then(a=>c(a,l)).then(a=>l.hooks?l.hooks.processAllTokens(a):a).then(a=>l.walkTokens?Promise.all(this.walkTokens(a,l.walkTokens)).then(()=>a):a).then(a=>o(a,l)).then(a=>l.hooks?l.hooks.postprocess(a):a).catch(i);try{l.hooks&&(n=l.hooks.preprocess(n));let a=c(n,l);l.hooks&&(a=l.hooks.processAllTokens(a)),l.walkTokens&&this.walkTokens(a,l.walkTokens);let u=o(a,l);return l.hooks&&(u=l.hooks.postprocess(u)),u}catch(a){return i(a)}}}onError(e,t){return n=>{if(n.message+=
Please report this to https://github.com/markedjs/marked.`,e){const r="

An error occurred:

"+S(n.message+"",!0)+"
";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}}const T=new it;function d(h,e){return T.parse(h,e)}d.options=d.setOptions=function(h){return T.setOptions(h),d.defaults=T.defaults,ae(d.defaults),d};d.getDefaults=H;d.defaults=z;d.use=function(...h){return T.use(...h),d.defaults=T.defaults,ae(d.defaults),d};d.walkTokens=function(h,e){return T.walkTokens(h,e)};d.parseInline=T.parseInline;d.Parser=R;d.parser=R.parse;d.Renderer=v;d.TextRenderer=V;d.Lexer=$;d.lexer=$.lex;d.Tokenizer=E;d.Hooks=L;d.parse=d;d.options;d.setOptions;d.use;d.walkTokens;d.parseInline;R.parse;$.lex;function at(){const{slug:h}=$e(),[e,t]=M.useState(null),[n,r]=M.useState([]),s=new d.Renderer;if(s.link=(i,c,o)=>{if(!i||typeof i!="string")return console.warn("Invalid href detected:",i),o;const a=c?title="${c}":"";return<a href="${i.trim()}" ${a} target="_blank" rel="noopener noreferrer">${o}</a>},M.useEffect(()=>{(async()=>{try{const o=await(await fetch("/data/blogs.json")).json();r(o);const a=o.find(x=>x.slug===h);if(!a){t(null);return}const p=await(await fetch(/blogs/${h}.md)).text();console.log("Raw Markdown Content:",p);const g=p.replace(/^---[\s\S]*?---/,"").trim();console.log("Cleaned Markdown Content:",g);let m=d(g,{renderer:s});console.log("Generated HTML from Markdown:",m),m=m.replace(/<a /g,'<a target="_blank" rel="noopener noreferrer" '),t({...a,content:m})}catch(c){console.error("Error fetching blog content:",c)}})()},[h]),e===null)return f.jsx("p",{className:"text-center mt-10 text-gray-500",children:"Loading blog post..."});if(!e)return f.jsx("p",{className:"text-center mt-10 text-red-500",children:"Blog post not found."});console.log("Final Blog Content to Render:",e.content);const l={"@context":"https://schema.org","@type":"Article",headline:e.title,url:`https://officialai.io/blog/${e.slug}`,author:{"@type":"Person",name:e.author},datePublished:`${e.date}T12:00:00Z`,dateModified:`${e.date}T12:00:00Z`,image:e.image,publisher:{"@type":"Organization",name:"OfficialAI.io",logo:{"@type":"ImageObject",url:"https://officialai.io/logo.png"}},description:e.description,mainEntityOfPage:{"@type":"WebPage","@id":`https://officialai.io/blog/${e.slug}`}};return f.jsxs("div",{className:"max-w-3xl mx-auto p-6",children:[f.jsxs(Re,{children:[f.jsxs("title",{children:[e.title," | OfficialAI.io"]}),f.jsx("meta",{name:"description",content:e.description}),f.jsx("meta",{name:"keywords",content:e.tags.join(", ")}),f.jsx("meta",{name:"author",content:e.author}),f.jsx("meta",{property:"og:title",content:e.title}),f.jsx("meta",{property:"og:description",content:e.description}),f.jsx("meta",{property:"og:image",content:e.image}),f.jsx("meta",{property:"og:url",content:https://officialai.io/blog/${e.slug}}),f.jsx("meta",{property:"og:type",content:"article"}),f.jsx("meta",{name:"twitter:card",content:"summary_large_image"}),f.jsx("meta",{name:"twitter:title",content:e.title}),f.jsx("meta",{name:"twitter:description",content:e.description}),f.jsx("meta",{name:"twitter:image",content:e.image}),f.jsx("script",{type:"application/ld+json",children:JSON.stringify(l)})]}),f.jsx("img",{src:e.image,alt:e.title,className:"w-full h-60 object-cover rounded-lg mb-4"}),f.jsx("h1",{className:"text-3xl font-bold mb-2",children:e.title}),f.jsxs("p",{className:"text-gray-500 text-sm",children:["By ",e.author," • ",e.date," • ",e.reading_time]}),f.jsx("p",{className:"text-gray-600 mb-4",children:e.description}),f.jsx("div",{className:"mt-2 flex flex-wrap gap-2",children:e.tags.map((i,c)=>f.jsx("span",{className:"bg-blue-100 text-blue-600 text-xs px-2 py-1 rounded-full",children:i},c))}),f.jsx("hr",{className:"my-4"}),f.jsx("div",{className:"blog-container",children:f.jsx("div",{className:"blog-content prose max-w-none",dangerouslySetInnerHTML:{__html:e.content}})}),f.jsxs("div",{className:"mt-6 flex justify-between",children:[n.findIndex(i=>i.slug===h)>0&&f.jsxs(O,{to:/blog/${n[n.findIndex(i=>i.slug===h)-1].slug},className:"text-blue-500 hover:underline",children:["← ",n[n.findIndex(i=>i.slug===h)-1].title]}),f.jsx(O,{to:"/blog",className:"text-blue-500 hover:underline",children:"Return to Blog"}),n.findIndex(i=>i.slug===h)<n.length-1&&f.jsxs(O,{to:/blog/${n[n.findIndex(i=>i.slug===h)+1].slug},className:"text-blue-500 hover:underline",children:[n[n.findIndex(i=>i.slug===h)+1].title," →"]})]})]})}export{at as default};
`

This is my code :

` import { marked } from "marked";
useEffect(() => {
const fetchData = async () => {
try {
// ✅ Fetch blog metadata
const response = await fetch("/data/blogs.json");
const data = await response.json();
setBlogs(data);

    const foundBlog = data.find((b) => b.slug === slug);
    if (!foundBlog) {
      setBlog(null);
      return;
    }

    // ✅ Fetch the markdown content
    const markdownResponse = await fetch(`/blogs/${slug}.md`);
    const rawContent = await markdownResponse.text();
    console.log("Raw Markdown Content:", rawContent);

    // ✅ Remove frontmatter (metadata at the top of markdown)
    const cleanedContent = rawContent.replace(/^---[\s\S]*?---/, "").trim();
    console.log("Cleaned Markdown Content:", cleanedContent);

    // ✅ Convert Markdown to HTML
    let htmlContent = marked(cleanedContent, { renderer });
    console.log("Generated HTML from Markdown:", htmlContent);

    // ✅ Ensure all links open in a new tab
    htmlContent = htmlContent.replace(/<a /g, '<a target="_blank" rel="noopener noreferrer" ');

    setBlog({ ...foundBlog, content: htmlContent });
  } catch (error) {
    console.error("Error fetching blog content:", error);
  }
};`

Expected behavior
I want URLs to open in new tab when user click on link in my blog.

@deepsky1d
Copy link
Author

deepsky1d commented Feb 27, 2025

found workaround, simply put link in html format in md file itself, example:
Learn more about AI
<a href="https://officialai.io/" target="_blank" rel="noopener noreferrer">Learn more about AI</a>

@UziTech
Copy link
Member

UziTech commented Feb 27, 2025

In your code you are passing a renderer to marked that is not shown. My guess is it is somewhere in there that the issue is happening.

marked(cleanedContent, { renderer });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants