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

Angularize the output markdown input for the purpose of links #125

Open
craig-dae opened this issue Jan 2, 2019 · 28 comments · May be fixed by #506
Open

Angularize the output markdown input for the purpose of links #125

craig-dae opened this issue Jan 2, 2019 · 28 comments · May be fixed by #506
Labels
enhancement Improvement request

Comments

@craig-dae
Copy link

We are creating a knowledge base using markdown and would like to be able to navigate between the different pages using markdown links.

I want to be able to do like: [click this link](routerlink#./other%20document.md) or something like that and have it render as <a [routerLink]="['./other document.md']">click this link</a>

I can do that using the renderer, but Angular doesn't pick up the routerLink and bootstrap it. The link is unclickable.

This means that when I click links, the entire application reloads instead of using SPA routing.

Is there a way to do what I'm trying to do here? Can my documentation writers create links in our markdown documents?

@jfcere jfcere added the enhancement Improvement request label Jan 13, 2019
@emrerocky
Copy link

+1. I'm currently stuck because of this. Would love to see it added or hear of a work around.

@puneetg1983
Copy link

+1. Need this functionality

@andrewjrvs
Copy link

Since the angular compiler isn't included when you use AOT compilation (which I wanted) I came up with this alternative.
Once the content is loaded (using the ngx-markdown load event) I attached an event listener to catch any Anchor 'click' events and redirect them to the Angular router if they are relative links.

<div markdown #postDiv [src]="'./assets/blog/blog.md'" (load)="onMarkdownLoad($event);"></div>
const isAbsolute = new RegExp('(?:^[a-z][a-z0-9+.-]*:|\/\/)', 'i');
export class MainComponent implements OnInit, OnDestroy {
  private listenObj: any;

  @ViewChild('postDiv', {static: false})
  private postDiv: MarkdownComponent;
constructor(private markdownService: MarkdownService, private renderer: Renderer2, private router: Router,) { }

  public onMarkdownLoad() {
    // because MarkdownComponent isn't 'compiled' the links don't use the angular router,
    // so I'll catch the link click events here and pass them to the router...
    if (this.postDiv) {
      this.listenObj = this.renderer.listen(this.postDiv.element.nativeElement, 'click', (e: Event) => {
        if (e.target && (e.target as any).tagName === 'A') {
          const el = (e.target as HTMLElement);
          const linkURL = el.getAttribute && el.getAttribute('href');
          if (linkURL && !isAbsolute.test(linkURL)) {
            e.preventDefault();
            this.router.navigate([linkURL]);
          }
        }
      });
    }
  }

  ngOnInit() {  }

  ngOnDestroy(): void {
    if (this.listenObj) {
      this.listenObj();
    }
  }

}

@jfcere
Copy link
Owner

jfcere commented Aug 20, 2019

Hi fellas,

I just want you guys to know that this will be my next priority when I'll get some time to work on the library (which is pretty hard these days).

In the meanwhile, if any of you want to jump into the wagon, please feel free to contribute as this might not be an easy one!

Thanks for understanding.

@Razkaroth
Copy link

Razkaroth commented Aug 26, 2019

@jfcere, I'll have some free time mid-week and I'd be more than glad to help with this.

If I'm not mistaken, the main issue is that routerlink is a directive, not an attribute, and as far as I know angular does not support dynamic directives natively.

However, I found this StackOverflow thread. If this works, then the markdown component could do something like this.

import {RouterLink} from '@angular/router';
// code...

export class MarkdownComponent {
    // code...

    @HostBinding('attr.routerLink') dynamicRouterLink = new RouterLink(this.elementRef);
    

}

I'll test this as soon as I can. But most likely that will be in a couple of days. If someone can test it out sooner that would help.

Edit: It seems like the RouterLink does not have a ngOnInit method, so I removed that part.

@maxime1992
Copy link
Contributor

Hey guys I'm stuck too because of that (need of routerLink and fragment).

Has anyone found a good workaround?

@zakhenry
Copy link

zakhenry commented Oct 6, 2019

This is very much a hack, but what we use to work around this limitation

import { ElementRef, Injectable } from '@angular/core';
import { Router } from '@angular/router';

const WIRED_LINK_ATTRIBUTE = '_ngx_markdown_rewired_link';

/**
 * !!! HERE BE DRAGONS !!!
 * @hack in order to support local links created from markdown rather than shipping the entire angular compiler just
 * to compile a <a [routerLink]=""> directive, instead we attach a custom event handler to dispatch a router navigation
 * event.
 * This is truly awful and if a better solution exists, slay this beast! Maybe the Ivy compiler will be our saviour?
 */
@Injectable({ providedIn: 'root' })
export class LinkHandlerService {
  constructor(private router: Router) {}

  private linkShouldBeWired(link: HTMLAnchorElement): boolean {
    const attributeNames = link.getAttributeNames();

    const isAngularLink = attributeNames.some(n => n.startsWith('_ngcontent'));
    const isAlreadyWired = attributeNames.some(n => n === WIRED_LINK_ATTRIBUTE);

    return !isAngularLink && !isAlreadyWired && link.getAttribute('href').startsWith('/');
  }

  public wireInsertedLinks(element: ElementRef): void {
    const allLinks: HTMLAnchorElement[] = Array.from(element.nativeElement.getElementsByTagName('a'));

    allLinks.filter(this.linkShouldBeWired).forEach(link => {
      link.addEventListener('click', $event => {
        $event.preventDefault();
        this.router.navigateByUrl(link.getAttribute('href'));
      });

      link.setAttribute(WIRED_LINK_ATTRIBUTE, '');
    });
  }
}

To use this, inject the service into your component (or directive) that contains links and pass the ElementRef to wireInsertedLinks(element).

Note this does not handle relative links; only links starting with /, but it should be relatively easy to extend that capability.

@jfcere
Copy link
Owner

jfcere commented Jan 12, 2020

Hi fellas,

Working on the new demo for ngx-markdown I stumbled across this issue. I red workaround propositions and did some reverse-engineering with angular routerLink directive and came up with creating a dedicated service.

This is not an official solution that is integrated to ngx-markdown yet but this is something I am considering (unless Ivy solves the problem in a more fashionable way).

For anybody who end up here, I'd like you to give it a try and comment on how was the integration, if you had issues or any possible improvements that could benefit all of us.

AnchorService

I've created an AnchorService to centralize all the logic around manipulating generated links from markdown.

🤷‍♂ Don't like the name? You can call it as you want! I didn't give it more thought about an appropriate name yet.

import { LocationStrategy } from '@angular/common';
import { Injectable } from '@angular/core';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';

/**
 * Service to handle links generated through markdown parsing.
 * The following `RouterModule` configuration is required to enabled anchors
 * to be scrolled to when URL has a fragment via the Angular router:
 * ```
 * RouterModule.forRoot(routes, {
 *  anchorScrolling: 'enabled',           // scrolls to the anchor element when the URL has a fragment
 *  scrollOffset: [0, 64],                // scroll offset when scrolling to an element (optional)
 *  scrollPositionRestoration: 'enabled', // restores the previous scroll position on backward navigation
 * })
 * ```
 * _Refer to [Angular Router documentation](https://angular.io/api/router/ExtraOptions#anchorScrolling) for more details._
 */
@Injectable({ providedIn: 'root' })
export class AnchorService {

  constructor(
    private locationStrategy: LocationStrategy,
    private route: ActivatedRoute,
    private router: Router,
  ) { }

  /**
   * Intercept clicks on `HTMLAnchorElement` to use `Router.navigate()`
   * when `href` is an internal URL not handled by `routerLink` directive.
   * @param event The event to evaluated for link click.
   */
  interceptClick(event: Event) {
    const element = event.target;
    if (!(element instanceof HTMLAnchorElement)) {
      return;
    }
    const href = element.getAttribute('href');
    if (this.isExternalUrl(href) || this.isRouterLink(element)) {
      return;
    }
    this.navigate(href);
    event.preventDefault();
  }

  /**
   * Navigate to URL using angular `Router`.
   * @param url Destination path to navigate to.
   * @param replaceUrl If `true`, replaces current state in browser history.
   */
  navigate(url: string, replaceUrl = false) {
    const urlTree = this.getUrlTree(url);
    this.router.navigated = false;
    this.router.navigateByUrl(urlTree, { replaceUrl });
  }

  /**
   * Transform a relative URL to its absolute representation according to current router state.
   * @param url Relative URL path.
   * @return Absolute URL based on the current route.
   */
  normalizeExternalUrl(url: string): string {
    if (this.isExternalUrl(url)) {
      return url;
    }
    const urlTree = this.getUrlTree(url);
    const serializedUrl = this.router.serializeUrl(urlTree);
    return this.locationStrategy.prepareExternalUrl(serializedUrl);
  }

  /**
   * Scroll view to the anchor corresponding to current route fragment.
   */
  scrollToAnchor() {
    const url = this.router.parseUrl(this.router.url);
    if (url.fragment) {
      this.navigate(this.router.url, true);
    }
  }

  private getUrlTree(url: string): UrlTree {
    const urlPath = this.stripFragment(url) || this.stripFragment(this.router.url);
    const urlFragment = this.router.parseUrl(url).fragment;
    return this.router.createUrlTree([urlPath], { relativeTo: this.route, fragment: urlFragment });
  }

  private isExternalUrl(url: string): boolean {
    return /^(?!http(s?):\/\/).+$/.exec(url) == null;
  }

  private isRouterLink(element: HTMLAnchorElement): boolean {
    return element.getAttributeNames().some(n => n.startsWith('_ngcontent'));
  }

  private stripFragment(url: string): string {
    return /[^#]*/.exec(url)[0];
  }
}

RouterModule configuration

The following RouterModule configuration is required to enabled anchors be scrolled to when URL has a fragment via the Angular router:

app-routing.module.ts

RouterModule.forRoot(routes, {
  anchorScrolling: 'enabled', // scrolls to the anchor element when the URL has a fragment
  scrollOffset: [0, 64],  // scroll offset when scrolling to an element (optional)
  scrollPositionRestoration: 'enabled', // restores the previous scroll position on backward navigation
})

📘 Refer to Angular Router documentation for more details.

Intercept link click event

This is where the magic happens! Using HostListener wiith document:click event, it is possible to intercept the click event on any HTML element.

Doing so in the AppComponent to call AnchorService.interceptClick(event: Event) will use Router.navigate() to navigate if the following conditions are all true:

  • the clicked element is an HTMLAnchorElement
  • the href value of the element is an internal link
  • the link is not already handled by routerLink directive

💡 The AppComponent is the one and only place you will need to apply the following code, all other component links will also be intercepted since we are listening on document.

app.component.ts
@HostListener('document:click', ['$event'])
onDocumentClick(event: Event) {
  this.anchorService.interceptClick(event);
}

constructor(
  private anchorService: AnchorService,
) { }

Landing directly on a page with fragment (hash)

To be able to scroll to an element when loading the application for the first time when there is a fragment (#hash) in the URL you can call AnchorService.scrollToAnchor() when the content of the DOM is available and markdown have been parsed.

👿 This is the tricky part, it can be hard to find the right place to call it as markdown might not be parsed if loaded from an external source during ngAfterViewInit lifecycle hook.

Fix generated href path

In order to fix the link URLs for the case where somebody would want to use the "copy link adress" context menu option of the browser, you can override the link token using MarkedRenderer when importing MarkdownModule through markedOptions configuration property.

By calling AnchorService.normalizeExternalUrl(url) and passing the result to url parameter to the original prototype function, it will reuses the original link token generation function and have the correct href value without rewritting the function.

app.module.ts

export function markedOptionsFactory(anchorService: AnchorService): MarkedOptions {
  const renderer = new MarkedRenderer();

  // fix `href` for absolute link with fragments so that _copy-paste_ urls are correct
  renderer.link = (href, title, text) => {
    return MarkedRenderer.prototype.link.call(renderer, anchorService.normalizeExternalUrl(href), title, text);
  };

  return { renderer };
}

MarkdownModule.forRoot({
  loader: HttpClient,
  markedOptions: {
    provide: MarkedOptions,
    useFactory: markedOptionsFactory,
    deps: [AnchorService],
  },
}),

@szymarcus
Copy link

szymarcus commented May 8, 2020

Hi I recently had a similar problem with scrolling to the right position on page load because the markdown component was not fully loaded when the rest of the page already was.

Thanks @jfcere for the solution provided above. You wrote there:

Landing directly on a page with fragment (hash)
To be able to scroll to an element when loading the application for the first time when there is a fragment (#hash) in the URL you can call AnchorService.scrollToAnchor() when the content of the DOM is available and markdown have been parsed.

👿 This is the tricky part, it can be hard to find the right place to call it as markdown might not be parsed if loaded from an external source during ngAfterViewInit lifecycle hook.

Exactly that was my problem. I could not find the correct lifecycle hook to scroll to the markdown element due to loading the content from an external .md file. After a while I found a solution for it in creating a directive that fires an event when the markdown view is initialized properly:

Element ready directive

import { Directive, EventEmitter, Output, AfterViewInit } from '@angular/core';

@Directive({
  selector: '[elementReady]'
})
export class ElementReadyDirective implements AfterViewInit {
  @Output() initEvent: EventEmitter<any> = new EventEmitter();

  constructor() {}

  ngAfterViewInit() {
    this.initEvent.emit();
  }
}

Attach directive in template of the component

<markdown elementReady (initEvent)="scrollToAnchor()" [innerHtml]="compiledFAQ"></markdown>

Just wanted to share this solution. Maybe it helps someone. Inspired by this SO question: https://stackoverflow.com/questions/48335720/angular-i-want-to-call-a-function-when-element-is-loaded?noredirect=1&lq=1

Thanks a lot for the library @jfcere!

Cheers

@Simon-TheHelpfulCat
Copy link

I got to the point this afternoon where I had to tackle this and inspired by the answers above, I got to this, which is working for me.

First provide a custom Renderer:

import { MarkedRenderer } from 'ngx-markdown';

export class MarkdownRenderer extends MarkedRenderer {

    public link(href: string | null, title: string | null, text: string): string {
        return `<a routerLink="${href || ''}">${text}</a>`;
    }
}

This is passed into the renderer options in MarkdownModule.forRoot() as usual.

The second part of the solution is a directive to post-process the output of the component or directive:

import {
  Directive,
  ElementRef,
  Injector,
  ApplicationRef,
  ComponentFactoryResolver,
  Component,
  Input,
  Inject,
  HostListener
} from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({
  template: '<a [routerLink]="href">{{text}}</a>',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class RouterLinkComponent {
  @Input() public href: string;
  @Input() public text: string;
}

@Directive({
  // tslint:disable-next-line: directive-selector
  selector: 'markdown,[markdown]'
})
export class ConvertLinksDirective {

  constructor(
    @Inject(DOCUMENT) private document: Document,
    private injector: Injector,
    private applicationRef: ApplicationRef,
    private componentFactoryResolver: ComponentFactoryResolver,
    private element: ElementRef<HTMLElement>
  ) { }

  @HostListener('ready')
  public processAnchors() {
    this.element.nativeElement.querySelectorAll(
      'a[routerLink]'
    ).forEach(a => {
      const parent = a.parentElement;
      if (parent) {
        const container = this.document.createElement('span');
        const component = this.componentFactoryResolver.resolveComponentFactory(
          RouterLinkComponent
        ).create(this.injector, [], container);
        this.applicationRef.attachView(component.hostView);
        component.instance.href = a.getAttribute('routerLink') || '';
        component.instance.text = a.textContent || '';
        parent.replaceChild(container, a);
      }
    });
  }
}

As shown, the renderer doesn't deal with external links, which should be dealt with by rendering href instead of routerLink. The module that contains the directive needs to declare both the component and the directive along with importing RouterModule. Angular won't be able to resolve [routerLink] without the component being declared.

This won't help if you use the pipe rather than the component or directive.

This can probably be improved on, but it seems to do what I need.

@znorman-harris
Copy link

I just came across this and I really liked the solution that @Simon-TheHelpfulCat implemented. I thought it was simple and super easy to extend/change (i.e. can replace routerLink with click for custom logic prior to routing).

@flensrocker
Copy link

The solution of @Simon-TheHelpfulCat did not work for me, because Angular stripped the anchor attributes to prevent XSS.

The AnchorService of @jfcere works fine. I don't use it on document:click but only on the component, where I need.

@coloz
Copy link

coloz commented Feb 12, 2021

This is probably the simplest solution
Intercept all href clicks and replace them with ng router

  // 拦截MarkdownComponent下所有href
  @ViewChild(MarkdownComponent, { read: ElementRef }) markdownZone: ElementRef;

  test() {
      let links = this.markdownZone.nativeElement.querySelectorAll('a');
      links.forEach(link => {
        //console.log(link.href);
        if (link.href.indexOf(window.location.origin) > -1) {
          let target = link.href.replace(window.location.origin, '')
          //console.log(target);
          link.addEventListener('click', (evt) => {
            evt.preventDefault()
            this.router.navigateByUrl(target)
          })
        }
      })
  }

@sourabhsparkala
Copy link

The solution of @Simon-TheHelpfulCat did not work for me, because Angular stripped the anchor attributes to prevent XSS.

The AnchorService of @jfcere works fine. I don't use it on document:click but only on the component, where I need.
@flensrocker and @jfcere seems to be something I am looking for as well. But the event click does not seem to work for me.

By inspecting element of the link, I can see that
The link address points to https://url/path#identifier

But below link hits the router, but nothing happens

@HostListener('document:click', ['$event'])
onDocumentClick(event: Event) {
  this.anchorService.interceptClick(event);
}

Should something be done in RouterModule, may be add a type of path in one of the routes?,

I have already added

@NgModule({
  imports: [RouterModule.forRoot(routes, <ExtraOptions>{ 
    useHash: true, 
    enableTracing: false,
    scrollPositionRestoration: 'enabled',
    anchorScrolling: 'enabled'
  })],
  exports: [RouterModule]
})

@flensrocker
Copy link

flensrocker commented Mar 24, 2021

I used @jfcere solution as a starting point and modified for my use case (I use no hash fragments in my project).

I ended up with this:

import { Injectable } from "@angular/core";
import { ActivatedRoute, Router, UrlTree } from "@angular/router";

@Injectable({
  providedIn: "root",
})
export class BsMarkdownAnchorService {
  constructor(
    private _route: ActivatedRoute,
    private _router: Router,
  ) {
  }

  isExternalUrl(href: string | null): boolean {
    return !href
      || href.startsWith("http:")
      || href.startsWith("https:")
      || href.startsWith("mailto:")
      || href.startsWith("tel:")
      || href.startsWith("/");
  }

  stripQuery(url: string): string {
    return /[^?]*/.exec(url)[0];
  }

  stripFragmentAndQuery(url: string): string {
    return this.stripQuery(/[^#]*/.exec(url)[0]);
  }

  getUrlTree(url: string): UrlTree {
    const urlPath = this.stripFragmentAndQuery(url) || this.stripFragmentAndQuery(this._router.url);
    const parsedUrl = this._router.parseUrl(url);
    const fragment = parsedUrl.fragment;
    const queryParams = parsedUrl.queryParams;
    return this._router.createUrlTree([urlPath], { relativeTo: this._route, fragment, queryParams });
  }

  navigate(url: string, replaceUrl = false) {
    const urlTree = this.getUrlTree(url);
    this._router.navigated = false;
    this._router.navigateByUrl(urlTree, { replaceUrl });
  }

  interceptClick(event: Event) {
    const element = event.target;
    if (!(element instanceof HTMLAnchorElement)) {
      return;
    }

    const href = element.getAttribute("href");
    if (this.isExternalUrl(href)) {
      return;
    }

    this.navigate(`/${href}`);
    event.preventDefault();
  }
}

No extra router configuration or anything else.

@eostermueller
Copy link

eostermueller commented Jun 1, 2021

Hi @flensrocker thanks for the class. I cloned https://github.com/jfcere/ngx-markdown.git and added your class to my project. What should my markdown links look like? What else needs to be done?

I added the following to ./demo/src/app/cheat-sheet/markdown/links.md.

[just lists](lists)
[just lists dot md](lists.md)
[dot slash lists](./lists)
[dot slash lists dot md](./lists.md)

[md prefix just lists](markdown/lists)
[md prefix just lists dot md](markdown/lists.md)
[md prefix dot slash lists](./markdown/lists)
[md prefix dot slash lists dot md](./markdown/lists.md)

Every time I click on one of these links at http://localhost:4200/cheat-sheet, the browser sends me back to the http://localhost:4200/get-started page. I was hoping it would render the lists.md page.

Any suggestions?

@eostermueller
Copy link

ok, replaying to self here. What I attempted above won't work because the markdown files are loaded statically, probably at startup, per this code and this html.....but still would appreciate some suggestions on how to implement your BsMarkdownAnchorService

@flensrocker
Copy link

Ok, this is missing in my example.
The component which displays the markdown component has this piece of code:

  @HostListener("click", ["$event"])
  onDocumentClick(event: Event) {
    this._anchorService.interceptClick(event);
  }

@eostermueller
Copy link

Will try that later, thx.

@FirstVertex
Copy link

FirstVertex commented Jul 15, 2021

Due to "special" reasons in our application we are not able to use ActivatedRoute. Thus, I had to develop this "special" technique to manually figure out the relative link path based on the current window.location.

@Directive({
    // tslint:disable-next-line: directive-selector
    selector: 'markdown,[markdown]'
})
export class ConvertMarkdownLinksDirective {
    constructor(private _router: Router, private _element: ElementRef<HTMLElement>) {}

    // unable to use the standard technique involving ActivatedRoute
    @HostListener('ready')
    processAnchors() {
        const isAbsolute = new RegExp('(?:^[a-z][a-z0-9+.-]*:|//)', 'i');
        this._element.nativeElement.querySelectorAll('a').forEach(a => {
            const linkURL = a.getAttribute && a.getAttribute('href');
            if (linkURL) {
                if (isAbsolute.test(linkURL)) {
                    a.target = '_blank';
                } else {
                    a.addEventListener('click', e => {
                        e.preventDefault();
                        const url = new URL(window.location.href);
                        const path = url.pathname;
                        const pathParts = path.split('/').filter(p => !!p);
                        const linkParts = linkURL.split('/').filter(p => !!p);
                        while (linkParts[0] === '..') {
                            linkParts.shift();
                            pathParts.pop();
                        }
                        let newPath = '/' + [...pathParts, ...linkParts].join('/') + '/';
                        const linkRest = linkURL.split('?')[1];
                        if (linkRest) {
                            newPath += `?${linkRest}`;
                        }
                        this._router.navigateByUrl(newPath);
                    });
                }
            }
        });
    }
}

@muuvmuuv
Copy link

muuvmuuv commented Aug 2, 2021

I found a waaay simpler solution for this if anyone wants to listen on any click/touch event on a specific element inside any rendered content:

import { Directive, HostListener } from "@angular/core"

/**
 * Handle click events on HTML elements inside a safe HTML content projection.
 * 
 * @example
 * ```html
 * <div [innerHtml]="..." appSafeHTMLClickHandler></div>
 * ```
 */
@Directive({
  selector: "[appSafeHTMLClickHandler]",
})
export class SafeHTMLClickDirective {
  constructor() {}

  /**
   * Listener for click events in any HTMLElement.
   */
  @HostListener("click", ["$event"])
  onClick($event: MouseEvent) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const target = $event.target as any

    switch ($event.target.constructor) {
      case HTMLAnchorElement:
        $event.preventDefault()
        this.handleAnchorElement(target)
        // add even more handlers for different stuff you want to do on an anchor element
        break
      // or add even more handlers
    }
  }

  /**
   * Handle anchor element click
   */
  private async handleAnchorElement(target: HTMLAnchorElement): Promise<void> {
    // for example: if starts with `mailto:` exec function to open native client
    // or if no protocol and url is in router navigate to internal route
    // ANY Angular/Ionic/Capacitor stuff will work here
  }
}

Might need to add that this does not require and unsubscribing functions or event listerner removal.

@TicTak21
Copy link

Any updates?

@delogix-daniel
Copy link

recap..

component.html

<div *ngIf="data" [innerHTML]="data | markdown"></div>

component.ts

export class MdViewComponent implements OnInit {

  @HostListener('click', ['$event'])
 public data: string = ""

  onClick(event: any): void {
    event.preventDefault();
    // find anchor                   
    const anchor = event.target.closest("a");
    if (!anchor) return;
    // navigate
    const href = anchor.getAttribute('href')
    this.router.navigate([href])
  }

  constructor(
    private router: Router,
    private route: ActivatedRoute,
    private httpClient: HttpClient,
  ) {
  }

  ngOnInit(): void {
    console.log("Markdown home init")
    this.httpClient.get("my-md-file", { responseType: 'text' })
        .subscribe(data => {
          this.data = data
        });
}
}

@MGlauser
Copy link

I cannot believe this is still a problem in Markdown in Angular, but @delogix-daniel solution works, with the addition of handling a couple of cases with target or href starting with specific URLs like "/api/..."

handleMarkdownAnchorClick(event: any): void {
    event.preventDefault();
    // find anchor                   
    const anchor = event.target.closest("a");
    if (!anchor) return;
    // navigate
    const href = anchor.getAttribute('href')

   // ADDED THIS:
    const target = anchor.getAttribute('target');
    if (target) {
      window.open(href, target);
      return;
    }
    if (href.startsWith('/api/')) {
      window.open(href, '_blank');
      return;
    }
   // END OF MY ADDED CODE
    this.router.navigate([href])
  }

@midzer
Copy link

midzer commented Mar 4, 2024

Follow-up on @szymarcus comment about scrolling an id into view. This worked for me:

const hash = window.location.hash;
if (hash) {
  const id = window.decodeURIComponent(hash).replace('#', '');
  const element = document.getElementById(id);
  if (element) {
    element.scrollIntoView();
  }
}

in onLoad() handler of Markdown component.

@TheColorRed TheColorRed linked a pull request Mar 12, 2024 that will close this issue
@TheColorRed
Copy link

I created a PR for anyone that is interested: #506

@yedajiang44
Copy link

If it is a toc anchor point, you also need renderer heading

renderer.heading = (text: string, level: 1 | 2 | 3 | 4 | 5 | 6) => {
  return `<h${level} id="${slugger.slug(text)}" class="h${level}">${text}</h${level}>`;
};

I am using github-slugger

import GithubSlugger from 'github-slugger';
//...
const slugger = new GithubSlugger();
//...

@Lakskanth
Copy link

Lakskanth commented Oct 18, 2024

app.module.ts


import { Renderer } from 'marked';
export function markedOptionsFactory(): MarkedOptions {
    const renderer = new Renderer();
    renderer.link = (href, title, text) => {
        const link = Renderer.prototype.link.call(renderer, href, title, text);
        return link.replace('<a', `<a target="_blank"`);
    };
    return { renderer };
}

@NgModule({
    imports: [
    MarkdownModule.forRoot({
            loader: HttpClient,
            markedOptions: {
                provide: MARKED_OPTIONS,
                useFactory: markedOptionsFactory,
            },
        }),
 ],
  • This works fine for me. Let me know this implement cause any issues.

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

Successfully merging a pull request may close this issue.