%PDF- %PDF-
Direktori : /proc/985914/root/data/old/home/stash/atlassian-stash/static/util/ |
Current File : //proc/985914/root/data/old/home/stash/atlassian-stash/static/util/html.js |
define('util/html', [ 'aui', 'util/function', 'exports' ], function( AJS, fn, exports ) { 'use strict'; var logError = window.console && console.error ? function() { console.error.apply(console, arguments); } : AJS.log; var NodeType = { OPEN : 'open', CLOSE : 'close' }; function NodeStream() {} /** * Gets an object representing the next opening or closing html tag in this stream. * open tags look like: { * type : NodeType.OPEN, * value : '<tagName>', * textPosition: 0, * close : { * type : NodeType.CLOSE, * value : '</tagName>' * } * } * close tags look like: { * type : NodeType.CLOSE, * value : '</tagName>' * } */ NodeStream.prototype.next = function() { logError('NodeStream.next is abstract and must be implemented'); }; /** * Gets the NodeType for the upcoming tag, or null if there are no more tags. */ NodeStream.prototype.getNextType = function() { logError('NodeStream.getNextType is abstract and must be implemented'); }; /** * Gets the text position of the upcoming tag, or null if there are no more tags. * E.g., the following will log 0, then 2: * var stream = htmlNodeStream("<div>hi</div>"); * console.log(stream.getNextTextPosition()); // 0 * stream.next(); * console.log(stream.getNextTextPosition()); // 2 */ NodeStream.prototype.getNextTextPosition = function() { logError('NodeStream.getNextTextPosition is abstract and must be implemented'); }; var tagNameRegex = /<\s*(\w+)(?=\s|>)/, tagNameGroup = 1; /** * Returns a nodeStream based on a well-formed HTML string * @param html the html string to stream nodes from. * @param customUnescape a custom function to use when unescaping text to determine next text position */ function htmlNodeStream(html, customUnescape) { var pos = 0, textPos = 0, len = html.length, tagBegin = '<', tagEnd = '>'; var nextType, moved; var unescape = customUnescape || function(value) { logError('Not yet implemented'); }; function resetInternals() { nextType = null; moved = false; } function moveToNextNode() { if (moved || pos == null) return; moved = true; var nextTag = html.indexOf(tagBegin, pos); if (nextTag === pos) return; if (nextTag === -1) { pos = null; textPos = null; } else { textPos += unescape(html.substring(pos, nextTag)).length; pos = nextTag; } } function getNextType() { moveToNextNode(); if (pos == null) { return null; } return html[pos + 1] === '/' ? NodeType.CLOSE : NodeType.OPEN; } var newStream = new NodeStream(); var openPositions = []; newStream.getNextType = function() { return nextType || (nextType = getNextType()); }; newStream.next = function() { moveToNextNode(); if (pos == null) return null; var currentType = this.getNextType(); var until = html.indexOf(tagEnd, pos), /* if this segment goes to the end, newPos = length of the input. * else the segment includes the match if it's an end tag (>), but not if it's a begin tag (<) */ newPos = until === -1 ? len : until + 1, currentTag = html.substring(pos, newPos); var node = { type : currentType, value : currentTag, textPosition : textPos }; if (currentType === NodeType.OPEN) { node.close = { type : NodeType.CLOSE, value : '</' + currentTag.match(tagNameRegex)[tagNameGroup] + '>' }; openPositions.push(textPos); } pos = newPos; resetInternals(); return node; }; newStream.getNextTextPosition = function() { moveToNextNode(); return textPos; }; return newStream; } /** * Returns a nodeStream based on the output from highlight.js * @param html the output from highlight.js */ function highlightJsNodeStream(html) { return htmlNodeStream(html, function (value) { //undoes the custom escaping of HLJS return value.replace(/&|<|>/gm, function(input) { switch (input) { case '&': return '&'; case '<': return '<'; case '>': return '>'; } }); }); } function toGetter(val, defaultVal, valType) { valType = valType || 'string'; return typeof val === 'function' ? val : fn.constant(typeof val === valType ? val : defaultVal); } /** * Returns a nodeStream that, given an array of lines, will output a wrapping <pre> tag around each one. * @param lines the array of objects containing a text property. */ function lineNodeStream(lines, getLineText, opts) { opts = opts || {}; var lineIndex = 0, textPos = 0, len = lines.length, state = NodeType.OPEN, lineStart = toGetter(opts.lineStart, ''), lineEnd = toGetter(opts.lineEnd, '<br />'), emptyLine = toGetter(opts.emptyLine, ''), lineOffset = toGetter( opts.lineOffset, 1, 'number'), // the 1 is for the implied \n openNode, closeNode; getLineText = getLineText || fn.dot('text'); function open() { state = NodeType.CLOSE; var lineLength = getLineText(lines[lineIndex], lineIndex).length; closeNode = { type : NodeType.CLOSE, value : lineLength ? lineEnd(lines[lineIndex], lineIndex) : (emptyLine(lines[lineIndex], lineIndex) + lineEnd(lines[lineIndex], lineIndex)) }; openNode = { type : NodeType.OPEN, value : lineStart(lines[lineIndex], lineIndex), close : closeNode, textPosition : textPos }; textPos = textPos + lineLength + lineOffset(lines[lineIndex], lineIndex); return openNode; } function close() { lineIndex++; closeNode.textPosition = textPos; state = NodeType.OPEN; openNode = null; return closeNode; } var states = { open : open, close : close }; var newStream = new NodeStream(); newStream.getNextType = function() { if (lineIndex >= len) return null; return state; }; newStream.next = function() { if (lineIndex >= len) return null; return states[state](); }; newStream.getNextTextPosition = function() { if (lineIndex >= len) return null; return textPos; }; return newStream; } //internal: determines the next stream to pull a node from. function nextStream(streams) { var minI, minTextPosition = Infinity, firstStream = { getNextTextPosition : fn.constant(null) }; for (var i = 0, len = streams.length; i < len; i++) { var stream = streams[i], streamTextPosition = stream.getNextTextPosition(); if (streamTextPosition != null && streamTextPosition < minTextPosition) { firstStream = stream; minTextPosition = streamTextPosition; minI = i; continue; } if (streamTextPosition === minTextPosition) { /* all things equal, open lower-index streams first and close them last. e.g. if all streams have nodes that open and close at the same text position, then you want to handle this: streams = [stream1, stream2, stream3] as: handle(stream1.open) handle(stream2.open) handle(stream3.open) ...handle inner text... | handle(stream3.close) // close higher indexes first | handle(stream2.close) | handle(stream1.close) | handle(stream1.open) // then open lower indexes first | handle(stream2.open) | handle(stream3.open) ...handle inner text... ... */ if (stream.getNextType() === NodeType.CLOSE) { firstStream = stream; minI = i; } } } return { index : minI, stream : firstStream }; } /** * internal: functions like a stack, but lower priorities are inserted further down the stack. * Negative priorities are not allowed. */ function PriorityStack() { this._backing = []; } PriorityStack.prototype.pushAtPriority = function(item, priority) { var queueForPriority = this._backing[priority] || (this._backing[priority] = []); queueForPriority.push(item); }; PriorityStack.prototype.popAtPriority = function(priority) { var queueForPriority = this._backing[priority] || (this._backing[priority] = []); return queueForPriority.pop(); }; PriorityStack.prototype.popPrioritiesAbove = function(priority) { var ret = [], backing = this._backing, popUntilLength = priority + 1; while(backing.length > popUntilLength) { var queueForPriority = this._backing.pop(); if (queueForPriority) { for (var j = queueForPriority.length - 1; j >= 0; j--) { ret.push(queueForPriority[j]); } } } return ret; }; PriorityStack.prototype.peek = function() { var priority = this._backing.length; while(priority--) { if (this._backing[priority] && this._backing[priority].length) { return this._backing[priority][this._backing[priority].length - 1]; } } return undefined; }; /** * Given canonical text and a number of node streams based on that text, will return a string containing * the text and html from all streams. * The order of stream arguments is important: earlier stream arguments are considered to always wrap later stream arguments. * @param text * @param streams... */ function mergeStreams(text/*, streams*/) { var streams = Array.prototype.slice.call(arguments, 1), textPos = 0, openNodes = new PriorityStack(), toReopen = [], reopenFromStreamIndex; var out = ""; while(true) { var nextStreamResult = nextStream(streams), streamIndex = nextStreamResult.index, stream = nextStreamResult.stream, newTextPos = stream.getNextTextPosition(), reachedEnd = newTextPos == null, textPositionChanged = reachedEnd || newTextPos > textPos; // We want to reopen any tags that were prematurely closed // We want to delay reopening them until we have to - that is, until a tag they should surround (as // determined by the streamIndex) appears // OR if the text position is about to change, we want to reopen them first. if (toReopen.length && (textPositionChanged || streamIndex >= reopenFromStreamIndex)) { for (var i = toReopen.length - 1; i >= 0; i--) { var reopenedNode = toReopen[i]; out += reopenedNode.tag.value; openNodes.pushAtPriority(reopenedNode, reopenedNode.streamIndex); } toReopen = []; reopenFromStreamIndex = null; } if (textPositionChanged) { out += AJS.escapeHtml(text.substring(textPos, newTextPos || undefined)); } if (reachedEnd) { return out; } var tag = stream.next(); if (tag != null) { if (tag.type === NodeType.CLOSE) { // when you close a tag, close all the inner tags first. openNodes.popAtPriority(streamIndex); } else { openNodes.pushAtPriority({ streamIndex : streamIndex, tag: tag }, streamIndex); } var toClose = openNodes.popPrioritiesAbove(streamIndex); if (toClose.length) { reopenFromStreamIndex = toClose[0].streamIndex; for (var j = 0, len = toClose.length; j < len; j++) { out += toClose[j].tag.close.value; } toReopen.push.apply(toReopen, toClose); } out += tag.value; } textPos = newTextPos; } } var holder = document.createElement('div'); // jQuery does lots of cleaning / guarding that takes about 500ms in IE. We assume our html is clean (no scripts, etc) // and skip all that. Still super slow though... function quickNDirtyAttach(element, html, attachmentMethod) { if (attachmentMethod === 'html' || !element.hasChildNodes()) { element.innerHTML = html; return; } // create nodes holder.innerHTML = html; // append them individually toa fragment. var documentFragment = document.createDocumentFragment(), i = holder.childNodes.length; while(i--) { documentFragment.appendChild(holder.childNodes[0]); } // append the document fragment appropriately if (attachmentMethod === 'append') { element.appendChild(documentFragment); } else { // prepend element.insertBefore(documentFragment, element.firstChild); } } /** * jQuery uses [.] and : as special characters for selectors. These characters are still valid in HTML ids. * Use this method to return a sanitized version of the id for use in jQuery selectors. * @param id id to sanitize */ function sanitizeId(id) { return id.replace(/:/g, "\\:").replace(/\./g, "\\."); } exports.quickNDirtyAttach = quickNDirtyAttach; exports.NodeStream = NodeStream; exports.NodeType = NodeType; exports.htmlNodeStream = htmlNodeStream; exports.highlightJsNodeStream = highlightJsNodeStream; exports.lineNodeStream = lineNodeStream; exports.mergeStreams = mergeStreams; exports.sanitizeId = sanitizeId; });