/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.8
src/language/HTMLUtils.js
572 строки
26 KB
ficristo
Move most inline jslint directives to config files
12 авг 2016, 10:22
12 авг 2016, 10:22
074382a
Код
Авторство
О чём код?
/* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ define(function (require, exports, module) { "use strict"; var CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"), TokenUtils = require("utils/TokenUtils"); // Constants var TAG_NAME = "tagName", CLOSING_TAG = "closingTag", ATTR_NAME = "attr.name", ATTR_VALUE = "attr.value"; // Regular expression for token types with "tag" prefixed var tagPrefixedRegExp = /^tag/; /** * @private * Sometimes as attr values are getting typed, if the quotes aren't balanced yet * some extra 'non attribute value' text gets included in the token. This attempts * to assure the attribute value we grab is always good * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return { val:{string}, offset:{number}} */ function _extractAttrVal(ctx) { var attrValue = ctx.token.string, startChar = attrValue.charAt(0), endChar = attrValue.charAt(attrValue.length - 1), offset = TokenUtils.offsetInToken(ctx), foundEqualSign = false; //If this is a fully quoted value, return the whole //thing regardless of position if (attrValue.length > 1 && (startChar === "'" || startChar === '"') && endChar === startChar) { // Find an equal sign before the end quote. If found, // then the user may be entering an attribute value right before // another attribute and we're getting a false balanced string. // An example of this case is <link rel" href="foo"> where the // cursor is right after the first double quote. foundEqualSign = (attrValue.match(/\=\s*['"]$/) !== null); if (!foundEqualSign) { //strip the quotes and return; attrValue = attrValue.substring(1, attrValue.length - 1); offset = offset - 1 > attrValue.length ? attrValue.length : offset - 1; return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: true}; } } if (foundEqualSign) { var spaceIndex = attrValue.indexOf(" "), bracketIndex = attrValue.indexOf(">"), upToIndex = (spaceIndex !== -1 && spaceIndex < bracketIndex) ? spaceIndex : bracketIndex; attrValue = attrValue.substring(0, (upToIndex > offset) ? upToIndex : offset); } else if (offset > 0 && (startChar === "'" || startChar === '"')) { //The att value is getting edit in progress. There is possible extra //stuff in this token state since the quote isn't closed, so we assume //the stuff from the quote to the current pos is definitely in the attribute //value. attrValue = attrValue.substring(0, offset); } //If the attrValue start with a quote, trim that now startChar = attrValue.charAt(0); if (startChar === "'" || startChar === '"') { attrValue = attrValue.substring(1); offset--; } else { startChar = ""; // Make attr value empty and set offset to zero if it has the ">" // which is the closing of the tag. if (endChar === ">") { attrValue = ""; offset = 0; } } return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: false}; } /** * @private * Gets the tagname from where ever you are in the currect state * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {string} */ function _extractTagName(ctx) { var mode = ctx.editor.getMode(), innerModeData = CodeMirror.innerMode(mode, ctx.token.state); if (ctx.token.type === "tag bracket") { return innerModeData.state.tagName; } // If the ctx is inside the tag name of an end tag, innerModeData.state.tagName is // undefined. So return token string as the tag name. return innerModeData.state.tagName || ctx.token.string; } /** * Compiles a list of used attributes for a given tag * @param {CodeMirror} editor An instance of a CodeMirror editor * @param {ch:{string}, line:{number}} pos A CodeMirror position * @return {Array.<string>} A list of the used attributes inside the current tag */ function getTagAttributes(editor, pos) { var attrs = [], backwardCtx = TokenUtils.getInitialContext(editor._codeMirror, pos), forwardCtx = $.extend({}, backwardCtx); if (editor.getModeForSelection() === "html") { if (backwardCtx.token && !tagPrefixedRegExp.test(backwardCtx.token.type)) { while (TokenUtils.movePrevToken(backwardCtx) && !tagPrefixedRegExp.test(backwardCtx.token.type)) { if (backwardCtx.token.type === "error" && backwardCtx.token.string.indexOf("<") === 0) { break; } if (backwardCtx.token.type === "attribute") { attrs.push(backwardCtx.token.string); } } while (TokenUtils.moveNextToken(forwardCtx) && !tagPrefixedRegExp.test(forwardCtx.token.type)) { if (forwardCtx.token.type === "attribute") { // If the current tag is not closed, codemirror may return the next opening // tag as an attribute. Stop the search loop in that case. if (forwardCtx.token.string.indexOf("<") === 0) { break; } attrs.push(forwardCtx.token.string); } else if (forwardCtx.token.type === "error") { if (forwardCtx.token.string.indexOf("<") === 0 || forwardCtx.token.string.indexOf(">") === 0) { break; } // If we type the first letter of the next attribute, it comes as an error // token. We need to double check for possible invalidated attributes. if (/\S/.test(forwardCtx.token.string) && forwardCtx.token.string.indexOf("\"") === -1 && forwardCtx.token.string.indexOf("'") === -1 && forwardCtx.token.string.indexOf("=") === -1) { attrs.push(forwardCtx.token.string); } } } } } return attrs; } /** * Creates a tagInfo object and assures all the values are entered or are empty strings * @param {string=} tokenType what is getting edited and should be hinted * @param {number=} offset where the cursor is for the part getting hinted * @param {string=} tagName The name of the tag * @param {string=} attrName The name of the attribute * @param {string=} attrValue The value of the attribute * @return {{tagName:string, * attr:{name:string, value:string, valueAssigned:boolean, quoteChar:string, hasEndQuote:boolean}, * position:{tokenType:string, offset:number} * }} * A tagInfo object with some context about the current tag hint. */ function createTagInfo(tokenType, offset, tagName, attrName, attrValue, valueAssigned, quoteChar, hasEndQuote) { return { tagName: tagName || "", attr: { name: attrName || "", value: attrValue || "", valueAssigned: valueAssigned || false, quoteChar: quoteChar || "", hasEndQuote: hasEndQuote || false }, position: { tokenType: tokenType || "", offset: offset || 0 } }; } /** * @private * Gets the taginfo starting from the attribute value and moving backwards * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {string} */ function _getTagInfoStartingFromAttrValue(ctx) { // Assume we in the attr value // and validate that by going backwards var attrInfo = _extractAttrVal(ctx), attrVal = attrInfo.val, offset = attrInfo.offset, quoteChar = attrInfo.quoteChar, hasEndQuote = attrInfo.hasEndQuote, strLength = ctx.token.string.length; if ((ctx.token.type === "string" || ctx.token.type === "error") && ctx.pos.ch === ctx.token.end && strLength > 1) { var firstChar = ctx.token.string[0], lastChar = ctx.token.string[strLength - 1]; // We get here only when the cursor is immediately on the right of the end quote // of an attribute value. So we want to return an empty tag info so that the caller // can dismiss the code hint popup if it is still open. if (firstChar === lastChar && (firstChar === "'" || firstChar === "\"")) { return createTagInfo(); } } //Move to the prev token, and check if it's "=" if (!TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx) || ctx.token.string !== "=") { return createTagInfo(); } //Move to the prev token, and check if it's an attribute if (!TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx) || ctx.token.type !== "attribute") { return createTagInfo(); } var attrName = ctx.token.string; var tagName = _extractTagName(ctx); //We're good. return createTagInfo(ATTR_VALUE, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote); } /** * @private * Gets the taginfo starting from the attribute name and moving forwards * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @param {boolean} isPriorAttr indicates whether we're getting info for a prior attribute * @return {string} */ function _getTagInfoStartingFromAttrName(ctx, isPriorAttr) { //Verify We're in the attribute name, move forward and try to extract the rest of //the info. If the user it typing the attr the rest might not be here if (isPriorAttr === false && ctx.token.type !== "attribute") { return createTagInfo(); } var tagName = _extractTagName(ctx); var attrName = ctx.token.string; var offset = TokenUtils.offsetInToken(ctx); if (!TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx) || ctx.token.string !== "=") { // If we're checking for a prior attribute and the next token we get is a tag or an html comment or // an undefined token class, then we've already scanned past our original cursor location. // So just return an empty tag info. if (isPriorAttr && (!ctx.token.type || (ctx.token.type && ctx.token.type !== "attribute" && ctx.token.type.indexOf("error") === -1 && ctx.token.string.indexOf("<") !== -1))) { return createTagInfo(); } return createTagInfo(ATTR_NAME, offset, tagName, attrName); } if (!TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx)) { return createTagInfo(ATTR_NAME, offset, tagName, attrName); } //this should be the attrvalue var attrInfo = _extractAttrVal(ctx), attrVal = attrInfo.val, quoteChar = attrInfo.quoteChar, hasEndQuote = attrInfo.hasEndQuote; return createTagInfo(ATTR_NAME, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote); } /** * Figure out if we're in a tag, and if we are return info about it * An example token stream for this tag is <span id="open-files-disclosure-arrow"></span> : * className:tag string:"<span" * className: string:" " * className:attribute string:"id" * className: string:"=" * className:string string:""open-files-disclosure-arrow"" * className:tag string:"></span>" * @param {Editor} editor An instance of a Brackets editor * @param {{ch: number, line: number}} constPos A CM pos (likely from editor.getCursorPos()) * @return {{tagName:string, * attr:{name:string, value:string, valueAssigned:boolean, quoteChar:string, hasEndQuote:boolean}, * position:{tokenType:string, offset:number} * }} * A tagInfo object with some context about the current tag hint. */ function getTagInfo(editor, constPos) { // We're going to be changing pos a lot, but we don't want to mess up // the pos the caller passed in so we use extend to make a safe copy of it. var pos = $.extend({}, constPos), ctx = TokenUtils.getInitialContext(editor._codeMirror, pos), offset = TokenUtils.offsetInToken(ctx), tagInfo, tokenType; // Check if this is inside a style block. if (editor.getModeForSelection() !== "html") { return createTagInfo(); } // Check and see where we are in the tag if (ctx.token.string.length > 0 && !/\S/.test(ctx.token.string)) { // token at (i.e. before) pos is whitespace, so test token at next pos // // note: getTokenAt() does range checking for ch. If it detects that ch is past // EOL, it uses EOL, same token is returned, and the following condition fails, // so we don't need to worry about testPos being valid. var testPos = {ch: ctx.pos.ch + 1, line: ctx.pos.line}, testToken = editor._codeMirror.getTokenAt(testPos, true); if (testToken.string.length > 0 && /\S/.test(testToken.string) && testToken.string.charAt(0) !== ">") { // pos has whitespace before it and non-whitespace after it, so use token after ctx.token = testToken; // Check whether the token type is one of the types prefixed with "tag" // (e.g. "tag", "tag error", "tag brackets") if (tagPrefixedRegExp.test(ctx.token.type)) { // Check to see if the cursor is just before a "<" but not in any tag. if (ctx.token.string.charAt(0) === "<") { return createTagInfo(); } } else if (ctx.token.type === "attribute") { // Check to see if the user is going to add a new attr before an existing one return _getTagInfoStartingFromAttrName(ctx, false); } else if (ctx.token.string === "=") { // We're between a whitespace and "=", so return an empty tag info. return createTagInfo(); } } else { // We get here if ">" or white spaces after testPos. // Check if there is an equal sign after testPos by creating a new ctx // with the original pos. We can't use the current ctx since we need to // use it to scan backwards if we don't find an equal sign here. // Comment out this block to fix issue #1510. // if (testToken.string.length > 0 && testToken.string.charAt(0) !== ">") { // tempCtx = TokenUtils.getInitialContext(editor._codeMirror, pos); // if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, tempCtx) && tempCtx.token.string === "=") { // // Return an empty tag info since we're between an atribute name and the equal sign. // return createTagInfo(); // } // } // next, see what's before pos if (!TokenUtils.movePrevToken(ctx)) { return createTagInfo(); } if (ctx.token.type === "comment") { return createTagInfo(); } else if (!tagPrefixedRegExp.test(ctx.token.type) && ctx.token.string !== "=") { // If it wasn't the tag name, assume it was an attr value // Also we don't handle the "=" here. tagInfo = _getTagInfoStartingFromAttrValue(ctx); // Check to see if this is the closing of a tag (either the start or end) // or a comment tag. if (ctx.token.type === "comment" || (tagPrefixedRegExp.test(ctx.token.type) && (ctx.token.string === ">" || ctx.token.string === "/>" || ctx.token.string === "</"))) { return createTagInfo(); } // If it wasn't an attr value, assume it was an empty attr (ie. attr with no value) if (!tagInfo.tagName) { tagInfo = _getTagInfoStartingFromAttrName(ctx, true); } // We don't want to give context for the previous attr // and we want it to look like the user is going to add a new attr if (tagInfo.tagName) { return createTagInfo(ATTR_NAME, 0, tagInfo.tagName); } return createTagInfo(); } // We know the tag was here, so the user is adding an attr name tokenType = ATTR_NAME; offset = 0; } } if (tagPrefixedRegExp.test(ctx.token.type)) { if (ctx.token.type !== "tag bracket") { // Check if the user just typed a white space after "<" that made an existing tag invalid. if (TokenUtils.movePrevToken(ctx) && !/\S/.test(ctx.token.string)) { return createTagInfo(); } // Check to see if this is a closing tag if (ctx.token.type === "tag bracket" && ctx.token.string === "</") { tokenType = CLOSING_TAG; } // Restore the original ctx by moving back to next context since we call // movePrevToken above to detect "<" or "</". TokenUtils.moveNextToken(ctx); } // Check to see if this is the closing of a start tag or a self closing tag if (ctx.token.string === ">" || ctx.token.string === "/>") { return createTagInfo(); } // Make sure the cursor is not after an equal sign or a quote before we report the context as a tag. if (ctx.token.string !== "=" && ctx.token.string.match(/^["']/) === null) { if (!tokenType) { tokenType = TAG_NAME; if (ctx.token.type === "tag bracket") { // Check to see if this is a closing tag if (ctx.token.string === "</") { tokenType = CLOSING_TAG; offset -= 2; } else { offset = 0; } // If the cursor is right after the "<" or "</", then // move context to next one so that _extractTagName // call below can get the tag name if there is one. if (offset === 0) { TokenUtils.moveNextToken(ctx); } } } // We're actually in the tag, just return that as we have no relevant // info about what attr is selected return createTagInfo(tokenType, offset, _extractTagName(ctx)); } } if (ctx.token.string === "=") { // We could be between the attr and the value // Step back and check if (!TokenUtils.moveSkippingWhitespace(TokenUtils.movePrevToken, ctx) || ctx.token.type !== "attribute") { return createTagInfo(); } // The "=" is added, time to hint for values tokenType = ATTR_VALUE; offset = 0; } if (ctx.token.type === "attribute") { tagInfo = _getTagInfoStartingFromAttrName(ctx, false); // If we're in attr value, then we may need to calculate the correct offset // from the beginning of the attribute value. If the cursor position is to // the left of attr value, then the offset is negative. // e.g. if the cursor is just to the right of the "=" in <a rel= "rtl", then // the offset will be -2. if (tagInfo.attr.quoteChar) { offset = constPos.ch - ctx.pos.ch; } else if (tokenType === ATTR_VALUE && (constPos.ch + 1) < ctx.pos.ch) { // The cursor may be right before an unquoted attribute or another attribute name. // Since we can't distinguish between them, we will empty the value so that the // caller can just insert a new attribute value. tagInfo.attr.value = ""; } } else { // if we're not at a tag, "=", or attribute name, assume we're in the value tagInfo = _getTagInfoStartingFromAttrValue(ctx); } if (tokenType && tagInfo.tagName) { tagInfo.position.tokenType = tokenType; tagInfo.position.offset = offset; } return tagInfo; } /** * Returns an Array of info about all blocks whose token mode name matches that passed in, * in the given Editor's HTML document (assumes the Editor contains HTML text). * @param {!Editor} editor - the editor containing the HTML text * @param {string} modeName - the mode name of the tokens to look for * @return {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, text:string}>} */ function findBlocks(editor, modeName) { // Start scanning from beginning of file var ctx = TokenUtils.getInitialContext(editor._codeMirror, {line: 0, ch: 0}), blocks = [], currentBlock = null, inBlock = false, outerMode = editor._codeMirror.getMode(), tokenModeName, previousMode; while (TokenUtils.moveNextToken(ctx, false)) { tokenModeName = CodeMirror.innerMode(outerMode, ctx.token.state).mode.name; if (inBlock) { if (!currentBlock.end) { // Handle empty blocks currentBlock.end = currentBlock.start; } // Check for end of this block if (tokenModeName === previousMode) { // currentBlock.end is already set to pos of the last token by now currentBlock.text = editor.document.getRange(currentBlock.start, currentBlock.end); inBlock = false; } else { currentBlock.end = { line: ctx.pos.line, ch: ctx.pos.ch }; } } else { // Check for start of a block if (tokenModeName === modeName) { currentBlock = { start: { line: ctx.pos.line, ch: ctx.pos.ch } }; blocks.push(currentBlock); inBlock = true; } else { previousMode = tokenModeName; } // else, random token: ignore } } return blocks; } /** * Returns an Array of info about all <style> blocks in the given Editor's HTML document (assumes * the Editor contains HTML text). * @param {!Editor} editor * @return {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, text:string}>} */ function findStyleBlocks(editor) { return findBlocks(editor, "css"); } // Define public API exports.TAG_NAME = TAG_NAME; exports.CLOSING_TAG = CLOSING_TAG; exports.ATTR_NAME = ATTR_NAME; exports.ATTR_VALUE = ATTR_VALUE; exports.getTagInfo = getTagInfo; exports.getTagAttributes = getTagAttributes; //The createTagInfo is really only for the unit tests so they can make the same structure to //compare results with exports.createTagInfo = createTagInfo; exports.findStyleBlocks = findStyleBlocks; exports.findBlocks = findBlocks; });