miaolz123 / vue-markdown

A Powerful and Highspeed Markdown Parser for Vue
https://miaolz123.github.io/vue-markdown/
MIT License
1.89k stars 257 forks source link

Is it possible to use router-links with vue-markdown? #104

Open Steinblock opened 4 years ago

Steinblock commented 4 years ago

Obviously the following code

[contact us](/contact)

gets translated into

<a href="/contact">contact us</a>

Without vue-markdown I could write

<router-link to="/contact">contact us</router-link>

which will use client side routing instead of reloading the page. Is this possible to use this concept with vue-markdown?

insightcoder commented 3 years ago

I couldn't find an easy or efficient way to do this. But I did find a way.

What I did is defined an alternate markdown syntax for internal links as [text]{my-internal-link}. Notice the curly braces instead of parentheses. Then I defined a vue-markdown prerender method to intercept the markdown, extract the link information and ultimately substitute the content with a router-link.

I had to jump through a few hoops to make this happen, but I was able to get it to work in the end.

yannicklescure commented 3 years ago

I couldn't find an easy or efficient way to do this. But I did find a way.

What I did is defined an alternate markdown syntax for internal links as [text]{my-internal-link}. Notice the curly braces instead of parentheses. Then I defined a vue-markdown prerender method to intercept the markdown, extract the link information and ultimately substitute the content with a router-link.

I had to jump through a few hoops to make this happen, but I was able to get it to work in the end.

Could you detail your method with some code sample ?

insightcoder commented 3 years ago

Sure. Like I said, I had to jump through some hoops, so open to simplifying things. Ideally these features could be incorporated into vue-markdown.

Markdown

This is my md file. 

<div id="image">
</div>

[normal link](http://www.google.com)

[vue link]{/my/app/route}

Component Usage


<markdown :source="source">
  <template v-slot:image>
       <img src="/my/img"/>
  </template>
</markdown>

Component Code

<template>
  <div>
    <vue-markdown
      v-bind="$attrs"
      :toc="true"
      :breaks="true"
      :linkify="false"
      :toc-anchor-link-space="true"
      toc-anchor-link-symbol="¶"
      :prerender="prerender"
    >
      <slot></slot>
    </vue-markdown>
    <div  v-for="(i,s) in $slots" :key="s" :ref="s">
      <slot :name="s" v-if="slotsInserted">
      </slot>
    </div>

    <router-link v-for="(link) in internalLinks"
      :to="link.to" 
      :key="link.id" 
      :ref="link.id">{{ link.text }}</router-link>

  </div>
</template>

<script lang="ts">
import { CreateElement } from 'vue';
import { Vue, Component } from "vue-property-decorator";
import VueMarkdown from "vue-markdown";
import router from '@/router'; 

interface InternalLink {
  id: string;
  to: string;
  text: string;
}

/**
 * Component that wraps the <vue-markdown> component. 
 * 
 * This currently offers two main features:
 * 
 * 1) Allows for using named slots to inject vue code
 * into the markdown.  To do this, in the .md file create 
 * a div/span with a certain id.  Then create a slot with a name
 * that matches the id.  This will be injected into that placeholder
 * div/span. 
 * 
 * 2) Adds support for internal links using router-link. 
 * Adds preprocessor for syntax [text]{internal-url}.  Notice
 * the curly braces instead of the parentheses.
 */
@Component({
  components: {
    VueMarkdown
  }
})
export default class Markdown extends Vue {
  public numSlots = this.$slots.length;
  public slotsInserted = false;
  public internalLinks: InternalLink[] = [];
  public prerendered = false;

  public mounted() {
    this.insertSlotContentIntoMarkdown();
  } 

  public updated() {
    this.insertInternalLinksIntoMarkdown();
  }

  /**
   * Insert all slots content
   * into dom elements with id of slot name.
   */
  public insertSlotContentIntoMarkdown() {
    for(var slotName in this.$slots) {
      if(slotName == null) {
        continue;
      }
      var slot = (this.$refs[slotName] as Element[])[0];
      var parent = this.$el.querySelector(`#${slotName}`);
      if(slot !== null && parent !== null) {
        parent.appendChild(slot);
      } else {
        console.error(`Markdown mapping error for id/slot "${slotName}"`);
      }
    }
    this.slotsInserted = true;
  }

  /**
   * Insert all internal router-links
   * into dom elements with id.
   */
  public insertInternalLinksIntoMarkdown() {
    for(var link of this.internalLinks) {
      var routerLink = (this.$refs[link.id] as any[])[0];
      if (routerLink == null) {
        continue;
      }
      var parent = this.$el.querySelector(`#${link.id}`);
      if(routerLink !== null && parent !== null) {
        parent.appendChild(routerLink.$el);
      } else {
        console.error(`Markdown mapping error for id/slot "${link.id}"`);
      }
    }
    this.slotsInserted = true;
  }  

  public prerender(content: string): string {
    // substitute "internal" links ({text}(url) with div placeholder
    // for slots.
    const regex = /\[(.*?)\]\{(.*?)\}/g;
    var match;
    let internalLinks: InternalLink[] = [];
    while ((match = regex.exec(content)) !== null) {
      let to = match[2];
      let text = match[1]
      let id = `internal-link-${internalLinks.length}`;
      internalLinks.push({ id, to, text });
      let replacement = `<span id="${id}"></span>`;
      content = content.replace(match[0], replacement);
    } 
    if (!this.prerendered) {
      this.internalLinks = internalLinks;
      this.prerendered = true;
    }
    return content;
  }

}

</script>
ruralcoder commented 3 years ago

This feature should be prioritized. vue-markdown was so awesome, until I hit this issue. All my pages are FULL reloading. I use vue-markdown for localizing many of my pages.

soualid commented 3 years ago

Another (hacky) way I'm not proud of, but hey... it works!

document.querySelectorAll('#yourElement a').forEach(a => {
  a.addEventListener('click', (e) => {
    e.preventDefault()
    this.$router.push({ path: a.attributes.href.value })
  })
})
quiquelhappy commented 2 years ago

not having support for this is outrageous

c-malecki commented 1 year ago

For anyone else who happens to stumble upon this facing the same issue, here is the hacky approach I took which combines a bit of methodologies from other commentors.

<template>
  <vue-markdown v-bind="$attrs" :linkify="false" ref="VMD">
    <slot></slot>
  </vue-markdown>
</template>

<script>
import Vue from "vue";
import VueMarkdown from "vue-markdown";

export default {
  name: "WrapVueMarkdown",
  components: {
    VueMarkdown
  },
  mounted() {
    this.handleReplaceWithRouterLinks();
  },
  methods: {
    handleReplaceWithRouterLinks() {
      const links = this.$refs.VMD.$el.querySelectorAll("a");
      Array.from(links).forEach(aTag => {
        const path = aTag.attributes.href.value;
        const propsData = { to: path };
        const parent = this;
        const RouterLink = Vue.component("RouterLink");
        const routerLink = new RouterLink({ propsData, parent });
        routerLink.$mount(aTag);
        routerLink.$el.innerText = aTag.innerText;
      });
    }
  }
};
</script>

A simple wrapper that will replace a tags within the instance of that component with router-links after it's mounted. For my case, we're using markdown for the content of a Notifications list which also loads more Notifications as you scroll down, so I needed it to continually replace a tags as they were rendered.