Merge branch 'rewriting-version2.0' into improvements

# Conflicts:
#	build/codex-editor.js
#	build/codex-editor.js.map
This commit is contained in:
Murod Khaydarov 2017-12-19 21:22:44 +03:00
commit 551ae9e381
13 changed files with 632 additions and 133 deletions

View file

@ -95,7 +95,10 @@ var Module = function () {
*
* @param {EditorConfig} config
*/
function Module(config) {
function Module() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
config = _ref.config;
_classCallCheck(this, Module);
if (new.target === Module) {
@ -244,6 +247,20 @@ var Util = function () {
return Array.prototype.slice.call(collection);
}
/**
* Checks if object is empty
*
* @param {Object} object
* @return {boolean}
*/
}, {
key: "isEmpty",
value: function isEmpty(object) {
return Object.keys(object).length === 0 && object.constructor === Object;
}
}]);
return Util;
@ -456,8 +473,10 @@ module.exports = exports['default'];
/**
* @typedef {Object} EditorConfig
* @property {String} holderId - Element to append Editor
* @property {Array} data - Blocks list in JSON-format
* ...
* @property {String} initialBlock - Tool name which will be initial
* @property {@link Tools#ToolsConfig} tools - list of tools linked to the constructor (function)
* @property {Object} toolsConfig - list of configurations
* @property {Array} data - Blocks list in JSON-format
*/
@ -815,6 +834,7 @@ var map = {
"./blockManager.js": 5,
"./events.js": 7,
"./renderer.js": 8,
"./sanitizer.js": 9,
"./toolbar.js": 11,
"./tools.js": 12,
"./ui.js": 13
@ -862,9 +882,7 @@ var BlockManager = function () {
*
* @param {EditorConfig} config
*/
function BlockManager(_ref) {
var config = _ref.config;
function BlockManager(config) {
_classCallCheck(this, BlockManager);
this.config = config;
@ -1415,10 +1433,10 @@ var Events = function (_Module) {
/**
* @constructor
*/
function Events() {
function Events(config) {
_classCallCheck(this, Events);
var _this = _possibleConstructorReturn(this, (Events.__proto__ || Object.getPrototypeOf(Events)).call(this));
var _this = _possibleConstructorReturn(this, (Events.__proto__ || Object.getPrototypeOf(Events)).call(this, config));
_this.subscribers = {};
@ -1514,7 +1532,6 @@ var Renderer = function (_Module) {
/**
* @constructor
*
* @param {EditorConfig} config
*/
function Renderer(config) {
@ -1524,10 +1541,34 @@ var Renderer = function (_Module) {
}
/**
* @typedef {Object} RendererItems
* @property {String} type - tool name
* @property {Object} data - tool data
*/
/**
* @example
*
* items: [
* {
* type : 'paragraph',
* data : {
* text : 'Hello from Codex!'
* }
* },
* {
* type : 'paragraph',
* data : {
* text : 'Leave feedback if you like it!'
* }
* },
* ]
*
*/
/**
* Make plugin blocks from array of plugin`s data
*
* @param {Object[]} items
* @param {RendererItems[]} items
*/
@ -1784,8 +1825,372 @@ module.exports = exports["default"];
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(0), __webpack_require__(1)))
/***/ }),
/* 9 */,
/* 10 */,
/* 9 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
/* WEBPACK VAR INJECTION */(function(Module, _) {
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
/**
* CodeX Sanitizer
*
* @module Sanitizer
* Clears HTML from taint tags
*
* @version 2.0.0
*
* @example
* Module can be used within two ways:
* 1) When you have an instance
* - this.Editor.Sanitizer.clean(yourTaintString);
* 2) As static method
* - CodexEditor.Sanitizer.clean(yourTaintString, yourCustomConfiguration);
*
* {@link SanitizerConfig}
*/
/**
* @typedef {Object} SanitizerConfig
* @property {Object} tags - define tags restrictions
*
* @example
*
* tags : {
* p: true,
* a: {
* href: true,
* rel: "nofollow",
* target: "_blank"
* }
* }
*/
var Sanitizer = function (_Module) {
_inherits(Sanitizer, _Module);
/**
* Initializes Sanitizer module
* Sets default configuration if custom not exists
*
* @property {SanitizerConfig} this.defaultConfig
* @property {HTMLJanitor} this._sanitizerInstance - Sanitizer library
*
* @param {SanitizerConfig} config
*/
function Sanitizer(config) {
_classCallCheck(this, Sanitizer);
// default config
var _this = _possibleConstructorReturn(this, (Sanitizer.__proto__ || Object.getPrototypeOf(Sanitizer)).call(this, config));
_this.defaultConfig = null;
_this._sanitizerInstance = null;
/** Custom configuration */
_this.sanitizerConfig = config.settings ? config.settings.sanitizer : {};
/** HTML Janitor library */
_this.sanitizerInstance = __webpack_require__(10);
return _this;
}
/**
* If developer uses editor's API, then he can customize sanitize restrictions.
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration
*
* @uses https://www.npmjs.com/package/html-janitor
*
* @param {HTMLJanitor} library - sanitizer extension
*/
_createClass(Sanitizer, [{
key: 'clean',
/**
* Cleans string from unwanted tags
* @param {String} taintString - HTML string
*
* @return {String} clean HTML
*/
value: function clean(taintString) {
return this._sanitizerInstance.clean(taintString);
}
/**
* Cleans string from unwanted tags
* @static
*
* Method allows to use default config
*
* @param {String} taintString - taint string
* @param {SanitizerConfig} customConfig - allowed tags
*
* @return {String} clean HTML
*/
}, {
key: 'sanitizerInstance',
set: function set(library) {
this._sanitizerInstance = new library(this.defaultConfig);
}
/**
* Sets sanitizer configuration. Uses default config if user didn't pass the restriction
* @param {SanitizerConfig} config
*/
}, {
key: 'sanitizerConfig',
set: function set(config) {
if (_.isEmpty(config)) {
this.defaultConfig = {
tags: {
p: {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
}
}
};
} else {
this.defaultConfig = config;
}
}
}], [{
key: 'clean',
value: function clean(taintString, customConfig) {
var newInstance = Sanitizer(customConfig);
return newInstance.clean(taintString);
}
}]);
return Sanitizer;
}(Module);
Sanitizer.displayName = 'Sanitizer';
exports.default = Sanitizer;
module.exports = exports['default'];
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(0), __webpack_require__(1)))
/***/ }),
/* 10 */
/***/ (function(module, exports, __webpack_require__) {
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (root, factory) {
if (true) {
!(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :
__WEBPACK_AMD_DEFINE_FACTORY__),
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.HTMLJanitor = factory();
}
}(this, function () {
/**
* @param {Object} config.tags Dictionary of allowed tags.
* @param {boolean} config.keepNestedBlockElements Default false.
*/
function HTMLJanitor(config) {
var tagDefinitions = config['tags'];
var tags = Object.keys(tagDefinitions);
var validConfigValues = tags
.map(function(k) { return typeof tagDefinitions[k]; })
.every(function(type) { return type === 'object' || type === 'boolean' || type === 'function'; });
if(!validConfigValues) {
throw new Error("The configuration was invalid");
}
this.config = config;
}
// TODO: not exhaustive?
var blockElementNames = ['P', 'LI', 'TD', 'TH', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'PRE'];
function isBlockElement(node) {
return blockElementNames.indexOf(node.nodeName) !== -1;
}
var inlineElementNames = ['A', 'B', 'STRONG', 'I', 'EM', 'SUB', 'SUP', 'U', 'STRIKE'];
function isInlineElement(node) {
return inlineElementNames.indexOf(node.nodeName) !== -1;
}
HTMLJanitor.prototype.clean = function (html) {
var sandbox = document.createElement('div');
sandbox.innerHTML = html;
this._sanitize(sandbox);
return sandbox.innerHTML;
};
HTMLJanitor.prototype._sanitize = function (parentNode) {
var treeWalker = createTreeWalker(parentNode);
var node = treeWalker.firstChild();
if (!node) { return; }
do {
// Ignore nodes that have already been sanitized
if (node._sanitized) {
continue;
}
if (node.nodeType === Node.TEXT_NODE) {
// If this text node is just whitespace and the previous or next element
// sibling is a block element, remove it
// N.B.: This heuristic could change. Very specific to a bug with
// `contenteditable` in Firefox: http://jsbin.com/EyuKase/1/edit?js,output
// FIXME: make this an option?
if (node.data.trim() === ''
&& ((node.previousElementSibling && isBlockElement(node.previousElementSibling))
|| (node.nextElementSibling && isBlockElement(node.nextElementSibling)))) {
parentNode.removeChild(node);
this._sanitize(parentNode);
break;
} else {
continue;
}
}
// Remove all comments
if (node.nodeType === Node.COMMENT_NODE) {
parentNode.removeChild(node);
this._sanitize(parentNode);
break;
}
var isInline = isInlineElement(node);
var containsBlockElement;
if (isInline) {
containsBlockElement = Array.prototype.some.call(node.childNodes, isBlockElement);
}
// Block elements should not be nested (e.g. <li><p>...); if
// they are, we want to unwrap the inner block element.
var isNotTopContainer = !! parentNode.parentNode;
var isNestedBlockElement =
isBlockElement(parentNode) &&
isBlockElement(node) &&
isNotTopContainer;
var nodeName = node.nodeName.toLowerCase();
var allowedAttrs = getAllowedAttrs(this.config, nodeName, node);
var isInvalid = isInline && containsBlockElement;
// Drop tag entirely according to the whitelist *and* if the markup
// is invalid.
if (isInvalid || shouldRejectNode(node, allowedAttrs)
|| (!this.config.keepNestedBlockElements && isNestedBlockElement)) {
// Do not keep the inner text of SCRIPT/STYLE elements.
if (! (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE')) {
while (node.childNodes.length > 0) {
parentNode.insertBefore(node.childNodes[0], node);
}
}
parentNode.removeChild(node);
this._sanitize(parentNode);
break;
}
// Sanitize attributes
for (var a = 0; a < node.attributes.length; a += 1) {
var attr = node.attributes[a];
if (shouldRejectAttr(attr, allowedAttrs, node)) {
node.removeAttribute(attr.name);
// Shift the array to continue looping.
a = a - 1;
}
}
// Sanitize children
this._sanitize(node);
// Mark node as sanitized so it's ignored in future runs
node._sanitized = true;
} while ((node = treeWalker.nextSibling()));
};
function createTreeWalker(node) {
return document.createTreeWalker(node,
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT,
null, false);
}
function getAllowedAttrs(config, nodeName, node){
if (typeof config.tags[nodeName] === 'function') {
return config.tags[nodeName](node);
} else {
return config.tags[nodeName];
}
}
function shouldRejectNode(node, allowedAttrs){
if (typeof allowedAttrs === 'undefined') {
return true;
} else if (typeof allowedAttrs === 'boolean') {
return !allowedAttrs;
}
return false;
}
function shouldRejectAttr(attr, allowedAttrs, node){
var attrName = attr.name.toLowerCase();
if (allowedAttrs === true){
return false;
} else if (typeof allowedAttrs[attrName] === 'function'){
return !allowedAttrs[attrName](attr.value, node);
} else if (typeof allowedAttrs[attrName] === 'undefined'){
return true;
} else if (allowedAttrs[attrName] === false) {
return true;
} else if (typeof allowedAttrs[attrName] === 'string') {
return (allowedAttrs[attrName] !== attr.value);
}
return false;
}
return HTMLJanitor;
}));
/***/ }),
/* 11 */
/***/ (function(module, exports, __webpack_require__) {
@ -2041,8 +2446,6 @@ function _inherits(subClass, superClass) { if (typeof superClass !== "function"
*/
/**
* @todo update according to current API
*
* @typedef {Object} Tool
* @property render
* @property save
@ -2114,9 +2517,7 @@ var Tools = function (_Module) {
}]);
function Tools(_ref) {
var config = _ref.config;
function Tools(config) {
_classCallCheck(this, Tools);
var _this = _possibleConstructorReturn(this, (Tools.__proto__ || Object.getPrototypeOf(Tools)).call(this, config));
@ -2254,6 +2655,11 @@ var Tools = function (_Module) {
var plugin = this.toolClasses[tool],
config = this.config.toolsConfig[tool];
if (!config) {
config = this.defaultConfig;
}
var instance = new plugin(data, config);
return instance;
@ -2352,9 +2758,7 @@ var UI = function (_Module) {
*
* @param {EditorConfig} config
*/
function UI(_ref) {
var config = _ref.config;
function UI(config) {
_classCallCheck(this, UI);
var _this = _possibleConstructorReturn(this, (UI.__proto__ || Object.getPrototypeOf(UI)).call(this, config));

File diff suppressed because one or more lines are too long

34
package-lock.json generated
View file

@ -2986,6 +2986,14 @@
}
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"string-width": {
"version": "1.0.2",
"bundled": true,
@ -2996,14 +3004,6 @@
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"stringstream": {
"version": "0.0.5",
"bundled": true,
@ -7994,6 +7994,15 @@
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"dev": true
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@ -8021,15 +8030,6 @@
}
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",

View file

@ -46,8 +46,10 @@
/**
* @typedef {Object} EditorConfig
* @property {String} holderId - Element to append Editor
* @property {Array} data - Blocks list in JSON-format
* ...
* @property {String} initialBlock - Tool name which will be initial
* @property {@link Tools#ToolsConfig} tools - list of tools linked to the constructor (function)
* @property {Object} toolsConfig - list of configurations
* @property {Array} data - Blocks list in JSON-format
*/
'use strict';

View file

@ -14,7 +14,7 @@ export default class Module {
*
* @param {EditorConfig} config
*/
constructor(config) {
constructor({ config } = {}) {
if (new.target === Module) {

View file

@ -1,80 +0,0 @@
/**
* Codex Sanitizer
*/
module.exports = (function (sanitizer) {
/** HTML Janitor library */
let janitor = require('html-janitor');
/** Codex Editor */
let editor = codex.editor;
sanitizer.prepare = function () {
if (editor.settings.sanitizer && !editor.core.isEmpty(editor.settings.sanitizer)) {
Config.CUSTOM = editor.settings.sanitizer;
}
};
/**
* Basic config
*/
var Config = {
/** User configuration */
CUSTOM : null,
BASIC : {
tags: {
p: {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
}
}
}
};
sanitizer.Config = Config;
/**
*
* @param userCustomConfig
* @returns {*}
* @private
*
* @description If developer uses editor's API, then he can customize sane restrictions.
* Or, sane config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses BASIC Default configation
*/
let init_ = function (userCustomConfig) {
let configuration = userCustomConfig || Config.CUSTOM || Config.BASIC;
return new janitor(configuration);
};
/**
* Cleans string from unwanted tags
* @protected
* @param {String} dirtyString - taint string
* @param {Object} customConfig - allowed tags
*/
sanitizer.clean = function (dirtyString, customConfig) {
let janitorInstance = init_(customConfig);
return janitorInstance.clean(dirtyString);
};
return sanitizer;
})({});

View file

@ -12,7 +12,7 @@ class BlockManager {
*
* @param {EditorConfig} config
*/
constructor({ config }) {
constructor(config) {
this.config = config;
this.Editor = null;

View file

@ -15,10 +15,9 @@ export default class Events extends Module {
/**
* @constructor
*/
constructor() {
super();
constructor(config) {
super(config);
this.subscribers = {};
}

View file

@ -10,7 +10,6 @@ export default class Renderer extends Module {
/**
* @constructor
*
* @param {EditorConfig} config
*/
constructor(config) {
@ -20,10 +19,34 @@ export default class Renderer extends Module {
}
/**
* @typedef {Object} RendererItems
* @property {String} type - tool name
* @property {Object} data - tool data
*/
/**
* @example
*
* items: [
* {
* type : 'paragraph',
* data : {
* text : 'Hello from Codex!'
* }
* },
* {
* type : 'paragraph',
* data : {
* text : 'Leave feedback if you like it!'
* }
* },
* ]
*
*/
/**
* Make plugin blocks from array of plugin`s data
*
* @param {Object[]} items
* @param {RendererItems[]} items
*/
render(items) {

View file

@ -0,0 +1,135 @@
/**
* CodeX Sanitizer
*
* @module Sanitizer
* Clears HTML from taint tags
*
* @version 2.0.0
*
* @example
* Module can be used within two ways:
* 1) When you have an instance
* - this.Editor.Sanitizer.clean(yourTaintString);
* 2) As static method
* - CodexEditor.Sanitizer.clean(yourTaintString, yourCustomConfiguration);
*
* {@link SanitizerConfig}
*/
/**
* @typedef {Object} SanitizerConfig
* @property {Object} tags - define tags restrictions
*
* @example
*
* tags : {
* p: true,
* a: {
* href: true,
* rel: "nofollow",
* target: "_blank"
* }
* }
*/
export default class Sanitizer extends Module {
/**
* Initializes Sanitizer module
* Sets default configuration if custom not exists
*
* @property {SanitizerConfig} this.defaultConfig
* @property {HTMLJanitor} this._sanitizerInstance - Sanitizer library
*
* @param {SanitizerConfig} config
*/
constructor(config) {
super(config);
// default config
this.defaultConfig = null;
this._sanitizerInstance = null;
/** Custom configuration */
this.sanitizerConfig = config.settings ? config.settings.sanitizer : {};
/** HTML Janitor library */
this.sanitizerInstance = require('html-janitor');
}
/**
* If developer uses editor's API, then he can customize sanitize restrictions.
* Or, sanitizing config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses Default configuration
*
* @uses https://www.npmjs.com/package/html-janitor
*
* @param {HTMLJanitor} library - sanitizer extension
*/
set sanitizerInstance(library) {
this._sanitizerInstance = new library(this.defaultConfig);
}
/**
* Sets sanitizer configuration. Uses default config if user didn't pass the restriction
* @param {SanitizerConfig} config
*/
set sanitizerConfig(config) {
if (_.isEmpty(config)) {
this.defaultConfig = {
tags: {
p: {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
}
}
};
} else {
this.defaultConfig = config;
}
}
/**
* Cleans string from unwanted tags
* @param {String} taintString - HTML string
*
* @return {String} clean HTML
*/
clean(taintString) {
return this._sanitizerInstance.clean(taintString);
}
/**
* Cleans string from unwanted tags
* @static
*
* Method allows to use default config
*
* @param {String} taintString - taint string
* @param {SanitizerConfig} customConfig - allowed tags
*
* @return {String} clean HTML
*/
static clean(taintString, customConfig) {
let newInstance = Sanitizer(customConfig);
return newInstance.clean(taintString);
}
}

View file

@ -15,8 +15,6 @@
*/
/**
* @todo update according to current API
*
* @typedef {Object} Tool
* @property render
* @property save
@ -77,7 +75,7 @@ export default class Tools extends Module {
*
* @param {ToolsConfig} config
*/
constructor({ config }) {
constructor(config) {
super(config);
@ -205,6 +203,12 @@ export default class Tools extends Module {
let plugin = this.toolClasses[tool],
config = this.config.toolsConfig[tool];
if (!config) {
config = this.defaultConfig;
}
let instance = new plugin(data, config);
return instance;

View file

@ -62,7 +62,7 @@ export default class UI extends Module {
*
* @param {EditorConfig} config
*/
constructor({ config }) {
constructor(config) {
super(config);

View file

@ -97,4 +97,16 @@ export default class Util {
}
/**
* Checks if object is empty
*
* @param {Object} object
* @return {boolean}
*/
static isEmpty(object) {
return Object.keys(object).length === 0 && object.constructor === Object;
}
};