mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-23 08:56:17 -04:00
Merge branch 'develop'
This commit is contained in:
commit
5db0c8d1cf
30 changed files with 496 additions and 524 deletions
23
CHANGELOG.md
23
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
|
# 1.8.12
|
||||||
|
|
||||||
Special mention: Thanks to Sauce Labs for additional testing tunnels to help us grow! :)
|
Special mention: Thanks to Sauce Labs for additional testing tunnels to help us grow! :)
|
||||||
|
|
|
@ -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.
|
* [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.
|
* [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.
|
* [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
|
# Project Status
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,8 @@ fatal() { error "$@"; exit 1; }
|
||||||
LAST_EMAIL_SEND=0
|
LAST_EMAIL_SEND=0
|
||||||
|
|
||||||
# Move to the Etherpad base directory.
|
# Move to the Etherpad base directory.
|
||||||
MY_DIR=$(try cd "${0%/*}" && try pwd -P) || exit 1
|
MY_DIR=$(cd "${0%/*}" && pwd -P) || exit 1
|
||||||
try cd "${MY_DIR}/../.."
|
cd "${MY_DIR}/../.." || exit 1
|
||||||
|
|
||||||
# Check if a logfile parameter is set
|
# Check if a logfile parameter is set
|
||||||
LOG="$1"
|
LOG="$1"
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"pad.settings.stickychat": "Di ekranê de hertim çet bike",
|
"pad.settings.stickychat": "Di ekranê de hertim çet bike",
|
||||||
"pad.settings.chatandusers": "Çeta û Bikarhênera Nîşan bide",
|
"pad.settings.chatandusers": "Çeta û Bikarhênera Nîşan bide",
|
||||||
"pad.settings.colorcheck": "Rengên nivîskarîye",
|
"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.rtlcheck": "Bila naverok ji raste ber bi çepe be xwendin?",
|
||||||
"pad.settings.fontType": "Tîpa nivîsê:",
|
"pad.settings.fontType": "Tîpa nivîsê:",
|
||||||
"pad.settings.language": "Ziman:",
|
"pad.settings.language": "Ziman:",
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
const ueberDB = require('ueberdb2');
|
const ueberDB = require('ueberdb2');
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
const log4js = require('log4js');
|
const log4js = require('log4js');
|
||||||
|
const stats = require('../stats');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
|
|
||||||
// set database settings
|
// set database settings
|
||||||
|
@ -48,6 +49,13 @@ exports.init = async () => await new Promise((resolve, reject) => {
|
||||||
process.exit(1);
|
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
|
// everything ok, set up Promise-based methods
|
||||||
['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove'].forEach((fn) => {
|
['get', 'set', 'findKeys', 'getSub', 'setSub', 'remove'].forEach((fn) => {
|
||||||
exports[fn] = util.promisify(db[fn].bind(db));
|
exports[fn] = util.promisify(db[fn].bind(db));
|
||||||
|
|
|
@ -33,24 +33,12 @@ const util = require('util');
|
||||||
const fsp_writeFile = util.promisify(fs.writeFile);
|
const fsp_writeFile = util.promisify(fs.writeFile);
|
||||||
const fsp_unlink = util.promisify(fs.unlink);
|
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();
|
const tempDirectory = os.tmpdir();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* do a requested export
|
* 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
|
// avoid naming the read-only file as the original pad's id
|
||||||
let fileName = readOnlyId ? readOnlyId : padId;
|
let fileName = readOnlyId ? readOnlyId : padId;
|
||||||
|
|
||||||
|
@ -85,7 +73,7 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
|
||||||
const newHTML = await hooks.aCallFirst('exportHTMLSend', html);
|
const newHTML = await hooks.aCallFirst('exportHTMLSend', html);
|
||||||
if (newHTML.length) html = newHTML;
|
if (newHTML.length) html = newHTML;
|
||||||
res.send(html);
|
res.send(html);
|
||||||
throw 'stop';
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// else write the html export to a file
|
// else write the html export to a file
|
||||||
|
@ -98,7 +86,7 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
|
||||||
html = null;
|
html = null;
|
||||||
await TidyHtml.tidy(srcFile);
|
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}`;
|
const destFile = `${tempDirectory}/etherpad_export_${randNum}.${type}`;
|
||||||
|
|
||||||
// Allow plugins to overwrite the convert in export process
|
// 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) {
|
if (result.length > 0) {
|
||||||
// console.log("export handled by plugin", destFile);
|
// console.log("export handled by plugin", destFile);
|
||||||
} else {
|
} else {
|
||||||
// @TODO no Promise interface for convertors (yet)
|
const converter =
|
||||||
await new Promise((resolve, reject) => {
|
settings.soffice != null ? require('../utils/LibreOffice')
|
||||||
convertor.convertFile(srcFile, destFile, type, (err) => {
|
: settings.abiword != null ? require('../utils/Abiword')
|
||||||
err ? reject('convertFailed') : resolve();
|
: null;
|
||||||
});
|
await converter.convertFile(srcFile, destFile, type);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// send the file
|
// send the file
|
||||||
|
@ -128,11 +115,3 @@ const doExport = async (req, res, padId, readOnlyId, type) => {
|
||||||
await fsp_unlink(destFile);
|
await fsp_unlink(destFile);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.doExport = (req, res, padId, readOnlyId, type) => {
|
|
||||||
doExport(req, res, padId, readOnlyId, type).catch((err) => {
|
|
||||||
if (err !== 'stop') {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -55,17 +55,17 @@ const rm = async (path) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let convertor = null;
|
let converter = null;
|
||||||
let exportExtension = 'htm';
|
let exportExtension = 'htm';
|
||||||
|
|
||||||
// load abiword only if it is enabled and if soffice is disabled
|
// load abiword only if it is enabled and if soffice is disabled
|
||||||
if (settings.abiword != null && settings.soffice == null) {
|
if (settings.abiword != null && settings.soffice == null) {
|
||||||
convertor = require('../utils/Abiword');
|
converter = require('../utils/Abiword');
|
||||||
}
|
}
|
||||||
|
|
||||||
// load soffice only if it is enabled
|
// load soffice only if it is enabled
|
||||||
if (settings.soffice != null) {
|
if (settings.soffice != null) {
|
||||||
convertor = require('../utils/LibreOffice');
|
converter = require('../utils/LibreOffice');
|
||||||
exportExtension = 'html';
|
exportExtension = 'html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +80,8 @@ const doImport = async (req, res, padId) => {
|
||||||
// set html in the pad
|
// set html in the pad
|
||||||
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
|
const randNum = Math.floor(Math.random() * 0xFFFFFFFF);
|
||||||
|
|
||||||
// setting flag for whether to use convertor or not
|
// setting flag for whether to use converter or not
|
||||||
let useConvertor = (convertor != null);
|
let useConverter = (converter != null);
|
||||||
|
|
||||||
const form = new formidable.IncomingForm();
|
const form = new formidable.IncomingForm();
|
||||||
form.keepExtensions = true;
|
form.keepExtensions = true;
|
||||||
|
@ -170,30 +170,25 @@ const doImport = async (req, res, padId) => {
|
||||||
// convert file to html if necessary
|
// convert file to html if necessary
|
||||||
if (!importHandledByPlugin && !directDatabaseAccess) {
|
if (!importHandledByPlugin && !directDatabaseAccess) {
|
||||||
if (fileIsTXT) {
|
if (fileIsTXT) {
|
||||||
// Don't use convertor for text files
|
// Don't use converter for text files
|
||||||
useConvertor = false;
|
useConverter = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/ether/etherpad-lite/issues/2572
|
// See https://github.com/ether/etherpad-lite/issues/2572
|
||||||
if (fileIsHTML || !useConvertor) {
|
if (fileIsHTML || !useConverter) {
|
||||||
// if no convertor only rename
|
// if no converter only rename
|
||||||
await fs.rename(srcFile, destFile);
|
await fs.rename(srcFile, destFile);
|
||||||
} else {
|
} else {
|
||||||
// @TODO - no Promise interface for convertors (yet)
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
await converter.convertFile(srcFile, destFile, exportExtension);
|
||||||
convertor.convertFile(srcFile, destFile, exportExtension, (err) => {
|
} catch (err) {
|
||||||
// catch convert errors
|
|
||||||
if (err) {
|
|
||||||
logger.warn(`Converting Error: ${err.stack || err}`);
|
logger.warn(`Converting Error: ${err.stack || err}`);
|
||||||
return reject(new ImportError('convertFailed'));
|
throw new ImportError('convertFailed');
|
||||||
}
|
}
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!useConvertor && !directDatabaseAccess) {
|
if (!useConverter && !directDatabaseAccess) {
|
||||||
// Read the file with no encoding for raw buffer access.
|
// Read the file with no encoding for raw buffer access.
|
||||||
const buf = await fs.readFile(destFile);
|
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
|
// change text of the pad and broadcast the changeset
|
||||||
if (!directDatabaseAccess) {
|
if (!directDatabaseAccess) {
|
||||||
if (importHandledByPlugin || useConvertor || fileIsHTML) {
|
if (importHandledByPlugin || useConverter || fileIsHTML) {
|
||||||
try {
|
try {
|
||||||
await importHtml.setPadHTML(pad, text);
|
await importHtml.setPadHTML(pad, text);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -60,7 +60,7 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Exporting pad "${req.params.pad}" in ${req.params.type} format`);
|
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)));
|
})().catch((err) => next(err || new Error(err)));
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,12 +10,20 @@ const readOnlyManager = require('../../db/ReadOnlyManager');
|
||||||
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
||||||
|
|
||||||
const staticPathsRE = new RegExp(`^/(?:${[
|
const staticPathsRE = new RegExp(`^/(?:${[
|
||||||
'api/.*',
|
'api(?:/.*)?',
|
||||||
'favicon\\.ico',
|
'favicon\\.ico',
|
||||||
|
'ep/pad/connection-diagnostic-info',
|
||||||
|
'javascript',
|
||||||
'javascripts/.*',
|
'javascripts/.*',
|
||||||
|
'jserror/?',
|
||||||
'locales\\.json',
|
'locales\\.json',
|
||||||
|
'locales/.*',
|
||||||
|
'rest/.*',
|
||||||
'pluginfw/.*',
|
'pluginfw/.*',
|
||||||
|
'robots.txt',
|
||||||
'static/.*',
|
'static/.*',
|
||||||
|
'stats/?',
|
||||||
|
'tests/frontend(?:/.*)?'
|
||||||
].join('|')})$`);
|
].join('|')})$`);
|
||||||
|
|
||||||
exports.normalizeAuthzLevel = (level) => {
|
exports.normalizeAuthzLevel = (level) => {
|
||||||
|
|
|
@ -46,6 +46,7 @@ const hooks = require('../static/js/pluginfw/hooks');
|
||||||
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
||||||
const plugins = require('../static/js/pluginfw/plugins');
|
const plugins = require('../static/js/pluginfw/plugins');
|
||||||
const settings = require('./utils/Settings');
|
const settings = require('./utils/Settings');
|
||||||
|
const stats = require('./stats');
|
||||||
|
|
||||||
const logger = log4js.getLogger('server');
|
const logger = log4js.getLogger('server');
|
||||||
|
|
||||||
|
@ -104,8 +105,6 @@ exports.start = async () => {
|
||||||
// Check if Etherpad version is up-to-date
|
// Check if Etherpad version is up-to-date
|
||||||
UpdateCheck.check();
|
UpdateCheck.check();
|
||||||
|
|
||||||
// start up stats counting system
|
|
||||||
const stats = require('./stats');
|
|
||||||
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
|
stats.gauge('memoryUsage', () => process.memoryUsage().rss);
|
||||||
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
|
stats.gauge('memoryUsageHeap', () => process.memoryUsage().heapUsed);
|
||||||
|
|
||||||
|
@ -215,6 +214,7 @@ exports.exit = async (err = null) => {
|
||||||
logger.info('Received SIGTERM signal');
|
logger.info('Received SIGTERM signal');
|
||||||
err = null;
|
err = null;
|
||||||
} else if (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());
|
logger.error(err.stack || err.toString());
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
if (exitCalled) {
|
if (exitCalled) {
|
||||||
|
|
|
@ -24,111 +24,67 @@ const async = require('async');
|
||||||
const settings = require('./Settings');
|
const settings = require('./Settings');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
|
||||||
let doConvertTask;
|
|
||||||
|
|
||||||
// on windows we have to spawn a process for each convertion,
|
// on windows we have to spawn a process for each convertion,
|
||||||
// cause the plugin abicommand doesn't exist on this platform
|
// cause the plugin abicommand doesn't exist on this platform
|
||||||
if (os.type().indexOf('Windows') > -1) {
|
if (os.type().indexOf('Windows') > -1) {
|
||||||
|
exports.convertFile = async (srcFile, destFile, type) => {
|
||||||
|
const abiword = spawn(settings.abiword, [`--to=${destFile}`, srcFile]);
|
||||||
let stdoutBuffer = '';
|
let stdoutBuffer = '';
|
||||||
|
abiword.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||||
doConvertTask = (task, callback) => {
|
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||||
// span an abiword process to perform the conversion
|
await new Promise((resolve, reject) => {
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
// append error messages to the buffer
|
|
||||||
abiword.stderr.on('data', (data) => {
|
|
||||||
stdoutBuffer += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
// throw exceptions if abiword is dieing
|
|
||||||
abiword.on('exit', (code) => {
|
abiword.on('exit', (code) => {
|
||||||
if (code !== 0) {
|
if (code !== 0) return reject(new Error(`Abiword died with exit code ${code}`));
|
||||||
return callback(`Abiword died with exit code ${code}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stdoutBuffer !== '') {
|
if (stdoutBuffer !== '') {
|
||||||
console.log(stdoutBuffer);
|
console.log(stdoutBuffer);
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
callback();
|
});
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
exports.convertFile = (srcFile, destFile, type, callback) => {
|
|
||||||
doConvertTask({srcFile, destFile, type}, callback);
|
|
||||||
};
|
};
|
||||||
// on unix operating systems, we can start abiword with abicommand and
|
// on unix operating systems, we can start abiword with abicommand and
|
||||||
// communicate with it via stdin/stdout
|
// communicate with it via stdin/stdout
|
||||||
// thats much faster, about factor 10
|
// thats much faster, about factor 10
|
||||||
} else {
|
} else {
|
||||||
// spawn the abiword process
|
|
||||||
let abiword;
|
let abiword;
|
||||||
let stdoutCallback = null;
|
let stdoutCallback = null;
|
||||||
const spawnAbiword = () => {
|
const spawnAbiword = () => {
|
||||||
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
|
abiword = spawn(settings.abiword, ['--plugin', 'AbiCommand']);
|
||||||
let stdoutBuffer = '';
|
let stdoutBuffer = '';
|
||||||
let firstPrompt = true;
|
let firstPrompt = true;
|
||||||
|
abiword.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||||
// 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.on('exit', (code) => {
|
abiword.on('exit', (code) => {
|
||||||
spawnAbiword();
|
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) => {
|
abiword.stdout.on('data', (data) => {
|
||||||
// add data to buffer
|
|
||||||
stdoutBuffer += data.toString();
|
stdoutBuffer += data.toString();
|
||||||
|
|
||||||
// we're searching for the prompt, cause this means everything we need is in the buffer
|
// we're searching for the prompt, cause this means everything we need is in the buffer
|
||||||
if (stdoutBuffer.search('AbiWord:>') !== -1) {
|
if (stdoutBuffer.search('AbiWord:>') !== -1) {
|
||||||
// filter the feedback message
|
const err = stdoutBuffer.search('OK') !== -1 ? null : new Error(stdoutBuffer);
|
||||||
const err = stdoutBuffer.search('OK') !== -1 ? null : stdoutBuffer;
|
|
||||||
|
|
||||||
// reset the buffer
|
|
||||||
stdoutBuffer = '';
|
stdoutBuffer = '';
|
||||||
|
|
||||||
// call the callback with the error message
|
|
||||||
// skip the first prompt
|
|
||||||
if (stdoutCallback != null && !firstPrompt) {
|
if (stdoutCallback != null && !firstPrompt) {
|
||||||
stdoutCallback(err);
|
stdoutCallback(err);
|
||||||
stdoutCallback = null;
|
stdoutCallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
firstPrompt = false;
|
firstPrompt = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
spawnAbiword();
|
spawnAbiword();
|
||||||
|
|
||||||
doConvertTask = (task, callback) => {
|
const queue = async.queue((task, callback) => {
|
||||||
abiword.stdin.write(`convert ${task.srcFile} ${task.destFile} ${task.type}\n`);
|
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) => {
|
stdoutCallback = (err) => {
|
||||||
callback();
|
if (err != null) console.error('Abiword File failed to convert', err);
|
||||||
console.log('queue continue');
|
callback(err);
|
||||||
try {
|
|
||||||
task.callback(err);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Abiword File failed to convert', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
}, 1);
|
||||||
|
|
||||||
// Queue with the converts we have to do
|
exports.convertFile = async (srcFile, destFile, type) => {
|
||||||
const queue = async.queue(doConvertTask, 1);
|
await queue.pushAsync({srcFile, destFile, type});
|
||||||
exports.convertFile = (srcFile, destFile, type, callback) => {
|
|
||||||
queue.push({srcFile, destFile, type, callback});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const fs = require('fs');
|
const fs = require('fs').promises;
|
||||||
const log4js = require('log4js');
|
const log4js = require('log4js');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
@ -27,18 +27,11 @@ const spawn = require('child_process').spawn;
|
||||||
|
|
||||||
const libreOfficeLogger = log4js.getLogger('LibreOffice');
|
const libreOfficeLogger = log4js.getLogger('LibreOffice');
|
||||||
|
|
||||||
const doConvertTask = (task, callback) => {
|
const doConvertTask = async (task) => {
|
||||||
const tmpDir = os.tmpdir();
|
const tmpDir = os.tmpdir();
|
||||||
|
|
||||||
async.series([
|
|
||||||
/*
|
|
||||||
* use LibreOffice to convert task.srcFile to another format, given in
|
|
||||||
* task.type
|
|
||||||
*/
|
|
||||||
(callback) => {
|
|
||||||
libreOfficeLogger.debug(
|
libreOfficeLogger.debug(
|
||||||
`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`
|
`Converting ${task.srcFile} to format ${task.type}. The result will be put in ${tmpDir}`);
|
||||||
);
|
|
||||||
const soffice = spawn(settings.soffice, [
|
const soffice = spawn(settings.soffice, [
|
||||||
'--headless',
|
'--headless',
|
||||||
'--invisible',
|
'--invisible',
|
||||||
|
@ -57,46 +50,27 @@ const doConvertTask = (task, callback) => {
|
||||||
soffice.stdin.pause(); // required to kill hanging threads
|
soffice.stdin.pause(); // required to kill hanging threads
|
||||||
soffice.kill();
|
soffice.kill();
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
let stdoutBuffer = '';
|
let stdoutBuffer = '';
|
||||||
|
soffice.stdout.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||||
// Delegate the processing of stdout to another function
|
soffice.stderr.on('data', (data) => { stdoutBuffer += data.toString(); });
|
||||||
soffice.stdout.on('data', (data) => {
|
await new Promise((resolve, reject) => {
|
||||||
stdoutBuffer += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Append error messages to the buffer
|
|
||||||
soffice.stderr.on('data', (data) => {
|
|
||||||
stdoutBuffer += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
soffice.on('exit', (code) => {
|
soffice.on('exit', (code) => {
|
||||||
clearTimeout(hangTimeout);
|
clearTimeout(hangTimeout);
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
// Throw an exception if libreoffice failed
|
const err =
|
||||||
return callback(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`);
|
new Error(`LibreOffice died with exit code ${code} and message: ${stdoutBuffer}`);
|
||||||
|
libreOfficeLogger.error(err.stack);
|
||||||
|
return reject(err);
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
// 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 filename = path.basename(task.srcFile);
|
||||||
const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
|
const sourceFile = `${filename.substr(0, filename.lastIndexOf('.'))}.${task.fileExtension}`;
|
||||||
const sourcePath = path.join(tmpDir, sourceFile);
|
const sourcePath = path.join(tmpDir, sourceFile);
|
||||||
libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
|
libreOfficeLogger.debug(`Renaming ${sourcePath} to ${task.destFile}`);
|
||||||
fs.rename(sourcePath, task.destFile, callback);
|
await fs.rename(sourcePath, task.destFile);
|
||||||
},
|
|
||||||
], (err) => {
|
|
||||||
// Invoke the callback for the local queue
|
|
||||||
callback();
|
|
||||||
|
|
||||||
// Invoke the callback for the task
|
|
||||||
task.callback(err);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Conversion tasks will be queued up, so we don't overload the system
|
// 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 {String} type The type to convert into
|
||||||
* @param {Function} callback Standard callback function
|
* @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
|
// Used for the moving of the file, not the conversion
|
||||||
const fileExtension = type;
|
const fileExtension = type;
|
||||||
|
|
||||||
|
@ -129,23 +103,10 @@ exports.convertFile = (srcFile, destFile, type, callback) => {
|
||||||
// we need to convert to odt first, then to doc
|
// we need to convert to odt first, then to doc
|
||||||
// to avoid `Error: no export filter for /tmp/xxxx.doc` error
|
// to avoid `Error: no export filter for /tmp/xxxx.doc` error
|
||||||
if (type === 'doc') {
|
if (type === 'doc') {
|
||||||
queue.push({
|
const intermediateFile = destFile.replace(/\.doc$/, '.odt');
|
||||||
srcFile,
|
await queue.pushAsync({srcFile, destFile: intermediateFile, type: 'odt', fileExtension: 'odt'});
|
||||||
destFile: destFile.replace(/\.doc$/, '.odt'),
|
await queue.pushAsync({srcFile: intermediateFile, destFile, type, fileExtension});
|
||||||
type: 'odt',
|
|
||||||
callback: () => {
|
|
||||||
queue.push(
|
|
||||||
{
|
|
||||||
srcFile: srcFile.replace(/\.html$/, '.odt'),
|
|
||||||
destFile,
|
|
||||||
type,
|
|
||||||
callback,
|
|
||||||
fileExtension,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
queue.push({srcFile, destFile, type, callback, fileExtension});
|
await queue.pushAsync({srcFile, destFile, type, fileExtension});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
, "pad_automatic_reconnect.js"
|
, "pad_automatic_reconnect.js"
|
||||||
, "ace.js"
|
, "ace.js"
|
||||||
, "collab_client.js"
|
, "collab_client.js"
|
||||||
|
, "cssmanager.js"
|
||||||
, "pad_userlist.js"
|
, "pad_userlist.js"
|
||||||
, "pad_impexp.js"
|
, "pad_impexp.js"
|
||||||
, "pad_savedrevs.js"
|
, "pad_savedrevs.js"
|
||||||
|
@ -61,7 +62,6 @@
|
||||||
, "Changeset.js"
|
, "Changeset.js"
|
||||||
, "ChangesetUtils.js"
|
, "ChangesetUtils.js"
|
||||||
, "skiplist.js"
|
, "skiplist.js"
|
||||||
, "cssmanager.js"
|
|
||||||
, "colorutils.js"
|
, "colorutils.js"
|
||||||
, "undomodule.js"
|
, "undomodule.js"
|
||||||
, "$unorm/lib/unorm.js"
|
, "$unorm/lib/unorm.js"
|
||||||
|
|
45
src/package-lock.json
generated
45
src/package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ep_etherpad-lite",
|
"name": "ep_etherpad-lite",
|
||||||
"version": "1.8.12",
|
"version": "1.8.13",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -15,9 +15,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@azure/abort-controller": {
|
"@azure/abort-controller": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
|
||||||
"integrity": "sha512-kCibMwqffnwlw3c+e879rCE1Am1I2BfhjOeO54XNA8i/cEuzktnBQbTrzh67XwibHO05YuNgZzSWy9ocVfFAGw==",
|
"integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
@ -287,9 +287,9 @@
|
||||||
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
|
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "14.14.31",
|
"version": "14.14.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.34.tgz",
|
||||||
"integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g=="
|
"integrity": "sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA=="
|
||||||
},
|
},
|
||||||
"@types/node-fetch": {
|
"@types/node-fetch": {
|
||||||
"version": "2.5.8",
|
"version": "2.5.8",
|
||||||
|
@ -2051,9 +2051,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"etherpad-require-kernel": {
|
"etherpad-require-kernel": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/etherpad-require-kernel/-/etherpad-require-kernel-1.0.11.tgz",
|
||||||
"integrity": "sha1-7Y8E6f0szsOgBVu20t/p2ZkS5+I="
|
"integrity": "sha512-I03bkNiBMrcsJRSl0IqotUU70s9v6VISrITj/cQgAoVQSoRFbV/NUn2fPIF4LskysTpmwlmwJqgfL2FZpAtxEw=="
|
||||||
},
|
},
|
||||||
"etherpad-yajsml": {
|
"etherpad-yajsml": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
|
@ -2110,9 +2110,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"express-rate-limit": {
|
"express-rate-limit": {
|
||||||
"version": "5.2.5",
|
"version": "5.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.2.6.tgz",
|
||||||
"integrity": "sha512-fv9mf4hWRKZHVlY8ChVNYnGxa49m0zQ6CrJxNiXe2IjJPqicrqoA/JOyBbvs4ufSSLZ6NTzhtgEyLcdfbe+Q6Q=="
|
"integrity": "sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA=="
|
||||||
},
|
},
|
||||||
"express-session": {
|
"express-session": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
|
@ -7689,9 +7689,9 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"simple-git": {
|
"simple-git": {
|
||||||
"version": "2.35.2",
|
"version": "2.36.2",
|
||||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.35.2.tgz",
|
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.36.2.tgz",
|
||||||
"integrity": "sha512-UjOKsrz92Bx7z00Wla5V6qLSf5X2XSp0sL2gzKw1Bh7iJfDPDaU7gK5avIup0yo1/sMOSUMQer2b9GcnF6nmTQ==",
|
"integrity": "sha512-orBEf65GfSiQMsYedbJXSiRNnIRvhbeE5rrxZuEimCpWxDZOav0KLy2IEiPi1YJCF+zaC2quiJF8A4TsxI9/tw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@kwsites/file-exists": "^1.1.1",
|
"@kwsites/file-exists": "^1.1.1",
|
||||||
"@kwsites/promise-deferred": "^1.1.1",
|
"@kwsites/promise-deferred": "^1.1.1",
|
||||||
|
@ -8436,13 +8436,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ueberdb2": {
|
"ueberdb2": {
|
||||||
"version": "1.3.2",
|
"version": "1.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ueberdb2/-/ueberdb2-1.4.4.tgz",
|
||||||
"integrity": "sha512-7Ub5jDsIS+qjjsNV7yp1CHXHVe2K9ZUpwaHi9BZf3ai0DxtuHOfMada1wxL6iyEjwYXh/Nsu80iyId51wHFf4A==",
|
"integrity": "sha512-hcexgTdMa6gMquv5r6rOBsr76awMlqAjQiMMJ72qrzuatLYJ6D1EQTK/Jqo4nOD/jklXHM2yFw1mNcHsrlEzrw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
"cassandra-driver": "^4.5.1",
|
"cassandra-driver": "^4.5.1",
|
||||||
"channels": "0.0.4",
|
|
||||||
"dirty": "^1.1.1",
|
"dirty": "^1.1.1",
|
||||||
"elasticsearch": "^16.7.1",
|
"elasticsearch": "^16.7.1",
|
||||||
"mongodb": "^3.6.3",
|
"mongodb": "^3.6.3",
|
||||||
|
@ -8774,9 +8773,9 @@
|
||||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||||
},
|
},
|
||||||
"xmldom": {
|
"xmldom": {
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.5.0.tgz",
|
||||||
"integrity": "sha512-2E93k08T30Ugs+34HBSTQLVtpi6mCddaY8uO+pMNk1pqSjV5vElzn4mmh6KLxN3hki8rNcHSYzILoh3TEWORvA=="
|
"integrity": "sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA=="
|
||||||
},
|
},
|
||||||
"xmlhttprequest-ssl": {
|
"xmlhttprequest-ssl": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
|
|
|
@ -38,10 +38,10 @@
|
||||||
"cookie-parser": "1.4.5",
|
"cookie-parser": "1.4.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"ejs": "^3.1.6",
|
"ejs": "^3.1.6",
|
||||||
"etherpad-require-kernel": "1.0.9",
|
"etherpad-require-kernel": "1.0.11",
|
||||||
"etherpad-yajsml": "0.0.4",
|
"etherpad-yajsml": "0.0.4",
|
||||||
"express": "4.17.1",
|
"express": "4.17.1",
|
||||||
"express-rate-limit": "5.2.5",
|
"express-rate-limit": "5.2.6",
|
||||||
"express-session": "1.17.1",
|
"express-session": "1.17.1",
|
||||||
"find-root": "1.1.0",
|
"find-root": "1.1.0",
|
||||||
"formidable": "1.2.2",
|
"formidable": "1.2.2",
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
"threads": "^1.4.0",
|
"threads": "^1.4.0",
|
||||||
"tiny-worker": "^2.3.0",
|
"tiny-worker": "^2.3.0",
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"ueberdb2": "^1.3.2",
|
"ueberdb2": "^1.4.4",
|
||||||
"underscore": "1.12.0",
|
"underscore": "1.12.0",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"wtfnode": "^0.8.4"
|
"wtfnode": "^0.8.4"
|
||||||
|
@ -246,6 +246,6 @@
|
||||||
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
"test": "mocha --timeout 120000 --recursive tests/backend/specs ../node_modules/ep_*/static/tests/backend/specs",
|
||||||
"test-container": "mocha --timeout 5000 tests/container/specs/api"
|
"test-container": "mocha --timeout 5000 tests/container/specs/api"
|
||||||
},
|
},
|
||||||
"version": "1.8.12",
|
"version": "1.8.13",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
|
|
||||||
@import url('./lists_and_indents.css');
|
@import url('./lists_and_indents.css');
|
||||||
|
|
||||||
html.inner-editor {
|
html.outer-editor, html.inner-editor {
|
||||||
height: auto !important;
|
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
#outerdocbody {
|
#outerdocbody {
|
||||||
|
@ -38,6 +37,11 @@ html.inner-editor {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow-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 {
|
#innerdocbody, #sidediv {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
html, body {
|
html, body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
html:not(.inner-editor), html:not(.inner-editor) body {
|
|
||||||
|
/* used in pad and timeslider */
|
||||||
|
html.pad, html.pad body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -50,10 +50,9 @@ exports.error = (msg) => {
|
||||||
* @param b {boolean} assertion condition
|
* @param b {boolean} assertion condition
|
||||||
* @param msgParts {string} error to be passed if it fails
|
* @param msgParts {string} error to be passed if it fails
|
||||||
*/
|
*/
|
||||||
exports.assert = (b, msgParts) => {
|
exports.assert = (b, ...msgParts) => {
|
||||||
if (!b) {
|
if (!b) {
|
||||||
const msg = Array.prototype.slice.call(arguments, 1).join('');
|
exports.error(`Failed assertion: ${msgParts.join('')}`);
|
||||||
exports.error(`Failed assertion: ${msg}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -25,22 +25,69 @@
|
||||||
// requires: undefined
|
// requires: undefined
|
||||||
|
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||||
const pluginUtils = require('./pluginfw/shared');
|
const pluginUtils = require('./pluginfw/shared');
|
||||||
|
|
||||||
const debugLog = (...args) => {};
|
const debugLog = (...args) => {};
|
||||||
window.debugLog = debugLog;
|
|
||||||
|
|
||||||
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
|
// 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
|
// 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.
|
// errors out unless given an absolute URL for a JavaScript-created element.
|
||||||
const absUrl = (url) => new URL(url, window.location.href).href;
|
const absUrl = (url) => new URL(url, window.location.href).href;
|
||||||
|
|
||||||
const scriptTag =
|
const eventFired = async (obj, event, cleanups = [], predicate = () => true) => {
|
||||||
(source) => `<script type="text/javascript">\n${source.replace(/<\//g, '<\\/')}</script>`;
|
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 () {
|
const Ace2Editor = function () {
|
||||||
let info = {editor: this};
|
let info = {editor: this};
|
||||||
window.ace2EditorInfo = info; // Make it accessible to iframes.
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
|
||||||
let actionsPendingInit = [];
|
let actionsPendingInit = [];
|
||||||
|
@ -109,16 +156,19 @@ const Ace2Editor = function () {
|
||||||
// returns array of {error: <browser Error object>, time: +new Date()}
|
// returns array of {error: <browser Error object>, time: +new Date()}
|
||||||
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
|
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
|
||||||
|
|
||||||
const pushStyleTagsFor = (buffer, files) => {
|
const addStyleTagsFor = (doc, files) => {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
buffer.push(`<link rel="stylesheet" type="text/css" href="${absUrl(encodeURI(file))}"/>`);
|
const link = doc.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.type = 'text/css';
|
||||||
|
link.href = absUrl(encodeURI(file));
|
||||||
|
doc.head.appendChild(link);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.destroy = pendingInit(() => {
|
this.destroy = pendingInit(() => {
|
||||||
info.ace_dispose();
|
info.ace_dispose();
|
||||||
info.frame.parentNode.removeChild(info.frame);
|
info.frame.parentNode.removeChild(info.frame);
|
||||||
delete window.ace2EditorInfo;
|
|
||||||
info = null; // prevent IE 6 closure memory leaks
|
info = null; // prevent IE 6 closure memory leaks
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -127,7 +177,7 @@ const Ace2Editor = function () {
|
||||||
this.importText(initialCode);
|
this.importText(initialCode);
|
||||||
|
|
||||||
const includedCSS = [
|
const includedCSS = [
|
||||||
'../static/css/iframe_editor.css',
|
`../static/css/iframe_editor.css?v=${clientVars.randomVersionString}`,
|
||||||
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
|
`../static/css/pad.css?v=${clientVars.randomVersionString}`,
|
||||||
...hooks.callAll('aceEditorCSS').map(
|
...hooks.callAll('aceEditorCSS').map(
|
||||||
// Allow urls to external CSS - http(s):// and //some/path.css
|
// 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}`,
|
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
const doctype = '<!doctype html>';
|
const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== '');
|
||||||
|
|
||||||
const iframeHTML = [];
|
const outerFrame = document.createElement('iframe');
|
||||||
|
|
||||||
iframeHTML.push(doctype);
|
|
||||||
iframeHTML.push(`<html class='inner-editor ${clientVars.skinVariants}'><head>`);
|
|
||||||
pushStyleTagsFor(iframeHTML, includedCSS);
|
|
||||||
const requireKernelUrl =
|
|
||||||
absUrl(`../static/js/require-kernel.js?v=${clientVars.randomVersionString}`);
|
|
||||||
iframeHTML.push(`<script type="text/javascript" src="${requireKernelUrl}"></script>`);
|
|
||||||
// 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(`<script type="text/javascript" src="${url}"></script>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
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('<style type="text/css" title="dynamicsyntax"></style>');
|
|
||||||
|
|
||||||
hooks.callAll('aceInitInnerdocbodyHead', {
|
|
||||||
iframeHTML,
|
|
||||||
});
|
|
||||||
|
|
||||||
iframeHTML.push('</head><body id="innerdocbody" class="innerdocbody" role="application" ' +
|
|
||||||
'spellcheck="false"> </body></html>');
|
|
||||||
|
|
||||||
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, `<html class="inner-editor outerdoc ${clientVars.skinVariants}"><head>`];
|
|
||||||
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(
|
|
||||||
'<style type="text/css" title="dynamicsyntax"></style>',
|
|
||||||
'<link rel="stylesheet" type="text/css" href="data:text/css,"/>',
|
|
||||||
scriptTag(outerScript),
|
|
||||||
'</head>',
|
|
||||||
'<body id="outerdocbody" class="outerdocbody ', pluginNames.join(' '), '">',
|
|
||||||
'<div id="sidediv" class="sidediv"><!-- --></div>',
|
|
||||||
'<div id="linemetricsdiv">x</div>',
|
|
||||||
'</body></html>');
|
|
||||||
|
|
||||||
const outerFrame = document.createElement('IFRAME');
|
|
||||||
outerFrame.name = 'ace_outer';
|
outerFrame.name = 'ace_outer';
|
||||||
outerFrame.frameBorder = 0; // for IE
|
outerFrame.frameBorder = 0; // for IE
|
||||||
outerFrame.title = 'Ether';
|
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 = '<!DOCTYPE html>';
|
||||||
info.frame = outerFrame;
|
info.frame = outerFrame;
|
||||||
document.getElementById(containerId).appendChild(outerFrame);
|
document.getElementById(containerId).appendChild(outerFrame);
|
||||||
|
const outerWindow = outerFrame.contentWindow;
|
||||||
const editorDocument = outerFrame.contentWindow.document;
|
|
||||||
|
|
||||||
debugLog('Ace2Editor.init() waiting for outer frame');
|
debugLog('Ace2Editor.init() waiting for outer frame');
|
||||||
await new Promise((resolve, reject) => {
|
await frameReady(outerFrame);
|
||||||
info.onEditorReady = (err) => err != null ? reject(err) : resolve();
|
debugLog('Ace2Editor.init() outer frame ready');
|
||||||
editorDocument.open();
|
|
||||||
editorDocument.write(outerHTML.join(''));
|
// Firefox might replace the outerWindow.document object after iframe creation so this variable
|
||||||
editorDocument.close();
|
// is assigned after the Window's load event.
|
||||||
|
const outerDocument = outerWindow.document;
|
||||||
|
|
||||||
|
// <html> tag
|
||||||
|
outerDocument.documentElement.classList.add('outer-editor', 'outerdoc', ...skinVariants);
|
||||||
|
|
||||||
|
// <head> tag
|
||||||
|
addStyleTagsFor(outerDocument, includedCSS);
|
||||||
|
const outerStyle = outerDocument.createElement('style');
|
||||||
|
outerStyle.type = 'text/css';
|
||||||
|
outerStyle.title = 'dynamicsyntax';
|
||||||
|
outerDocument.head.appendChild(outerStyle);
|
||||||
|
|
||||||
|
// <body> 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 = '<!DOCTYPE html>';
|
||||||
|
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;
|
||||||
|
|
||||||
|
// <html> tag
|
||||||
|
innerDocument.documentElement.classList.add('inner-editor', ...skinVariants);
|
||||||
|
|
||||||
|
// <head> 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);
|
||||||
|
|
||||||
|
// <body> 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;
|
loaded = true;
|
||||||
doActionsPendingInit();
|
doActionsPendingInit();
|
||||||
debugLog('Ace2Editor.init() done');
|
debugLog('Ace2Editor.init() done');
|
||||||
|
|
|
@ -30,11 +30,10 @@ const htmlPrettyEscape = Ace2Common.htmlPrettyEscape;
|
||||||
const noop = Ace2Common.noop;
|
const noop = Ace2Common.noop;
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
|
||||||
function Ace2Inner(editorInfo) {
|
function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
||||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
|
||||||
const domline = require('./domline').domline;
|
const domline = require('./domline').domline;
|
||||||
const AttribPool = require('./AttributePool');
|
const AttribPool = require('./AttributePool');
|
||||||
const Changeset = require('./Changeset');
|
const Changeset = require('./Changeset');
|
||||||
|
@ -157,10 +156,6 @@ function Ace2Inner(editorInfo) {
|
||||||
|
|
||||||
const scheduler = parent; // hack for opera required
|
const scheduler = parent; // hack for opera required
|
||||||
|
|
||||||
let dynamicCSS = null;
|
|
||||||
let outerDynamicCSS = null;
|
|
||||||
let parentDynamicCSS = null;
|
|
||||||
|
|
||||||
const performDocumentReplaceRange = (start, end, newText) => {
|
const performDocumentReplaceRange = (start, end, newText) => {
|
||||||
if (start === undefined) start = rep.selStart;
|
if (start === undefined) start = rep.selStart;
|
||||||
if (end === undefined) end = rep.selEnd;
|
if (end === undefined) end = rep.selEnd;
|
||||||
|
@ -181,12 +176,6 @@ function Ace2Inner(editorInfo) {
|
||||||
performDocumentApplyChangeset(cs);
|
performDocumentApplyChangeset(cs);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initDynamicCSS = () => {
|
|
||||||
dynamicCSS = makeCSSManager('dynamicsyntax');
|
|
||||||
outerDynamicCSS = makeCSSManager('dynamicsyntax', 'outer');
|
|
||||||
parentDynamicCSS = makeCSSManager('dynamicsyntax', 'parent');
|
|
||||||
};
|
|
||||||
|
|
||||||
const changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
|
const changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
|
||||||
withCallbacks: (operationName, f) => {
|
withCallbacks: (operationName, f) => {
|
||||||
inCallStackIfNecessary(operationName, () => {
|
inCallStackIfNecessary(operationName, () => {
|
||||||
|
@ -214,15 +203,12 @@ function Ace2Inner(editorInfo) {
|
||||||
editorInfo.ace_getAuthorInfos = getAuthorInfos;
|
editorInfo.ace_getAuthorInfos = getAuthorInfos;
|
||||||
|
|
||||||
const setAuthorStyle = (author, info) => {
|
const setAuthorStyle = (author, info) => {
|
||||||
if (!dynamicCSS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));
|
const authorSelector = getAuthorColorClassSelector(getAuthorClassName(author));
|
||||||
|
|
||||||
const authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
|
const authorStyleSet = hooks.callAll('aceSetAuthorStyle', {
|
||||||
dynamicCSS,
|
dynamicCSS: cssManagers.inner,
|
||||||
parentDynamicCSS,
|
outerDynamicCSS: cssManagers.outer,
|
||||||
outerDynamicCSS,
|
parentDynamicCSS: cssManagers.parent,
|
||||||
info,
|
info,
|
||||||
author,
|
author,
|
||||||
authorSelector,
|
authorSelector,
|
||||||
|
@ -234,16 +220,16 @@ function Ace2Inner(editorInfo) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
dynamicCSS.removeSelectorStyle(authorSelector);
|
cssManagers.inner.removeSelectorStyle(authorSelector);
|
||||||
parentDynamicCSS.removeSelectorStyle(authorSelector);
|
cssManagers.parent.removeSelectorStyle(authorSelector);
|
||||||
} else if (info.bgcolor) {
|
} else if (info.bgcolor) {
|
||||||
let bgcolor = info.bgcolor;
|
let bgcolor = info.bgcolor;
|
||||||
if ((typeof info.fade) === 'number') {
|
if ((typeof info.fade) === 'number') {
|
||||||
bgcolor = fadeColor(bgcolor, info.fade);
|
bgcolor = fadeColor(bgcolor, info.fade);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorStyle = dynamicCSS.selectorStyle(authorSelector);
|
const authorStyle = cssManagers.inner.selectorStyle(authorSelector);
|
||||||
const parentAuthorStyle = parentDynamicCSS.selectorStyle(authorSelector);
|
const parentAuthorStyle = cssManagers.parent.selectorStyle(authorSelector);
|
||||||
|
|
||||||
// author color
|
// author color
|
||||||
authorStyle.backgroundColor = bgcolor;
|
authorStyle.backgroundColor = bgcolor;
|
||||||
|
@ -3895,8 +3881,8 @@ function Ace2Inner(editorInfo) {
|
||||||
editorInfo.ace_performDocumentApplyAttributesToRange =
|
editorInfo.ace_performDocumentApplyAttributesToRange =
|
||||||
(...args) => documentAttributeManager.setAttributesOnRange(...args);
|
(...args) => documentAttributeManager.setAttributesOnRange(...args);
|
||||||
|
|
||||||
this.init = (cb) => {
|
this.init = async () => {
|
||||||
$(document).ready(() => {
|
await $.ready;
|
||||||
doc = document; // defined as a var in scope outside
|
doc = document; // defined as a var in scope outside
|
||||||
inCallStack('setup', () => {
|
inCallStack('setup', () => {
|
||||||
const body = doc.getElementById('innerdocbody');
|
const body = doc.getElementById('innerdocbody');
|
||||||
|
@ -3906,8 +3892,6 @@ function Ace2Inner(editorInfo) {
|
||||||
root.classList.toggle('authorColors', true);
|
root.classList.toggle('authorColors', true);
|
||||||
root.classList.toggle('doesWrap', doesWrap);
|
root.classList.toggle('doesWrap', doesWrap);
|
||||||
|
|
||||||
initDynamicCSS();
|
|
||||||
|
|
||||||
enforceEditability();
|
enforceEditability();
|
||||||
|
|
||||||
// set up dom and rep
|
// set up dom and rep
|
||||||
|
@ -3926,13 +3910,10 @@ function Ace2Inner(editorInfo) {
|
||||||
rep,
|
rep,
|
||||||
documentAttributeManager,
|
documentAttributeManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
scheduler.setTimeout(cb, 0);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.init = (editorInfo, cb) => {
|
exports.init = async (editorInfo, cssManagers) => {
|
||||||
const editor = new Ace2Inner(editorInfo);
|
const editor = new Ace2Inner(editorInfo, cssManagers);
|
||||||
editor.init(cb);
|
await editor.init();
|
||||||
};
|
};
|
||||||
|
|
|
@ -468,14 +468,14 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
|
|
||||||
BroadcastSlider.onSlider(goToRevisionIfEnabled);
|
BroadcastSlider.onSlider(goToRevisionIfEnabled);
|
||||||
|
|
||||||
const dynamicCSS = makeCSSManager('dynamicsyntax');
|
const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet);
|
||||||
const authorData = {};
|
const authorData = {};
|
||||||
|
|
||||||
const receiveAuthorData = (newAuthorData) => {
|
const receiveAuthorData = (newAuthorData) => {
|
||||||
for (const [author, data] of Object.entries(newAuthorData)) {
|
for (const [author, data] of Object.entries(newAuthorData)) {
|
||||||
const bgcolor = typeof data.colorId === 'number'
|
const bgcolor = typeof data.colorId === 'number'
|
||||||
? clientVars.colorPalette[data.colorId] : data.colorId;
|
? clientVars.colorPalette[data.colorId] : data.colorId;
|
||||||
if (bgcolor && dynamicCSS) {
|
if (bgcolor) {
|
||||||
const selector = dynamicCSS.selectorStyle(`.${linestylefilter.getAuthorClassName(author)}`);
|
const selector = dynamicCSS.selectorStyle(`.${linestylefilter.getAuthorClassName(author)}`);
|
||||||
selector.backgroundColor = bgcolor;
|
selector.backgroundColor = bgcolor;
|
||||||
selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5)
|
selector.color = (colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5)
|
||||||
|
|
|
@ -22,37 +22,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const makeCSSManager = (emptyStylesheetTitle, doc) => {
|
exports.makeCSSManager = (browserSheet) => {
|
||||||
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);
|
|
||||||
|
|
||||||
const browserRules = () => (browserSheet.cssRules || browserSheet.rules);
|
const browserRules = () => (browserSheet.cssRules || browserSheet.rules);
|
||||||
|
|
||||||
const browserDeleteRule = (i) => {
|
const browserDeleteRule = (i) => {
|
||||||
|
@ -100,5 +70,3 @@ const makeCSSManager = (emptyStylesheetTitle, doc) => {
|
||||||
info: () => `${selectorList.length}:${browserRules().length}`,
|
info: () => `${selectorList.length}:${browserRules().length}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.makeCSSManager = makeCSSManager;
|
|
||||||
|
|
|
@ -501,7 +501,7 @@ const pad = {
|
||||||
// order of inits is important here:
|
// order of inits is important here:
|
||||||
padimpexp.init(this);
|
padimpexp.init(this);
|
||||||
padsavedrevs.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);
|
paduserlist.init(pad.myUserInfo, this);
|
||||||
padconnectionstatus.init();
|
padconnectionstatus.init();
|
||||||
padmodals.init(this);
|
padmodals.init(this);
|
||||||
|
|
|
@ -34,30 +34,20 @@ const padeditor = (() => {
|
||||||
ace: null,
|
ace: null,
|
||||||
// this is accessed directly from other files
|
// this is accessed directly from other files
|
||||||
viewZoom: 100,
|
viewZoom: 100,
|
||||||
init: (readyFunc, initialViewOptions, _pad) => {
|
init: async (initialViewOptions, _pad) => {
|
||||||
Ace2Editor = require('./ace').Ace2Editor;
|
Ace2Editor = require('./ace').Ace2Editor;
|
||||||
pad = _pad;
|
pad = _pad;
|
||||||
settings = pad.settings;
|
settings = pad.settings;
|
||||||
|
self.ace = new Ace2Editor();
|
||||||
const aceReady = () => {
|
await self.ace.init('editorcontainer', '');
|
||||||
$('#editorloadingbox').hide();
|
$('#editorloadingbox').hide();
|
||||||
if (readyFunc) {
|
|
||||||
readyFunc();
|
|
||||||
|
|
||||||
// Listen for clicks on sidediv items
|
// Listen for clicks on sidediv items
|
||||||
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
|
const $outerdoc = $('iframe[name="ace_outer"]').contents().find('#outerdocbody');
|
||||||
$outerdoc.find('#sidedivinner').on('click', 'div', function () {
|
$outerdoc.find('#sidedivinner').on('click', 'div', function () {
|
||||||
const targetLineNumber = $(this).index() + 1;
|
const targetLineNumber = $(this).index() + 1;
|
||||||
window.location.hash = `L${targetLineNumber}`;
|
window.location.hash = `L${targetLineNumber}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.focusOnLine(self.ace);
|
exports.focusOnLine(self.ace);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.ace = new Ace2Editor();
|
|
||||||
self.ace.init('editorcontainer', '').then(
|
|
||||||
() => aceReady(), (err) => { throw err || new Error(err); });
|
|
||||||
self.ace.setProperty('wraps', true);
|
self.ace.setProperty('wraps', true);
|
||||||
if (pad.getIsDebugEnabled()) {
|
if (pad.getIsDebugEnabled()) {
|
||||||
self.ace.setProperty('dmesg', pad.dmesg);
|
self.ace.setProperty('dmesg', pad.dmesg);
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
/* intentionally empty */
|
|
@ -7,7 +7,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
||||||
<% e.begin_block("htmlHead"); %>
|
<% e.begin_block("htmlHead"); %>
|
||||||
<html class="<%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
|
<html class="pad <%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
|
||||||
<% e.end_block(); %>
|
<% e.end_block(); %>
|
||||||
|
|
||||||
<title><%=settings.title%></title>
|
<title><%=settings.title%></title>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
|
, langs = require("ep_etherpad-lite/node/hooks/i18n").availableLangs
|
||||||
%>
|
%>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html class="<%=settings.skinVariants%>">
|
<html class="pad <%=settings.skinVariants%>">
|
||||||
<title data-l10n-id="timeslider.pageTitle" data-l10n-args='{ "appTitle": "<%=settings.title%>" }'><%=settings.title%> Timeslider</title>
|
<title data-l10n-id="timeslider.pageTitle" data-l10n-args='{ "appTitle": "<%=settings.title%>" }'><%=settings.title%> Timeslider</title>
|
||||||
<script>
|
<script>
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
* TODO: unify those two files, and merge in a single one.
|
* TODO: unify those two files, and merge in a single one.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const assert = require('assert').strict;
|
||||||
const common = require('../../common');
|
const common = require('../../common');
|
||||||
|
|
||||||
let agent;
|
let agent;
|
||||||
|
@ -226,6 +227,8 @@ const testImports = {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe(__filename, function () {
|
describe(__filename, function () {
|
||||||
|
this.timeout(1000);
|
||||||
|
|
||||||
before(async function () { agent = await common.init(); });
|
before(async function () { agent = await common.init(); });
|
||||||
|
|
||||||
Object.keys(testImports).forEach((testName) => {
|
Object.keys(testImports).forEach((testName) => {
|
||||||
|
@ -237,73 +240,34 @@ describe(__filename, function () {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
it('createPad', function (done) {
|
|
||||||
this.timeout(200);
|
it('createPad', async function () {
|
||||||
agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
|
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
|
||||||
.expect((res) => {
|
.expect(200)
|
||||||
if (res.body.code !== 0) throw new Error('Unable to create new Pad');
|
.expect('Content-Type', /json/);
|
||||||
})
|
assert.equal(res.body.code, 0);
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200, done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setHTML', function (done) {
|
it('setHTML', async function () {
|
||||||
this.timeout(150);
|
const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
|
||||||
agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
|
|
||||||
`&html=${encodeURIComponent(test.input)}`)
|
`&html=${encodeURIComponent(test.input)}`)
|
||||||
.expect((res) => {
|
.expect(200)
|
||||||
if (res.body.code !== 0) throw new Error(`Error:${testName}`);
|
.expect('Content-Type', /json/);
|
||||||
})
|
assert.equal(res.body.code, 0);
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200, done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getHTML', function (done) {
|
it('getHTML', async function () {
|
||||||
this.timeout(150);
|
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
|
||||||
agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect('Content-Type', /json/);
|
||||||
const gotHtml = res.body.data.html;
|
assert.equal(res.body.data.html, test.wantHTML);
|
||||||
if (gotHtml !== test.wantHTML) {
|
|
||||||
throw new Error(`HTML received from export is not the one we were expecting.
|
|
||||||
Test Name:
|
|
||||||
${testName}
|
|
||||||
|
|
||||||
Got:
|
|
||||||
${JSON.stringify(gotHtml)}
|
|
||||||
|
|
||||||
Want:
|
|
||||||
${JSON.stringify(test.wantHTML)}
|
|
||||||
|
|
||||||
Which is a different version of the originally imported one:
|
|
||||||
${test.input}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200, done);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getText', function (done) {
|
it('getText', async function () {
|
||||||
this.timeout(100);
|
const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`)
|
||||||
agent.get(`${endPoint('getText')}&padID=${testPadId}`)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect('Content-Type', /json/);
|
||||||
const gotText = res.body.data.text;
|
assert.equal(res.body.data.text, test.wantText);
|
||||||
if (gotText !== test.wantText) {
|
|
||||||
throw new Error(`Text received from export is not the one we were expecting.
|
|
||||||
Test Name:
|
|
||||||
${testName}
|
|
||||||
|
|
||||||
Got:
|
|
||||||
${JSON.stringify(gotText)}
|
|
||||||
|
|
||||||
Want:
|
|
||||||
${JSON.stringify(test.wantText)}
|
|
||||||
|
|
||||||
Which is a different version of the originally imported one:
|
|
||||||
${test.input}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.expect('Content-Type', /json/)
|
|
||||||
.expect(200, done);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
26
src/tests/backend/specs/export.js
Normal file
26
src/tests/backend/specs/export.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const padManager = require('../../../node/db/PadManager');
|
||||||
|
const settings = require('../../../node/utils/Settings');
|
||||||
|
|
||||||
|
describe(__filename, function () {
|
||||||
|
let agent;
|
||||||
|
const settingsBackup = {};
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
agent = await common.init();
|
||||||
|
settingsBackup.soffice = settings.soffice;
|
||||||
|
await padManager.getPad('testExportPad', 'test content');
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
Object.assign(settings, settingsBackup);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 500 on export error', async function () {
|
||||||
|
settings.soffice = 'false'; // '/bin/false' doesn't work on Windows
|
||||||
|
await agent.get('/p/testExportPad/export/doc')
|
||||||
|
.expect(500);
|
||||||
|
});
|
||||||
|
});
|
31
src/tests/frontend/specs/inner_height.js
Normal file
31
src/tests/frontend/specs/inner_height.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
describe('height regression after ace.js refactoring', function () {
|
||||||
|
before(function (cb) {
|
||||||
|
helper.newPad(cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
// everything fits inside the viewport
|
||||||
|
it('clientHeight should equal scrollHeight with few lines', function() {
|
||||||
|
const aceOuter = helper.padChrome$('iframe')[0].contentDocument;
|
||||||
|
const clientHeight = aceOuter.documentElement.clientHeight;
|
||||||
|
const scrollHeight = aceOuter.documentElement.scrollHeight;
|
||||||
|
expect(clientHeight).to.be(scrollHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('client height should be less than scrollHeight with many lines', async function () {
|
||||||
|
await helper.clearPad();
|
||||||
|
await helper.edit('Test line\n' +
|
||||||
|
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||||
|
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||||
|
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||||
|
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||||
|
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||||
|
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n' +
|
||||||
|
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
|
||||||
|
const aceOuter = helper.padChrome$('iframe')[0].contentDocument;
|
||||||
|
const clientHeight = aceOuter.documentElement.clientHeight;
|
||||||
|
const scrollHeight = aceOuter.documentElement.scrollHeight;
|
||||||
|
expect(clientHeight).to.be.lessThan(scrollHeight);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue