@todoist/remark-application-links 0.0.0-stage → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Henning Muszynski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,98 @@
1
- # Temporary Holding Version
1
+ ![logo](./.github/logo.png)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # remark-application-links
4
+
5
+ You want to allow your users to directly link deeply into their favorite applications like Notion or Mail. Per default remark only supports a handful of valid protocols when parsing links from text which makes this impossible. With the **remark-application-links** plugin you can support all the schemes and specify a block list of potentially dangerous ones.
6
+
7
+ Turn
8
+
9
+ ```md
10
+ Here's a link to my notion: notion://notion.so/user/123
11
+ ```
12
+
13
+ into
14
+
15
+ ```html
16
+ <p>
17
+ Here's a link to my notion:
18
+ <a href="notion://notion.so/user/123">notion://notion.so/user/123</a>
19
+ </p>
20
+ ```
21
+
22
+ Make sure you understand the security implications before using this plugin: [Security](#security)
23
+
24
+ Heavily inspired by [remark-linkify-regex](https://gitlab.com/staltz/remark-linkify-regex) but different enough to justify a separate plugin.
25
+
26
+ Bootstrapped and built with [tsdx](https://github.com/formium/tsdx).
27
+
28
+ ## Installation
29
+
30
+ Use [npm](https://www.npmjs.com/get-npm) or [yarn](https://yarnpkg.com/lang/en/docs/install/) to install:
31
+
32
+ ```sh
33
+ npm install @todoist/remark-application-links
34
+ # or
35
+ yarn add @todoist/remark-application-links
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ This plugin exposes a function which you need to call with the blocked URL schemes (or with nothing to accept the default ones).
41
+
42
+ ```js
43
+ remark().use(remarkApplicationLinksPlugin())
44
+ // or with custom schemes
45
+ remark().use(remarkApplicationLinksPlugin(['javascript', 'notion']))
46
+ ```
47
+
48
+ ## Security
49
+
50
+ Since this plugin is using a block list of URL schemes instead of explictly allowing explicit ones it potentially opens up your application for attackers. The general stance in the security community is that a complete block list is hard (read: impossible) to achieve. If you want to be on the safe side it's better to rely on an explicit allow list of supported URL schemes.
51
+
52
+ Per default we're blocking these url schemes:
53
+
54
+ ```js
55
+ const blockedUrlSchemes = ['data', 'jar', 'java', 'javascript', 'vbscript', 'view-source']
56
+ ```
57
+
58
+ In general it's recommended to not only rely on your parser to prevent inline script executions but apply a Content-Security Policy (CSP) on your page as reliable catch-all.
59
+
60
+ You're advised to thoroughly test your use cases to not open your users up to [cross-site scripting (XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting) attacks. You might find additional tools like [rehype-sanitize](https://github.com/rehypejs/rehype-sanitize) helpful.
61
+
62
+ ### Markdown links and react-markdown
63
+
64
+ This plugin will transform this text:
65
+
66
+ ```md
67
+ [link](<javascript:alert(1)>)
68
+ ```
69
+
70
+ into
71
+
72
+ ```html
73
+ <p><a href="javascript:alert(1)">link</a></p>
74
+ ```
75
+
76
+ Which is dangerous as it would execute JavaScript. If you allow users to enter the text this will open you up to cross-site scripting (XSS) attacks. As mentioned above a strong CSP will protect you from this and not allow the JavaScript to execute.
77
+
78
+ This plugin can be used in combination with [react-markdown](https://github.com/rexxars/react-markdown). In order to prevent the example above being parsed as a link you need to supply the `transformLinkUri` prop.
79
+
80
+ ## Changelog
81
+
82
+ We're maintaining a [changelog](./CHANGELOG.md) in this repository. Our versioning follows [semantic versioning](https://semver.org/).
83
+
84
+ ## Releasing
85
+
86
+ Versions follow the explicit `package.json` semver value and `vVERSION` release record. The first new-scope release is `@todoist/remark-application-links@1.0.5`; previous packages and releases remain unchanged.
87
+
88
+ The manual [release workflow](./.github/workflows/publish.yml) first builds and retains a checked tarball without publishing. An authorized maintainer then publishes that same artifact to npm and GitHub Packages. Mirror retries reuse the original artifact and version. Creating a GitHub release no longer rebuilds or publishes packages.
89
+
90
+ See [the release runbook](./RELEASING.md) for the required upstream publication, first interactive npm publication, workflow setup, and recovery commands.
91
+
92
+ ## Contributing
93
+
94
+ Contributions are welcome. Check our [contributing guide](./CONTRIBUTING.md).
95
+
96
+ ## License
97
+
98
+ This project is distributed under the [MIT License](./LICENSE).
@@ -0,0 +1,11 @@
1
+ /**
2
+ * This is heavily inspired by `remark-linkify-regex`
3
+ * https://gitlab.com/staltz/remark-linkify-regex/blob/master/index.js
4
+ *
5
+ * remark-linkify-regex works on an allow list to support more protocols (url
6
+ * schemes). This plugin works with a block list to support everything that
7
+ * remotely looks like a link but blocks potentially dangerous schemes.
8
+ */
9
+ import { Node } from 'unist';
10
+ declare function remarkApplicationLinks(blockedUrlSchemes?: string[]): () => (ast: Node) => Node;
11
+ export default remarkApplicationLinks;
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+
2
+ 'use strict'
3
+
4
+ if (process.env.NODE_ENV === 'production') {
5
+ module.exports = require('./remark-application-links.cjs.production.min.js')
6
+ } else {
7
+ module.exports = require('./remark-application-links.cjs.development.js')
8
+ }
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
+
7
+ var visitWithParents = _interopDefault(require('unist-util-visit-parents'));
8
+ var flatMap = _interopDefault(require('unist-util-flatmap'));
9
+
10
+ /**
11
+ * This is heavily inspired by `remark-linkify-regex`
12
+ * https://gitlab.com/staltz/remark-linkify-regex/blob/master/index.js
13
+ *
14
+ * remark-linkify-regex works on an allow list to support more protocols (url
15
+ * schemes). This plugin works with a block list to support everything that
16
+ * remotely looks like a link but blocks potentially dangerous schemes.
17
+ */
18
+ /**
19
+ * Default collection of url schemes that will be blocked by this plugin. All
20
+ * others schemes are allowed.
21
+ */
22
+
23
+ var BLOCKED_URL_SCHEMES = ['data', 'jar', 'java', 'javascript', 'vbscript', 'view-source'];
24
+
25
+ function buildTextNode(content) {
26
+ return {
27
+ type: 'text',
28
+ value: content
29
+ };
30
+ }
31
+
32
+ function buildLinkNode(url, children) {
33
+ return {
34
+ type: 'link',
35
+ title: null,
36
+ url: url,
37
+ children: children
38
+ };
39
+ } // looks for any non-whitespace character sequence that contains `://`
40
+
41
+
42
+ var REGEX_POTENTIAL_URL = /\S+:\/\/\S+/g;
43
+
44
+ function splitTextNode(textNode, blockedUrlSchemes) {
45
+ var content = textNode.value;
46
+ var result = [];
47
+ var textStartIndex = 0;
48
+ var match = [];
49
+
50
+ while ((match = REGEX_POTENTIAL_URL.exec(content)) !== null) {
51
+ var matchingText = match[0];
52
+ var matchStartIndex = match.index;
53
+ var urlScheme = matchingText.split('://')[0];
54
+ var isBlockedScheme = blockedUrlSchemes.includes(urlScheme); // create a new text node for everything before the match
55
+
56
+ if (textStartIndex !== matchStartIndex) {
57
+ result.push(buildTextNode(content.slice(textStartIndex, matchStartIndex)));
58
+ } // either insert another text node if the url scheme is blocked or insert a link
59
+
60
+
61
+ if (isBlockedScheme) {
62
+ result.push(buildTextNode(matchingText));
63
+ } else {
64
+ result.push(buildLinkNode(matchingText, [buildTextNode(matchingText)]));
65
+ } // move the pointer ahead so we can support multiple links
66
+
67
+
68
+ textStartIndex = matchStartIndex + matchingText.length;
69
+ } // this path will also be executed when no url has been found
70
+
71
+
72
+ var remainingText = content.slice(textStartIndex);
73
+
74
+ if (remainingText.length > 0) {
75
+ result.push(buildTextNode(remainingText));
76
+ }
77
+
78
+ return result;
79
+ }
80
+
81
+ function remarkApplicationLinks(blockedUrlSchemes) {
82
+ if (blockedUrlSchemes === void 0) {
83
+ blockedUrlSchemes = BLOCKED_URL_SCHEMES;
84
+ }
85
+
86
+ return function () {
87
+ return function (ast) {
88
+ visitWithParents(ast, 'text', function (textNode, parents) {
89
+ if (parents.length > 0 && parents[parents.length - 1].type === 'link') {
90
+ textNode._ignoreMe = true;
91
+ return;
92
+ }
93
+ });
94
+ flatMap(ast, function (node) {
95
+ if (node.type !== 'text') {
96
+ return [node];
97
+ }
98
+
99
+ if (node._ignoreMe) {
100
+ delete node._ignoreMe;
101
+ return [node];
102
+ }
103
+
104
+ return splitTextNode(node, blockedUrlSchemes);
105
+ });
106
+ return ast;
107
+ };
108
+ };
109
+ }
110
+
111
+ exports.default = remarkApplicationLinks;
112
+ //# sourceMappingURL=remark-application-links.cjs.development.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remark-application-links.cjs.development.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * This is heavily inspired by `remark-linkify-regex`\n * https://gitlab.com/staltz/remark-linkify-regex/blob/master/index.js\n *\n * remark-linkify-regex works on an allow list to support more protocols (url\n * schemes). This plugin works with a block list to support everything that\n * remotely looks like a link but blocks potentially dangerous schemes.\n */\n\nimport visitWithParents from 'unist-util-visit-parents'\nimport flatMap from 'unist-util-flatmap'\nimport { Node } from 'unist'\n\n/**\n * Default collection of url schemes that will be blocked by this plugin. All\n * others schemes are allowed.\n */\nconst BLOCKED_URL_SCHEMES = ['data', 'jar', 'java', 'javascript', 'vbscript', 'view-source']\n\nfunction buildTextNode(content: string) {\n return { type: 'text', value: content }\n}\n\nfunction buildLinkNode(url: string, children: Node[]) {\n return {\n type: 'link',\n title: null,\n url,\n children,\n }\n}\n\n// looks for any non-whitespace character sequence that contains `://`\nconst REGEX_POTENTIAL_URL = /\\S+:\\/\\/\\S+/g\n\nfunction splitTextNode(textNode: Node, blockedUrlSchemes: string[]) {\n const content = textNode.value as string\n const result: Node[] = []\n let textStartIndex = 0\n let match: RegExpExecArray | null | never[] = []\n\n while ((match = REGEX_POTENTIAL_URL.exec(content)) !== null) {\n const matchingText = match[0]\n const matchStartIndex = match.index\n const urlScheme = matchingText.split('://')[0]\n const isBlockedScheme = blockedUrlSchemes.includes(urlScheme)\n\n // create a new text node for everything before the match\n if (textStartIndex !== matchStartIndex) {\n result.push(buildTextNode(content.slice(textStartIndex, matchStartIndex)))\n }\n\n // either insert another text node if the url scheme is blocked or insert a link\n if (isBlockedScheme) {\n result.push(buildTextNode(matchingText))\n } else {\n result.push(buildLinkNode(matchingText, [buildTextNode(matchingText)]))\n }\n\n // move the pointer ahead so we can support multiple links\n textStartIndex = matchStartIndex + matchingText.length\n }\n\n // this path will also be executed when no url has been found\n const remainingText = content.slice(textStartIndex)\n if (remainingText.length > 0) {\n result.push(buildTextNode(remainingText))\n }\n\n return result\n}\n\nfunction remarkApplicationLinks(blockedUrlSchemes = BLOCKED_URL_SCHEMES) {\n return () => (ast: Node) => {\n visitWithParents(ast, 'text', (textNode, parents) => {\n if (parents.length > 0 && parents[parents.length - 1].type === 'link') {\n textNode._ignoreMe = true\n return\n }\n })\n\n flatMap(ast, (node: Node) => {\n if (node.type !== 'text') {\n return [node]\n }\n if (node._ignoreMe) {\n delete node._ignoreMe\n return [node]\n }\n return splitTextNode(node, blockedUrlSchemes)\n })\n\n return ast\n }\n}\n\nexport default remarkApplicationLinks\n"],"names":["BLOCKED_URL_SCHEMES","buildTextNode","content","type","value","buildLinkNode","url","children","title","REGEX_POTENTIAL_URL","splitTextNode","textNode","blockedUrlSchemes","result","textStartIndex","match","exec","matchingText","matchStartIndex","index","urlScheme","split","isBlockedScheme","includes","push","slice","length","remainingText","remarkApplicationLinks","ast","visitWithParents","parents","_ignoreMe","flatMap","node"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(exports,"__esModule",{value:!0});var t=e(require("unist-util-visit-parents")),n=e(require("unist-util-flatmap")),r=["data","jar","java","javascript","vbscript","view-source"];function u(e){return{type:"text",value:e}}var i=/\S+:\/\/\S+/g;exports.default=function(e){return void 0===e&&(e=r),function(){return function(r){return t(r,"text",(function(e,t){t.length>0&&"link"===t[t.length-1].type&&(e._ignoreMe=!0)})),n(r,(function(t){return"text"!==t.type?[t]:t._ignoreMe?(delete t._ignoreMe,[t]):function(e,t){for(var n=e.value,r=[],l=0,a=[];null!==(a=i.exec(n));){var o=a[0],c=a.index,s=o.split("://")[0],p=t.includes(s);l!==c&&r.push(u(n.slice(l,c))),r.push(p?u(o):{type:"link",title:null,url:o,children:[u(o)]}),l=c+o.length}var f=n.slice(l);return f.length>0&&r.push(u(f)),r}(t,e)})),r}}};
2
+ //# sourceMappingURL=remark-application-links.cjs.production.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remark-application-links.cjs.production.min.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * This is heavily inspired by `remark-linkify-regex`\n * https://gitlab.com/staltz/remark-linkify-regex/blob/master/index.js\n *\n * remark-linkify-regex works on an allow list to support more protocols (url\n * schemes). This plugin works with a block list to support everything that\n * remotely looks like a link but blocks potentially dangerous schemes.\n */\n\nimport visitWithParents from 'unist-util-visit-parents'\nimport flatMap from 'unist-util-flatmap'\nimport { Node } from 'unist'\n\n/**\n * Default collection of url schemes that will be blocked by this plugin. All\n * others schemes are allowed.\n */\nconst BLOCKED_URL_SCHEMES = ['data', 'jar', 'java', 'javascript', 'vbscript', 'view-source']\n\nfunction buildTextNode(content: string) {\n return { type: 'text', value: content }\n}\n\nfunction buildLinkNode(url: string, children: Node[]) {\n return {\n type: 'link',\n title: null,\n url,\n children,\n }\n}\n\n// looks for any non-whitespace character sequence that contains `://`\nconst REGEX_POTENTIAL_URL = /\\S+:\\/\\/\\S+/g\n\nfunction splitTextNode(textNode: Node, blockedUrlSchemes: string[]) {\n const content = textNode.value as string\n const result: Node[] = []\n let textStartIndex = 0\n let match: RegExpExecArray | null | never[] = []\n\n while ((match = REGEX_POTENTIAL_URL.exec(content)) !== null) {\n const matchingText = match[0]\n const matchStartIndex = match.index\n const urlScheme = matchingText.split('://')[0]\n const isBlockedScheme = blockedUrlSchemes.includes(urlScheme)\n\n // create a new text node for everything before the match\n if (textStartIndex !== matchStartIndex) {\n result.push(buildTextNode(content.slice(textStartIndex, matchStartIndex)))\n }\n\n // either insert another text node if the url scheme is blocked or insert a link\n if (isBlockedScheme) {\n result.push(buildTextNode(matchingText))\n } else {\n result.push(buildLinkNode(matchingText, [buildTextNode(matchingText)]))\n }\n\n // move the pointer ahead so we can support multiple links\n textStartIndex = matchStartIndex + matchingText.length\n }\n\n // this path will also be executed when no url has been found\n const remainingText = content.slice(textStartIndex)\n if (remainingText.length > 0) {\n result.push(buildTextNode(remainingText))\n }\n\n return result\n}\n\nfunction remarkApplicationLinks(blockedUrlSchemes = BLOCKED_URL_SCHEMES) {\n return () => (ast: Node) => {\n visitWithParents(ast, 'text', (textNode, parents) => {\n if (parents.length > 0 && parents[parents.length - 1].type === 'link') {\n textNode._ignoreMe = true\n return\n }\n })\n\n flatMap(ast, (node: Node) => {\n if (node.type !== 'text') {\n return [node]\n }\n if (node._ignoreMe) {\n delete node._ignoreMe\n return [node]\n }\n return splitTextNode(node, blockedUrlSchemes)\n })\n\n return ast\n }\n}\n\nexport default remarkApplicationLinks\n"],"names":["BLOCKED_URL_SCHEMES","buildTextNode","content","type","value","REGEX_POTENTIAL_URL","blockedUrlSchemes","ast","visitWithParents","textNode","parents","length","_ignoreMe","flatMap","node","result","textStartIndex","match","exec","matchingText","matchStartIndex","index","urlScheme","split","isBlockedScheme","includes","push","slice","title","url","children","remainingText","splitTextNode"],"mappings":"0NAiBMA,EAAsB,CAAC,OAAQ,MAAO,OAAQ,aAAc,WAAY,eAE9E,SAASC,EAAcC,SACZ,CAAEC,KAAM,OAAQC,MAAOF,GAalC,IAAMG,EAAsB,+BAuC5B,SAAgCC,mBAAAA,IAAAA,EAAoBN,GACzC,kBAAM,SAACO,UACVC,EAAiBD,EAAK,QAAQ,SAACE,EAAUC,GACjCA,EAAQC,OAAS,GAA0C,SAArCD,EAAQA,EAAQC,OAAS,GAAGR,OAClDM,EAASG,WAAY,MAK7BC,EAAQN,GAAK,SAACO,SACQ,SAAdA,EAAKX,KACE,CAACW,GAERA,EAAKF,kBACEE,EAAKF,UACL,CAACE,IApDxB,SAAuBL,EAAgBH,WAC7BJ,EAAUO,EAASL,MACnBW,EAAiB,GACnBC,EAAiB,EACjBC,EAA0C,GAES,QAA/CA,EAAQZ,EAAoBa,KAAKhB,KAAoB,KACnDiB,EAAeF,EAAM,GACrBG,EAAkBH,EAAMI,MACxBC,EAAYH,EAAaI,MAAM,OAAO,GACtCC,EAAkBlB,EAAkBmB,SAASH,GAG/CN,IAAmBI,GACnBL,EAAOW,KAAKzB,EAAcC,EAAQyB,MAAMX,EAAgBI,KAKxDL,EAAOW,KADPF,EACYvB,EAAckB,GA9B3B,CACHhB,KAAM,OACNyB,MAAO,KACPC,IA6B8BV,EA5B9BW,SA4B4C,CAAC7B,EAAckB,MAI3DH,EAAiBI,EAAkBD,EAAaR,WAI9CoB,EAAgB7B,EAAQyB,MAAMX,UAChCe,EAAcpB,OAAS,GACvBI,EAAOW,KAAKzB,EAAc8B,IAGvBhB,EAoBQiB,CAAclB,EAAMR,MAGxBC"}
@@ -0,0 +1,106 @@
1
+ import visitWithParents from 'unist-util-visit-parents';
2
+ import flatMap from 'unist-util-flatmap';
3
+
4
+ /**
5
+ * This is heavily inspired by `remark-linkify-regex`
6
+ * https://gitlab.com/staltz/remark-linkify-regex/blob/master/index.js
7
+ *
8
+ * remark-linkify-regex works on an allow list to support more protocols (url
9
+ * schemes). This plugin works with a block list to support everything that
10
+ * remotely looks like a link but blocks potentially dangerous schemes.
11
+ */
12
+ /**
13
+ * Default collection of url schemes that will be blocked by this plugin. All
14
+ * others schemes are allowed.
15
+ */
16
+
17
+ var BLOCKED_URL_SCHEMES = ['data', 'jar', 'java', 'javascript', 'vbscript', 'view-source'];
18
+
19
+ function buildTextNode(content) {
20
+ return {
21
+ type: 'text',
22
+ value: content
23
+ };
24
+ }
25
+
26
+ function buildLinkNode(url, children) {
27
+ return {
28
+ type: 'link',
29
+ title: null,
30
+ url: url,
31
+ children: children
32
+ };
33
+ } // looks for any non-whitespace character sequence that contains `://`
34
+
35
+
36
+ var REGEX_POTENTIAL_URL = /\S+:\/\/\S+/g;
37
+
38
+ function splitTextNode(textNode, blockedUrlSchemes) {
39
+ var content = textNode.value;
40
+ var result = [];
41
+ var textStartIndex = 0;
42
+ var match = [];
43
+
44
+ while ((match = REGEX_POTENTIAL_URL.exec(content)) !== null) {
45
+ var matchingText = match[0];
46
+ var matchStartIndex = match.index;
47
+ var urlScheme = matchingText.split('://')[0];
48
+ var isBlockedScheme = blockedUrlSchemes.includes(urlScheme); // create a new text node for everything before the match
49
+
50
+ if (textStartIndex !== matchStartIndex) {
51
+ result.push(buildTextNode(content.slice(textStartIndex, matchStartIndex)));
52
+ } // either insert another text node if the url scheme is blocked or insert a link
53
+
54
+
55
+ if (isBlockedScheme) {
56
+ result.push(buildTextNode(matchingText));
57
+ } else {
58
+ result.push(buildLinkNode(matchingText, [buildTextNode(matchingText)]));
59
+ } // move the pointer ahead so we can support multiple links
60
+
61
+
62
+ textStartIndex = matchStartIndex + matchingText.length;
63
+ } // this path will also be executed when no url has been found
64
+
65
+
66
+ var remainingText = content.slice(textStartIndex);
67
+
68
+ if (remainingText.length > 0) {
69
+ result.push(buildTextNode(remainingText));
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ function remarkApplicationLinks(blockedUrlSchemes) {
76
+ if (blockedUrlSchemes === void 0) {
77
+ blockedUrlSchemes = BLOCKED_URL_SCHEMES;
78
+ }
79
+
80
+ return function () {
81
+ return function (ast) {
82
+ visitWithParents(ast, 'text', function (textNode, parents) {
83
+ if (parents.length > 0 && parents[parents.length - 1].type === 'link') {
84
+ textNode._ignoreMe = true;
85
+ return;
86
+ }
87
+ });
88
+ flatMap(ast, function (node) {
89
+ if (node.type !== 'text') {
90
+ return [node];
91
+ }
92
+
93
+ if (node._ignoreMe) {
94
+ delete node._ignoreMe;
95
+ return [node];
96
+ }
97
+
98
+ return splitTextNode(node, blockedUrlSchemes);
99
+ });
100
+ return ast;
101
+ };
102
+ };
103
+ }
104
+
105
+ export default remarkApplicationLinks;
106
+ //# sourceMappingURL=remark-application-links.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remark-application-links.esm.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * This is heavily inspired by `remark-linkify-regex`\n * https://gitlab.com/staltz/remark-linkify-regex/blob/master/index.js\n *\n * remark-linkify-regex works on an allow list to support more protocols (url\n * schemes). This plugin works with a block list to support everything that\n * remotely looks like a link but blocks potentially dangerous schemes.\n */\n\nimport visitWithParents from 'unist-util-visit-parents'\nimport flatMap from 'unist-util-flatmap'\nimport { Node } from 'unist'\n\n/**\n * Default collection of url schemes that will be blocked by this plugin. All\n * others schemes are allowed.\n */\nconst BLOCKED_URL_SCHEMES = ['data', 'jar', 'java', 'javascript', 'vbscript', 'view-source']\n\nfunction buildTextNode(content: string) {\n return { type: 'text', value: content }\n}\n\nfunction buildLinkNode(url: string, children: Node[]) {\n return {\n type: 'link',\n title: null,\n url,\n children,\n }\n}\n\n// looks for any non-whitespace character sequence that contains `://`\nconst REGEX_POTENTIAL_URL = /\\S+:\\/\\/\\S+/g\n\nfunction splitTextNode(textNode: Node, blockedUrlSchemes: string[]) {\n const content = textNode.value as string\n const result: Node[] = []\n let textStartIndex = 0\n let match: RegExpExecArray | null | never[] = []\n\n while ((match = REGEX_POTENTIAL_URL.exec(content)) !== null) {\n const matchingText = match[0]\n const matchStartIndex = match.index\n const urlScheme = matchingText.split('://')[0]\n const isBlockedScheme = blockedUrlSchemes.includes(urlScheme)\n\n // create a new text node for everything before the match\n if (textStartIndex !== matchStartIndex) {\n result.push(buildTextNode(content.slice(textStartIndex, matchStartIndex)))\n }\n\n // either insert another text node if the url scheme is blocked or insert a link\n if (isBlockedScheme) {\n result.push(buildTextNode(matchingText))\n } else {\n result.push(buildLinkNode(matchingText, [buildTextNode(matchingText)]))\n }\n\n // move the pointer ahead so we can support multiple links\n textStartIndex = matchStartIndex + matchingText.length\n }\n\n // this path will also be executed when no url has been found\n const remainingText = content.slice(textStartIndex)\n if (remainingText.length > 0) {\n result.push(buildTextNode(remainingText))\n }\n\n return result\n}\n\nfunction remarkApplicationLinks(blockedUrlSchemes = BLOCKED_URL_SCHEMES) {\n return () => (ast: Node) => {\n visitWithParents(ast, 'text', (textNode, parents) => {\n if (parents.length > 0 && parents[parents.length - 1].type === 'link') {\n textNode._ignoreMe = true\n return\n }\n })\n\n flatMap(ast, (node: Node) => {\n if (node.type !== 'text') {\n return [node]\n }\n if (node._ignoreMe) {\n delete node._ignoreMe\n return [node]\n }\n return splitTextNode(node, blockedUrlSchemes)\n })\n\n return ast\n }\n}\n\nexport default remarkApplicationLinks\n"],"names":["BLOCKED_URL_SCHEMES","buildTextNode","content","type","value","buildLinkNode","url","children","title","REGEX_POTENTIAL_URL","splitTextNode","textNode","blockedUrlSchemes","result","textStartIndex","match","exec","matchingText","matchStartIndex","index","urlScheme","split","isBlockedScheme","includes","push","slice","length","remainingText","remarkApplicationLinks","ast","visitWithParents","parents","_ignoreMe","flatMap","node"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
- "name": "@todoist/remark-application-links",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "version": "1.0.5",
3
+ "repository": "https://github.com/todoist/remark-application-links",
4
+ "license": "MIT",
5
+ "main": "dist/index.js",
6
+ "typings": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "src"
10
+ ],
11
+ "engines": {
12
+ "node": ">=12"
13
+ },
14
+ "scripts": {
15
+ "start": "tsdx watch",
16
+ "build": "tsdx build",
17
+ "test": "tsdx test",
18
+ "test:ci": "tsdx test --ci --coverage --maxWorkers=2",
19
+ "test:watch": "tsdx test --watch",
20
+ "lint": "tsdx lint && prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
21
+ "prepare": "tsdx build",
22
+ "setup": "npm install && npm run validate",
23
+ "validate": "npm run lint && npm run test",
24
+ "test:release": "node --test scripts/*.test.mjs"
25
+ },
26
+ "peerDependencies": {},
27
+ "husky": {
28
+ "hooks": {
29
+ "pre-commit": "npm run lint"
30
+ }
31
+ },
32
+ "prettier": "@todoist/prettier-config",
33
+ "name": "@todoist/remark-application-links",
34
+ "author": "Henning Muszynski",
35
+ "module": "dist/remark-application-links.esm.js",
36
+ "devDependencies": {
37
+ "@todoist/prettier-config": "4.0.2",
38
+ "@types/unist": "^2.0.3",
39
+ "husky": "^4.3.0",
40
+ "prettier": "^3.0.0",
41
+ "remark": "^12.0.1",
42
+ "remark-html": "^12.0.0",
43
+ "tsdx": "^0.14.0",
44
+ "tslib": "^2.0.1",
45
+ "typescript": "^4.0.3"
46
+ },
47
+ "dependencies": {
48
+ "unist-util-flatmap": "^1.0.0",
49
+ "unist-util-visit-parents": "^3.1.0"
50
+ },
51
+ "eslint": {
52
+ "rules": {
53
+ "prettier/prettier": "off"
54
+ }
55
+ }
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * This is heavily inspired by `remark-linkify-regex`
3
+ * https://gitlab.com/staltz/remark-linkify-regex/blob/master/index.js
4
+ *
5
+ * remark-linkify-regex works on an allow list to support more protocols (url
6
+ * schemes). This plugin works with a block list to support everything that
7
+ * remotely looks like a link but blocks potentially dangerous schemes.
8
+ */
9
+
10
+ import visitWithParents from 'unist-util-visit-parents'
11
+ import flatMap from 'unist-util-flatmap'
12
+ import { Node } from 'unist'
13
+
14
+ /**
15
+ * Default collection of url schemes that will be blocked by this plugin. All
16
+ * others schemes are allowed.
17
+ */
18
+ const BLOCKED_URL_SCHEMES = ['data', 'jar', 'java', 'javascript', 'vbscript', 'view-source']
19
+
20
+ function buildTextNode(content: string) {
21
+ return { type: 'text', value: content }
22
+ }
23
+
24
+ function buildLinkNode(url: string, children: Node[]) {
25
+ return {
26
+ type: 'link',
27
+ title: null,
28
+ url,
29
+ children,
30
+ }
31
+ }
32
+
33
+ // looks for any non-whitespace character sequence that contains `://`
34
+ const REGEX_POTENTIAL_URL = /\S+:\/\/\S+/g
35
+
36
+ function splitTextNode(textNode: Node, blockedUrlSchemes: string[]) {
37
+ const content = textNode.value as string
38
+ const result: Node[] = []
39
+ let textStartIndex = 0
40
+ let match: RegExpExecArray | null | never[] = []
41
+
42
+ while ((match = REGEX_POTENTIAL_URL.exec(content)) !== null) {
43
+ const matchingText = match[0]
44
+ const matchStartIndex = match.index
45
+ const urlScheme = matchingText.split('://')[0]
46
+ const isBlockedScheme = blockedUrlSchemes.includes(urlScheme)
47
+
48
+ // create a new text node for everything before the match
49
+ if (textStartIndex !== matchStartIndex) {
50
+ result.push(buildTextNode(content.slice(textStartIndex, matchStartIndex)))
51
+ }
52
+
53
+ // either insert another text node if the url scheme is blocked or insert a link
54
+ if (isBlockedScheme) {
55
+ result.push(buildTextNode(matchingText))
56
+ } else {
57
+ result.push(buildLinkNode(matchingText, [buildTextNode(matchingText)]))
58
+ }
59
+
60
+ // move the pointer ahead so we can support multiple links
61
+ textStartIndex = matchStartIndex + matchingText.length
62
+ }
63
+
64
+ // this path will also be executed when no url has been found
65
+ const remainingText = content.slice(textStartIndex)
66
+ if (remainingText.length > 0) {
67
+ result.push(buildTextNode(remainingText))
68
+ }
69
+
70
+ return result
71
+ }
72
+
73
+ function remarkApplicationLinks(blockedUrlSchemes = BLOCKED_URL_SCHEMES) {
74
+ return () => (ast: Node) => {
75
+ visitWithParents(ast, 'text', (textNode, parents) => {
76
+ if (parents.length > 0 && parents[parents.length - 1].type === 'link') {
77
+ textNode._ignoreMe = true
78
+ return
79
+ }
80
+ })
81
+
82
+ flatMap(ast, (node: Node) => {
83
+ if (node.type !== 'text') {
84
+ return [node]
85
+ }
86
+ if (node._ignoreMe) {
87
+ delete node._ignoreMe
88
+ return [node]
89
+ }
90
+ return splitTextNode(node, blockedUrlSchemes)
91
+ })
92
+
93
+ return ast
94
+ }
95
+ }
96
+
97
+ export default remarkApplicationLinks
@@ -0,0 +1 @@
1
+ declare module 'unist-util-flatmap'