From 92d350168d9c7d6707df0473b1b6e614fe19f702 Mon Sep 17 00:00:00 2001
From: v8tenko
Date: Tue, 8 Aug 2023 12:42:52 +0300
Subject: [PATCH] feat!: @diplodoc/transform@v4
BREAKING CHANGE:
- New term's linter
- Enabling `needToSanitizeHtml` by default
---
.github/workflows/release.yml | 2 +-
CHANGELOG.diplodoc.md | 38 ++
package-lock.json | 285 ++++++++-
package.json | 8 +-
src/transform/log.ts | 2 +-
src/transform/md.ts | 2 +-
src/transform/plugins/code.ts | 2 +-
src/transform/sanitize.ts | 118 +++-
src/transform/yfmlint/index.ts | 3 +-
.../yfmlint/markdownlint-custom-rule/index.ts | 1 +
.../markdownlint-custom-rule/yfm009.ts | 54 ++
src/transform/yfmlint/yfmlint.ts | 1 +
.../__snapshots__/highlight-code.test.ts.snap | 2 +-
test/checkbox.test.ts | 2 +-
test/file.test.ts | 18 +-
test/markdownlint-custom-rules/yfm004.test.ts | 4 +-
test/markdownlint-custom-rules/yfm009.test.ts | 68 +++
test/sanitize-html.test.ts | 118 +++-
test/table.test.ts | 37 +-
test/term.test.ts | 33 +-
test/xss.test.ts | 558 ++++++++++++++++++
21 files changed, 1251 insertions(+), 105 deletions(-)
create mode 100644 CHANGELOG.diplodoc.md
create mode 100644 src/transform/yfmlint/markdownlint-custom-rule/yfm009.ts
create mode 100644 test/markdownlint-custom-rules/yfm009.test.ts
create mode 100644 test/xss.test.ts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dab80590..a4360889 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,5 +11,5 @@ jobs:
- uses: yandex-cloud/ui-release-action@main
with:
github-token: ${{ secrets.YC_UI_BOT_GITHUB_TOKEN }}
- npm-token: ${{ secrets.YC_UI_BOT_NPM_TOKEN }}
+ npm-token: ${{ secrets.ROBOT_DATAUI_NPM_TOKEN }}
node-version: 14
diff --git a/CHANGELOG.diplodoc.md b/CHANGELOG.diplodoc.md
new file mode 100644
index 00000000..23c0d805
--- /dev/null
+++ b/CHANGELOG.diplodoc.md
@@ -0,0 +1,38 @@
+# Changelog @diplodoc/transform@4.0.0
+
+## It's major update of @doc-tools/transform@3.11.0 with security changes.
+
+### New term's linter
+
+The main feature of term is generating a hidden content, that will be show on click. Terms plugins creates MarkadownIt tokens at place, where term was defined and it can brake our `@doc-tools/docs` navigation. Now `@diplodoc/transform` has new yfmlint rule: `no-term-definition-in-content`. There are several restrictions: - You can't define content between term-def - All term-def should be placed at the end of file.
+
+### Enabling `needToSanitizeHtml` by default
+
+The sanitizer includes default options with safe, allowed tags, and attributes. However, by default, both the `style` tag and the `style` attribute are also allowed. The values will be processed by the [cssfilter](https://github.com/leizongmin/js-css-filter) module to prevent XSS attacks. The cssfilter module includes a default CSS whitelist.
+
+You can override the options for sanitizer like this:
+
+```javascript
+const transform = require('@doc-tools/transform');
+const {defaultOptions} = require('@doc-tools/transform/lib/sanitize');
+
+const sanitizeOptions = Object.assign({}, defaultOptions);
+
+// Allow css property
+sanitizeOptions.cssWhiteList['position'] = true;
+
+// Disallow css property
+delete sanitizeOptions.cssWhiteList['color'];
+
+// Disable `style` tag
+sanitizeOptions.allowedTags = sanitizeOptions.allowedTags.filter((tag) => tag !== 'style');
+
+// Disable `style` attribute
+sanitizeOptions.allowedAttributes['*'] = sanitizeOptions.allowedAttributes['*'].filter(
+ (attr) => attr !== 'style',
+);
+
+const {
+ result: {html},
+} = transform(content, {sanitizeOptions});
+```
diff --git a/package-lock.json b/package-lock.json
index 39c00cbc..d4e2fde1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
- "name": "@doc-tools/transform",
- "version": "3.11.0",
+ "name": "@diplodoc/transform",
+ "version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -4248,6 +4248,12 @@
"@babel/types": "^7.3.0"
}
},
+ "@types/css": {
+ "version": "0.0.34",
+ "resolved": "https://registry.npmjs.org/@types/css/-/css-0.0.34.tgz",
+ "integrity": "sha512-nyZiWXt6gBG7Jj7BYB4zdHXyrhcAjJH8qv+BUsO8FMJR5be7H5ETOaibB3uvXeX5lc57LWkecNJv03q0+JpbXA==",
+ "dev": true
+ },
"@types/github-slugger": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@types/github-slugger/-/github-slugger-1.3.0.tgz",
@@ -4687,6 +4693,11 @@
"integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=",
"dev": true
},
+ "atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
+ },
"autoprefixer": {
"version": "10.4.15",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.15.tgz",
@@ -4894,6 +4905,11 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
+ "boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -5002,6 +5018,114 @@
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
"dev": true
},
+ "cheerio": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
+ "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
+ "requires": {
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "htmlparser2": "^8.0.1",
+ "parse5": "^7.0.0",
+ "parse5-htmlparser2-tree-adapter": "^7.0.0"
+ },
+ "dependencies": {
+ "dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "requires": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ }
+ },
+ "domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "requires": {
+ "domelementtype": "^2.3.0"
+ }
+ },
+ "domutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+ "requires": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ }
+ },
+ "entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
+ },
+ "htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "requires": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ }
+ }
+ },
+ "cheerio-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+ "requires": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "dependencies": {
+ "dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "requires": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ }
+ },
+ "domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "requires": {
+ "domelementtype": "^2.3.0"
+ }
+ },
+ "domutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+ "requires": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ }
+ },
+ "entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
+ }
+ }
+ },
"chokidar": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
@@ -5044,7 +5168,7 @@
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
- "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
"dev": true
},
"collect-v8-coverage": {
@@ -5110,6 +5234,16 @@
"which": "^2.0.1"
}
},
+ "css": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
+ "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
+ "requires": {
+ "inherits": "^2.0.4",
+ "source-map": "^0.6.1",
+ "source-map-resolve": "^0.6.0"
+ }
+ },
"css-blank-pseudo": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-6.0.0.tgz",
@@ -5136,6 +5270,58 @@
"integrity": "sha512-03QGAk/FXIRseDdLb7XAiu6gidQ0Nd8945xuM7VFVPpc6goJsG9uIO8xQjTxwbPdPIIV4o4AJoOJyt8gwDl67g==",
"dev": true
},
+ "css-select": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
+ "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+ "requires": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "dependencies": {
+ "dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "requires": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ }
+ },
+ "domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "requires": {
+ "domelementtype": "^2.3.0"
+ }
+ },
+ "domutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+ "requires": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ }
+ },
+ "entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
+ }
+ }
+ },
+ "css-what": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
+ },
"cssdb": {
"version": "7.7.0",
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.7.0.tgz",
@@ -5148,6 +5334,11 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
+ "cssfilter": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
+ "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="
+ },
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
@@ -5157,10 +5348,15 @@
"ms": "2.1.2"
}
},
+ "decode-uri-component": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
+ "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ=="
+ },
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
- "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
+ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
"dev": true
},
"deep-eql": {
@@ -5765,7 +5961,7 @@
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
- "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
"dev": true
},
"expect": {
@@ -5815,7 +6011,7 @@
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"fastq": {
@@ -6148,7 +6344,7 @@
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true
},
"inflight": {
@@ -6164,8 +6360,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"internal-slot": {
"version": "1.0.3",
@@ -6930,7 +7125,7 @@
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"json5": {
@@ -7002,13 +7197,13 @@
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
- "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true
},
"lodash.merge": {
@@ -7188,7 +7383,7 @@
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
- "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
+ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"merge-stream": {
"version": "2.0.0",
@@ -7241,13 +7436,13 @@
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
- "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true
},
"node-releases": {
@@ -7277,6 +7472,14 @@
"path-key": "^3.0.0"
}
},
+ "nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "requires": {
+ "boolbase": "^1.0.0"
+ }
+ },
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -7432,6 +7635,40 @@
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
},
+ "parse5": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+ "requires": {
+ "entities": "^4.4.0"
+ },
+ "dependencies": {
+ "entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
+ }
+ }
+ },
+ "parse5-htmlparser2-tree-adapter": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz",
+ "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==",
+ "requires": {
+ "domhandler": "^5.0.2",
+ "parse5": "^7.0.0"
+ },
+ "dependencies": {
+ "domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "requires": {
+ "domelementtype": "^2.3.0"
+ }
+ }
+ }
+ },
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8174,7 +8411,7 @@
"safe-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
- "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
+ "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==",
"dev": true,
"requires": {
"ret": "~0.1.10"
@@ -8269,14 +8506,22 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
+ "source-map-resolve": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
+ "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
+ "requires": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0"
+ }
+ },
"source-map-support": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
@@ -8443,7 +8688,7 @@
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true
},
"tmpl": {
@@ -8603,7 +8848,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"v8-compile-cache": {
diff --git a/package.json b/package.json
index a5c59049..6774a32b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
- "name": "@doc-tools/transform",
- "version": "3.11.0",
+ "name": "@diplodoc/transform",
+ "version": "4.0.0",
"description": "A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML",
"author": "YFM Team ",
"license": "MIT",
@@ -27,6 +27,9 @@
"dependencies": {
"@diplodoc/tabs-extension": "^2.0.11",
"chalk": "4.1.2",
+ "cheerio": "^1.0.0-rc.12",
+ "css": "^3.0.0",
+ "cssfilter": "0.0.10",
"get-root-node-polyfill": "1.0.0",
"github-slugger": "1.4.0",
"js-yaml": "^4.1.0",
@@ -45,6 +48,7 @@
"@babel/core": "7.18.10",
"@babel/plugin-transform-modules-commonjs": "7.18.6",
"@babel/preset-env": "7.18.10",
+ "@types/css": "0.0.34",
"@types/github-slugger": "1.3.0",
"@types/jest": "28.1.7",
"@types/js-yaml": "^4.0.5",
diff --git a/src/transform/log.ts b/src/transform/log.ts
index 27b5e482..22f06682 100644
--- a/src/transform/log.ts
+++ b/src/transform/log.ts
@@ -18,7 +18,7 @@ function createLogger(type: LogLevels) {
const formatter: Record string> = {
[LogLevels.INFO]: (msg) => `${green('INFO')} ${msg}`,
[LogLevels.WARN]: (msg) => `${yellow('WARN')} ${msg}`,
- [LogLevels.ERROR]: (msg) => `${red('ERR ')} ${msg}`,
+ [LogLevels.ERROR]: (msg) => `${red('ERR')} ${msg}`,
};
return function log(msg: string) {
diff --git a/src/transform/md.ts b/src/transform/md.ts
index 245d17c6..c46568be 100644
--- a/src/transform/md.ts
+++ b/src/transform/md.ts
@@ -111,7 +111,7 @@ function initParser(md: MarkdownIt, options: OptionsType, env: EnvType) {
}
function initCompiler(md: MarkdownIt, options: OptionsType, env: EnvType) {
- const {needToSanitizeHtml = false, sanitizeOptions} = options;
+ const {needToSanitizeHtml = true, sanitizeOptions} = options;
return (tokens: Token[]) => {
const html = md.renderer.render(tokens, md.options, env);
diff --git a/src/transform/plugins/code.ts b/src/transform/plugins/code.ts
index 4f62ba38..49405f0e 100644
--- a/src/transform/plugins/code.ts
+++ b/src/transform/plugins/code.ts
@@ -15,7 +15,7 @@ const wrapInClipboard = (element: string | undefined, id: number) => {
diff --git a/src/transform/sanitize.ts b/src/transform/sanitize.ts
index 292eac92..36f58340 100644
--- a/src/transform/sanitize.ts
+++ b/src/transform/sanitize.ts
@@ -1,4 +1,8 @@
import sanitizeHtml from 'sanitize-html';
+// @ts-ignore
+import cssfilter from 'cssfilter';
+import * as cheerio from 'cheerio';
+import css from 'css';
const htmlTags = [
'a',
@@ -45,7 +49,6 @@ const htmlTags = [
'figure',
'font',
'footer',
- 'form',
'h1',
'h2',
'h3',
@@ -118,6 +121,7 @@ const htmlTags = [
'video',
'wbr',
'iframe',
+ 'style',
];
const svgTags = [
@@ -279,10 +283,15 @@ const htmlAttrs = [
'scrolling',
'allow',
'target',
+ 'attributeName',
+ 'aria-hidden',
+ 'referrerpolicy',
+ 'aria-describedby',
'data-*',
];
const svgAttrs = [
+ 'viewBox',
'accent-height',
'accumulate',
'additive',
@@ -468,23 +477,120 @@ const svgAttrs = [
'to',
];
+const yfmHtmlAttrs = ['note-type', 'yfm2xliff-explicit', 'term-key'];
+
const allowedTags = Array.from(
new Set([...htmlTags, ...svgTags, ...sanitizeHtml.defaults.allowedTags]),
);
-const allowedAttributes = Array.from(new Set([...htmlAttrs, ...svgAttrs]));
+const allowedAttributes = Array.from(new Set([...htmlAttrs, ...svgAttrs, ...yfmHtmlAttrs]));
-type DefaultOptions = sanitizeHtml.IDefaults;
-export type SanitizeOptions = sanitizeHtml.IOptions;
+export type CssWhiteList = {[property: string]: boolean};
-export const defaultOptions: DefaultOptions = {
+export interface SanitizeOptions extends sanitizeHtml.IOptions {
+ cssWhiteList?: CssWhiteList;
+}
+
+export const defaultParseOptions = {
+ lowerCaseAttributeNames: false,
+};
+
+export const defaultOptions: SanitizeOptions = {
...sanitizeHtml.defaults,
allowedTags,
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
'*': allowedAttributes,
},
+ allowVulnerableTags: true,
+ parser: defaultParseOptions,
+ cssWhiteList: cssfilter.whiteList,
};
+function sanitizeStyleTags(dom: cheerio.CheerioAPI, cssWhiteList: CssWhiteList) {
+ const styleTags = dom('style');
+
+ styleTags.each((_index: number, element: cheerio.Element) => {
+ const styleText = dom(element).text();
+
+ try {
+ const parsedCSS = css.parse(styleText);
+
+ if (!parsedCSS.stylesheet) {
+ return;
+ }
+
+ parsedCSS.stylesheet.rules = parsedCSS.stylesheet.rules.filter(
+ (rule: css.Rule) => rule.type === 'rule',
+ );
+
+ parsedCSS.stylesheet.rules.forEach((rule: css.Rule) => {
+ if (!rule.declarations) {
+ return;
+ }
+
+ rule.declarations = rule.declarations.filter((declaration: css.Declaration) => {
+ if (!declaration.property || !declaration.value) {
+ return false;
+ }
+
+ const isWhiteListed = cssWhiteList[declaration.property];
+
+ if (isWhiteListed) {
+ declaration.value = cssfilter.safeAttrValue(
+ declaration.property,
+ declaration.value,
+ );
+ }
+
+ if (!declaration.value) {
+ return false;
+ }
+
+ return isWhiteListed;
+ });
+ });
+
+ dom(element).text(css.stringify(parsedCSS));
+ } catch {}
+ });
+}
+
+function sanitizeStyleAttrs(dom: cheerio.CheerioAPI, cssWhiteList: CssWhiteList) {
+ const options = {
+ whiteList: cssWhiteList,
+ };
+ const cssSanitizer = new cssfilter.FilterCSS(options);
+
+ dom('*').each((_index, element) => {
+ const styleAttrValue = dom(element).attr('style');
+
+ if (!styleAttrValue) {
+ return;
+ }
+
+ dom(element).attr('style', cssSanitizer.process(styleAttrValue));
+ });
+}
+
+function sanitizeStyles(html: string, options: SanitizeOptions) {
+ const cssWhiteList = options.cssWhiteList || {};
+
+ const $ = cheerio.load(html);
+
+ sanitizeStyleTags($, cssWhiteList);
+
+ sanitizeStyleAttrs($, cssWhiteList);
+
+ const styles = $('head').html() || '';
+ const content = $('body').html() || '';
+
+ return styles + content;
+}
+
export default function sanitize(html: string, options?: SanitizeOptions) {
- return sanitizeHtml(html, options || defaultOptions);
+ const sanitizeOptions = options || defaultOptions;
+
+ const modifiedHtml = sanitizeStyles(html, sanitizeOptions);
+
+ return sanitizeHtml(modifiedHtml, sanitizeOptions);
}
diff --git a/src/transform/yfmlint/index.ts b/src/transform/yfmlint/index.ts
index e75df24b..ea26096a 100644
--- a/src/transform/yfmlint/index.ts
+++ b/src/transform/yfmlint/index.ts
@@ -19,8 +19,9 @@ import {errorToString, getLogLevel} from './utils';
import {Options} from './typings';
import {Dictionary} from 'lodash';
import {Logger, LogLevels} from '../log';
+import {yfm009} from './markdownlint-custom-rule/yfm009';
-const defaultLintRules = [yfm001, yfm002, yfm003, yfm004, yfm005, yfm006, yfm007, yfm008];
+const defaultLintRules = [yfm001, yfm002, yfm003, yfm004, yfm005, yfm006, yfm007, yfm008, yfm009];
const lintCache = new Set();
diff --git a/src/transform/yfmlint/markdownlint-custom-rule/index.ts b/src/transform/yfmlint/markdownlint-custom-rule/index.ts
index 52194af1..1f5f5b92 100644
--- a/src/transform/yfmlint/markdownlint-custom-rule/index.ts
+++ b/src/transform/yfmlint/markdownlint-custom-rule/index.ts
@@ -6,3 +6,4 @@ export {yfm005} from './yfm005';
export {yfm006} from './yfm006';
export {yfm007} from './yfm007';
export {yfm008} from './yfm008';
+export {yfm009} from './yfm009';
diff --git a/src/transform/yfmlint/markdownlint-custom-rule/yfm009.ts b/src/transform/yfmlint/markdownlint-custom-rule/yfm009.ts
new file mode 100644
index 00000000..0a01d9ea
--- /dev/null
+++ b/src/transform/yfmlint/markdownlint-custom-rule/yfm009.ts
@@ -0,0 +1,54 @@
+import {Rule} from 'markdownlint';
+
+export const yfm009: Rule = {
+ names: ['YFM009', 'no-term-definition-in-content'],
+ description: 'Term definition should be placed at the end of file',
+ tags: ['term'],
+ function: function YFM009(params, onError) {
+ const {config} = params;
+ if (!config) {
+ return;
+ }
+
+ let lastCloseIndex = -1;
+ const size = params.tokens.length;
+
+ for (let i = 0; i < size; i++) {
+ if (params.tokens[i].type === 'template_close') {
+ lastCloseIndex = i;
+ }
+
+ if (params.tokens[i].type !== 'template_close') {
+ continue;
+ }
+
+ if (i === size - 1) {
+ continue;
+ }
+
+ if (params.tokens[i + 1].type === 'template_open') {
+ continue;
+ }
+
+ if (i < size - 2 && params.tokens[i + 2].type === 'template_open') {
+ continue;
+ }
+
+ onError({
+ lineNumber: params.tokens[i + 1].lineNumber,
+ detail: 'There is a content between term definition. All term defitions should be placed at the end of file.',
+ });
+ }
+
+ if (lastCloseIndex === -1) {
+ return;
+ }
+
+ if (lastCloseIndex !== size - 1) {
+ onError({
+ lineNumber: params.tokens[lastCloseIndex + 1].lineNumber,
+ detail: 'The file must end with term only.',
+ });
+ }
+ },
+};
diff --git a/src/transform/yfmlint/yfmlint.ts b/src/transform/yfmlint/yfmlint.ts
index 94fb29c4..ec54669d 100644
--- a/src/transform/yfmlint/yfmlint.ts
+++ b/src/transform/yfmlint/yfmlint.ts
@@ -61,6 +61,7 @@ const index: LintConfig = {
YFM006: LogLevels.WARN, // Term definition duplicated
YFM007: LogLevels.WARN, // Term used without definition
YFM008: LogLevels.WARN, // Term inside definition not allowed
+ YFM009: LogLevels.ERROR, // Term definition used not at the end of file
},
// Inline code length
diff --git a/test/__snapshots__/highlight-code.test.ts.snap b/test/__snapshots__/highlight-code.test.ts.snap
index 7839e2bc..2df71d72 100644
--- a/test/__snapshots__/highlight-code.test.ts.snap
+++ b/test/__snapshots__/highlight-code.test.ts.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should highlight code 1`] = `
-"const x : string = 'y' ;
+"const x : string = 'y' ;
"
`;
diff --git a/test/checkbox.test.ts b/test/checkbox.test.ts
index 618efff5..fc4c352a 100644
--- a/test/checkbox.test.ts
+++ b/test/checkbox.test.ts
@@ -82,7 +82,7 @@ describe('markdown-it-checkbox', function () {
it('should parse inline markup in label', () => {
expect(transformYfm('[X] text *italic* **bold** label')).toBe(
'\n' +
- ' \n' +
+ ' \n' +
'text italic bold label \n' +
'
\n',
);
diff --git a/test/file.test.ts b/test/file.test.ts
index 4403925d..1c960b4f 100644
--- a/test/file.test.ts
+++ b/test/file.test.ts
@@ -28,12 +28,8 @@ describe('File plugin', () => {
});
it('should not render file without all required attrs', () => {
- expect(transform('{% file src="../file" %}')).toBe(
- '{% file src="../file" %}
\n',
- );
- expect(transform('{% file name="file.txt" %}')).toBe(
- '{% file name="file.txt" %}
\n',
- );
+ expect(transform('{% file src="../file" %}')).toBe('{% file src="../file" %}
\n');
+ expect(transform('{% file name="file.txt" %}')).toBe('{% file name="file.txt" %}
\n');
});
it('should render file with text before', () => {
@@ -118,15 +114,9 @@ describe('File plugin', () => {
);
});
- it('should escape attrs', () => {
- expect(transform('{% file src="ind${iconHtml}ind"ex.ht&ml
\n`,
- );
- });
-
it('should allow quoutes in attribute value', () => {
expect(transform('{% file src="ind\'ex.txt" name=\'ind"ex.html\' %}')).toBe(
- `${iconHtml}ind"ex.html
\n`,
+ `${iconHtml}ind"ex.html
\n`,
);
});
@@ -140,7 +130,7 @@ describe('File plugin', () => {
it('should not render file without spaces around attrs', () => {
expect(transform('{% file src="index.txt"name="index.html"type="text/html" %}')).toBe(
- `{% file src="index.txt"name="index.html"type="text/html" %}
\n`,
+ `{% file src="index.txt"name="index.html"type="text/html" %}
\n`,
);
});
diff --git a/test/markdownlint-custom-rules/yfm004.test.ts b/test/markdownlint-custom-rules/yfm004.test.ts
index 0ccf6dc4..9484b660 100644
--- a/test/markdownlint-custom-rules/yfm004.test.ts
+++ b/test/markdownlint-custom-rules/yfm004.test.ts
@@ -22,10 +22,12 @@ const tableWithCloseToken = `
|#
`.trim();
+const random = () => Math.floor(Math.random() * 1e8);
+
const lint = (input: string) => {
yfmlint({
input,
- pluginOptions: {log, path: 'test.md'},
+ pluginOptions: {log, path: `${random()}.md`},
lintConfig: {
'log-levels': {
MD047: LogLevels.DISABLED,
diff --git a/test/markdownlint-custom-rules/yfm009.test.ts b/test/markdownlint-custom-rules/yfm009.test.ts
new file mode 100644
index 00000000..79cbcf0c
--- /dev/null
+++ b/test/markdownlint-custom-rules/yfm009.test.ts
@@ -0,0 +1,68 @@
+import {log, LogLevels} from '../../src/transform/log';
+import term from '../../src/transform/plugins/term';
+import yfmlint from '../../src/transform/yfmlint';
+import {yfm009} from '../../src/transform/yfmlint/markdownlint-custom-rule';
+
+const withIncludes = `\
+[*widget-popup1]: {% include [ ](./_includes/widget.md) %}
+
+[*button-popup2]: {% include [ ](./_includes/button) %}
+
+[*button-popup3]: {% include [ ](./_includes/button) %}`.trim();
+
+const plainText = `\
+[*widget-popup1]: hello world
+
+hi i am text
+
+[*widget-popup2]: it's will fail`.trim();
+
+const appendText = (text: string) => {
+ return `${text}\n\nhi i am text after`;
+};
+
+const prependText = (text: string) => {
+ return `hi i am text before\n\n${text}`;
+};
+
+const random = () => Math.floor(Math.random() * 1e8);
+
+const lint = (input: string) => {
+ yfmlint({
+ input,
+ pluginOptions: {log, path: `${random()}.md`},
+ lintConfig: {
+ 'log-levels': {
+ YFM009: LogLevels.ERROR,
+ },
+ },
+ customLintRules: [yfm009],
+ plugins: [term],
+ });
+};
+
+describe('YFM009', () => {
+ beforeEach(() => {
+ log.clear();
+ });
+
+ it('not accepts text between terms', () => {
+ lint(plainText);
+ expect(log.get().error[0]).toMatch('YFM009');
+ });
+
+ it('not accepts includes with text after', () => {
+ lint(appendText(withIncludes));
+ expect(log.get().error[0]).toMatch('YFM009');
+ });
+
+ it('accepts includes', () => {
+ lint(withIncludes);
+ expect(log.isEmpty()).toBeTruthy();
+ });
+
+ it('accepts includes with text before', () => {
+ lint(prependText(withIncludes));
+ expect(log.isEmpty()).toBeTruthy();
+ });
+});
diff --git a/test/sanitize-html.test.ts b/test/sanitize-html.test.ts
index 997e392c..69ff1095 100644
--- a/test/sanitize-html.test.ts
+++ b/test/sanitize-html.test.ts
@@ -1,5 +1,5 @@
import transform from '../src/transform';
-import sanitizeHtml from '../src/transform/sanitize';
+import sanitizeHtml, {defaultOptions} from '../src/transform/sanitize';
const transformYfm = (text: string, options?: Parameters[1]) => {
const {
@@ -16,13 +16,117 @@ describe('Sanitize HTML utility', () => {
expect(sanitizeHtml(' ')).toBe(' ');
});
- it('transform should sanitize html', () => {
- expect(transformYfm(' ', {needToSanitizeHtml: true})).toBe(
- ' ',
- );
+ describe('transform should sanitize html by default l', () => {
+ describe('html in markdown', () => {
+ it('should sanitize danger attributes', () => {
+ expect(transformYfm(' ')).toBe(' ');
+ });
+
+ it('should sanitize danger style attributes', () => {
+ expect(transformYfm('
')).toBe(
+ '
',
+ );
+ });
+
+ it('should sanitize danger properties in style tag', () => {
+ expect(transformYfm('')).toBe(
+ '',
+ );
+ });
+
+ it('should sanitize form tag', () => {
+ expect(
+ transformYfm(''),
+ ).toBe('do ');
+ });
+ });
+
+ describe('plugin markdown-it-attrs', () => {
+ it('should sanitize danger attributes', () => {
+ expect(transformYfm('Click {onfocus="alert(1)" onclick="alert(1)"}')).toBe(
+ 'Click
\n',
+ );
+ });
+
+ it('should not sanitize safe attributes', () => {
+ expect(transformYfm('Click {.style-me data-toggle=modal}')).toBe(
+ 'Click
\n',
+ );
+ });
+
+ it('should sanitize danger style attributes', () => {
+ expect(
+ transformYfm(
+ '[example.com](https://example.com){style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: red; opacity: 0.5"}',
+ ),
+ ).toBe(
+ 'example.com
\n',
+ );
+ });
+ });
});
- it('by default transform should not sanitize html', () => {
- expect(transformYfm(' ')).toBe(' ');
+ describe('rewrite default sanitize options', () => {
+ it('should not sanitize form tag if form is allowed', () => {
+ const sanitizeOptions = Object.assign({}, defaultOptions);
+
+ // @ts-ignore
+ sanitizeOptions.allowedTags = sanitizeOptions.allowedTags.concat(['form']);
+
+ expect(
+ transformYfm('', {
+ sanitizeOptions,
+ }),
+ ).toBe('');
+ });
+
+ it('should filter style tag', () => {
+ const sanitizeOptions = Object.assign({}, defaultOptions);
+
+ // @ts-ignore
+ sanitizeOptions.allowedTags = sanitizeOptions.allowedTags.filter(
+ (tag: string) => tag !== 'style',
+ );
+
+ expect(
+ transformYfm('', {
+ sanitizeOptions,
+ }),
+ ).toBe('');
+ });
+
+ it('should filter style attribute', () => {
+ const sanitizeOptions = Object.assign({}, defaultOptions);
+
+ // @ts-ignore
+ sanitizeOptions.allowedAttributes['*'] = sanitizeOptions.allowedAttributes['*'].filter(
+ (attr: string) => attr !== 'style',
+ );
+
+ expect(
+ transformYfm('
', {
+ sanitizeOptions,
+ }),
+ ).toBe('
');
+ });
+
+ it('should not sanitize property if it is passed in cssWhiteList', () => {
+ const sanitizeOptions = Object.assign({}, defaultOptions);
+
+ // @ts-ignore
+ sanitizeOptions.cssWhiteList['position'] = true;
+
+ expect(
+ transformYfm('', {
+ sanitizeOptions,
+ }),
+ ).toBe('');
+ });
+ });
+
+ it('transform should not sanitize html if needToSanitizeHtml is false', () => {
+ expect(transformYfm(' ', {needToSanitizeHtml: false})).toBe(
+ ' ',
+ );
});
});
diff --git a/test/table.test.ts b/test/table.test.ts
index 91ddf78f..75bd74b9 100644
--- a/test/table.test.ts
+++ b/test/table.test.ts
@@ -150,7 +150,7 @@ describe('Table plugin', () => {
'\n' +
'\n' +
'\n' +
- '
\n' +
+ '
\n' +
' \n' +
'\n' +
'link to README.md
\n' +
@@ -187,7 +187,7 @@ describe('Table plugin', () => {
' \n' +
'\n' +
'\n' +
- 'Text with \n' +
+ '
Text with \n' +
'new line
\n' +
' \n' +
'\n' +
@@ -208,7 +208,7 @@ describe('Table plugin', () => {
'Element B \n' +
'\n' +
'\n' +
- 'Element 2 \n' +
+ ' Element 2 \n' +
'test \n' +
'\n' +
' \n' +
@@ -349,13 +349,12 @@ describe('Table plugin', () => {
'|Cell in column 2, row 3||',
),
).toBe(
- '\n' +
- '#| \n' +
- '||Cell in column 1, row 1 \n' +
- '|Cell in column 2, row 1|| \n' +
- '||Cell in column 1, row 2 \n' +
- '|Cell in column 2, row 2|| \n' +
- '||Cell in column 1, row 3 \n' +
+ '
#| \n' +
+ '||Cell in column 1, row 1 \n' +
+ '|Cell in column 2, row 1|| \n' +
+ '||Cell in column 1, row 2 \n' +
+ '|Cell in column 2, row 2|| \n' +
+ '||Cell in column 1, row 3 \n' +
'|Cell in column 2, row 3||
\n',
);
});
@@ -371,10 +370,10 @@ describe('Table plugin', () => {
'Test',
),
).toBe(
- '||Cell in column 1, row 1 \n' +
- '|Cell in column 2, row 1 \n' +
- '||Cell in column 1, row 2 \n' +
- '|Cell in column 2, row 2 \n' +
+ '
||Cell in column 1, row 1 \n' +
+ '|Cell in column 2, row 1 \n' +
+ '||Cell in column 1, row 2 \n' +
+ '|Cell in column 2, row 2 \n' +
'|#
\n' +
'Test
\n',
);
@@ -392,12 +391,12 @@ describe('Table plugin', () => {
'|#',
),
).toBe(
- 'Test \n' +
- '#| \n' +
- '||Cell in column 1, row 1 \n' +
+ '
Test \n' +
+ '#| \n' +
+ '||Cell in column 1, row 1 \n' +
'|Cell in column 2, row 1||
\n' +
- '||Cell in column 1, row 2 \n' +
- '|Cell in column 2, row 2|| \n' +
+ '
||Cell in column 1, row 2 \n' +
+ '|Cell in column 2, row 2|| \n' +
'|#
\n',
);
});
diff --git a/test/term.test.ts b/test/term.test.ts
index 6653365a..16685533 100644
--- a/test/term.test.ts
+++ b/test/term.test.ts
@@ -78,35 +78,10 @@ describe('Terms', () => {
-
-
-
-
+
+
+
+
diff --git a/test/xss.test.ts b/test/xss.test.ts
new file mode 100644
index 00000000..8453feed
--- /dev/null
+++ b/test/xss.test.ts
@@ -0,0 +1,558 @@
+/* eslint no-useless-escape: 0 */
+
+import transform from '../src/transform';
+
+const transformYfm = (text: string, options?: Parameters[1]) => {
+ const {
+ result: {html},
+ } = transform(text, {
+ allowHTML: true,
+ ...options,
+ });
+ return html;
+};
+
+// https://sking7.github.io/articles/218647712.html
+
+describe('XSS Filter', () => {
+ it('XSS Locator', () => {
+ const input = `';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//-->">'>`;
+ const expected = `';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//-->">'>
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('XSS locator 2', () => {
+ const input = `'';!--"=&{()}`;
+ const expected = `'';!--"=&
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('No Filter Evasion', () => {
+ const input = ``;
+ const expected = '';
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Image XSS using the JavaScript directive', () => {
+ const input = ` `;
+ const expected = ' ';
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('No quotes and no semicolon', () => {
+ const input = ` `;
+ const expected = `<img src=javascript:alert('XSS')>
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Case insensitive XSS attack vector', () => {
+ const input = ` `;
+ const expected = `<img src=JaVaScRiPt:alert('XSS')>
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('HTML entities', () => {
+ const input = ` `;
+ const expected = `<img src=javascript:alert("XSS")>
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Grave accent obfuscation', () => {
+ const input = " ";
+ const expected = `<img src='javascript:alert("RSnake says, 'XSS'")'>
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Malformed A tags', () => {
+ const input = 'xxs link ';
+ const expected = `xxs link
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Malformed img tags', () => {
+ const input = ' ">';
+ const expected = `<img """>">
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('fromCharCode', () => {
+ const input = ' ';
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Default src tag to get past filters that check src domain', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Default src tag by leaving it empty', () => {
+ const input = ` `;
+ const expected = `<img src= onmouseover="alert('xxs')">
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Default src tag by leaving it out entirely', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Decimal HTML character references', () => {
+ const input = ` `;
+ const expected = `<img src=javascript:alert( \n'XSS')>
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Decimal HTML character references without trailing semicolons', () => {
+ const input = ` `;
+ const expected = `<img src=javascript:a& \n#0000108ert('XSS')>
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Hexadecimal HTML character references without trailing semicolons', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Embedded tab', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Embedded Encoded tab', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Embedded newline to break up XSS', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Embedded carriage return to break up XSS', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Null breaks up JavaScript directive', () => {
+ const input = `perl -e 'print " ";' > out`;
+ const expected = `perl -e 'print "<img src=java�script:alert("XSS")>";' > out
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Spaces and meta chars before the JavaScript in images for XSS', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Non-alpha-non-digit XSS 1', () => {
+ const input = ``;
+ const expected = `<script/xss src="http://ha.ckers.org/xss.js">
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Non-alpha-non-digit XSS 2', () => {
+ const input = '';
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Non-alpha-non-digit XSS 3', () => {
+ const input = '';
+ const expected = `<script/src="http://ha.ckers.org/xss.js">
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Non-alpha-non-digit XSS 4', () => {
+ const input = '<';
+ const expected = `<
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('No closing script tags', () => {
+ const input = '`;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('input image', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('body image', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('img Dynsrc', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('img lowsrc', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('List-style-image', () => {
+ const input = `XSS`;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('VBscript in an image', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Livescript', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('body tag', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('BGSOUND', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('& JavaScript includes', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('style sheet', () => {
+ const input = ` `;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Remote style sheet', () => {
+ const input = ` `;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Remote style sheet part 2', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Remote style sheet part 3', () => {
+ const input = ` `;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Remote style sheet part 4', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('style tags with broken up JavaScript for XSS', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('style tags with broken up JavaScript for XSS part 2', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('img style with expression', () => {
+ const input = `exp/*//*");\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('img style with expression', () => {
+ const input = `exp/* //*");\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('style tag', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('style tag using background-image', () => {
+ const input = ` `;
+ const expected = ` `;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('style tag using background', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Anonymous HTML with style attribute', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('Local htc file', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('US-ASCII encoding', () => {
+ const input = `¼script¾alert(¢XSS¢)¼/script¾`;
+ const expected = `¼script¾alert(¢XSS¢)¼/script¾
\n`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('meta', () => {
+ const input = ` `;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('iframe', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('iframe Event based', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('frame', () => {
+ const input = ` `;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('TABLE', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('TD', () => {
+ const input = ``;
+ const expected = ``;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('DIV background-image', () => {
+ const input = ``;
+ const expected = `
`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('DIV background-image with unicoded XSS exploit', () => {
+ const input = `
`;
+ const expected = `
`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('DIV background-image with unicoded XSS exploit 2', () => {
+ const input = `
`;
+ const expected = `
`;
+
+ expect(transformYfm(input)).toBe(expected);
+ });
+
+ it('DIV expression', () => {
+ const input = `