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 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 _mdContainer = require("./utils/md-container");
16
+ var _markdownIt = _interopRequireDefault(require("markdown-it"));
19
17
 
20
- var _mdRendererFence = require("./utils/md-renderer-fence");
18
+ var _sanitizer = require("./sanitizer");
21
19
 
22
- var _mdLinkAttributes = require("./utils/md-link-attributes");
23
-
24
- var _mdLinkifyToCard = require("./utils/md-linkify-to-card");
20
+ var _markdownItImsize = _interopRequireDefault(require("@steelydylan/markdown-it-imsize"));
25
21
 
26
- var _mdKatex = require("./utils/md-katex");
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 _markdownItImsize = _interopRequireDefault(require("@steelydylan/markdown-it-imsize"));
30
+ var _mdKatex = require("./utils/md-katex");
33
31
 
34
- var _markdownItAnchor = _interopRequireDefault(require("markdown-it-anchor"));
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' + '<div class="footnotes-title">脚注</div>\n' + '<ol class="footnotes-list">\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;
@@ -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 sanitaizeEmbedToken(str) {
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 `<div class="embed-youtube"><iframe src="https://www.youtube.com/embed/${escapedVideoId}?loop=1&playlist=${escapedVideoId}${startQuery}" allowfullscreen loading="lazy"></iframe></div>`;
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 `<div class="zenn-embedded zenn-embedded-${encodedType}"><iframe id="${id}" src="${iframeSrc}" data-content="${encodedSrc}" frameborder="0" scrolling="no" loading="lazy"></iframe></div>`;
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 `<div class="embed-slideshare"><iframe src="https://www.slideshare.net/slideshow/embed_code/key/${(0, _utils.escapeHtml)(key)}" scrolling="no" allowfullscreen loading="lazy"></iframe></div>`;
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 `<div class="embed-speakerdeck"><iframe src="https://speakerdeck.com/player/${(0, _utils.escapeHtml)(key)}" scrolling="no" allowfullscreen allow="encrypted-media" loading="lazy"></iframe></div>`;
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 `<div class="embed-jsfiddle"><iframe src="${sanitaizeEmbedToken(url)}" scrolling="no" frameborder="no" loading="lazy"></iframe></div>`;
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 `<div class="embed-codepen"><iframe src="${sanitaizeEmbedToken(url.toString())}" scrolling="no" frameborder="no" loading="lazy"></iframe></div>`;
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 `<div class="embed-codesandbox"><iframe src="${sanitaizeEmbedToken(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></div>`;
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 `<div class="embed-stackblitz"><iframe src="${sanitaizeEmbedToken(str)}" scrolling="no" frameborder="no" loading="lazy"></iframe></div>`;
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 `<div class="embed-blueprintue"><iframe src="${sanitaizeEmbedToken(str)}" width="100%" style="aspect-ratio: 16/9" scrolling="no" frameborder="no" loading="lazy" allowfullscreen></iframe></div>`;
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 `<div class="embed-figma"><iframe src="https://www.figma.com/embed?embed_host=zenn&url=${sanitaizeEmbedToken(str)}" width="100%" style="aspect-ratio: 16/9" scrolling="no" frameborder="no" loading="lazy" allowfullscreen></iframe></div>`;
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.127",
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": "1f58a345bc4698c9d69a57e447bf61ebee1adeb8",
58
+ "gitHead": "021f59280b081b53781f367d4568a052842231e2",
57
59
  "publishConfig": {
58
60
  "access": "public"
59
61
  }