[d24f17c] | 1 | import Autolinker from 'autolinker';
|
---|
| 2 |
|
---|
| 3 | // Autoconvert URL-like texts to links
|
---|
| 4 |
|
---|
| 5 |
|
---|
| 6 | var LINK_SCAN_RE = /www|@|\:\/\//;
|
---|
| 7 |
|
---|
| 8 |
|
---|
| 9 | function isLinkOpen(str) {
|
---|
| 10 | return /^<a[>\s]/i.test(str);
|
---|
| 11 | }
|
---|
| 12 | function isLinkClose(str) {
|
---|
| 13 | return /^<\/a\s*>/i.test(str);
|
---|
| 14 | }
|
---|
| 15 |
|
---|
| 16 | // Stupid fabric to avoid singletons, for thread safety.
|
---|
| 17 | // Required for engines like Nashorn.
|
---|
| 18 | //
|
---|
| 19 | function createLinkifier() {
|
---|
| 20 | var links = [];
|
---|
| 21 | var autolinker = new Autolinker({
|
---|
| 22 | stripPrefix: false,
|
---|
| 23 | url: true,
|
---|
| 24 | email: true,
|
---|
| 25 | replaceFn: function (match) {
|
---|
| 26 | // Only collect matched strings but don't change anything.
|
---|
| 27 | switch (match.getType()) {
|
---|
| 28 | /*eslint default-case:0*/
|
---|
| 29 | case 'url':
|
---|
| 30 | links.push({
|
---|
| 31 | text: match.matchedText,
|
---|
| 32 | url: match.getUrl()
|
---|
| 33 | });
|
---|
| 34 | break;
|
---|
| 35 | case 'email':
|
---|
| 36 | links.push({
|
---|
| 37 | text: match.matchedText,
|
---|
| 38 | // normalize email protocol
|
---|
| 39 | url: 'mailto:' + match.getEmail().replace(/^mailto:/i, '')
|
---|
| 40 | });
|
---|
| 41 | break;
|
---|
| 42 | }
|
---|
| 43 | return false;
|
---|
| 44 | }
|
---|
| 45 | });
|
---|
| 46 |
|
---|
| 47 | return {
|
---|
| 48 | links: links,
|
---|
| 49 | autolinker: autolinker
|
---|
| 50 | };
|
---|
| 51 | }
|
---|
| 52 |
|
---|
| 53 |
|
---|
| 54 | function parseTokens(state) {
|
---|
| 55 | var i, j, l, tokens, token, text, nodes, ln, pos, level, htmlLinkLevel,
|
---|
| 56 | blockTokens = state.tokens,
|
---|
| 57 | linkifier = null, links, autolinker;
|
---|
| 58 |
|
---|
| 59 | for (j = 0, l = blockTokens.length; j < l; j++) {
|
---|
| 60 | if (blockTokens[j].type !== 'inline') { continue; }
|
---|
| 61 | tokens = blockTokens[j].children;
|
---|
| 62 |
|
---|
| 63 | htmlLinkLevel = 0;
|
---|
| 64 |
|
---|
| 65 | // We scan from the end, to keep position when new tags added.
|
---|
| 66 | // Use reversed logic in links start/end match
|
---|
| 67 | for (i = tokens.length - 1; i >= 0; i--) {
|
---|
| 68 | token = tokens[i];
|
---|
| 69 |
|
---|
| 70 | // Skip content of markdown links
|
---|
| 71 | if (token.type === 'link_close') {
|
---|
| 72 | i--;
|
---|
| 73 | while (tokens[i].level !== token.level && tokens[i].type !== 'link_open') {
|
---|
| 74 | i--;
|
---|
| 75 | }
|
---|
| 76 | continue;
|
---|
| 77 | }
|
---|
| 78 |
|
---|
| 79 | // Skip content of html tag links
|
---|
| 80 | if (token.type === 'htmltag') {
|
---|
| 81 | if (isLinkOpen(token.content) && htmlLinkLevel > 0) {
|
---|
| 82 | htmlLinkLevel--;
|
---|
| 83 | }
|
---|
| 84 | if (isLinkClose(token.content)) {
|
---|
| 85 | htmlLinkLevel++;
|
---|
| 86 | }
|
---|
| 87 | }
|
---|
| 88 | if (htmlLinkLevel > 0) { continue; }
|
---|
| 89 |
|
---|
| 90 | if (token.type === 'text' && LINK_SCAN_RE.test(token.content)) {
|
---|
| 91 |
|
---|
| 92 | // Init linkifier in lazy manner, only if required.
|
---|
| 93 | if (!linkifier) {
|
---|
| 94 | linkifier = createLinkifier();
|
---|
| 95 | links = linkifier.links;
|
---|
| 96 | autolinker = linkifier.autolinker;
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 | text = token.content;
|
---|
| 100 | links.length = 0;
|
---|
| 101 | autolinker.link(text);
|
---|
| 102 |
|
---|
| 103 | if (!links.length) { continue; }
|
---|
| 104 |
|
---|
| 105 | // Now split string to nodes
|
---|
| 106 | nodes = [];
|
---|
| 107 | level = token.level;
|
---|
| 108 |
|
---|
| 109 | for (ln = 0; ln < links.length; ln++) {
|
---|
| 110 |
|
---|
| 111 | if (!state.inline.validateLink(links[ln].url)) { continue; }
|
---|
| 112 |
|
---|
| 113 | pos = text.indexOf(links[ln].text);
|
---|
| 114 |
|
---|
| 115 | if (pos) {
|
---|
| 116 | nodes.push({
|
---|
| 117 | type: 'text',
|
---|
| 118 | content: text.slice(0, pos),
|
---|
| 119 | level: level
|
---|
| 120 | });
|
---|
| 121 | }
|
---|
| 122 | nodes.push({
|
---|
| 123 | type: 'link_open',
|
---|
| 124 | href: links[ln].url,
|
---|
| 125 | title: '',
|
---|
| 126 | level: level++
|
---|
| 127 | });
|
---|
| 128 | nodes.push({
|
---|
| 129 | type: 'text',
|
---|
| 130 | content: links[ln].text,
|
---|
| 131 | level: level
|
---|
| 132 | });
|
---|
| 133 | nodes.push({
|
---|
| 134 | type: 'link_close',
|
---|
| 135 | level: --level
|
---|
| 136 | });
|
---|
| 137 | text = text.slice(pos + links[ln].text.length);
|
---|
| 138 | }
|
---|
| 139 | if (text.length) {
|
---|
| 140 | nodes.push({
|
---|
| 141 | type: 'text',
|
---|
| 142 | content: text,
|
---|
| 143 | level: level
|
---|
| 144 | });
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | // replace current node
|
---|
| 148 | blockTokens[j].children = tokens = [].concat(tokens.slice(0, i), nodes, tokens.slice(i + 1));
|
---|
| 149 | }
|
---|
| 150 | }
|
---|
| 151 | }
|
---|
| 152 | }
|
---|
| 153 | function linkify(md) {
|
---|
| 154 | md.core.ruler.push('linkify', parseTokens);
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | export { linkify };
|
---|