diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12a9fd431..5f60b235f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,26 @@
+# 1.8.13
+
+### Notable fixes
+
+* Fixed a bug in the safeRun.sh script (#4935)
+* Don't create sessions on some static resources (#4921)
+* Fixed issue with non-opening device keyboard on smartphones (#4929)
+* Add version string to iframe_editor.css to prevent stale cache entry (#4964)
+
+### Notable enhancements
+
+* Refactor pad loading (no document.write anymore) (#4960)
+* Improve import/export functionality, logging and tests (#4957)
+* Refactor CSS manager creation (#4963)
+* Better metrics
+* Add test for client height (#4965)
+
+### Dependencies
+
+* ueberDB2 1.3.2 -> 1.4.4
+* express-rate-limit 5.2.5 -> 5.2.6
+* etherpad-require-kernel 1.0.9 -> 1.0.11
+
# 1.8.12
Special mention: Thanks to Sauce Labs for additional testing tunnels to help us grow! :)
diff --git a/README.md b/README.md
index 4bc972285..d3543186b 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ Etherpad is extremely flexible providing you the means to modify it to solve wha
* [Video Chat](https://video.etherpad.com) - Plugins to enable Video and Audio chat in a pad.
* [Collaboration++](https://collab.etherpad.com) - Plugins to improve the really-real time collaboration experience, suitable for busy pads.
* [Document Analysis](https://analysis.etherpad.com) - Plugins to improve author and document analysis during and post creation.
+* [Scale](https://shard.etherpad.com) - Etherpad running at scale with pad sharding which allows Etherpad to scale to ∞ number of Active Pads with up to ~20,000 edits per second, per pad.
# Project Status
diff --git a/src/bin/safeRun.sh b/src/bin/safeRun.sh
index d9efa241a..d980b9300 100755
--- a/src/bin/safeRun.sh
+++ b/src/bin/safeRun.sh
@@ -24,8 +24,8 @@ fatal() { error "$@"; exit 1; }
LAST_EMAIL_SEND=0
# Move to the Etherpad base directory.
-MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1
-try cd "${MY_DIR}/../.."
+MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
+cd "${MY_DIR}/../.." || exit 1
# Check if a logfile parameter is set
LOG="$1"
diff --git a/src/locales/ku-latn.json b/src/locales/ku-latn.json
index d1194ce04..537c4074f 100644
--- a/src/locales/ku-latn.json
+++ b/src/locales/ku-latn.json
@@ -31,7 +31,7 @@
"pad.settings.stickychat": "Di ekranê de hertim çet bike",
"pad.settings.chatandusers": "Çeta û Bikarhênera Nîşan bide",
"pad.settings.colorcheck": "Rengên nivîskarîye",
- "pad.settings.linenocheck": "Hejmarên rêze",
+ "pad.settings.linenocheck": "Hejmarên rêzê",
"pad.settings.rtlcheck": "Bila naverok ji raste ber bi çepe be xwendin?",
"pad.settings.fontType": "Tîpa nivîsê:",
"pad.settings.language": "Ziman:",
diff --git a/src/node/db/DB.js b/src/node/db/DB.js
index c0993e8ec..e1097e905 100644
--- a/src/node/db/DB.js
+++ b/src/node/db/DB.js
@@ -24,6 +24,7 @@
const ueberDB = require('ueberdb2');
const settings = require('../utils/Settings');
const log4js = require('log4js');
+const stats = require('../stats');
const util = require('util');
// set database settings
@@ -48,6 +49,13 @@ exports.init = async () => await new Promise((resolve, reject) => {
process.exit(1);
}
+ if (db.metrics != null) {
+ for (const [metric, value] of Object.entries(db.metrics)) {
+ if (typeof value !== 'number') continue;
+ stats.gauge(`ueberdb_${metric}`, () => db.metrics[metric]);
+ }
+ }
+
// everything ok, set up Promise-based methods
['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove'].forEach((fn) => {
exports[fn] = util.promisify(db[fn].bind(db));
diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js
index fbb9e57da..0bf75a17f 100644
--- a/src/node/handler/ExportHandler.js
+++ b/src/node/handler/ExportHandler.js
@@ -33,24 +33,12 @@ const util = require('util');
const fsp_writeFile = util.promisify(fs.writeFile);
const fsp_unlink = util.promisify(fs.unlink);
-let convertor = null;
-
-// load abiword only if it is enabled
-if (settings.abiword != null) {
- convertor = require('../utils/Abiword');
-}
-
-// Use LibreOffice if an executable has been defined in the settings
-if (settings.soffice != null) {
- convertor = require('../utils/LibreOffice');
-}
-
const tempDirectory = os.tmpdir();
/**
* do a requested export
*/
-const doExport = async (req, res, padId, readOnlyId, type) => {
+exports.doExport = async (req, res, padId, readOnlyId, type) => {
// avoid naming the read-only file as the original pad's id
let fileName = readOnlyId ? readOnlyId : padId;
@@ -85,7 +73,7 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
const newHTML = await hooks.aCallFirst('exportHTMLSend', html);
if (newHTML.length) html = newHTML;
res.send(html);
- throw 'stop';
+ return;
}
// else write the html export to a file
@@ -98,7 +86,7 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
html = null;
await TidyHtml.tidy(srcFile);
- // send the convert job to the convertor (abiword, libreoffice, ..)
+ // send the convert job to the converter (abiword, libreoffice, ..)
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
// Allow plugins to overwrite the convert in export process
@@ -106,12 +94,11 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
if (result.length > 0) {
// console.log("export handled by plugin", destFile);
} else {
- // @TODO no Promise interface for convertors (yet)
- await new Promise((resolve, reject) => {
- convertor.convertFile(srcFile, destFile, type, (err) => {
- err ? reject('convertFailed') : resolve();
- });
- });
+ const converter =
+ settings.soffice != null ? require('../utils/LibreOffice')
+ : settings.abiword != null ? require('../utils/Abiword')
+ : null;
+ await converter.convertFile(srcFile, destFile, type);
}
// send the file
@@ -128,11 +115,3 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
await fsp_unlink(destFile);
}
};
-
-exports.doExport = (req, res, padId, readOnlyId, type) => {
- doExport(req, res, padId, readOnlyId, type).catch((err) => {
- if (err !== 'stop') {
- throw err;
- }
- });
-};
diff --git a/src/node/handler/ImportHandler.js b/src/node/handler/ImportHandler.js
index d97ce91ef..acaaf0927 100644
--- a/src/node/handler/ImportHandler.js
+++ b/src/node/handler/ImportHandler.js
@@ -55,17 +55,17 @@ const rm = async (path) => {
}
};
-let convertor = null;
+let converter = null;
let exportExtension = 'htm';
// load abiword only if it is enabled and if soffice is disabled
if (settings.abiword != null && settings.soffice == null) {
- convertor = require('../utils/Abiword');
+ converter = require('../utils/Abiword');
}
// load soffice only if it is enabled
if (settings.soffice != null) {
- convertor = require('../utils/LibreOffice');
+ converter = require('../utils/LibreOffice');
exportExtension = 'html';
}
@@ -80,8 +80,8 @@ const doImport = async (req, res, padId) => {
// set html in the pad
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
- // setting flag for whether to use convertor or not
- let useConvertor = (convertor != null);
+ // setting flag for whether to use converter or not
+ let useConverter = (converter != null);
const form = new formidable.IncomingForm();
form.keepExtensions = true;
@@ -170,30 +170,25 @@ const doImport = async (req, res, padId) => {
// convert file to html if necessary
if (!importHandledByPlugin && !directDatabaseAccess) {
if (fileIsTXT) {
- // Don't use convertor for text files
- useConvertor = false;
+ // Don't use converter for text files
+ useConverter = false;
}
// See https://github.com/ether/etherpad-lite/issues/2572
- if (fileIsHTML || !useConvertor) {
- // if no convertor only rename
+ if (fileIsHTML || !useConverter) {
+ // if no converter only rename
await fs.rename(srcFile, destFile);
} else {
- // @TODO - no Promise interface for convertors (yet)
- await new Promise((resolve, reject) => {
- convertor.convertFile(srcFile, destFile, exportExtension, (err) => {
- // catch convert errors
- if (err) {
- logger.warn(`Converting Error: ${err.stack || err}`);
- return reject(new ImportError('convertFailed'));
- }
- resolve();
- });
- });
+ try {
+ await converter.convertFile(srcFile, destFile, exportExtension);
+ } catch (err) {
+ logger.warn(`Converting Error: ${err.stack || err}`);
+ throw new ImportError('convertFailed');
+ }
}
}
- if (!useConvertor && !directDatabaseAccess) {
+ if (!useConverter && !directDatabaseAccess) {
// Read the file with no encoding for raw buffer access.
const buf = await fs.readFile(destFile);
@@ -224,7 +219,7 @@ const doImport = async (req, res, padId) => {
// change text of the pad and broadcast the changeset
if (!directDatabaseAccess) {
- if (importHandledByPlugin || useConvertor || fileIsHTML) {
+ if (importHandledByPlugin || useConverter || fileIsHTML) {
try {
await importHtml.setPadHTML(pad, text);
} catch (err) {
diff --git a/src/node/hooks/express/importexport.js b/src/node/hooks/express/importexport.js
index d6f287c6b..ab8d60376 100644
--- a/src/node/hooks/express/importexport.js
+++ b/src/node/hooks/express/importexport.js
@@ -60,7 +60,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
}
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
- exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
+ await exportHandler.doExport(req, res, padId, readOnlyId, req.params.type);
}
})().catch((err) => next(err || new Error(err)));
});
diff --git a/src/node/hooks/express/webaccess.js b/src/node/hooks/express/webaccess.js
index 51d57ae2e..5ff957a52 100644
--- a/src/node/hooks/express/webaccess.js
+++ b/src/node/hooks/express/webaccess.js
@@ -10,12 +10,20 @@ const readOnlyManager = require('../../db/ReadOnlyManager');
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
const staticPathsRE = new RegExp(`^/(?:${[
- 'api/.*',
+ 'api(?:/.*)?',
'favicon\\.ico',
+ 'ep/pad/connection-diagnostic-info',
+ 'javascript',
'javascripts/.*',
+ 'jserror/?',
'locales\\.json',
+ 'locales/.*',
+ 'rest/.*',
'pluginfw/.*',
+ 'robots.txt',
'static/.*',
+ 'stats/?',
+ 'tests/frontend(?:/.*)?'
].join('|')})$`);
exports.normalizeAuthzLevel = (level) => {
diff --git a/src/node/server.js b/src/node/server.js
index fc62b4471..a574116eb 100755
--- a/src/node/server.js
+++ b/src/node/server.js
@@ -46,6 +46,7 @@ const hooks = require('../static/js/pluginfw/hooks');
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
const plugins = require('../static/js/pluginfw/plugins');
const settings = require('./utils/Settings');
+const stats = require('./stats');
const logger = log4js.getLogger('server');
@@ -104,8 +105,6 @@ exports.start = async () => {
// Check if Etherpad version is up-to-date
UpdateCheck.check();
- // start up stats counting system
- const stats = require('./stats');
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
@@ -215,6 +214,7 @@ exports.exit = async (err = null) => {
logger.info('Received SIGTERM signal');
err = null;
} else if (err != null) {
+ logger.error(`Metrics at time of fatal error:\n${JSON.stringify(stats.toJSON(), null, 2)}`);
logger.error(err.stack || err.toString());
process.exitCode = 1;
if (exitCalled) {
diff --git a/src/node/utils/Abiword.js b/src/node/utils/Abiword.js
index b93646cd5..1ed487ae1 100644
--- a/src/node/utils/Abiword.js
+++ b/src/node/utils/Abiword.js
@@ -24,111 +24,67 @@ const async = require('async');
const settings = require('./Settings');
const os = require('os');
-let doConvertTask;
-
// on windows we have to spawn a process for each convertion,
// cause the plugin abicommand doesn't exist on this platform
if (os.type().indexOf('Windows') > -1) {
- let stdoutBuffer = '';
-
- doConvertTask = (task, callback) => {
- // span an abiword process to perform the conversion
- const abiword = spawn(settings.abiword, [`--to=${task.destFile}`, task.srcFile]);
-
- // delegate the processing of stdout to another function
- abiword.stdout.on('data', (data) => {
- // add data to buffer
- stdoutBuffer += data.toString();
+ exports.convertFile = async (srcFile, destFile, type) => {
+ const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
+ let stdoutBuffer = '';
+ abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
+ abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
+ await new Promise((resolve, reject) => {
+ abiword.on('exit', (code) => {
+ if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
+ if (stdoutBuffer !== '') {
+ console.log(stdoutBuffer);
+ }
+ resolve();
+ });
});
-
- // append error messages to the buffer
- abiword.stderr.on('data', (data) => {
- stdoutBuffer += data.toString();
- });
-
- // throw exceptions if abiword is dieing
- abiword.on('exit', (code) => {
- if (code !== 0) {
- return callback(`Abiword died with exit code ${code}`);
- }
-
- if (stdoutBuffer !== '') {
- console.log(stdoutBuffer);
- }
-
- callback();
- });
- };
-
- exports.convertFile = (srcFile, destFile, type, callback) => {
- doConvertTask({srcFile, destFile, type}, callback);
};
// on unix operating systems, we can start abiword with abicommand and
// communicate with it via stdin/stdout
// thats much faster, about factor 10
} else {
- // spawn the abiword process
let abiword;
let stdoutCallback = null;
const spawnAbiword = () => {
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
let stdoutBuffer = '';
let firstPrompt = true;
-
- // append error messages to the buffer
- abiword.stderr.on('data', (data) => {
- stdoutBuffer += data.toString();
- });
-
- // abiword died, let's restart abiword and return an error with the callback
+ abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
abiword.on('exit', (code) => {
spawnAbiword();
- stdoutCallback(`Abiword died with exit code ${code}`);
+ if (stdoutCallback != null) {
+ stdoutCallback(new Error(`Abiword died with exit code ${code}`));
+ stdoutCallback = null;
+ }
});
-
- // delegate the processing of stdout to a other function
abiword.stdout.on('data', (data) => {
- // add data to buffer
stdoutBuffer += data.toString();
-
// we're searching for the prompt, cause this means everything we need is in the buffer
if (stdoutBuffer.search('AbiWord:>') !== -1) {
- // filter the feedback message
- const err = stdoutBuffer.search('OK') !== -1 ? null : stdoutBuffer;
-
- // reset the buffer
+ const err = stdoutBuffer.search('OK') !== -1 ? null : new Error(stdoutBuffer);
stdoutBuffer = '';
-
- // call the callback with the error message
- // skip the first prompt
if (stdoutCallback != null && !firstPrompt) {
stdoutCallback(err);
stdoutCallback = null;
}
-
firstPrompt = false;
}
});
};
spawnAbiword();
- doConvertTask = (task, callback) => {
+ const queue = async.queue((task, callback) => {
abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
- // create a callback that calls the task callback and the caller callback
stdoutCallback = (err) => {
- callback();
- console.log('queue continue');
- try {
- task.callback(err);
- } catch (e) {
- console.error('Abiword File failed to convert', e);
- }
+ if (err != null) console.error('Abiword File failed to convert', err);
+ callback(err);
};
- };
+ }, 1);
- // Queue with the converts we have to do
- const queue = async.queue(doConvertTask, 1);
- exports.convertFile = (srcFile, destFile, type, callback) => {
- queue.push({srcFile, destFile, type, callback});
+ exports.convertFile = async (srcFile, destFile, type) => {
+ await queue.pushAsync({srcFile, destFile, type});
};
}
diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.js
index 914d79a4b..276bc3003 100644
--- a/src/node/utils/LibreOffice.js
+++ b/src/node/utils/LibreOffice.js
@@ -18,7 +18,7 @@
*/
const async = require('async');
-const fs = require('fs');
+const fs = require('fs').promises;
const log4js = require('log4js');
const os = require('os');
const path = require('path');
@@ -27,76 +27,50 @@ const spawn = require('child_process').spawn;
const libreOfficeLogger = log4js.getLogger('LibreOffice');
-const doConvertTask = (task, callback) => {
+const doConvertTask = async (task) => {
const tmpDir = os.tmpdir();
- async.series([
- /*
- * use LibreOffice to convert task.srcFile to another format, given in
- * task.type
- */
- (callback) => {
- libreOfficeLogger.debug(
- `Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`
- );
- const soffice = spawn(settings.soffice, [
- '--headless',
- '--invisible',
- '--nologo',
- '--nolockcheck',
- '--writer',
- '--convert-to',
- task.type,
- task.srcFile,
- '--outdir',
- tmpDir,
- ]);
- // Soffice/libreoffice is buggy and often hangs.
- // To remedy this we kill the spawned process after a while.
- const hangTimeout = setTimeout(() => {
- soffice.stdin.pause(); // required to kill hanging threads
- soffice.kill();
- }, 120000);
-
- let stdoutBuffer = '';
-
- // Delegate the processing of stdout to another function
- soffice.stdout.on('data', (data) => {
- stdoutBuffer += data.toString();
- });
-
- // Append error messages to the buffer
- soffice.stderr.on('data', (data) => {
- stdoutBuffer += data.toString();
- });
-
- soffice.on('exit', (code) => {
- clearTimeout(hangTimeout);
- if (code !== 0) {
- // Throw an exception if libreoffice failed
- return callback(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`);
- }
-
- // if LibreOffice exited succesfully, go on with processing
- callback();
- });
- },
-
- // Move the converted file to the correct place
- (callback) => {
- const filename = path.basename(task.srcFile);
- const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
- const sourcePath = path.join(tmpDir, sourceFile);
- libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
- fs.rename(sourcePath, task.destFile, callback);
- },
- ], (err) => {
- // Invoke the callback for the local queue
- callback();
-
- // Invoke the callback for the task
- task.callback(err);
+ libreOfficeLogger.debug(
+ `Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`);
+ const soffice = spawn(settings.soffice, [
+ '--headless',
+ '--invisible',
+ '--nologo',
+ '--nolockcheck',
+ '--writer',
+ '--convert-to',
+ task.type,
+ task.srcFile,
+ '--outdir',
+ tmpDir,
+ ]);
+ // Soffice/libreoffice is buggy and often hangs.
+ // To remedy this we kill the spawned process after a while.
+ const hangTimeout = setTimeout(() => {
+ soffice.stdin.pause(); // required to kill hanging threads
+ soffice.kill();
+ }, 120000);
+ let stdoutBuffer = '';
+ soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
+ soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
+ await new Promise((resolve, reject) => {
+ soffice.on('exit', (code) => {
+ clearTimeout(hangTimeout);
+ if (code !== 0) {
+ const err =
+ new Error(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`);
+ libreOfficeLogger.error(err.stack);
+ return reject(err);
+ }
+ resolve();
+ });
});
+
+ const filename = path.basename(task.srcFile);
+ const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
+ const sourcePath = path.join(tmpDir, sourceFile);
+ libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
+ await fs.rename(sourcePath, task.destFile);
};
// Conversion tasks will be queued up, so we don't overload the system
@@ -110,7 +84,7 @@ const queue = async.queue(doConvertTask, 1);
* @param {String} type The type to convert into
* @param {Function} callback Standard callback function
*/
-exports.convertFile = (srcFile, destFile, type, callback) => {
+exports.convertFile = async (srcFile, destFile, type) => {
// Used for the moving of the file, not the conversion
const fileExtension = type;
@@ -129,23 +103,10 @@ exports.convertFile = (srcFile, destFile, type, callback) => {
// we need to convert to odt first, then to doc
// to avoid `Error: no export filter for /tmp/xxxx.doc` error
if (type === 'doc') {
- queue.push({
- srcFile,
- destFile: destFile.replace(/\.doc$/, '.odt'),
- type: 'odt',
- callback: () => {
- queue.push(
- {
- srcFile: srcFile.replace(/\.html$/, '.odt'),
- destFile,
- type,
- callback,
- fileExtension,
- }
- );
- },
- });
+ const intermediateFile = destFile.replace(/\.doc$/, '.odt');
+ await queue.pushAsync({srcFile, destFile: intermediateFile, type: 'odt', fileExtension: 'odt'});
+ await queue.pushAsync({srcFile: intermediateFile, destFile, type, fileExtension});
} else {
- queue.push({srcFile, destFile, type, callback, fileExtension});
+ await queue.pushAsync({srcFile, destFile, type, fileExtension});
}
};
diff --git a/src/node/utils/tar.json b/src/node/utils/tar.json
index 8fcc45aa2..7f1fe0135 100644
--- a/src/node/utils/tar.json
+++ b/src/node/utils/tar.json
@@ -14,6 +14,7 @@
, "pad_automatic_reconnect.js"
, "ace.js"
, "collab_client.js"
+ , "cssmanager.js"
, "pad_userlist.js"
, "pad_impexp.js"
, "pad_savedrevs.js"
@@ -61,7 +62,6 @@
, "Changeset.js"
, "ChangesetUtils.js"
, "skiplist.js"
- , "cssmanager.js"
, "colorutils.js"
, "undomodule.js"
, "$unorm/lib/unorm.js"
diff --git a/src/package-lock.json b/src/package-lock.json
index 14421f869..409d3a332 100644
--- a/src/package-lock.json
+++ b/src/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "ep_etherpad-lite",
- "version": "1.8.12",
+ "version": "1.8.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -15,9 +15,9 @@
}
},
"@azure/abort-controller": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.3.tgz",
- "integrity": "sha512-kCibMwqffnwlw3c+e879rCE1Am1I2BfhjOeO54XNA8i/cEuzktnBQbTrzh67XwibHO05YuNgZzSWy9ocVfFAGw==",
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
+ "integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
"requires": {
"tslib": "^2.0.0"
},
@@ -287,9 +287,9 @@
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
},
"@types/node": {
- "version": "14.14.31",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
- "integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g=="
+ "version": "14.14.34",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.34.tgz",
+ "integrity": "sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA=="
},
"@types/node-fetch": {
"version": "2.5.8",
@@ -2051,9 +2051,9 @@
}
},
"etherpad-require-kernel": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.9.tgz",
- "integrity": "sha1-7Y8E6f0szsOgBVu20t/p2ZkS5+I="
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.11.tgz",
+ "integrity": "sha512-I03bkNiBMrcsJRSl0IqotUU70s9v6VISrITj/cQgAoVQSoRFbV/NUn2fPIF4LskysTpmwlmwJqgfL2FZpAtxEw=="
},
"etherpad-yajsml": {
"version": "0.0.4",
@@ -2110,9 +2110,9 @@
}
},
"express-rate-limit": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.5.tgz",
- "integrity": "sha512-fv9mf4hWRKZHVlY8ChVNYnGxa49m0zQ6CrJxNiXe2IjJPqicrqoA/JOyBbvs4ufSSLZ6NTzhtgEyLcdfbe+Q6Q=="
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.6.tgz",
+ "integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA=="
},
"express-session": {
"version": "1.17.1",
@@ -7689,9 +7689,9 @@
"optional": true
},
"simple-git": {
- "version": "2.35.2",
- "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.35.2.tgz",
- "integrity": "sha512-UjOKsrz92Bx7z00Wla5V6qLSf5X2XSp0sL2gzKw1Bh7iJfDPDaU7gK5avIup0yo1/sMOSUMQer2b9GcnF6nmTQ==",
+ "version": "2.36.2",
+ "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.36.2.tgz",
+ "integrity": "sha512-orBEf65GfSiQMsYedbJXSiRNnIRvhbeE5rrxZuEimCpWxDZOav0KLy2IEiPi1YJCF+zaC2quiJF8A4TsxI9/tw==",
"requires": {
"@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1",
@@ -8436,13 +8436,12 @@
}
},
"ueberdb2": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.2.tgz",
- "integrity": "sha512-7Ub5jDsIS+qjjsNV7yp1CHXHVe2K9ZUpwaHi9BZf3ai0DxtuHOfMada1wxL6iyEjwYXh/Nsu80iyId51wHFf4A==",
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.4.tgz",
+ "integrity": "sha512-hcexgTdMa6gMquv5r6rOBsr76awMlqAjQiMMJ72qrzuatLYJ6D1EQTK/Jqo4nOD/jklXHM2yFw1mNcHsrlEzrw==",
"requires": {
"async": "^3.2.0",
"cassandra-driver": "^4.5.1",
- "channels": "0.0.4",
"dirty": "^1.1.1",
"elasticsearch": "^16.7.1",
"mongodb": "^3.6.3",
@@ -8774,9 +8773,9 @@
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
},
"xmldom": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz",
- "integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA=="
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.5.0.tgz",
+ "integrity": "sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA=="
},
"xmlhttprequest-ssl": {
"version": "1.5.5",
diff --git a/src/package.json b/src/package.json
index ada568d95..853065a22 100644
--- a/src/package.json
+++ b/src/package.json
@@ -38,10 +38,10 @@
"cookie-parser": "1.4.5",
"cross-spawn": "^7.0.3",
"ejs": "^3.1.6",
- "etherpad-require-kernel": "1.0.9",
+ "etherpad-require-kernel": "1.0.11",
"etherpad-yajsml": "0.0.4",
"express": "4.17.1",
- "express-rate-limit": "5.2.5",
+ "express-rate-limit": "5.2.6",
"express-session": "1.17.1",
"find-root": "1.1.0",
"formidable": "1.2.2",
@@ -69,7 +69,7 @@
"threads": "^1.4.0",
"tiny-worker": "^2.3.0",
"tinycon": "0.6.8",
- "ueberdb2": "^1.3.2",
+ "ueberdb2": "^1.4.4",
"underscore": "1.12.0",
"unorm": "1.6.0",
"wtfnode": "^0.8.4"
@@ -246,6 +246,6 @@
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
"test-container": "mocha --timeout 5000 tests/container/specs/api"
},
- "version": "1.8.12",
+ "version": "1.8.13",
"license": "Apache-2.0"
}
diff --git a/src/static/css/iframe_editor.css b/src/static/css/iframe_editor.css
index 2a63c3802..912174411 100644
--- a/src/static/css/iframe_editor.css
+++ b/src/static/css/iframe_editor.css
@@ -4,8 +4,7 @@
@import url('./lists_and_indents.css');
-html.inner-editor {
- height: auto !important;
+html.outer-editor, html.inner-editor {
background-color: transparent !important;
}
#outerdocbody {
@@ -38,6 +37,11 @@ html.inner-editor {
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
+ /*
+ * Make the contenteditable area at least as big as the screen so that mobile
+ * users can tap anywhere to bring up their device's keyboard.
+ */
+ min-height: 100vh;
}
#innerdocbody, #sidediv {
diff --git a/src/static/css/pad/layout.css b/src/static/css/pad/layout.css
index e5b79c268..7f77ca58d 100644
--- a/src/static/css/pad/layout.css
+++ b/src/static/css/pad/layout.css
@@ -1,11 +1,14 @@
html, body {
width: 100%;
- height: 100%;
+ height: auto;
margin: 0;
padding: 0;
}
-html:not(.inner-editor), html:not(.inner-editor) body {
+
+/* used in pad and timeslider */
+html.pad, html.pad body {
overflow: hidden;
+ height: 100%;
}
body {
display: flex;
diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js
index b47979c19..8c3627b04 100644
--- a/src/static/js/Changeset.js
+++ b/src/static/js/Changeset.js
@@ -50,10 +50,9 @@ exports.error = (msg) => {
* @param b {boolean} assertion condition
* @param msgParts {string} error to be passed if it fails
*/
-exports.assert = (b, msgParts) => {
+exports.assert = (b, ...msgParts) => {
if (!b) {
- const msg = Array.prototype.slice.call(arguments, 1).join('');
- exports.error(`Failed assertion: ${msg}`);
+ exports.error(`Failed assertion: ${msgParts.join('')}`);
}
};
diff --git a/src/static/js/ace.js b/src/static/js/ace.js
index 44556dd0f..059bac76e 100644
--- a/src/static/js/ace.js
+++ b/src/static/js/ace.js
@@ -25,22 +25,69 @@
// requires: undefined
const hooks = require('./pluginfw/hooks');
+const makeCSSManager = require('./cssmanager').makeCSSManager;
const pluginUtils = require('./pluginfw/shared');
const debugLog = (...args) => {};
-window.debugLog = debugLog;
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
// errors out unless given an absolute URL for a JavaScript-created element.
const absUrl = (url) => new URL(url, window.location.href).href;
-const scriptTag =
- (source) => ``;
+const eventFired = async (obj, event, cleanups = [], predicate = () => true) => {
+ if (typeof cleanups === 'function') {
+ predicate = cleanups;
+ cleanups = [];
+ }
+ await new Promise((resolve, reject) => {
+ let cleanup;
+ const successCb = () => {
+ if (!predicate()) return;
+ debugLog(`Ace2Editor.init() ${event} event on`, obj);
+ cleanup();
+ resolve();
+ };
+ const errorCb = () => {
+ const err = new Error(`Ace2Editor.init() error event while waiting for ${event} event`);
+ debugLog(`${err} on object`, obj);
+ cleanup();
+ reject(err);
+ };
+ cleanup = () => {
+ cleanup = () => {};
+ obj.removeEventListener(event, successCb);
+ obj.removeEventListener('error', errorCb);
+ };
+ cleanups.push(cleanup);
+ obj.addEventListener(event, successCb);
+ obj.addEventListener('error', errorCb);
+ });
+};
+
+// Resolves when the frame's document is ready to be mutated. Browsers seem to be quirky about
+// iframe ready events so this function throws the kitchen sink at the problem. Maybe one day we'll
+// find a concise general solution.
+const frameReady = async (frame) => {
+ // Can't do `const doc = frame.contentDocument;` because Firefox seems to asynchronously replace
+ // the document object after the frame is first created for some reason. ¯\_(ツ)_/¯
+ const doc = () => frame.contentDocument;
+ const cleanups = [];
+ try {
+ await Promise.race([
+ eventFired(frame, 'load', cleanups),
+ eventFired(frame.contentWindow, 'load', cleanups),
+ eventFired(doc(), 'load', cleanups),
+ eventFired(doc(), 'DOMContentLoaded', cleanups),
+ eventFired(doc(), 'readystatechange', cleanups, () => doc.readyState === 'complete'),
+ ]);
+ } finally {
+ for (const cleanup of cleanups) cleanup();
+ }
+};
const Ace2Editor = function () {
let info = {editor: this};
- window.ace2EditorInfo = info; // Make it accessible to iframes.
let loaded = false;
let actionsPendingInit = [];
@@ -109,16 +156,19 @@ const Ace2Editor = function () {
// returns array of {error: , time: +new Date()}
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
- const pushStyleTagsFor = (buffer, files) => {
+ const addStyleTagsFor = (doc, files) => {
for (const file of files) {
- buffer.push(``);
+ const link = doc.createElement('link');
+ link.rel = 'stylesheet';
+ link.type = 'text/css';
+ link.href = absUrl(encodeURI(file));
+ doc.head.appendChild(link);
}
};
this.destroy = pendingInit(() => {
info.ace_dispose();
info.frame.parentNode.removeChild(info.frame);
- delete window.ace2EditorInfo;
info = null; // prevent IE 6 closure memory leaks
});
@@ -127,7 +177,7 @@ const Ace2Editor = function () {
this.importText(initialCode);
const includedCSS = [
- '../static/css/iframe_editor.css',
+ `../static/css/iframe_editor.css?v=${clientVars.randomVersionString}`,
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
...hooks.callAll('aceEditorCSS').map(
// Allow urls to external CSS - http(s):// and //some/path.css
@@ -135,110 +185,135 @@ const Ace2Editor = function () {
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`,
];
- const doctype = '';
+ const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== '');
- const iframeHTML = [];
-
- iframeHTML.push(doctype);
- iframeHTML.push(``);
- pushStyleTagsFor(iframeHTML, includedCSS);
- const requireKernelUrl =
- absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
- iframeHTML.push(``);
- // Pre-fetch modules to improve load performance.
- for (const module of ['ace2_inner', 'ace2_common']) {
- const url = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
- `?callback=require.define&v=${clientVars.randomVersionString}`);
- iframeHTML.push(``);
- }
-
- iframeHTML.push(scriptTag(`(async () => {
- parent.parent.debugLog('Ace2Editor.init() inner frame ready');
- const require = window.require;
- require.setRootURI(${JSON.stringify(absUrl('../javascripts/src'))});
- require.setLibraryURI(${JSON.stringify(absUrl('../javascripts/lib'))});
- require.setGlobalKeyPath('require');
-
- // intentially moved before requiring client_plugins to save a 307
- window.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
- window.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
- window.plugins.adoptPluginsFromAncestorsOf(window);
-
- window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
-
- parent.parent.debugLog('Ace2Editor.init() waiting for plugins');
- await new Promise((resolve, reject) => window.plugins.ensure(
- (err) => err != null ? reject(err) : resolve()));
- parent.parent.debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
- const editorInfo = parent.parent.ace2EditorInfo;
- await new Promise((resolve, reject) => window.Ace2Inner.init(
- editorInfo, (err) => err != null ? reject(err) : resolve()));
- parent.parent.debugLog('Ace2Editor.init() Ace2Inner.init() returned');
- editorInfo.onEditorReady();
- })();`));
-
- iframeHTML.push('');
-
- hooks.callAll('aceInitInnerdocbodyHead', {
- iframeHTML,
- });
-
- iframeHTML.push(' ');
-
- const outerScript = `(async () => {
- await new Promise((resolve) => { window.onload = () => resolve(); });
- parent.debugLog('Ace2Editor.init() outer frame ready');
- window.onload = null;
- await new Promise((resolve) => setTimeout(resolve, 0));
- const iframe = document.createElement('iframe');
- iframe.name = 'ace_inner';
- iframe.title = 'pad';
- iframe.scrolling = 'no';
- iframe.frameBorder = 0;
- iframe.allowTransparency = true; // for IE
- iframe.ace_outerWin = window;
- document.body.insertBefore(iframe, document.body.firstChild);
- const doc = iframe.contentWindow.document;
- doc.open();
- doc.write(${JSON.stringify(iframeHTML.join('\n'))});
- doc.close();
- parent.debugLog('Ace2Editor.init() waiting for inner frame');
- })();`;
-
- const outerHTML =
- [doctype, ``];
- pushStyleTagsFor(outerHTML, includedCSS);
-
- // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly
- // (throbs busy while typing)
- const pluginNames = pluginUtils.clientPluginNames();
- outerHTML.push(
- '',
- '',
- scriptTag(outerScript),
- '',
- '',
- '',
- 'x
',
- '');
-
- const outerFrame = document.createElement('IFRAME');
+ const outerFrame = document.createElement('iframe');
outerFrame.name = 'ace_outer';
outerFrame.frameBorder = 0; // for IE
outerFrame.title = 'Ether';
+ // Some browsers do strange things unless the iframe has a src or srcdoc property:
+ // - Firefox replaces the frame's contentWindow.document object with a different object after
+ // the frame is created. This can be worked around by waiting for the window's load event
+ // before continuing.
+ // - Chrome never fires any events on the frame or document. Eventually the document's
+ // readyState becomes 'complete' even though it never fires a readystatechange event.
+ // - Safari behaves like Chrome.
+ outerFrame.srcdoc = '';
info.frame = outerFrame;
document.getElementById(containerId).appendChild(outerFrame);
-
- const editorDocument = outerFrame.contentWindow.document;
+ const outerWindow = outerFrame.contentWindow;
debugLog('Ace2Editor.init() waiting for outer frame');
- await new Promise((resolve, reject) => {
- info.onEditorReady = (err) => err != null ? reject(err) : resolve();
- editorDocument.open();
- editorDocument.write(outerHTML.join(''));
- editorDocument.close();
+ await frameReady(outerFrame);
+ debugLog('Ace2Editor.init() outer frame ready');
+
+ // Firefox might replace the outerWindow.document object after iframe creation so this variable
+ // is assigned after the Window's load event.
+ const outerDocument = outerWindow.document;
+
+ // tag
+ outerDocument.documentElement.classList.add('outer-editor', 'outerdoc', ...skinVariants);
+
+ // tag
+ addStyleTagsFor(outerDocument, includedCSS);
+ const outerStyle = outerDocument.createElement('style');
+ outerStyle.type = 'text/css';
+ outerStyle.title = 'dynamicsyntax';
+ outerDocument.head.appendChild(outerStyle);
+
+ // tag
+ outerDocument.body.id = 'outerdocbody';
+ outerDocument.body.classList.add('outerdocbody', ...pluginUtils.clientPluginNames());
+ const sideDiv = outerDocument.createElement('div');
+ sideDiv.id = 'sidediv';
+ sideDiv.classList.add('sidediv');
+ outerDocument.body.appendChild(sideDiv);
+ const lineMetricsDiv = outerDocument.createElement('div');
+ lineMetricsDiv.id = 'linemetricsdiv';
+ lineMetricsDiv.appendChild(outerDocument.createTextNode('x'));
+ outerDocument.body.appendChild(lineMetricsDiv);
+
+ const innerFrame = outerDocument.createElement('iframe');
+ innerFrame.name = 'ace_inner';
+ innerFrame.title = 'pad';
+ innerFrame.scrolling = 'no';
+ innerFrame.frameBorder = 0;
+ innerFrame.allowTransparency = true; // for IE
+ // The iframe MUST have a src or srcdoc property to avoid browser quirks. See the comment above
+ // outerFrame.srcdoc.
+ innerFrame.srcdoc = '';
+ innerFrame.ace_outerWin = outerWindow;
+ outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild);
+ const innerWindow = innerFrame.contentWindow;
+
+ debugLog('Ace2Editor.init() waiting for inner frame');
+ await frameReady(innerFrame);
+ debugLog('Ace2Editor.init() inner frame ready');
+
+ // Firefox might replace the innerWindow.document object after iframe creation so this variable
+ // is assigned after the Window's load event.
+ const innerDocument = innerWindow.document;
+
+ // tag
+ innerDocument.documentElement.classList.add('inner-editor', ...skinVariants);
+
+ // tag
+ addStyleTagsFor(innerDocument, includedCSS);
+ const requireKernel = innerDocument.createElement('script');
+ requireKernel.type = 'text/javascript';
+ requireKernel.src =
+ absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
+ innerDocument.head.appendChild(requireKernel);
+ // Pre-fetch modules to improve load performance.
+ for (const module of ['ace2_inner', 'ace2_common']) {
+ const script = innerDocument.createElement('script');
+ script.type = 'text/javascript';
+ script.src = absUrl(`../javascripts/lib/ep_etherpad-lite/static/js/${module}.js` +
+ `?callback=require.define&v=${clientVars.randomVersionString}`);
+ innerDocument.head.appendChild(script);
+ }
+ const innerStyle = innerDocument.createElement('style');
+ innerStyle.type = 'text/css';
+ innerStyle.title = 'dynamicsyntax';
+ innerDocument.head.appendChild(innerStyle);
+ const headLines = [];
+ hooks.callAll('aceInitInnerdocbodyHead', {iframeHTML: headLines});
+ const tmp = innerDocument.createElement('div');
+ tmp.innerHTML = headLines.join('\n');
+ while (tmp.firstChild) innerDocument.head.appendChild(tmp.firstChild);
+
+ // tag
+ innerDocument.body.id = 'innerdocbody';
+ innerDocument.body.classList.add('innerdocbody');
+ innerDocument.body.setAttribute('role', 'application');
+ innerDocument.body.setAttribute('spellcheck', 'false');
+ innerDocument.body.appendChild(innerDocument.createTextNode('\u00A0')); //
+
+ debugLog('Ace2Editor.init() waiting for require kernel load');
+ await eventFired(requireKernel, 'load');
+ debugLog('Ace2Editor.init() require kernel loaded');
+ const require = innerWindow.require;
+ require.setRootURI(absUrl('../javascripts/src'));
+ require.setLibraryURI(absUrl('../javascripts/lib'));
+ require.setGlobalKeyPath('require');
+
+ // intentially moved before requiring client_plugins to save a 307
+ innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner');
+ innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins');
+ innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow);
+
+ innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery;
+
+ debugLog('Ace2Editor.init() waiting for plugins');
+ await new Promise((resolve, reject) => innerWindow.plugins.ensure(
+ (err) => err != null ? reject(err) : resolve()));
+ debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
+ await innerWindow.Ace2Inner.init(info, {
+ inner: makeCSSManager(innerStyle.sheet),
+ outer: makeCSSManager(outerStyle.sheet),
+ parent: makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet),
});
+ debugLog('Ace2Editor.init() Ace2Inner.init() returned');
loaded = true;
doActionsPendingInit();
debugLog('Ace2Editor.init() done');
diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js
index 8fef627b5..d57773196 100644
--- a/src/static/js/ace2_inner.js
+++ b/src/static/js/ace2_inner.js
@@ -30,11 +30,10 @@ const htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks');
-function Ace2Inner(editorInfo) {
+function Ace2Inner(editorInfo, cssManagers) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
const makeContentCollector = require('./contentcollector').makeContentCollector;
- const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset');
@@ -157,10 +156,6 @@ function Ace2Inner(editorInfo) {
const scheduler = parent; // hack for opera required
- let dynamicCSS = null;
- let outerDynamicCSS = null;
- let parentDynamicCSS = null;
-
const performDocumentReplaceRange = (start, end, newText) => {
if (start === undefined) start = rep.selStart;
if (end === undefined) end = rep.selEnd;
@@ -181,12 +176,6 @@ function Ace2Inner(editorInfo) {
performDocumentApplyChangeset(cs);
};
- const initDynamicCSS = () => {
- dynamicCSS = makeCSSManager('dynamicsyntax');
- outerDynamicCSS = makeCSSManager('dynamicsyntax', 'outer');
- parentDynamicCSS = makeCSSManager('dynamicsyntax', 'parent');
- };
-
const changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
withCallbacks: (operationName, f) => {
inCallStackIfNecessary(operationName, () => {
@@ -214,15 +203,12 @@ function Ace2Inner(editorInfo) {
editorInfo.ace_getAuthorInfos = getAuthorInfos;
const setAuthorStyle = (author, info) => {
- if (!dynamicCSS) {
- return;
- }
const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));
const authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
- dynamicCSS,
- parentDynamicCSS,
- outerDynamicCSS,
+ dynamicCSS: cssManagers.inner,
+ outerDynamicCSS: cssManagers.outer,
+ parentDynamicCSS: cssManagers.parent,
info,
author,
authorSelector,
@@ -234,16 +220,16 @@ function Ace2Inner(editorInfo) {
}
if (!info) {
- dynamicCSS.removeSelectorStyle(authorSelector);
- parentDynamicCSS.removeSelectorStyle(authorSelector);
+ cssManagers.inner.removeSelectorStyle(authorSelector);
+ cssManagers.parent.removeSelectorStyle(authorSelector);
} else if (info.bgcolor) {
let bgcolor = info.bgcolor;
if ((typeof info.fade) === 'number') {
bgcolor = fadeColor(bgcolor, info.fade);
}
- const authorStyle = dynamicCSS.selectorStyle(authorSelector);
- const parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector);
+ const authorStyle = cssManagers.inner.selectorStyle(authorSelector);
+ const parentAuthorStyle = cssManagers.parent.selectorStyle(authorSelector);
// author color
authorStyle.backgroundColor = bgcolor;
@@ -3895,44 +3881,39 @@ function Ace2Inner(editorInfo) {
editorInfo.ace_performDocumentApplyAttributesToRange =
(...args) => documentAttributeManager.setAttributesOnRange(...args);
- this.init = (cb) => {
- $(document).ready(() => {
- doc = document; // defined as a var in scope outside
- inCallStack('setup', () => {
- const body = doc.getElementById('innerdocbody');
- root = body; // defined as a var in scope outside
- if (browser.firefox) $(root).addClass('mozilla');
- if (browser.safari) $(root).addClass('safari');
- root.classList.toggle('authorColors', true);
- root.classList.toggle('doesWrap', doesWrap);
+ this.init = async () => {
+ await $.ready;
+ doc = document; // defined as a var in scope outside
+ inCallStack('setup', () => {
+ const body = doc.getElementById('innerdocbody');
+ root = body; // defined as a var in scope outside
+ if (browser.firefox) $(root).addClass('mozilla');
+ if (browser.safari) $(root).addClass('safari');
+ root.classList.toggle('authorColors', true);
+ root.classList.toggle('doesWrap', doesWrap);
- initDynamicCSS();
+ enforceEditability();
- enforceEditability();
+ // set up dom and rep
+ while (root.firstChild) root.removeChild(root.firstChild);
+ const oneEntry = createDomLineEntry('');
+ doRepLineSplice(0, rep.lines.length(), [oneEntry]);
+ insertDomLines(null, [oneEntry.domInfo]);
+ rep.alines = Changeset.splitAttributionLines(
+ Changeset.makeAttribution('\n'), '\n');
- // set up dom and rep
- while (root.firstChild) root.removeChild(root.firstChild);
- const oneEntry = createDomLineEntry('');
- doRepLineSplice(0, rep.lines.length(), [oneEntry]);
- insertDomLines(null, [oneEntry.domInfo]);
- rep.alines = Changeset.splitAttributionLines(
- Changeset.makeAttribution('\n'), '\n');
+ bindTheEventHandlers();
+ });
- bindTheEventHandlers();
- });
-
- hooks.callAll('aceInitialized', {
- editorInfo,
- rep,
- documentAttributeManager,
- });
-
- scheduler.setTimeout(cb, 0);
+ hooks.callAll('aceInitialized', {
+ editorInfo,
+ rep,
+ documentAttributeManager,
});
};
}
-exports.init = (editorInfo, cb) => {
- const editor = new Ace2Inner(editorInfo);
- editor.init(cb);
+exports.init = async (editorInfo, cssManagers) => {
+ const editor = new Ace2Inner(editorInfo, cssManagers);
+ await editor.init();
};
diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js
index 26370f6b5..909b6a085 100644
--- a/src/static/js/broadcast.js
+++ b/src/static/js/broadcast.js
@@ -468,14 +468,14 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
BroadcastSlider.onSlider(goToRevisionIfEnabled);
- const dynamicCSS = makeCSSManager('dynamicsyntax');
+ const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet);
const authorData = {};
const receiveAuthorData = (newAuthorData) => {
for (const [author, data] of Object.entries(newAuthorData)) {
const bgcolor = typeof data.colorId === 'number'
? clientVars.colorPalette[data.colorId] : data.colorId;
- if (bgcolor && dynamicCSS) {
+ if (bgcolor) {
const selector = dynamicCSS.selectorStyle(`.${linestylefilter.getAuthorClassName(author)}`);
selector.backgroundColor = bgcolor;
selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5)
diff --git a/src/static/js/cssmanager.js b/src/static/js/cssmanager.js
index 0fcdad403..5bf2adb30 100644
--- a/src/static/js/cssmanager.js
+++ b/src/static/js/cssmanager.js
@@ -22,37 +22,7 @@
* limitations under the License.
*/
-const makeCSSManager = (emptyStylesheetTitle, doc) => {
- if (doc === true) {
- doc = 'parent';
- } else if (!doc) {
- doc = 'inner';
- }
-
- const getSheetByTitle = (title) => {
- let win;
- if (doc === 'parent') {
- win = window.parent.parent;
- } else if (doc === 'inner') {
- win = window;
- } else if (doc === 'outer') {
- win = window.parent;
- } else {
- throw new Error('Unknown dynamic style container');
- }
- const allSheets = win.document.styleSheets;
-
- for (let i = 0; i < allSheets.length; i++) {
- const s = allSheets[i];
- if (s.title === title) {
- return s;
- }
- }
- return null;
- };
-
- const browserSheet = getSheetByTitle(emptyStylesheetTitle);
-
+exports.makeCSSManager = (browserSheet) => {
const browserRules = () => (browserSheet.cssRules || browserSheet.rules);
const browserDeleteRule = (i) => {
@@ -100,5 +70,3 @@ const makeCSSManager = (emptyStylesheetTitle, doc) => {
info: () => `${selectorList.length}:${browserRules().length}`,
};
};
-
-exports.makeCSSManager = makeCSSManager;
diff --git a/src/static/js/pad.js b/src/static/js/pad.js
index 77a37cd1f..485a4b2fe 100644
--- a/src/static/js/pad.js
+++ b/src/static/js/pad.js
@@ -501,7 +501,7 @@ const pad = {
// order of inits is important here:
padimpexp.init(this);
padsavedrevs.init(this);
- padeditor.init(postAceInit, pad.padOptions.view || {}, this);
+ padeditor.init(pad.padOptions.view || {}, this).then(postAceInit);
paduserlist.init(pad.myUserInfo, this);
padconnectionstatus.init();
padmodals.init(this);
diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.js
index 75481fe5e..d8c3ae5ac 100644
--- a/src/static/js/pad_editor.js
+++ b/src/static/js/pad_editor.js
@@ -34,30 +34,20 @@ const padeditor = (() => {
ace: null,
// this is accessed directly from other files
viewZoom: 100,
- init: (readyFunc, initialViewOptions, _pad) => {
+ init: async (initialViewOptions, _pad) => {
Ace2Editor = require('./ace').Ace2Editor;
pad = _pad;
settings = pad.settings;
-
- const aceReady = () => {
- $('#editorloadingbox').hide();
- if (readyFunc) {
- readyFunc();
-
- // Listen for clicks on sidediv items
- const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
- $outerdoc.find('#sidedivinner').on('click', 'div', function () {
- const targetLineNumber = $(this).index() + 1;
- window.location.hash = `L${targetLineNumber}`;
- });
-
- exports.focusOnLine(self.ace);
- }
- };
-
self.ace = new Ace2Editor();
- self.ace.init('editorcontainer', '').then(
- () => aceReady(), (err) => { throw err || new Error(err); });
+ await self.ace.init('editorcontainer', '');
+ $('#editorloadingbox').hide();
+ // Listen for clicks on sidediv items
+ const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
+ $outerdoc.find('#sidedivinner').on('click', 'div', function () {
+ const targetLineNumber = $(this).index() + 1;
+ window.location.hash = `L${targetLineNumber}`;
+ });
+ exports.focusOnLine(self.ace);
self.ace.setProperty('wraps', true);
if (pad.getIsDebugEnabled()) {
self.ace.setProperty('dmesg', pad.dmesg);
diff --git a/src/static/skins/no-skin/pad.css b/src/static/skins/no-skin/pad.css
index e69de29bb..a9eae81f2 100644
--- a/src/static/skins/no-skin/pad.css
+++ b/src/static/skins/no-skin/pad.css
@@ -0,0 +1 @@
+/* intentionally empty */
diff --git a/src/templates/pad.html b/src/templates/pad.html
index 26243806a..7bf2346f9 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -7,7 +7,7 @@
<% e.begin_block("htmlHead"); %>
-
+
<% e.end_block(); %>
<%=settings.title%>
diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html
index c577f3013..b3fd3d006 100644
--- a/src/templates/timeslider.html
+++ b/src/templates/timeslider.html
@@ -3,7 +3,7 @@
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
%>
-
+
<%=settings.title%> Timeslider