zenn-markdown-html 0.1.127 → 0.1.128-alpha.0
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/README.md +4 -0
- package/lib/index.js +15 -13
- package/lib/markdown-to-simple-html.js +3 -1
- package/lib/sanitizer.d.ts +1 -0
- package/lib/sanitizer.js +67 -0
- package/lib/utils/embed-helper.js +11 -11
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -32,6 +32,10 @@ const html = markdownToSimpleHtml(markdown);
|
|
|
32
32
|
|
|
33
33
|
## 開発者向けのドキュメント
|
|
34
34
|
|
|
35
|
+
### サニタイズ方針
|
|
36
|
+
|
|
37
|
+
[sanitize-html](https://github.com/apostrophecms/sanitize-html)を利用しています。サポートしているマークダウン記法から利用できるHTMLタグと属性を指定してサニタイズしています。網羅しているつもりですが、もし抜け漏れがあったらISSUEで報告してください。
|
|
38
|
+
|
|
35
39
|
### Babel の使用について
|
|
36
40
|
|
|
37
41
|
`zenn-markdown-html` では、PrismJS の言語プラグインを予め全て読み込むために `babel-plugin-prismjs` を使用しているため、ソースコードのビルドには `babel` を使用し、型ファイル(\*.d.ts)のビルドには `tsc` を使用してビルドしています。
|
package/lib/index.js
CHANGED
|
@@ -11,27 +11,29 @@ Object.defineProperty(exports, "markdownToSimpleHtml", {
|
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
var _markdownIt = _interopRequireDefault(require("markdown-it"));
|
|
15
|
-
|
|
16
14
|
var _crypto = _interopRequireDefault(require("crypto"));
|
|
17
15
|
|
|
18
|
-
var
|
|
16
|
+
var _markdownIt = _interopRequireDefault(require("markdown-it"));
|
|
19
17
|
|
|
20
|
-
var
|
|
18
|
+
var _sanitizer = require("./sanitizer");
|
|
21
19
|
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
var _mdLinkifyToCard = require("./utils/md-linkify-to-card");
|
|
20
|
+
var _markdownItImsize = _interopRequireDefault(require("@steelydylan/markdown-it-imsize"));
|
|
25
21
|
|
|
26
|
-
var
|
|
22
|
+
var _markdownItAnchor = _interopRequireDefault(require("markdown-it-anchor"));
|
|
27
23
|
|
|
28
24
|
var _mdBr = require("./utils/md-br");
|
|
29
25
|
|
|
26
|
+
var _mdContainer = require("./utils/md-container");
|
|
27
|
+
|
|
30
28
|
var _mdCustomBlock = require("./utils/md-custom-block");
|
|
31
29
|
|
|
32
|
-
var
|
|
30
|
+
var _mdKatex = require("./utils/md-katex");
|
|
33
31
|
|
|
34
|
-
var
|
|
32
|
+
var _mdLinkAttributes = require("./utils/md-link-attributes");
|
|
33
|
+
|
|
34
|
+
var _mdLinkifyToCard = require("./utils/md-linkify-to-card");
|
|
35
|
+
|
|
36
|
+
var _mdRendererFence = require("./utils/md-renderer-fence");
|
|
35
37
|
|
|
36
38
|
var _markdownToSimpleHtml = require("./markdown-to-simple-html");
|
|
37
39
|
|
|
@@ -65,7 +67,7 @@ md.use(_mdBr.mdBr).use(_mdRendererFence.mdRendererFence).use(_markdownItImsize.d
|
|
|
65
67
|
tabIndex: false
|
|
66
68
|
}).use(_mdKatex.mdKatex).use(_mdLinkifyToCard.mdLinkifyToCard); // custom footnote
|
|
67
69
|
|
|
68
|
-
md.renderer.rules.footnote_block_open = () => '<section class="footnotes">\n' + '<
|
|
70
|
+
md.renderer.rules.footnote_block_open = () => '<section class="footnotes">\n' + '<span class="footnotes-title">脚注</span>\n' + '<ol class="footnotes-list">\n';
|
|
69
71
|
|
|
70
72
|
const markdownToHtml = text => {
|
|
71
73
|
if (!(text && text.length)) return ''; // docIdは複数のコメントが1ページに指定されたときに脚注のリンク先が重複しないように指定する
|
|
@@ -75,9 +77,9 @@ const markdownToHtml = text => {
|
|
|
75
77
|
|
|
76
78
|
const docId = _crypto.default.randomBytes(2).toString('hex');
|
|
77
79
|
|
|
78
|
-
return md.render(text, {
|
|
80
|
+
return (0, _sanitizer.sanitize)(md.render(text, {
|
|
79
81
|
docId
|
|
80
|
-
});
|
|
82
|
+
}));
|
|
81
83
|
};
|
|
82
84
|
|
|
83
85
|
var _default = markdownToHtml;
|
|
@@ -9,6 +9,8 @@ var _markdownIt = _interopRequireDefault(require("markdown-it"));
|
|
|
9
9
|
|
|
10
10
|
var _mdLinkAttributes = require("./utils/md-link-attributes");
|
|
11
11
|
|
|
12
|
+
var _sanitizer = require("./sanitizer");
|
|
13
|
+
|
|
12
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
15
|
|
|
14
16
|
// preset 'zero' はデフォルトで全ての変換を無効化したプリセットです。
|
|
@@ -40,7 +42,7 @@ md.use(_mdLinkAttributes.mdLinkAttributes); // 限られた記法のみをHTML
|
|
|
40
42
|
|
|
41
43
|
const markdownToSimpleHtml = text => {
|
|
42
44
|
if (!(text && text.length)) return '';
|
|
43
|
-
return md.render(text);
|
|
45
|
+
return (0, _sanitizer.sanitize)(md.render(text));
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
exports.markdownToSimpleHtml = markdownToSimpleHtml;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sanitize: (html: string) => string;
|
package/lib/sanitizer.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.sanitize = void 0;
|
|
7
|
+
|
|
8
|
+
var _sanitizeHtml = _interopRequireDefault(require("sanitize-html"));
|
|
9
|
+
|
|
10
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
11
|
+
|
|
12
|
+
const tags = ['a', 'aside', 'blockquote', 'br', 'circle', 'code', 'details', 'div', 'em', 'embed-katex', 'eq', 'eqn', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'iframe', 'img', 'input', 'li', 'ol', 'p', 'pre', 's', 'section', 'span', 'strong', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'text', 'th', 'thead', 'tr', 'ul'];
|
|
13
|
+
const attributes = {
|
|
14
|
+
a: ['aria-hidden', 'class', 'href', 'id', 'rel', 'style', 'target', 'title'],
|
|
15
|
+
aside: ['class'],
|
|
16
|
+
blockquote: [],
|
|
17
|
+
br: ['style'],
|
|
18
|
+
circle: ['cx', 'cy', 'fill', 'r'],
|
|
19
|
+
code: ['class'],
|
|
20
|
+
details: [],
|
|
21
|
+
div: ['class'],
|
|
22
|
+
em: [],
|
|
23
|
+
'embed-katex': ['display-mode'],
|
|
24
|
+
eq: ['class'],
|
|
25
|
+
eqn: [],
|
|
26
|
+
h1: ['id'],
|
|
27
|
+
h2: ['id'],
|
|
28
|
+
h3: ['id'],
|
|
29
|
+
h4: ['id'],
|
|
30
|
+
h5: [],
|
|
31
|
+
h6: [],
|
|
32
|
+
hr: [],
|
|
33
|
+
iframe: ['allow', 'allowfullscreen', 'allowtransparency', 'data-content', 'frameborder', 'id', 'loading', 'sandbox', 'scrolling', 'src', 'style', 'width'],
|
|
34
|
+
img: ['alt', 'class', 'height', 'loading', 'src', 'title', 'width'],
|
|
35
|
+
input: ['checked', 'class', 'type'],
|
|
36
|
+
li: ['class', 'id'],
|
|
37
|
+
ol: ['class', 'start'],
|
|
38
|
+
p: [],
|
|
39
|
+
pre: ['class'],
|
|
40
|
+
s: [],
|
|
41
|
+
section: ['class'],
|
|
42
|
+
span: ['class', 'title'],
|
|
43
|
+
strong: [],
|
|
44
|
+
summary: [],
|
|
45
|
+
sup: ['class'],
|
|
46
|
+
// キャメルケースはすべて小文字に変換でなければ認識してくれないそうなので、念の為両方指定しておく
|
|
47
|
+
// refs: https://github.com/apostrophecms/sanitize-html/issues/489
|
|
48
|
+
svg: ['aria-label', 'class', 'role', 'viewBox', 'viewbox', 'xmlns'],
|
|
49
|
+
table: [],
|
|
50
|
+
tbody: [],
|
|
51
|
+
td: ['style'],
|
|
52
|
+
text: ['dominant-baseline', 'fill', 'font-size', 'font-weight', 'text-anchor', 'x', 'y'],
|
|
53
|
+
th: ['style'],
|
|
54
|
+
thead: [],
|
|
55
|
+
tr: [],
|
|
56
|
+
ul: ['class']
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const sanitize = html => (0, _sanitizeHtml.default)(html, {
|
|
60
|
+
allowedTags: tags,
|
|
61
|
+
allowedAttributes: attributes,
|
|
62
|
+
disallowedTagsMode: 'discard',
|
|
63
|
+
// from: default value https://github.com/apostrophecms/sanitize-html#default-options
|
|
64
|
+
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta']
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
exports.sanitize = sanitize;
|
|
@@ -45,7 +45,7 @@ const validateEmbedToken = (str, type) => {
|
|
|
45
45
|
|
|
46
46
|
exports.validateEmbedToken = validateEmbedToken;
|
|
47
47
|
|
|
48
|
-
function
|
|
48
|
+
function sanitizeEmbedToken(str) {
|
|
49
49
|
return str.replace(/"/g, '%22');
|
|
50
50
|
}
|
|
51
51
|
/** URL文字列か判定する */
|
|
@@ -67,7 +67,7 @@ function generateYoutubeHtmlFromVideoId(videoId, start) {
|
|
|
67
67
|
const time = Math.min(Number(start || 0), 48 * 60 * 60); // 48時間以内
|
|
68
68
|
|
|
69
69
|
const startQuery = time ? `&start=${time}` : '';
|
|
70
|
-
return `<
|
|
70
|
+
return `<span class="embed-block embed-youtube"><iframe src="https://www.youtube.com/embed/${escapedVideoId}?loop=1&playlist=${escapedVideoId}${startQuery}" allowfullscreen loading="lazy"></iframe></span>`;
|
|
71
71
|
}
|
|
72
72
|
/** Youtube の埋め込み要素の文字列を生成する */
|
|
73
73
|
|
|
@@ -90,7 +90,7 @@ function generateEmbedIframe(type, src) {
|
|
|
90
90
|
const encodedSrc = encodeURIComponent(src);
|
|
91
91
|
const id = `zenn-embedded__${Math.random().toString(16).slice(2)}`;
|
|
92
92
|
const iframeSrc = `https://embed.zenn.studio/${encodedType}#${id}`;
|
|
93
|
-
return `<
|
|
93
|
+
return `<span class="embed-block zenn-embedded zenn-embedded-${encodedType}"><iframe id="${id}" src="${iframeSrc}" data-content="${encodedSrc}" frameborder="0" scrolling="no" loading="lazy"></iframe></span>`;
|
|
94
94
|
}
|
|
95
95
|
/** 渡された文字列から埋め込み要素のHTMLを生成する関数をまとめたオブジェクト */
|
|
96
96
|
|
|
@@ -109,7 +109,7 @@ const embedGenerators = {
|
|
|
109
109
|
return 'Slide Shareのkeyが不正です';
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
return `<
|
|
112
|
+
return `<span class="embed-block embed-slideshare"><iframe src="https://www.slideshare.net/slideshow/embed_code/key/${(0, _utils.escapeHtml)(key)}" scrolling="no" allowfullscreen loading="lazy"></iframe></span>`;
|
|
113
113
|
},
|
|
114
114
|
|
|
115
115
|
speakerdeck(key) {
|
|
@@ -117,7 +117,7 @@ const embedGenerators = {
|
|
|
117
117
|
return 'Speaker Deckのkeyが不正です';
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
return `<
|
|
120
|
+
return `<span class="embed-block embed-speakerdeck"><iframe src="https://speakerdeck.com/player/${(0, _utils.escapeHtml)(key)}" scrolling="no" allowfullscreen allow="encrypted-media" loading="lazy"></iframe></span>`;
|
|
121
121
|
},
|
|
122
122
|
|
|
123
123
|
jsfiddle(str) {
|
|
@@ -133,7 +133,7 @@ const embedGenerators = {
|
|
|
133
133
|
url = url.endsWith('/') ? `${url}embedded/` : `${url}/embedded/`;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
return `<
|
|
136
|
+
return `<span class="embed-block embed-jsfiddle"><iframe src="${sanitizeEmbedToken(url)}" scrolling="no" frameborder="no" loading="lazy"></iframe></span>`;
|
|
137
137
|
},
|
|
138
138
|
|
|
139
139
|
codepen(str) {
|
|
@@ -143,7 +143,7 @@ const embedGenerators = {
|
|
|
143
143
|
|
|
144
144
|
const url = new URL(str.replace('/pen/', '/embed/'));
|
|
145
145
|
url.searchParams.set('embed-version', '2');
|
|
146
|
-
return `<
|
|
146
|
+
return `<span class="embed-block embed-codepen"><iframe src="${sanitizeEmbedToken(url.toString())}" scrolling="no" frameborder="no" loading="lazy"></iframe></span>`;
|
|
147
147
|
},
|
|
148
148
|
|
|
149
149
|
codesandbox(str) {
|
|
@@ -151,7 +151,7 @@ const embedGenerators = {
|
|
|
151
151
|
return '「https://codesandbox.io/embed/」から始まる正しいURLを入力してください';
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
return `<
|
|
154
|
+
return `<span class="embed-block embed-codesandbox"><iframe src="${sanitizeEmbedToken(str)}" style="width:100%;height:500px;border:none;overflow:hidden;" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" loading="lazy" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe></span>`;
|
|
155
155
|
},
|
|
156
156
|
|
|
157
157
|
stackblitz(str) {
|
|
@@ -159,7 +159,7 @@ const embedGenerators = {
|
|
|
159
159
|
return 'StackBlitzのembed用のURLを指定してください';
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
return `<
|
|
162
|
+
return `<span class="embed-block embed-stackblitz"><iframe src="${sanitizeEmbedToken(str)}" scrolling="no" frameborder="no" loading="lazy"></iframe></span>`;
|
|
163
163
|
},
|
|
164
164
|
|
|
165
165
|
tweet(str) {
|
|
@@ -169,12 +169,12 @@ const embedGenerators = {
|
|
|
169
169
|
|
|
170
170
|
blueprintue(str) {
|
|
171
171
|
if (!(0, _urlMatcher.isBlueprintUEUrl)(str)) return '「https://blueprintue.com/render/」から始まる正しいURLを指定してください';
|
|
172
|
-
return `<
|
|
172
|
+
return `<span class="embed-block embed-blueprintue"><iframe src="${sanitizeEmbedToken(str)}" width="100%" style="aspect-ratio: 16/9" scrolling="no" frameborder="no" loading="lazy" allowfullscreen></iframe></span>`;
|
|
173
173
|
},
|
|
174
174
|
|
|
175
175
|
figma(str) {
|
|
176
176
|
if (!(0, _urlMatcher.isFigmaUrl)(str)) return 'ファイルまたはプロトタイプのFigma URLを指定してください';
|
|
177
|
-
return `<
|
|
177
|
+
return `<span class="embed-block embed-figma"><iframe src="https://www.figma.com/embed?embed_host=zenn&url=${sanitizeEmbedToken(str)}" width="100%" style="aspect-ratio: 16/9" scrolling="no" frameborder="no" loading="lazy" allowfullscreen></iframe></span>`;
|
|
178
178
|
},
|
|
179
179
|
|
|
180
180
|
card(str) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zenn-markdown-html",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.128-alpha.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Convert markdown to zenn flavor html.",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@types/markdown-it": "^12.2.3",
|
|
31
31
|
"@types/node": "^14.0.5",
|
|
32
32
|
"@types/prismjs": "^1.26.0",
|
|
33
|
+
"@types/sanitize-html": "^2.6.2",
|
|
33
34
|
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
|
34
35
|
"@typescript-eslint/parser": "^4.23.0",
|
|
35
36
|
"babel-jest": "^28.1.1",
|
|
@@ -51,9 +52,10 @@
|
|
|
51
52
|
"markdown-it-inline-comments": "^1.0.1",
|
|
52
53
|
"markdown-it-link-attributes": "^4.0.1",
|
|
53
54
|
"markdown-it-task-lists": "^2.1.1",
|
|
54
|
-
"prismjs": "^1.27.0"
|
|
55
|
+
"prismjs": "^1.27.0",
|
|
56
|
+
"sanitize-html": "^2.7.2"
|
|
55
57
|
},
|
|
56
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "021f59280b081b53781f367d4568a052842231e2",
|
|
57
59
|
"publishConfig": {
|
|
58
60
|
"access": "public"
|
|
59
61
|
}
|