question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Angularize the output markdown input for the purpose of links

See original GitHub issue

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?

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:31
  • Comments:23 (3 by maintainers)

github_iconTop GitHub Comments

7reactions
jfcerecommented, 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.

5reactions
jfcerecommented, Jan 20, 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],
  },
}),
Read more comments on GitHub >

github_iconTop Results From Across the Web

Markdown to HTML to Angular Components at Runtime
Generating HTML from Markdown at runtime is a great way to load content, but the generated HTML sits outside of the the Angular...
Read more >
Rendering Markdown In Angular - Medium
How to use Angular directives to render markdown documents ... a navigate output emitting the url string when the user click on a...
Read more >
Using markdown-parser in Angular - Stack Overflow
i am using this markdown-parser in my angular6 app. on giving input # markdown-it rulezz!, **hi** the expected output is it would be ......
Read more >
ngx-markdown | Demo
To add ngx-markdown library to your package.json use the following commands. ... any string and specify the prefix using the filterOutput input property....
Read more >
Embedding Links - Markdown Monster Documentation
Using inline text linking (ctrl-shift-k); Using raw Markdown Syntax; Using raw HTML Syntax. The Paste Link Dialog (ctrl-k). You can use the Link...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found