-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
131 lines (119 loc) · 4.07 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* @typedef {import('hast').Root} Root
* @typedef {import('hast').Properties} Properties
* @typedef {import('hast').Element} Element
*
* @typedef {Element['children'][number]} ElementChild
*
* @typedef {'_self'|'_blank'|'_parent'|'_top'} Target
* @typedef {Array<string>|string|false} Rel
* @typedef {Array<string>} Protocols
* @typedef {ElementChild|Array<ElementChild>} Content
* @typedef {Properties} ContentProperties
*
* @callback TargetCallback
* @param {Element} node
* @returns {Target|null|undefined}
*
* @callback RelCallback
* @param {Element} node
* @returns {Rel|null|undefined}
*
* @callback ProtocolsCallback
* @param {Element} node
* @returns {Protocols|null|undefined}
*
* @callback ContentCallback
* @param {Element} node
* @returns {Content|null|undefined}
*
* @callback ContentPropertiesCallback
* @param {Element} node
* @returns {Properties|null|undefined}
*
* @typedef Options
* Configuration.
* @property {Target|TargetCallback} [target]
* How to display referenced documents (`string?`: `_self`, `_blank`,
* `_parent`, or `_top`, default: `_blank`).
* The default (nothing) is to not set `target`s on links.
* @property {Rel|RelCallback} [rel=['nofollow', 'noopener', 'noreferrer']]
* Link types to hint about the referenced documents.
* Pass `false` to not set `rel`s on links.
*
* **Note**: when using a `target`, add `noopener` and `noreferrer` to avoid
* exploitation of the `window.opener` API.
* @property {Protocols|ProtocolsCallback} [protocols=['http', 'https']]
* Protocols to check, such as `mailto` or `tel`.
* @property {Content|ContentCallback} [content]
* hast content to insert at the end of external links.
* Will be inserted in a `<span>` element.
*
* Useful for improving accessibility by giving users advanced warning when
* opening a new window.
* @property {ContentProperties|ContentPropertiesCallback} [contentProperties]
* hast properties to add to the `span` wrapping `content`, when given.
*/
import {visit} from 'unist-util-visit'
import {parse} from 'space-separated-tokens'
import absolute from 'is-absolute-url'
import extend from 'extend'
const defaultRel = ['nofollow']
const defaultProtocols = ['http', 'https']
/**
* If this is a value, return that.
* If this is a function instead, call it to get the result.
*
* @template T
* @param {T} value
* @param {Element} node
* @returns {T extends Function ? ReturnType<T> : T}
*/
function callIfNeeded(value, node) {
return typeof value === 'function' ? value(node) : value
}
/**
* Plugin to automatically add `target` and `rel` attributes to external links.
*
* @type {import('unified').Plugin<[Options?] | Array<void>, Root>}
*/
export default function rehypeExternalLinks(options = {}) {
return (tree) => {
visit(tree, 'element', (node) => {
if (
node.tagName === 'a' &&
node.properties &&
typeof node.properties.href === 'string'
) {
const url = node.properties.href
const protocol = url.slice(0, url.indexOf(':'))
const target = callIfNeeded(options.target, node)
const relRaw = callIfNeeded(options.rel, node)
const rel = typeof relRaw === 'string' ? parse(relRaw) : relRaw
const protocols =
callIfNeeded(options.protocols, node) || defaultProtocols
const contentRaw = callIfNeeded(options.content, node)
const content =
contentRaw && !Array.isArray(contentRaw) ? [contentRaw] : contentRaw
const contentProperties =
callIfNeeded(options.contentProperties, node) || {}
if (absolute(url) && protocols.includes(protocol)) {
if (target) {
node.properties.target = target
}
if (rel !== false) {
node.properties.rel = (rel || defaultRel).concat()
}
if (content) {
node.children.push({
type: 'element',
tagName: 'span',
properties: extend(true, contentProperties),
children: extend(true, content)
})
}
}
}
})
}
}