Auto convert keywords to hyperlinks


(Florian Schmaus) #1

I am looking for a feature that automatically converts keywords to hyperlinks.

For example JIRA issues: The have usually the format -<ISSUE_NUMBER>, eg. FOO-42. It would be really nice if discourse would automatically convert those to hyperlinks to the issues page. So FOO-42 link would be jira.example.com/browse/FOO-42, therefore discourse should convert FOO-42 to FOO-42


(Régis Hanol) #2

That’s definitely plugin territory.


(Florian Schmaus) #3

I tried creating a plugin following sam’s ALL_CAPS plugin:

plugins/keywords_links/plugin.rb

# name: KEYWORDS_LINKS
# about: create links from keywords
# version: 0.1
# authors: Flow

register_asset "javascripts/keywords_links.js", :server_side

plugins/keywords_links/assets/javascripts/keywords_links.js

Discourse.Dialect.postProcessText(function (text) {
    return text.replace(/(KEYWORD-[\d]+)/g, "[$1](https://jira.example.com/browse/$1)");
});

But it doesn’t work :frowning: The keywords are substituted, but the post displays the raw link, i.e. foo instead of a clickable hyperlink “foo”. It seems that discourse is using internally another format…


Converting keywords in posts on the fly
(Robin Ward) #4

Right, postProcessText is meant to operate on text only so any HTML you produce will be thought of as text. You’ll want to use inlineReplace, which produces JsonML:

Discourse.Dialect.inlineRegexp({
  start: "KEYWORD",
  matcher: /KEYWORD-(\d+)/gm,
  spaceBoundary: true,

  emitter: function(matches) {
    var jiraId = matches[1];
    return ['a', {href: "http://example.jira.com/browse/" + jiraId}, jiraId];
  }
});

Please help me to finish my inlineRegexp plugin to append href to posts
(Florian Schmaus) #5

That does the trick :clap: . I slightly modified your code

return ['a', {href: "http://example.jira.com/browse/KEYWORD-" + jiraId}, "KEYWORD-" + jiraId];

It would be great if the “plugin” would take an map from keywords to urls, so that multiple keywords can be defined.


(Régis Hanol) #6

Nothing prevents you from calling Discourse.Dialect.inlineRegexp multiple times :wink:


(Florian Schmaus) #7

Correct, but I don’t like code duplication :smile:

Anyway, I am very happy with the solution right now. Thanks everybody.


(Régis Hanol) #8

Where’s the duplication of code in doing something like the following pseudocode?

[
  ["FOO", "http://www.com"],
  ["BAR", "//domain.com"],
].each(function(a) {
  Discourse.Dialect.inlineRegexp({
    start: a[0],
    matcher: /a[0]-(\d+)/gm,
    spaceBoundary: true,

    emitter: function(matches) {
      var jiraId = matches[1];
      return ['a', {href: a[1] + jiraId}, jiraId];
    }
  });
};

(Florian Schmaus) #9

Sorry, I misunderstood your answer that I should write the function multiple times.

I am not that familiar with js, but where does KEYWORD get substituted with FOO and BAR in the code? Same with the URL.


(Régis Hanol) #10

Bear in mind that it’s not working javascript but only pseudocode so you can get the general idea.


(Florian Schmaus) #11

Now I am curious about the working code :slight_smile:


(Ben T) #12

Here’s a breakdown of what went on. (I’m actually in a class that’s about just javascript right now, and we just finished covering stuff like this… it’s cool to see it broken out!)

An array of possible start entries is created within the []. There are two arrays within the original array. So, there are two elements of this array which has no name. ["FOO", "http://www.com"] and ["BAR", "//domain.com"]. If the array was called myArray we could get “foo” by accessing myArray[0][0], and the URL by myArray[0][1].

Pretty nifty! So, a for each loop is preformed over the elements in the array, which in fact are just more arrays. This is called a in the example above. The loop is ran for these inner elements of the larger array. Since they are called a, they can be referenced from the inner function like I described above. So, when you see a[0], it’s the iterator that’s looking for the inner array elements. a is ["FOO", "http://www.com"] the first time through, so a[0] is just "FOO".

The first time through:
a[0] is “FOO”
a[1] is “http://www.com

The second time through:
a[0] is “BAR”
a[1] is “//domain.com

And, if you add more elements in, it will continue to iterate over them.

Hopefully this sets you on the right course!

You know, I wrote this and assumed little knowledge in javascript… sorry if this is a little off point; but it may help out someone down the line!


(Florian Schmaus) #13

Now I’d like to auto create commit links, so that as soon as somebody mentions a git sha1, a link to the commit is created. Of course creating a regex for git sha1 is impossible, therefore the sha1 needs to be prefixed with a keyword (“commit”).

But if I copy the same plugin I am using successfully for the other keywords and replace the .js with the code below, discourse isn’t loading anymore. Any ideas?

Discourse.Dialect.inlineRegexp({
    start: "commit",
    matcher: /commit \b[0-9a-f]{7,40}\b/gm,
    spaceBoundary: true

    emitter: function(matches) {
        var commitId = matches[1];
        return ['a', {href: "https://bitbucket.org/foo/bar/commits/" + commitId}, "commit "+commitId];
    }
});

(Régis Hanol) #14

You’re missing a comma right after spaceBoundary: true.


(Florian Schmaus) #15

Good catch. Thank you. For the record, there was also a regex group missing in the matcher pattern. It finally looks like this

plugins/git_commit_links/assets/javascripts/git_commit_links.js

Discourse.Dialect.inlineRegexp({
    start: "commit",
    matcher: /commit (\b[0-9a-f]{7,40}\b)/gm,
    spaceBoundary: true,

    emitter: function(matches) {
        var commitId = matches[1];
        return ['a', {href: "https://bitbucket.org/foo/bar/commits/" + commitId}, "commit "+commitId];
    }
});

(Adam Capriola) #16

I’m attempting to do something similar to this, but am a javascript noob and can’t get it working. Any ideas what I might have messed up? This is my .js file:

[
	["Term 1A", "Term 1B", "Term 1C"],
	["Term 2A", "Term 2B", "Term 2C"],
]
.each(function(a) {
	Discourse.Dialect.inlineRegexp({
		start: a[0],
		matcher: /a[0]\b(?![^<>]*<\/a>|[^<]*>)/,

		emitter: function() {
			return ['a', {href: "http://mysite.com/api/?q=" + encodeURIComponent( a[1].toLowerCase() )}, a[2]];
		}
	});
};

As an aside, I’m not sure if the negative lookahead is warranted here. I was using it on the non-Discourse portion of my website to prevent links from potentially breaking, but I’m not sure if this runs before or after markdown parses.

EDIT: Derp. I realized @zogstrip’s example was pseudocode. :turtle:


(Tuan Anh Tran) #17

@eviltrout how would i change markdown parser behavior to: see new line -> create new paragraph instead of inserting br tag?


(Robin Ward) #18

This is really complicated as new lines can be created within paragraphs but paragraphs cannot. I am not sure off the top of my head. Why do you need it?


(Adam Capriola) #20

Is there a way to add a class to the link? I made a couple attempts but couldn’t figure it out.

return ['a', {href: "http://example.com"}, {class: "css-class"}, "keyword"]; // fail
return ['a', {href: "http://example.com", class: "css-class"}, "keyword"]; // fail

(Robin Ward) #21

The second version is correct, but you’ll also have to whitelist that CSS class in our html sanitizer. Something like:

Discourse.Markdown.whiteListTag('a', 'class', /^css\-class$/);