angular.module('textAngular.factories', [])
.factory('taBrowserTag', [function(){
return function(tag){
/* istanbul ignore next: ie specific test */
if(!tag) return (_browserDetect.ie <= 8)? 'P' : 'p';
else if(tag === '') return (_browserDetect.ie === undefined)? 'div' : (_browserDetect.ie <= 8)? 'P' : 'p';
else return (_browserDetect.ie <= 8)? tag.toUpperCase() : tag;
};
}]).factory('taApplyCustomRenderers', ['taCustomRenderers', 'taDOM', function(taCustomRenderers, taDOM){
return function(val){
var element = angular.element('
');
element[0].innerHTML = val;
angular.forEach(taCustomRenderers, function(renderer){
var elements = [];
// get elements based on what is defined. If both defined do secondary filter in the forEach after using selector string
if(renderer.selector && renderer.selector !== '')
elements = element.find(renderer.selector);
/* istanbul ignore else: shouldn't fire, if it does we're ignoring everything */
else if(renderer.customAttribute && renderer.customAttribute !== '')
elements = taDOM.getByAttribute(element, renderer.customAttribute);
// process elements if any found
angular.forEach(elements, function(_element){
_element = angular.element(_element);
if(renderer.selector && renderer.selector !== '' && renderer.customAttribute && renderer.customAttribute !== ''){
if(_element.attr(renderer.customAttribute) !== undefined) renderer.renderLogic(_element);
} else renderer.renderLogic(_element);
});
});
return element[0].innerHTML;
};
}]).factory('taFixChrome', function(){
// get whaterever rubbish is inserted in chrome
// should be passed an html string, returns an html string
var taFixChrome = function(html){
if(!html || !angular.isString(html) || html.length <= 0) return html;
// grab all elements with a style attibute
var spanMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
var match, styleVal, newTag, finalHtml = '', lastIndex = 0;
while(match = spanMatch.exec(html)){
// one of the quoted values ' or "
/* istanbul ignore next: quotations match */
styleVal = match[3] || match[4];
// test for chrome inserted junk
if(styleVal && styleVal.match(/line-height: 1.[0-9]{3,12};|color: inherit; line-height: 1.1;/i)){
// replace original tag with new tag
styleVal = styleVal.replace(/( |)font-family: inherit;|( |)line-height: 1.[0-9]{3,12};|( |)color: inherit;/ig, '');
newTag = '<' + match[1].trim();
if(styleVal.trim().length > 0) newTag += ' style=' + match[2].substring(0,1) + styleVal + match[2].substring(0,1);
newTag += match[5].trim() + ">";
finalHtml += html.substring(lastIndex, match.index) + newTag;
lastIndex = match.index + match[0].length;
}
}
finalHtml += html.substring(lastIndex);
// only replace when something has changed, else we get focus problems on inserting lists
if(lastIndex > 0){
// replace all empty strings
return finalHtml.replace(/(.*?)<\/span>(
|)/ig, '$1');
} else return html;
};
return taFixChrome;
}).factory('taSanitize', ['$sanitize', function taSanitizeFactory($sanitize){
var convert_infos = [
{
property: 'font-weight',
values: [ 'bold' ],
tag: 'b'
},
{
property: 'font-style',
values: [ 'italic' ],
tag: 'i'
}
];
var styleMatch = [];
for(var i = 0; i < convert_infos.length; i++){
var _partialStyle = '(' + convert_infos[i].property + ':\\s*(';
for(var j = 0; j < convert_infos[i].values.length; j++){
/* istanbul ignore next: not needed to be tested yet */
if(j > 0) _partialStyle += '|';
_partialStyle += convert_infos[i].values[j];
}
_partialStyle += ');)';
styleMatch.push(_partialStyle);
}
var styleRegexString = '(' + styleMatch.join('|') + ')';
function wrapNested(html, wrapTag) {
var depth = 0;
var lastIndex = 0;
var match;
var tagRegex = /<[^>]*>/ig;
while(match = tagRegex.exec(html)){
lastIndex = match.index;
if(match[0].substr(1, 1) === '/'){
if(depth === 0) break;
else depth--;
}else depth++;
}
return wrapTag +
html.substring(0, lastIndex) +
// get the start tags reversed - this is safe as we construct the strings with no content except the tags
angular.element(wrapTag)[0].outerHTML.substring(wrapTag.length) +
html.substring(lastIndex);
}
function transformLegacyStyles(html){
if(!html || !angular.isString(html) || html.length <= 0) return html;
var i;
var styleElementMatch = /<([^>\/]+?)style=("([^"]+)"|'([^']+)')([^>]*)>/ig;
var match, subMatch, styleVal, newTag, lastNewTag = '', newHtml, finalHtml = '', lastIndex = 0;
while(match = styleElementMatch.exec(html)){
// one of the quoted values ' or "
/* istanbul ignore next: quotations match */
styleVal = match[3] || match[4];
var styleRegex = new RegExp(styleRegexString, 'i');
// test for style values to change
if(angular.isString(styleVal) && styleRegex.test(styleVal)){
// remove build tag list
newTag = '';
// init regex here for exec
var styleRegexExec = new RegExp(styleRegexString, 'ig');
// find relevand tags and build a string of them
while(subMatch = styleRegexExec.exec(styleVal)){
for(i = 0; i < convert_infos.length; i++){
if(!!subMatch[(i*2) + 2]){
newTag += '<' + convert_infos[i].tag + '>';
}
}
}
// recursively find more legacy styles in html before this tag and after the previous match (if any)
newHtml = transformLegacyStyles(html.substring(lastIndex, match.index));
// build up html
if(lastNewTag.length > 0){
finalHtml += wrapNested(newHtml, lastNewTag);
}else finalHtml += newHtml;
// grab the style val without the transformed values
styleVal = styleVal.replace(new RegExp(styleRegexString, 'ig'), '');
// build the html tag
finalHtml += '<' + match[1].trim();
if(styleVal.length > 0) finalHtml += ' style="' + styleVal + '"';
finalHtml += match[5] + '>';
// update the start index to after this tag
lastIndex = match.index + match[0].length;
lastNewTag = newTag;
}
}
if(lastNewTag.length > 0){
finalHtml += wrapNested(html.substring(lastIndex), lastNewTag);
}
else finalHtml += html.substring(lastIndex);
return finalHtml;
}
function transformLegacyAttributes(html){
if(!html || !angular.isString(html) || html.length <= 0) return html;
// replace all align='...' tags with text-align attributes
var attrElementMatch = /<([^>\/]+?)align=("([^"]+)"|'([^']+)')([^>]*)>/ig;
var match, finalHtml = '', lastIndex = 0;
// match all attr tags
while(match = attrElementMatch.exec(html)){
// add all html before this tag
finalHtml += html.substring(lastIndex, match.index);
// record last index after this tag
lastIndex = match.index + match[0].length;
// construct tag without the align attribute
var newTag = '<' + match[1] + match[5];
// add the style attribute
if(/style=("([^"]+)"|'([^']+)')/ig.test(newTag)){
/* istanbul ignore next: quotations match */
newTag = newTag.replace(/style=("([^"]+)"|'([^']+)')/i, 'style="$2$3 text-align:' + (match[3] || match[4]) + ';"');
}else{
/* istanbul ignore next: quotations match */
newTag += ' style="text-align:' + (match[3] || match[4]) + ';"';
}
newTag += '>';
// add to html
finalHtml += newTag;
}
// return with remaining html
return finalHtml + html.substring(lastIndex);
}
return function taSanitize(unsafe, oldsafe, ignore){
// unsafe html should NEVER built into a DOM object via angular.element. This allows XSS to be inserted and run.
if ( !ignore ) {
try {
unsafe = transformLegacyStyles(unsafe);
} catch (e) {
}
}
// unsafe and oldsafe should be valid HTML strings
// any exceptions (lets say, color for example) should be made here but with great care
// setup unsafe element for modification
unsafe = transformLegacyAttributes(unsafe);
var safe;
try {
safe = $sanitize(unsafe);
// do this afterwards, then the $sanitizer should still throw for bad markup
if(ignore) safe = unsafe;
} catch (e){
safe = oldsafe || '';
}
// Do processing for tags, removing tabs and return carriages outside of them
var _preTags = safe.match(/(]*>.*?<\/pre[^>]*>)/ig);
var processedSafe = safe.replace(/((9|10);)*/ig, '');
var re = /]*>.*?<\/pre[^>]*>/ig;
var index = 0;
var lastIndex = 0;
var origTag;
safe = '';
while((origTag = re.exec(processedSafe)) !== null && index < _preTags.length){
safe += processedSafe.substring(lastIndex, origTag.index) + _preTags[index];
lastIndex = origTag.index + origTag[0].length;
index++;
}
return safe + processedSafe.substring(lastIndex);
};
}]).factory('taToolExecuteAction', ['$q', '$log', function($q, $log){
// this must be called on a toolScope or instance
return function(editor){
if(editor !== undefined) this.$editor = function(){ return editor; };
var deferred = $q.defer(),
promise = deferred.promise,
_editor = this.$editor();
// pass into the action the deferred function and also the function to reload the current selection if rangy available
var result;
try{
result = this.action(deferred, _editor.startAction());
// We set the .finally callback here to make sure it doesn't get executed before any other .then callback.
promise['finally'](function(){
_editor.endAction.call(_editor);
});
}catch(exc){
$log.error(exc);
}
if(result || result === undefined){
// if true or undefined is returned then the action has finished. Otherwise the deferred action will be resolved manually.
deferred.resolve();
}
};
}]);