ace.js: fix the logic to inline CSS

fix-css-inlining
webzwo0i 2020-08-25 20:46:10 +02:00
parent b3b3040204
commit 37e8e7af32
2 changed files with 191 additions and 25 deletions

View File

@ -1,6 +1,7 @@
/**
* This Module manages all /minified/* requests. It controls the
* minification && compression of Javascript and CSS.
* @file
* This Module manages all /static/* requests. It controls the
* minification and compression of Javascript and CSS.
*/
/*
@ -23,8 +24,6 @@ var ERR = require("async-stacktrace");
var settings = require('./Settings');
var async = require('async');
var fs = require('fs');
var StringDecoder = require('string_decoder').StringDecoder;
var CleanCSS = require('clean-css');
var path = require('path');
var plugins = require("ep_etherpad-lite/static/js/pluginfw/plugin_defs");
var RequireKernel = require('etherpad-require-kernel');
@ -32,6 +31,7 @@ var urlutil = require('url');
var mime = require('mime-types')
var Threads = require('threads')
var log4js = require('log4js');
let _ = require('underscore')
var logger = log4js.getLogger("Minify");
@ -55,6 +55,14 @@ var LIBRARY_WHITELIST = [
// Rewrite tar to include modules with no extensions and proper rooted paths.
var LIBRARY_PREFIX = 'ep_etherpad-lite/static/js';
exports.tar = {};
/**
* Prefix a path with `LIBRARY_PREFIX`
* If path starts with '$' it is not prefixed
*
* @param {string} path
* @returns {string}
*/
function prefixLocalLibraryPath(path) {
if (path.charAt(0) == '$') {
return path.slice(1);
@ -76,8 +84,13 @@ for (var key in tar) {
);
}
// What follows is a terrible hack to avoid loop-back within the server.
// TODO: Serve files from another service, or directly from the file system.
/**
* @param {string} url The file to be retrieved
* @param {'GET'|'HEAD'} method The request method
*
* What follows is a terrible hack to avoid loop-back within the server.
* @todo Serve files from another service, or directly from the file system.
*/
function requestURI(url, method, headers, callback) {
var parsedURL = urlutil.parse(url);
@ -142,8 +155,7 @@ function requestURIs(locations, method, headers, callback) {
/**
* creates the minifed javascript for the given minified name
* @param req the Express request
* @param res the Express response
*
*/
function minify(req, res)
{
@ -233,16 +245,53 @@ function minify(req, res)
}, 3);
}
// find all includes in ace.js and embed them.
/**
* Find all includes in ace.js and if `settings.minify`
* is enabled it compresses and embeds them.
* In case `settings.minify` is false, it only embeds the
* `require-kernel`.
* The format of the INCLUDE_-lines is explained in `ace.js`
*
* @param {Function} callback
*
*/
function getAceFile(callback) {
fs.readFile(ROOT_DIR + 'js/ace.js', "utf8", function(err, data) {
if(ERR(err, callback)) return;
// Find all includes in ace.js and embed them
var founds = data.match(/\$\$INCLUDE_[a-zA-Z_]+\("[^"]*"\)/gi);
if (!settings.minify) {
founds = [];
let parts = [];
let founds = [];
let result;
// files are inlined only when minify is true
if(settings.minify){
/**
* @example
* $$INCLUDE_CSS("../static/css/iframe_editor.css")
* $$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString);
* $$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString);
* $$INCLUDE_CSS("../static/skins/" + clientVars.skinName + "/pad.css?v=" + clientVars.randomVersionString);
*/
// matches a INCLUDE_CSS line and parses it's parts
let includesLine = data.match(/^\s+?\$\$INCLUDE_CSS\(.*$/mg);
includesLine.map(function(line){
parts = line.split(/ |\+|"|;|\)/);
parts.push('")');
result = parts.map(function(part){
if(~part.indexOf("clientVars.skinName")){
return settings.skinName;
} else if(~part.indexOf("clientVars.randomVersionString")){
return settings.randomVersionString;
} else if(part === "$$INCLUDE_CSS("){
return '$$INCLUDE_CSS("';
} else {
return part;
}
}).filter(Boolean).join("");
founds.push(result);
})
founds = _.uniq(founds);
}
// Always include the require kernel.
founds.push('$$INCLUDE_JS("../static/js/require-kernel.js")');
@ -253,10 +302,14 @@ function getAceFile(callback) {
// them into the file.
async.forEach(founds, function (item, callback) {
var filename = item.match(/"([^"]*)"/)[1];
// the file that is included from client side contains the version string,
// so we need to keep it as key for Ace2Editor.EMBEDED. However, the file
// is located at a path that does not contain the version string.
var resource = filename.split("?v=")[0];
// Hostname "invalid.invalid" is a dummy value to allow parsing as a URI.
var baseURI = 'http://invalid.invalid';
var resourceURI = baseURI + path.normalize(path.join('/static/', filename));
var resourceURI = baseURI + path.normalize(path.join('/static/', resource));
resourceURI = resourceURI.replace(/\\/g, '/'); // Windows (safe generally?)
requestURI(resourceURI, 'GET', {}, function (status, headers, body) {
@ -275,7 +328,14 @@ function getAceFile(callback) {
});
}
// Check for the existance of the file and get the last modification date.
/**
*
* Check for the existance of the file and get the last modification date.
*
* @param {string} filename The name of the file
* @param {Function} callback
* @param {number} dirStatLimit
*/
function statFile(filename, callback, dirStatLimit) {
/*
* The only external call to this function provides an explicit value for
@ -314,6 +374,16 @@ function statFile(filename, callback, dirStatLimit) {
});
}
}
/**
* Iterates over `static/js` and `static/css` to find the modifiedtime of the most recent modified
* file. Used for `ace.js` because it has inlined files
*
* @todo Iterating over `static/js` is not necessary anymore, because inlining JS functionality has been removed.
* only require-kernel needs to be checked.
*
* @param {Function} callback
*/
function lastModifiedDateOfEverything(callback) {
var folders2check = [ROOT_DIR + 'js/', ROOT_DIR + 'css/'];
var latestModification = 0;
@ -354,16 +424,40 @@ function lastModifiedDateOfEverything(callback) {
});
}
// This should be provided by the module, but until then, just use startup
// time.
/**
* @todo This should be provided by the module, but until then, just use startup
time.
*/
var _requireLastModified = new Date();
/**
* Returns the startup time as UTCString
*
* @returns {string} startup time as UTCString
*/
function requireLastModified() {
return _requireLastModified.toUTCString();
}
/**
* Returns the source code of `etherpad-require-kernel`s kernel definition
* @todo compress if minify is enabled
*
* @returns {string} the `etherpad-require-kernel` definition
*/
function requireDefinition() {
return 'var require = ' + RequireKernel.kernelSource + ';\n';
}
/**
* Calls `getFile` to retrieve file content and if it's javascript or css it compresses
* the file via `threadPool` and calls `callback` with the compressed content.
* In case `settings.minify` is false it just returns the uncompressed content
*
* @param {string} filename file to be handled
* @param {string} contentType content type of file
* @param {Function} callback Function to be called with the compressed content
*/
function getFileCompressed(filename, contentType, callback) {
getFile(filename, function (error, content) {
if (error || !content || !settings.minify) {
@ -405,6 +499,13 @@ function getFileCompressed(filename, contentType, callback) {
});
}
/**
* Gets the file `filename` and calls callback with the file's content
* Special handling of `ace.js` and `require-kernel.js` included
*
* @param {string} filename The file to be processed
* @param {Function} callback function to be called with the file's content
*/
function getFile(filename, callback) {
if (filename == 'js/ace.js') {
getAceFile(callback);

View File

@ -1,4 +1,13 @@
/**
* @file
* This file loads the iframes of the Editor.
* It constructs the HTML for the inner iframe, that contains all the pad lines, and
* for the outer iframe, that contains the sidediv with line numbers. The outer iframe
* dynamically inserts the inner iframe in it's onload event handler.
* The file is dynamically modified before delivered to clients in `Minify.js`. If `settings.minify`
* is enabled, then CSS files included here with `INCLUDE_CSS` are inlined. If `settings.minify`
* is false, then only the require-kernel is included.
*
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
@ -33,6 +42,12 @@ var hooks = require('./pluginfw/hooks');
var pluginUtils = require('./pluginfw/shared');
var _ = require('./underscore');
/**
* Puts javascript code given in parameter `source` inside script tags
*
* @param {string} source javascript code
* @returns {string} A script element containing `source`
*/
function scriptTag(source) {
return (
'<script type="text/javascript">\n'
@ -160,6 +175,13 @@ function Ace2Editor()
/**
* Takes an array of filenames and if they are keys in `Ace2Editor.EMBEDED` they
* are embeded and if not they should be included as remote links
*
* @param {string[]} files array of filenames
* @returns {{embeded: string[], remote: string[]}} An object containing filenames to embed and those to be included as remote files
*/
function sortFilesByEmbeded(files) {
var embededFiles = [];
var remoteFiles = [];
@ -179,6 +201,14 @@ function Ace2Editor()
return {embeded: embededFiles, remote: remoteFiles};
}
/**
* Inserts style tags with css content from the files in `files`
* and link tags that reference remote stylesheets
*
* @param {string[]} buffer An array of html tags as strings
* @param {string[]} files An array of filenames
*/
function pushStyleTagsFor(buffer, files) {
var sorted = sortFilesByEmbeded(files);
var embededFiles = sorted.embeded;
@ -192,8 +222,8 @@ function Ace2Editor()
}
buffer.push('<\/style>');
}
for (var i = 0, ii = remoteFiles.length; i < ii; i++) {
var file = remoteFiles[i];
for (i = 0, ii = remoteFiles.length; i < ii; i++) {
file = remoteFiles[i];
buffer.push('<link rel="stylesheet" type="text/css" href="' + encodeURI(file) + '"\/>');
}
}
@ -227,10 +257,31 @@ function Ace2Editor()
iframeHTML.push(doctype);
iframeHTML.push("<html class='inner-editor " + clientVars.skinVariants + "'><head>");
// calls to these functions ($$INCLUDE_...) are replaced when this file is processed
// and compressed, putting the compressed code from the named file directly into the
// source here.
// these lines must conform to a specific format because they are passed by the build script:
/**
* calls to these functions ($$INCLUDE_...) are replaced when this file is processed
* and compressed, putting the compressed code from the named file directly into the
* source here.
*
* When changing this lines ensure that the logic in `Minify.js` that inlines the files,
* knows which files to include. Files that should be inlined must appear on a newline with
* only whitespaces before them and can include pure filenames
*
* $$INCLUDE_CSS("../static/css/iframe_editor.css")
*
* filenames with an version string attached
* $$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString);
* in which case it assumes the string after `?v=` is based on `settings.randomVersionString`
*
* and filenames matching /skins/, either
* $$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString);
* or
* $$INCLUDE_CSS("../static/skins/" + clientVars.skinName + "/pad.css?v=" + clientVars.randomVersionString);
* in which case it assumes the first string to be the path, followed after
* `settings.skinName`, followed by the filename and `settings.randomVersionString` in case
* the filename contains `?v=`
*
* Double quotes *must* be used
*/
var includedCSS = [];
var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)};
$$INCLUDE_CSS("../static/css/iframe_editor.css");
@ -240,6 +291,10 @@ function Ace2Editor()
$$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString);
}
/**
* @todo
* css from plugins is not inlined
*/
var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){
if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css
return path;
@ -316,12 +371,19 @@ window.onload = function () {\n\
var outerHTML = [doctype, '<html class="inner-editor outerdoc ' + clientVars.skinVariants + '"><head>']
var includedCSS = [];
var $$INCLUDE_CSS = function(filename) {includedCSS.push(filename)};
includedCSS = [];
/*
* When using $$INCLUDE_CSS read the comment above first
*/
$$INCLUDE_CSS = function(filename) {includedCSS.push(filename)};
$$INCLUDE_CSS("../static/css/iframe_editor.css");
$$INCLUDE_CSS("../static/css/pad.css?v=" + clientVars.randomVersionString);
/**
* @todo
* css from plugins is not inlined
*/
var additionalCSS = _(hooks.callAll("aceEditorCSS")).map(function(path){
if (path.match(/\/\//)) { // Allow urls to external CSS - http(s):// and //some/path.css
return path;
@ -346,6 +408,9 @@ window.onload = function () {\n\
'<div id="linemetricsdiv">x</div>',
'</body></html>');
/**
* @type {HTMLIFrameElement}
*/
var outerFrame = document.createElement("IFRAME");
outerFrame.name = "ace_outer";
outerFrame.frameBorder = 0; // for IE