jfcere / ngx-markdown

Angular markdown component/directive/pipe/service to parse static, dynamic or remote content to HTML with syntax highlight and more...
https://jfcere.github.io/ngx-markdown
MIT License
1.06k stars 182 forks source link

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

Open craig-dae opened 5 years ago

craig-dae commented 5 years ago

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?

emrerocky commented 5 years ago

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

puneetg1983 commented 5 years ago

+1. Need this functionality

andrewjrvs commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@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 commented 5 years ago

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

Has anyone found a good workaround?

zakhenry commented 5 years ago

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 commented 4 years ago

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 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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

Will try that later, thx.

FirstVertex commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

Any updates?

delogix-daniel commented 1 year ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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

yedajiang44 commented 3 weeks ago

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();
//...