/* * GNU AGPL-3.0 License * * Copyright (c) 2021 - present core.ai . All rights reserved. * Original work Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License * for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. * */ /** * LiveHTMLDocument manages a single HTML source document. Edits to the HTML are applied live in * the browser, and the DOM node corresponding to the selection is highlighted. * * LiveHTMLDocument relies on HTMLInstrumentation in order to map tags in the HTML source text * to DOM nodes in the browser, so edits can be incrementally applied. */ define(function (require, exports, module) { var EventDispatcher = require("utils/EventDispatcher"), PerfUtils = require("utils/PerfUtils"), _ = require("thirdparty/lodash"), LiveDocument = require("LiveDevelopment/MultiBrowserImpl/documents/LiveDocument"), HTMLInstrumentation = require("LiveDevelopment/MultiBrowserImpl/language/HTMLInstrumentation"), CSSUtils = require("language/CSSUtils"); /** * @constructor * @see LiveDocument * @param {LiveDevProtocol} protocol The protocol to use for communicating with the browser. * @param {function(string): string} urlResolver A function that, given a path on disk, should return * the URL that Live Development serves that path at. * @param {Document} doc The Brackets document that this live document is connected to. * @param {?Editor} editor If specified, a particular editor that this live document is managing. * If not specified initially, the LiveDocument will connect to the editor for the given document * when it next becomes the active editor. */ function LiveHTMLDocument(protocol, urlResolver, doc, editor) { LiveDocument.apply(this, arguments); this.doc.addRef(); this._instrumentationEnabled = false; this._relatedDocuments = { stylesheets: {}, scripts: {} }; this._onChange = this._onChange.bind(this); this.doc.on("change", this._onChange); this._onRelated = this._onRelated.bind(this); this.protocol.on("DocumentRelated", this._onRelated); this._onStylesheetAdded = this._onStylesheetAdded.bind(this); this.protocol.on("StylesheetAdded", this._onStylesheetAdded); this._onStylesheetRemoved = this._onStylesheetRemoved.bind(this); this.protocol.on("StylesheetRemoved", this._onStylesheetRemoved); this._onScriptAdded = this._onScriptAdded.bind(this); this.protocol.on("ScriptAdded", this._onScriptAdded); this._onScriptRemoved = this._onScriptRemoved.bind(this); this.protocol.on("ScriptRemoved", this._onScriptRemoved); } LiveHTMLDocument.prototype = Object.create(LiveDocument.prototype); LiveHTMLDocument.prototype.constructor = LiveHTMLDocument; LiveHTMLDocument.prototype.parentClass = LiveDocument.prototype; EventDispatcher.makeEventDispatcher(LiveHTMLDocument.prototype); /** * @override * Returns true if document edits appear live in the connected browser. * @return {boolean} */ LiveHTMLDocument.prototype.isLiveEditingEnabled = function () { return this._instrumentationEnabled; }; /** * @override * Called to turn instrumentation on or off for this file. Triggered by being * requested from the browser. * TODO: this doesn't seem necessary...if we're a live document, we should * always have instrumentation on anyway. * @param {boolean} enabled * @param {boolean} [force] */ LiveHTMLDocument.prototype.setInstrumentationEnabled = function (enabled, force) { if (!this.editor) { // TODO: error return; } if (enabled && (force || !this._instrumentationEnabled)) { // TODO: not clear why we do this here instead of waiting for the next time we want to // generate the instrumented HTML. This won't work if the dom offsets are out of date. HTMLInstrumentation.scanDocument(this.doc); HTMLInstrumentation._markText(this.editor); } this._instrumentationEnabled = enabled; }; /** * Returns the instrumented version of the file. * @return {{body: string}} instrumented doc */ LiveHTMLDocument.prototype.getResponseData = function (enabled) { var body; if (this._instrumentationEnabled) { body = HTMLInstrumentation.generateInstrumentedHTML(this.editor, this.protocol.getRemoteScript()); } return { body: body || this.doc.getText() }; }; /** * @override * Closes the live document, terminating its connection to the browser. */ LiveHTMLDocument.prototype.close = function () { this.doc.releaseRef(); this.doc.off("change", this._onChange); this.protocol.off("DocumentRelated", this._onRelated); this.protocol.off("StylesheetAdded", this._onStylesheetAdded); this.protocol.off("StylesheetRemoved", this._onStylesheetRemoved); this.protocol.off("ScriptAdded", this._onScriptAdded); this.protocol.off("ScriptRemoved", this._onScriptRemoved); this.parentClass.close.call(this); }; /** * @override * Update the highlights in the browser based on the cursor position. */ LiveHTMLDocument.prototype.updateHighlight = function () { if (!this.editor || !this.isHighlightEnabled()) { return; } var editor = this.editor, mode = editor.getModeForSelection(), ids = [], selectors = []; // check if the cursor is in a stylesheet context (internal styles) if (mode === "css" || mode === "text/x-scss" || mode === "text/x-less") { // find the css selector _.each(this.editor.getSelections(), function (sel) { let selector = CSSUtils.findSelectorAtDocumentPos(editor, (sel.reversed ? sel.end : sel.start)); if (selector) { selectors.push(selector); } }); if (selectors.length) { // to highlight the elements that match the css selectors this.highlightRule(selectors.join(",")); return; } } // its not found in css context, then it must be a inline style or a normal html element _.each(this.editor.getSelections(), function (sel) { var tagID = HTMLInstrumentation._getTagIDAtDocumentPos( editor, sel.reversed ? sel.end : sel.start ); if (tagID !== -1) { ids.push(tagID); } }); if (!ids.length) { this.hideHighlight(); } else { this.highlightDomElement(ids); } }; /** * @private * For the given editor change, compare the resulting browser DOM with the * in-editor DOM. If there are any diffs, a warning is logged to the * console along with each diff. * @param {Object} change CodeMirror editor change data */ LiveHTMLDocument.prototype._compareWithBrowser = function (change) { // TODO: Not implemented. }; function _isSamePos(position, line, char) { return position.line === line && position.ch === char; } /** * @private * Handles edits to the document. Determines what's changed in the source and sends DOM diffs to the browser. * @param {$.Event} event * @param {Document} doc * @param {Object} change */ LiveHTMLDocument.prototype._onChange = function (event, doc, change) { // Make sure LiveHTML is turned on if (!this._instrumentationEnabled) { return; } // Apply DOM edits is async, so previous PerfUtils timer may still be // running. PerfUtils does not support running multiple timers with same // name, so do not start another timer in this case. var perfTimerName = "LiveHTMLDocument applyDOMEdits", isNestedTimer = PerfUtils.isActive(perfTimerName); if (!isNestedTimer) { PerfUtils.markStart(perfTimerName); } var self = this, result = HTMLInstrumentation.getUnappliedEditList(this.editor, change), applyEditsPromise; if (result.edits) { let changedLineCount = change.length === 1 ? (change[0].to.line - change[0].from.line + 1) : null; if(changedLineCount && changedLineCount > 1 && changedLineCount === change[0].removed.length && _isSamePos( change[0].from, 0, 0)){ // whole file change. this.protocol.reload(); PerfUtils.addMeasurement(perfTimerName); } else { applyEditsPromise = this.protocol.evaluate("_LD.applyDOMEdits(" + JSON.stringify(result.edits) + ")"); applyEditsPromise.always(function () { if (!isNestedTimer) { PerfUtils.addMeasurement(perfTimerName); } }); } } this.errors = result.errors || []; this._updateErrorDisplay(); // Debug-only: compare in-memory vs. in-browser DOM // edit this file or set a conditional breakpoint at the top of this function: // "this._debug = true, false" if (this._debug) { console.log("Edits applied to browser were:"); console.log(JSON.stringify(result.edits, null, 2)); applyEditsPromise.done(function () { self._compareWithBrowser(change); }); } }; /** * @private * Handles message DocumentRelated from the browser. * @param {$.Event} event * @param {Object} msg */ LiveHTMLDocument.prototype._onRelated = function (event, msg) { this._relatedDocuments = msg.related; return; }; /** * @private * Handles message Stylesheet.Added from the browser. * @param {$.Event} event * @param {Object} msg */ LiveHTMLDocument.prototype._onStylesheetAdded = function (event, msg) { this._relatedDocuments.stylesheets[msg.href] = true; return; }; /** * @private * Handles message Stylesheet.Removed from the browser. * @param {$.Event} event * @param {Object} msg */ LiveHTMLDocument.prototype._onStylesheetRemoved = function (event, msg) { delete (this._relatedDocuments.stylesheets[msg.href]); return; }; /** * @private * Handles message Script.Added from the browser. * @param {$.Event} event * @param {Object} msg */ LiveHTMLDocument.prototype._onScriptAdded = function (event, msg) { this._relatedDocuments.scripts[msg.src] = true; return; }; /** * @private * Handles message Script.Removed from the browser. * @param {$.Event} event * @param {Object} msg */ LiveHTMLDocument.prototype._onScriptRemoved = function (event, msg) { delete (this._relatedDocuments.scripts[msg.src]); return; }; /** * For the given path, check if the document is related to the live HTML document. * Related means that is an external Javascript or CSS file that is included as part of the DOM. * @param {String} fullPath. * @return {boolean} - is related or not. */ LiveHTMLDocument.prototype.isRelated = function (fullPath) { return (this._relatedDocuments.scripts[this.urlResolver(fullPath)] || this._relatedDocuments.stylesheets[this.urlResolver(fullPath)]); }; LiveHTMLDocument.prototype.getRelated = function () { return this._relatedDocuments; }; // Export the class module.exports = LiveHTMLDocument; });