Skip to content

Commit

Permalink
Shirohana feature/custom href pattern (#17)
Browse files Browse the repository at this point in the history
* Implement custom link attributes

* Update README.md

* Update `test/index.js`

* Update index.js

* Update test/index.js

* Update README.md

* Adjust code for v2.0.0
  • Loading branch information
crookedneighbor authored Sep 21, 2017
1 parent ccef5db commit 7d7069f
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 79 deletions.
99 changes: 93 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ bower install markdown-it-link-attributes --save

## Use

### Basic Configuration

You can pass an object with an attrs property. Each link parsed with this config will have the passed attributes.

```js
var md = require('markdown-it')()
var mila = require('markdown-it-link-attributes')
```

```js
md.use(mila, {
target: '_blank',
rel: 'noopener'
attrs: {
target: '_blank',
rel: 'noopener'
}
})

var html = md.render('[link](https://google.com)')
html // <p><a href="https://google.com" target="_blank" rel="noopener">link</a></p>
var result = md.render('[Example](https://example.com')

result // <a href="https://example.com" target="_blank" rel="noopener">Example</a>
```

If the `linkify` option is set to `true` on `markdown-it`, then the attributes will be applied to plain links as well.
Expand All @@ -44,6 +49,88 @@ var html = md.render('foo https://google.com bar')
html // <p>foo <a href="https://google.com" target="_blank" rel="noopener">https://google.com</a> bar</p>
```

### Pattern

You can also specify a pattern property. The link's href property will be checked against the pattern RegExp provided and only apply the attributes if it matches the pattern.

```js
md.use(mila, {
pattern: /^https:/,
attrs: {
target: '_blank',
rel: 'noopener'
}
})

var matchingResult = md.render('[Matching Example](https://example.com')
var ignoredResult = md.render('[Not Matching Example](http://example.com')

matchingResult // <a href="https://example.com" target="_blank" rel="noopener">Matching Example</a>
ignoredResult // <a href="http://example.com">Not Matching Example</a>
```

### Applying classes

You can either apply a `class` to a link by using a `class` or a `className` property. Either one will work, but use only one, not both.

```js
md.use(mila, {
attrs: {
class: 'my-class'
}
})

// or
md.use(mila, {
attrs: {
className: 'my-class'
}
})
```

### Multiple Configurations

Alternatively, you can pass an Array of configurations. The first pattern to match will be applied to the link.

```js
md.use(mila, [{
pattern: /^https?:\/\//,
attrs: {
class: 'external-link'
}
}, {
pattern: /^\//,
attrs: {
class: 'absolute-link'
}
}, {
pattern: /blue/,
attrs: {
class: 'link-that-contains-the-word-blue'
}
}])

var externalResult = md.render('[external](https://example.com')
var absoluteResult = md.render('[absolute](/some-page')
var blueResult = md.render('[blue](relative/link/with/blue/in/the/name')

externalResult // <a href="https://example.com" class="external-link">external</a>
absoluteResult // <a href="/some-page" class="absolute-link">absolute</a>
blueResult // <a href="relative/link/with/blue/in/the/name" class="link-that-contains-the-word-blue">blue</a>
```

If multiple patterns match, the first configuration to match will be used.

```
// This matches both the "starts with http or https" rule and the "contains the word blue" rule.
// Since the http/https rule was defined first, that is the configuration that is used.
var result = md.render('[external](https://example.com/blue')
result // <a href="https://example.com/blue" class="external-link">external</a>
```

## Usage in the browser

_Differences in browser._ If you load script directly into the page, without a package system, the module will add itself globally as `window.markdownitLinkAttributes`.

## Testing
Expand Down
64 changes: 51 additions & 13 deletions dist/markdown-it-link-attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,61 @@

// Adapted from https://github.com/markdown-it/markdown-it/blob/fbc6b0fed563ba7c00557ab638fd19752f8e759d/docs/architecture.md

function markdownitLinkAttributes (md, config) {
config = config || {}
function findFirstMatchingConfig (link, configs) {
var i, config
var href = link.attrs[link.attrIndex('href')][1]

for (i = 0; i < configs.length; ++i) {
config = configs[i]

// if there is no pattern, config matches for all links
// otherwise, only return config if href matches the pattern set
if (!config.pattern || config.pattern.test(href)) {
return config
}
}
}

function applyAttributes (idx, tokens, attributes) {
Object.keys(attributes).forEach(function (attr) {
var attrIndex
var value = attributes[attr]

if (attr === 'className') {
// when dealing with applying classes
// programatically, some programmers
// may prefer to use the className syntax
attr = 'class'
}

attrIndex = tokens[idx].attrIndex(attr)

if (attrIndex < 0) { // attr doesn't exist, add new attribute
tokens[idx].attrPush([attr, value])
} else { // attr already exists, overwrite it
tokens[idx].attrs[attrIndex][1] = value // replace value of existing attr
}
})
}

function markdownitLinkAttributes (md, configs) {
if (!configs) {
configs = []
} else {
configs = Array.isArray(configs) ? configs : [configs]
}

Object.freeze(configs)

var defaultRender = md.renderer.rules.link_open || this.defaultRender
var attributes = Object.keys(config)

md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
attributes.forEach(function (attr) {
var value = config[attr]
var aIndex = tokens[idx].attrIndex(attr)

if (aIndex < 0) { // attr doesn't exist, add new attribute
tokens[idx].attrPush([attr, value])
} else { // attr already exists, overwrite it
tokens[idx].attrs[aIndex][1] = value // replace value of existing attr
}
})
var config = findFirstMatchingConfig(tokens[idx], configs)
var attributes = config && config.attrs

if (attributes) {
applyAttributes(idx, tokens, attributes)
}

// pass token to default renderer.
return defaultRender(tokens, idx, options, env, self)
Expand Down
2 changes: 1 addition & 1 deletion dist/markdown-it-link-attributes.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 79 additions & 37 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/6.0.1/markdown-it.min.js"></script>
<script src="../dist/markdown-it-link-attributes.min.js"></script>
<script src="../dist/markdown-it-link-attributes.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<style>
body {
Expand All @@ -29,60 +29,102 @@
<body>
<div class="ui container">
<h1>Markdown-It Link Attributes</h1>
</div>

<h3>Default Behavior (Without Plugin)</h3>

<div id="no-plugin-example" class="ui horizontal segments">
<pre class="ui segment"><code class="js">md.render('foo [link](https://google.com) bar')</code></pre>

<pre class="ui segment code-html"><code class="html">&lt;p&gt;
foo &lt;a href="https://google.com"&gt;link&lt;/a&gt;
&lt;/p&gt;</code></pre>

<div class="ui segment result"></div>
</div>

<h3>Plugin With Configured Attributes</h3>

<div id="attributes-plugin-example" class="ui horizontal segments">
<pre class="ui segment"><code class="js">md.use(markdownitLinkAttributes, {
target: '_blank'
})
md.render('foo [link](https://google.com) bar')</code></pre>
<script>
var container = document.querySelector('.ui.container')

<pre class="ui segment code-html"><code class="html">&lt;p&gt;
foo &lt;a href="https://google.com" target="_blank"&gt;link&lt;/a&gt;
&lt;/p&gt;</code></pre>
function getJsCodeBlob (config, pieces) {
var jsCodeBlob = ''

<div class="ui segment result"></div>
</div>
</div>
if (config) {
var clonedConfig = JSON.parse(JSON.stringify(config))

if (config.pattern) {
clonedConfig.pattern = config.pattern.toString()
} else if (Array.isArray(config)) {
config.forEach(function (c, index) {
if (c.pattern) {
clonedConfig[index].pattern = c.pattern.toString().replace(/\\\//g, '&bsol;&sol;')
}
})
}

jsCodeBlob = 'md.use(markdownitLinkAttributes, ' +
JSON.stringify(clonedConfig, null, 2)
.replace(/"(\w*)":/g, '$1:')
.replace(/"/g, "'")
.replace(/pattern: '(.*)',/g, 'pattern: $1') +
')\n'
}

<script>
var link = 'foo [link](https://google.com) bar'
return jsCodeBlob + pieces.map(function (piece) {
return 'md.render(\'' + piece + '\')\n'
}).join('\n')
}

function renderMarkdown (id, config) {
function renderExample (set) {
var config = set.config
var md = window.markdownit()
var result = document.querySelector('#' + id + ' .result')
var section = document.createElement('div')
var pieces = set.source.split('|')
var source = pieces.join('\n\n')
var parsedMarkdown

section.id = set.id

if (config) {
md.use(window.markdownitLinkAttributes, config)
}

var html = md.render(link)
result.innerHTML = html
parsedMarkdown = md.render(source)

section.innerHTML = '<h3>' + set.title + '</h3>' +
'<div class="ui horizontal segments">' +
'<pre class="ui segment"><code class="js">' +
getJsCodeBlob(config, pieces) +
'</code></pre>' +
'<pre class="ui segment code-html"><code class="html">' +
parsedMarkdown.replace(/</g, '&lt;').replace(/>/g, '&gt;') +
'</code></pre>' +
'<div class="ui segment result">' +
parsedMarkdown +
'</div>' +
'</div>'

container.appendChild(section)
}

[{
id: 'no-plugin-example'
id: 'no-plugin-example',
title: 'Default Behavior (Without Plugin)',
source: 'a [link](https://google.com) without modification'
}, {
id: 'attributes-plugin-example',
title: 'Plugin With Configured Attributes',
source: 'a [link](https://google.com) with `target="_blank"` added to it',
config: {
target: '_blank'
attrs: {
target: '_blank'
}
}
}].forEach(function (set) {
renderMarkdown(set.id, set.config)
})
}, {
id: 'attributes-plugin-with-multiple-configs-example',
title: 'Plugin With Multiple Configurations',
source: 'an external [link](https://google.com)|and an other [link](example)',
config: [{
pattern: /^https?:\/\//,
attrs: {
className: 'external-link',
target: '_blank',
rel: 'noopener'
}
}, {
attrs: {
className: 'other-link'
}
}]
}].forEach(renderExample)
</script>
</body>
</html>
Loading

0 comments on commit 7d7069f

Please sign in to comment.