mirror of
https://github.com/fastmail/Squire.git
synced 2024-12-22 15:23:29 -05:00
357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
/*jshint strict:false, undef:false, unused:false */
|
|
|
|
var fontSizes = {
|
|
1: 10,
|
|
2: 13,
|
|
3: 16,
|
|
4: 18,
|
|
5: 24,
|
|
6: 32,
|
|
7: 48
|
|
};
|
|
|
|
var styleToSemantic = {
|
|
backgroundColor: {
|
|
regexp: notWS,
|
|
replace: function ( doc, colour ) {
|
|
return createElement( doc, 'SPAN', {
|
|
'class': HIGHLIGHT_CLASS,
|
|
style: 'background-color:' + colour
|
|
});
|
|
}
|
|
},
|
|
color: {
|
|
regexp: notWS,
|
|
replace: function ( doc, colour ) {
|
|
return createElement( doc, 'SPAN', {
|
|
'class': COLOUR_CLASS,
|
|
style: 'color:' + colour
|
|
});
|
|
}
|
|
},
|
|
fontWeight: {
|
|
regexp: /^bold|^700/i,
|
|
replace: function ( doc ) {
|
|
return createElement( doc, 'B' );
|
|
}
|
|
},
|
|
fontStyle: {
|
|
regexp: /^italic/i,
|
|
replace: function ( doc ) {
|
|
return createElement( doc, 'I' );
|
|
}
|
|
},
|
|
fontFamily: {
|
|
regexp: notWS,
|
|
replace: function ( doc, family ) {
|
|
return createElement( doc, 'SPAN', {
|
|
'class': FONT_FAMILY_CLASS,
|
|
style: 'font-family:' + family
|
|
});
|
|
}
|
|
},
|
|
fontSize: {
|
|
regexp: notWS,
|
|
replace: function ( doc, size ) {
|
|
return createElement( doc, 'SPAN', {
|
|
'class': FONT_SIZE_CLASS,
|
|
style: 'font-size:' + size
|
|
});
|
|
}
|
|
},
|
|
textDecoration: {
|
|
regexp: /^underline/i,
|
|
replace: function ( doc ) {
|
|
return createElement( doc, 'U' );
|
|
}
|
|
}
|
|
};
|
|
|
|
var replaceWithTag = function ( tag ) {
|
|
return function ( node, parent ) {
|
|
var el = createElement( node.ownerDocument, tag );
|
|
parent.replaceChild( el, node );
|
|
el.appendChild( empty( node ) );
|
|
return el;
|
|
};
|
|
};
|
|
|
|
var replaceStyles = function ( node, parent ) {
|
|
var style = node.style;
|
|
var doc = node.ownerDocument;
|
|
var attr, converter, css, newTreeBottom, newTreeTop, el;
|
|
|
|
for ( attr in styleToSemantic ) {
|
|
converter = styleToSemantic[ attr ];
|
|
css = style[ attr ];
|
|
if ( css && converter.regexp.test( css ) ) {
|
|
el = converter.replace( doc, css );
|
|
if ( !newTreeTop ) {
|
|
newTreeTop = el;
|
|
}
|
|
if ( newTreeBottom ) {
|
|
newTreeBottom.appendChild( el );
|
|
}
|
|
newTreeBottom = el;
|
|
node.style[ attr ] = '';
|
|
}
|
|
}
|
|
|
|
if ( newTreeTop ) {
|
|
newTreeBottom.appendChild( empty( node ) );
|
|
if ( node.nodeName === 'SPAN' ) {
|
|
parent.replaceChild( newTreeTop, node );
|
|
} else {
|
|
node.appendChild( newTreeTop );
|
|
}
|
|
}
|
|
|
|
return newTreeBottom || node;
|
|
};
|
|
|
|
var stylesRewriters = {
|
|
P: replaceStyles,
|
|
SPAN: replaceStyles,
|
|
STRONG: replaceWithTag( 'B' ),
|
|
EM: replaceWithTag( 'I' ),
|
|
INS: replaceWithTag( 'U' ),
|
|
STRIKE: replaceWithTag( 'S' ),
|
|
FONT: function ( node, parent ) {
|
|
var face = node.face,
|
|
size = node.size,
|
|
colour = node.color,
|
|
doc = node.ownerDocument,
|
|
fontSpan, sizeSpan, colourSpan,
|
|
newTreeBottom, newTreeTop;
|
|
if ( face ) {
|
|
fontSpan = createElement( doc, 'SPAN', {
|
|
'class': FONT_FAMILY_CLASS,
|
|
style: 'font-family:' + face
|
|
});
|
|
newTreeTop = fontSpan;
|
|
newTreeBottom = fontSpan;
|
|
}
|
|
if ( size ) {
|
|
sizeSpan = createElement( doc, 'SPAN', {
|
|
'class': FONT_SIZE_CLASS,
|
|
style: 'font-size:' + fontSizes[ size ] + 'px'
|
|
});
|
|
if ( !newTreeTop ) {
|
|
newTreeTop = sizeSpan;
|
|
}
|
|
if ( newTreeBottom ) {
|
|
newTreeBottom.appendChild( sizeSpan );
|
|
}
|
|
newTreeBottom = sizeSpan;
|
|
}
|
|
if ( colour && /^#?([\dA-F]{3}){1,2}$/i.test( colour ) ) {
|
|
if ( colour.charAt( 0 ) !== '#' ) {
|
|
colour = '#' + colour;
|
|
}
|
|
colourSpan = createElement( doc, 'SPAN', {
|
|
'class': COLOUR_CLASS,
|
|
style: 'color:' + colour
|
|
});
|
|
if ( !newTreeTop ) {
|
|
newTreeTop = colourSpan;
|
|
}
|
|
if ( newTreeBottom ) {
|
|
newTreeBottom.appendChild( colourSpan );
|
|
}
|
|
newTreeBottom = colourSpan;
|
|
}
|
|
if ( !newTreeTop ) {
|
|
newTreeTop = newTreeBottom = createElement( doc, 'SPAN' );
|
|
}
|
|
parent.replaceChild( newTreeTop, node );
|
|
newTreeBottom.appendChild( empty( node ) );
|
|
return newTreeBottom;
|
|
},
|
|
TT: function ( node, parent ) {
|
|
var el = createElement( node.ownerDocument, 'SPAN', {
|
|
'class': FONT_FAMILY_CLASS,
|
|
style: 'font-family:menlo,consolas,"courier new",monospace'
|
|
});
|
|
parent.replaceChild( el, node );
|
|
el.appendChild( empty( node ) );
|
|
return el;
|
|
}
|
|
};
|
|
|
|
var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/;
|
|
|
|
var blacklist = /^(?:HEAD|META|STYLE)/;
|
|
|
|
var walker = new TreeWalker( null, SHOW_TEXT|SHOW_ELEMENT, function () {
|
|
return true;
|
|
});
|
|
|
|
/*
|
|
Two purposes:
|
|
|
|
1. Remove nodes we don't want, such as weird <o:p> tags, comment nodes
|
|
and whitespace nodes.
|
|
2. Convert inline tags into our preferred format.
|
|
*/
|
|
var cleanTree = function cleanTree ( node, preserveWS ) {
|
|
var children = node.childNodes,
|
|
nonInlineParent, i, l, child, nodeName, nodeType, rewriter, childLength,
|
|
startsWithWS, endsWithWS, data, sibling;
|
|
|
|
nonInlineParent = node;
|
|
while ( isInline( nonInlineParent ) ) {
|
|
nonInlineParent = nonInlineParent.parentNode;
|
|
}
|
|
walker.root = nonInlineParent;
|
|
|
|
for ( i = 0, l = children.length; i < l; i += 1 ) {
|
|
child = children[i];
|
|
nodeName = child.nodeName;
|
|
nodeType = child.nodeType;
|
|
rewriter = stylesRewriters[ nodeName ];
|
|
if ( nodeType === ELEMENT_NODE ) {
|
|
childLength = child.childNodes.length;
|
|
if ( rewriter ) {
|
|
child = rewriter( child, node );
|
|
} else if ( blacklist.test( nodeName ) ) {
|
|
node.removeChild( child );
|
|
i -= 1;
|
|
l -= 1;
|
|
continue;
|
|
} else if ( !allowedBlock.test( nodeName ) && !isInline( child ) ) {
|
|
i -= 1;
|
|
l += childLength - 1;
|
|
node.replaceChild( empty( child ), child );
|
|
continue;
|
|
}
|
|
if ( childLength ) {
|
|
cleanTree( child, preserveWS || ( nodeName === 'PRE' ) );
|
|
}
|
|
} else {
|
|
if ( nodeType === TEXT_NODE ) {
|
|
data = child.data;
|
|
startsWithWS = !notWS.test( data.charAt( 0 ) );
|
|
endsWithWS = !notWS.test( data.charAt( data.length - 1 ) );
|
|
if ( preserveWS || ( !startsWithWS && !endsWithWS ) ) {
|
|
continue;
|
|
}
|
|
// Iterate through the nodes; if we hit some other content
|
|
// before the start of a new block we don't trim
|
|
if ( startsWithWS ) {
|
|
walker.currentNode = child;
|
|
while ( sibling = walker.previousPONode() ) {
|
|
nodeName = sibling.nodeName;
|
|
if ( nodeName === 'IMG' ||
|
|
( nodeName === '#text' &&
|
|
notWS.test( sibling.data ) ) ) {
|
|
break;
|
|
}
|
|
if ( !isInline( sibling ) ) {
|
|
sibling = null;
|
|
break;
|
|
}
|
|
}
|
|
data = data.replace( /^[ \t\r\n]+/g, sibling ? ' ' : '' );
|
|
}
|
|
if ( endsWithWS ) {
|
|
walker.currentNode = child;
|
|
while ( sibling = walker.nextNode() ) {
|
|
if ( nodeName === 'IMG' ||
|
|
( nodeName === '#text' &&
|
|
notWS.test( sibling.data ) ) ) {
|
|
break;
|
|
}
|
|
if ( !isInline( sibling ) ) {
|
|
sibling = null;
|
|
break;
|
|
}
|
|
}
|
|
data = data.replace( /[ \t\r\n]+$/g, sibling ? ' ' : '' );
|
|
}
|
|
if ( data ) {
|
|
child.data = data;
|
|
continue;
|
|
}
|
|
}
|
|
node.removeChild( child );
|
|
i -= 1;
|
|
l -= 1;
|
|
}
|
|
}
|
|
return node;
|
|
};
|
|
|
|
// ---
|
|
|
|
var removeEmptyInlines = function removeEmptyInlines ( node ) {
|
|
var children = node.childNodes,
|
|
l = children.length,
|
|
child;
|
|
while ( l-- ) {
|
|
child = children[l];
|
|
if ( child.nodeType === ELEMENT_NODE && !isLeaf( child ) ) {
|
|
removeEmptyInlines( child );
|
|
if ( isInline( child ) && !child.firstChild ) {
|
|
node.removeChild( child );
|
|
}
|
|
} else if ( child.nodeType === TEXT_NODE && !child.data ) {
|
|
node.removeChild( child );
|
|
}
|
|
}
|
|
};
|
|
|
|
// ---
|
|
|
|
var notWSTextNode = function ( node ) {
|
|
return node.nodeType === ELEMENT_NODE ?
|
|
node.nodeName === 'BR' :
|
|
notWS.test( node.data );
|
|
};
|
|
var isLineBreak = function ( br ) {
|
|
var block = br.parentNode,
|
|
walker;
|
|
while ( isInline( block ) ) {
|
|
block = block.parentNode;
|
|
}
|
|
walker = new TreeWalker(
|
|
block, SHOW_ELEMENT|SHOW_TEXT, notWSTextNode );
|
|
walker.currentNode = br;
|
|
return !!walker.nextNode();
|
|
};
|
|
|
|
// <br> elements are treated specially, and differently depending on the
|
|
// browser, when in rich text editor mode. When adding HTML from external
|
|
// sources, we must remove them, replacing the ones that actually affect
|
|
// line breaks by wrapping the inline text in a <div>. Browsers that want <br>
|
|
// elements at the end of each block will then have them added back in a later
|
|
// fixCursor method call.
|
|
var cleanupBRs = function ( node, root ) {
|
|
var brs = node.querySelectorAll( 'BR' ),
|
|
brBreaksLine = [],
|
|
l = brs.length,
|
|
i, br, parent;
|
|
|
|
// Must calculate whether the <br> breaks a line first, because if we
|
|
// have two <br>s next to each other, after the first one is converted
|
|
// to a block split, the second will be at the end of a block and
|
|
// therefore seem to not be a line break. But in its original context it
|
|
// was, so we should also convert it to a block split.
|
|
for ( i = 0; i < l; i += 1 ) {
|
|
brBreaksLine[i] = isLineBreak( brs[i] );
|
|
}
|
|
while ( l-- ) {
|
|
br = brs[l];
|
|
// Cleanup may have removed it
|
|
parent = br.parentNode;
|
|
if ( !parent ) { continue; }
|
|
// If it doesn't break a line, just remove it; it's not doing
|
|
// anything useful. We'll add it back later if required by the
|
|
// browser. If it breaks a line, wrap the content in div tags
|
|
// and replace the brs.
|
|
if ( !brBreaksLine[l] ) {
|
|
detach( br );
|
|
} else if ( !isInline( parent ) ) {
|
|
fixContainer( parent, root );
|
|
}
|
|
}
|
|
};
|