@happeouikit/content-renderer 1.0.48 → 3.0.1
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/CHANGELOG.md +19 -1
- package/dist/ContentRenderer.js +59 -42
- package/dist/htmlConverter.js +1 -0
- package/dist/utils.js +27 -15
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.0.1
|
|
4
|
+
|
|
5
|
+
- [Fixed] ContentRenderer should now deal with all kinds of xss
|
|
6
|
+
|
|
7
|
+
## 3.0.0
|
|
8
|
+
|
|
9
|
+
- [Fixed] the xss converter now accepts a whiteList for CSS properties that includes vertical-align
|
|
10
|
+
- [Updated] new dependencies updates: React 17 and and Styled Components 5
|
|
11
|
+
|
|
12
|
+
## 2.0.1
|
|
13
|
+
|
|
14
|
+
- [Added] Changed type to uppercase and to handle both upper and lowercase. Add warning if lowecase is used.
|
|
15
|
+
- [Added] Handle widgetType=`page` by removing forced styles from component.
|
|
16
|
+
|
|
17
|
+
## 2.0.0
|
|
18
|
+
|
|
19
|
+
- [Breaking change] Links rendered from markdown content now always open in a new tab (not configurable).
|
|
20
|
+
|
|
3
21
|
## 1.0.48
|
|
4
22
|
|
|
5
23
|
- [Fixed] Decode HTML entites causing crashes
|
|
6
24
|
|
|
7
25
|
## 1.0.46
|
|
8
26
|
|
|
9
|
-
- [Fixed] Use correct font-family for all the tags inside
|
|
27
|
+
- [Fixed] Use correct font-family for all the tags inside paragraphs and list elements etc.
|
|
10
28
|
|
|
11
29
|
## 1.0.45
|
|
12
30
|
|
package/dist/ContentRenderer.js
CHANGED
|
@@ -59,7 +59,7 @@ function _templateObject() {
|
|
|
59
59
|
* ContentRenderer
|
|
60
60
|
*
|
|
61
61
|
*/
|
|
62
|
-
import React, { useRef, useEffect, useState, useCallback } from "react";
|
|
62
|
+
import React, { useRef, useEffect, useState, useCallback, useMemo } from "react";
|
|
63
63
|
import ReactDOM from "react-dom";
|
|
64
64
|
import PropTypes from "prop-types";
|
|
65
65
|
import styled, { css } from "styled-components";
|
|
@@ -68,8 +68,11 @@ import { CopyToClipboardButton } from "@happeouikit/copy-to-clipboard-button";
|
|
|
68
68
|
import { BodyUI, sansFamily } from "@happeouikit/typography";
|
|
69
69
|
import { margin200, media, padding200 } from "@happeouikit/layout";
|
|
70
70
|
import { active, gray04, gray07, gray09, lighten, warn } from "@happeouikit/colors";
|
|
71
|
-
import { toHtml,
|
|
71
|
+
import { toHtml, loadCodeFont, decodeHtmlEntities, removeSpecialCharacters, toSafeText } from "./utils";
|
|
72
72
|
import { ELEMENT_TYPE_PRE, ELEMENT_TYPE_CODE, HIGHLIGHJS_CSS, HIGHLIGHJS_JS, CODE_BLOCK_AVAILABLE_LANGUAGES } from "./constants";
|
|
73
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
74
|
+
import { Fragment as _Fragment } from "react/jsx-runtime";
|
|
75
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
73
76
|
|
|
74
77
|
var ContentRenderer = function ContentRenderer(_ref) {
|
|
75
78
|
var content = _ref.content,
|
|
@@ -172,45 +175,47 @@ var ContentRenderer = function ContentRenderer(_ref) {
|
|
|
172
175
|
scrollAndHighlightHash();
|
|
173
176
|
}
|
|
174
177
|
}, [element, createAnchors, useHashAutoscroll, scrollAndHighlightHash]);
|
|
175
|
-
var
|
|
178
|
+
var safeText = useMemo(function () {
|
|
179
|
+
var lowercaseType = type && typeof type === "string" ? type.toLowerCase() : "html";
|
|
180
|
+
var transformedContent = lowercaseType === "text" ? toSafeText("<p>".concat(content, "</p>")) : toHtml(content, lowercaseType);
|
|
176
181
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (decodeHtmlEntitiesOnRender) {
|
|
182
|
-
transformedContent = decodeHtmlEntities(transformedContent);
|
|
183
|
-
}
|
|
182
|
+
if (decodeHtmlEntitiesOnRender) {
|
|
183
|
+
transformedContent = decodeHtmlEntities(transformedContent);
|
|
184
|
+
}
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
return transformedContent;
|
|
187
|
+
}, [content, type, decodeHtmlEntitiesOnRender]); // Use dangerouslySetInnerHTML as there might be nested <p>'s header etc
|
|
186
188
|
// This requires that all content is run through the toSafeText filter
|
|
187
189
|
// before displaying. This will run it through xss filter.
|
|
188
190
|
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
191
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
192
|
+
children: [/*#__PURE__*/_jsx(Wrapper, {
|
|
193
|
+
className: "content-renderer",
|
|
194
|
+
ref: element,
|
|
195
|
+
widgetType: widgetType,
|
|
196
|
+
headerFont: headerFont,
|
|
197
|
+
bodyFont: bodyFont,
|
|
198
|
+
createAnchors: createAnchors,
|
|
199
|
+
dangerouslySetInnerHTML: {
|
|
200
|
+
__html: safeText
|
|
201
|
+
}
|
|
202
|
+
}), copyBtns.map(function (el) {
|
|
203
|
+
var clipboardContent = el.textContent || "";
|
|
204
|
+
|
|
205
|
+
if (el.tagName !== ELEMENT_TYPE_PRE) {
|
|
206
|
+
clipboardContent = "".concat(window.location.href.split("#")[0], "#").concat(el.id);
|
|
207
|
+
} // eslint-disable-next-line no-control-regex
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
clipboardContent = removeSpecialCharacters(clipboardContent);
|
|
211
|
+
return /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/_jsx(BtnContainer, {
|
|
212
|
+
className: "content-copy-btn",
|
|
213
|
+
children: /*#__PURE__*/_jsx(CopyToClipboardButton, {
|
|
214
|
+
clipboardContent: clipboardContent
|
|
215
|
+
})
|
|
216
|
+
}), el);
|
|
217
|
+
})]
|
|
218
|
+
});
|
|
214
219
|
};
|
|
215
220
|
|
|
216
221
|
var BtnContainer = styled.div.withConfig({
|
|
@@ -222,18 +227,30 @@ var Wrapper = styled(BodyUI).attrs({
|
|
|
222
227
|
}).withConfig({
|
|
223
228
|
displayName: "ContentRenderer__Wrapper",
|
|
224
229
|
componentId: "sc-1mleu9k-1"
|
|
225
|
-
})(["
|
|
230
|
+
})(["", " h1.highlight,h2.highlight,h3.highlight,h4.highlight,h5.highlight,h6.highlight{border-radius:6px;position:relative;:after{content:\" \";position:absolute;top:-8px;bottom:-8px;left:-8px;right:-8px;border-radius:6px;border:5px solid ", ";}}pre{border-radius:4px;background:", ";padding:8px;margin-bottom:10px;overflow:auto;white-space:pre;position:relative;overflow:visible;*:not(p){font-weight:inherit;font-family:\"IBM Plex Mono\",monospace !important;}.content-copy-btn{position:absolute;top:0;right:0;padding:", " 0;}:hover button{opacity:1;}}.mention{border-radius:0.2em;background-color:#fff5cc;border:1px solid #fff5cc;color:#0f1621;font-weight:400;&:hover{text-decoration:none;border:1px solid #fabd24;}}.extraMention{cursor:inherit;}", " ", ""], function (_ref2) {
|
|
226
231
|
var createAnchors = _ref2.createAnchors;
|
|
227
232
|
return createAnchors ? "\n h1, h2, h3, h4, h5, h6 {\n display: flex;\n align-items: center;\n :hover button {\n opacity: 0.4;\n }\n }\n " : "";
|
|
228
|
-
}, lighten(warn, 0.8),
|
|
229
|
-
var widgetType = _ref3.widgetType
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
}, lighten(warn, 0.8), gray09, padding200, function (_ref3) {
|
|
234
|
+
var widgetType = _ref3.widgetType;
|
|
235
|
+
return widgetType === "page" ? "" : "\n * {\n font-family: ".concat(sansFamily, " !important;\n }\n\n \n\n h1 {\n font-weight: 500;\n font-size: 24px;\n letter-spacing: -0.5px;\n line-height: 32px;\n + h1,\n + h2,\n + h3,\n + h4,\n + p {\n margin-top: 16px;\n }\n }\n h2 {\n font-weight: 500;\n font-size: 20px;\n line-height: 28px;\n + h1,\n + h2,\n + h3,\n + h4,\n + p {\n margin-top: 14px;\n }\n }\n h3 {\n font-weight: 500;\n font-size: 16px;\n line-height: 24px;\n + h1,\n + h2,\n + h3,\n + h4,\n + p {\n margin-top: 12px;\n }\n }\n h4 {\n font-size: 16px;\n line-height: 24px;\n + h1,\n + h2,\n + h3,\n + h4,\n + p {\n margin-top: 12px;\n }\n }\n p {\n font-size: 16px;\n line-height: 24px;\n letter-spacing: 0px;\n + h1,\n + h2,\n + h3,\n + h4,\n + p {\n margin-top: 12px;\n }\n }\n a {\n color: ").concat(active, ";\n text-decoration: none;\n :hover {\n text-decoration: underline;\n }\n }\n ul {\n list-style-type: disc;\n }\n ol {\n list-style-type: decimal;\n counter-reset: item;\n li {\n padding-left: 12px;\n text-indent: -33px;\n list-style-type: none;\n counter-increment: item;\n &:before {\n display: inline-block;\n width: 13px;\n padding-right: 15px;\n padding-left: 5px;\n font-weight: bold;\n text-align: right;\n content: counter(item) \".\";\n }\n }\n }\n ol,\n ul {\n font-size: 16px;\n line-height: 24px;\n margin: 18px 16px;\n display: block;\n -webkit-margin-before: 0.5em;\n -webkit-margin-after: 0.5em;\n -webkit-padding-start: 0.5em;\n li,\n li {\n margin-bottom: 5px;\n padding-left: 10px;\n > ul,\n > ol {\n -webkit-margin-before: 8px !important;\n -webkit-margin-after: 8px !important;\n }\n }\n\n &:first-child,\n &:first-child {\n -webkit-margin-before: 0;\n }\n &:last-child,\n &:last-child {\n -webkit-margin-after: 0;\n }\n }\n hr {\n -webkit-margin-before: 16px;\n -webkit-margin-after: 13px;\n border-color: @border-color;\n border-style: solid;\n height: 0px;\n border-width: 1px 0 0 0;\n }\n table {\n width: 100%;\n margin: 15px 0;\n text-align: left;\n thead th {\n background: @white;\n }\n th,\n td {\n border-width: 1px;\n border-style: solid;\n border-color: @border-color;\n padding: 0.5em;\n }\n tr:nth-child(even) {\n background: @almost-white;\n }\n tr:nth-child(odd) {\n background: @white;\n }\n }\n table.no-borders {\n th,\n td {\n border: none;\n }\n }\n table.padded-2 {\n th,\n td {\n padding: 2em;\n }\n }\n table.padded-1 {\n th,\n td {\n padding: 1em;\n }\n }\n img {\n max-width: 100%;\n margin: 15px 0;\n & + img {\n margin-top: 0;\n }\n }\n .xl-emoji * {\n font-size: 40px;\n line-height: 1.2;\n }\n \n blockquote {\n border-top: 2px solid ").concat(gray07, ";\n border-bottom: 2px solid ").concat(gray07, ";\n font-size: 18px;\n padding: 24px 48px;\n position: relative;\n text-align: center;\n margin: 48px 0;\n }\n ");
|
|
236
|
+
}, function (_ref4) {
|
|
237
|
+
var widgetType = _ref4.widgetType,
|
|
238
|
+
bodyFont = _ref4.bodyFont,
|
|
239
|
+
headerFont = _ref4.headerFont;
|
|
232
240
|
return widgetType === "article" && css(["h1,h2,h3{font-family:", " !important;& *{font-family:", " !important;}}h4,h5,h6,p,ul,ol,li,table{font-family:", " !important;& *{font-family:", " !important;}}h1{font-size:32px;line-height:1.13;margin:0 0 20px;letter-spacing:-0.75px;}h2{font-size:24px;line-height:1.17;margin:0 0 16px;letter-spacing:-0.5px;}h3{font-size:20px;line-height:1.2;margin:0 0 14px;}h4,h5,h6,p,ul,ol,li{font-size:18px;line-height:1.56;margin-bottom:13px;margin-top:0;}hr{border-color:", ";}table{width:100% !important;margin:15px 0;font-size:18px !important;text-align:left;border-collapse:collapse;border-spacing:0;thead th{background:@white;}th,td{border-width:1px;border-style:solid;border-color:", ";padding:0.5em;word-wrap:break-word;}tr:nth-child(odd){background:", ";}tr:nth-child(even){background:@white;}}table.no-borders{th,td{border:none;}tr:nth-child(odd){background:", ";}tr:nth-child(even){background:@white;}}pre{position:relative;background-color:", ";padding:1em;margin:1em 0;border-radius:4px;white-space:pre;}img,span.fr-img-caption,span.fr-video{display:block;margin:30px 0;", ";&.article-align-left{margin:30px auto;display:block;max-width:100%;", "}&.article-align-center{margin:30px auto;display:block;}&.article-align-right{margin:30px auto;display:block;max-width:100%;", "}&.article-align-overflow{margin:30px auto;display:block;max-width:100%;", " img{width:100%;}}}img{&.image-border-radius{border-radius:6px;}&.image-box-shadow{box-shadow:0 4px 16px 0 #bacad5;}}span.fr-img-caption{&.image-border-radius img{border-radius:6px;}&.image-box-shadow img{box-shadow:0 4px 16px 0 #bacad5;}}p{img:first-child,span.fr-img-caption:first-child{&.article-align-left,&.article-align-right{", "}}}span.fr-img-caption{text-align:center;img{margin:0;}.fr-inner{font-weight:400;font-size:16px;margin:8px auto 0 auto;padding:0 8px;color:", ";display:block;max-width:671px;}}span.fr-video{position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden;border-radius:6px;&.article-align-overflow{padding-top:170px;}iframe{position:absolute;top:0;left:0;width:100% !important;height:100% !important;}}blockquote{border-left:0 !important;padding-left:0 !important;margin:0 !important;color:inherit !important;p{line-height:30px;font-size:26px;text-align:center;margin:0 auto;}}.content-attachments{width:100%;}"], headerFont, headerFont, bodyFont, bodyFont, gray07, gray07, gray09, gray09, gray09, media.only.xs(_templateObject()), media.min.md(_templateObject2()), media.min.md(_templateObject3()), media.min.md(_templateObject4()), media.min.md(_templateObject5()), gray04);
|
|
233
241
|
});
|
|
234
242
|
ContentRenderer.propTypes = {
|
|
235
243
|
content: PropTypes.string,
|
|
236
|
-
type:
|
|
244
|
+
type: function type(props, propName) {
|
|
245
|
+
if (propName === "type" && props[propName] !== props[propName].toUpperCase()) {
|
|
246
|
+
// eslint-disable-next-line no-console
|
|
247
|
+
console.warn("Please use uppercase type 'HTML', 'MARKDOWN' or 'TEXT'");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (propName === "type" && !["HTML", "MARKDOWN", "TEXT"].includes(props[propName].toUpperCase())) {
|
|
251
|
+
throw new Error("Invalid type, use 'HTML', 'MARKDOWN' or 'TEXT'");
|
|
252
|
+
}
|
|
253
|
+
},
|
|
237
254
|
widgetType: PropTypes.oneOf(["post", "article", "page"]),
|
|
238
255
|
bodyFont: PropTypes.string,
|
|
239
256
|
headerFont: PropTypes.string,
|
package/dist/htmlConverter.js
CHANGED
package/dist/utils.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
|
|
3
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
|
4
|
+
|
|
5
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
6
|
+
|
|
1
7
|
import linkifyHtml from "linkifyjs/html";
|
|
2
8
|
import xss from "xss";
|
|
3
9
|
import { decode as decodeHtmlEntities } from "html-entities";
|
|
@@ -22,6 +28,26 @@ var processMentions = function processMentions() {
|
|
|
22
28
|
var str = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
|
|
23
29
|
return str.replace(mentioningRegexGroupCaseInsensitive, processMatch).replace(mentioningHtmlRegexGroupCaseInsensitive, processMatch);
|
|
24
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* @name toSafeText
|
|
33
|
+
* @description Returns filtered text with filterXSS library
|
|
34
|
+
* @author Antero Hanhirova
|
|
35
|
+
* @param {String} - Text to whitelist
|
|
36
|
+
* @returns {String}
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
var toSafeText = function toSafeText(text) {
|
|
41
|
+
return xss(text, {
|
|
42
|
+
whiteList: safeTagsAndAttributes,
|
|
43
|
+
css: {
|
|
44
|
+
whiteList: _objectSpread({}, xss.getDefaultCSSWhiteList(), {
|
|
45
|
+
"vertical-align": true
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
stripIgnoreTag: true
|
|
49
|
+
}).replace(/ /g, " ").replace(/\u2060/g, ""); // Replaces a hidden character that resulted in a "?"-character in backend
|
|
50
|
+
};
|
|
25
51
|
/**
|
|
26
52
|
* @name toHtml
|
|
27
53
|
* @description Converts markdown to html, adds space to end of String
|
|
@@ -44,26 +70,12 @@ var toHtml = function toHtml() {
|
|
|
44
70
|
newString = processMentions(newString);
|
|
45
71
|
}
|
|
46
72
|
|
|
73
|
+
newString = toSafeText(newString);
|
|
47
74
|
return linkifyHtml(newString, {
|
|
48
75
|
target: "_blank",
|
|
49
76
|
ignoreTags: ["pre", "code", "script", "style"]
|
|
50
77
|
});
|
|
51
78
|
};
|
|
52
|
-
/**
|
|
53
|
-
* @name toSafeText
|
|
54
|
-
* @description Returns filtered text with filterXSS library
|
|
55
|
-
* @author Antero Hanhirova
|
|
56
|
-
* @param {String} - Text to whitelist
|
|
57
|
-
* @returns {String}
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
var toSafeText = function toSafeText(text) {
|
|
62
|
-
return xss(text, {
|
|
63
|
-
whiteList: safeTagsAndAttributes,
|
|
64
|
-
stripIgnoreTag: true
|
|
65
|
-
}).replace(/ /g, " ").replace(/\u2060/g, ""); // Replaces a hidden character that resulted in a "?"-character in backend
|
|
66
|
-
};
|
|
67
79
|
|
|
68
80
|
var loadCodeFont = function loadCodeFont() {
|
|
69
81
|
var id = "ibmMonoOnDemand";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happeouikit/content-renderer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Rendering html and markdown content",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"prop-types": "^15.6.2",
|
|
32
|
-
"react": "^
|
|
33
|
-
"styled-components": "^
|
|
32
|
+
"react": "^17.0.2",
|
|
33
|
+
"styled-components": "^5.3.1"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|