mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 22:23:33 +01:00
Merge branch 'develop' into session-creation-tests
This commit is contained in:
commit
d696a048dc
19 changed files with 322 additions and 237 deletions
83
.github/workflows/major-version-git-pull-update.yml
vendored
Normal file
83
.github/workflows/major-version-git-pull-update.yml
vendored
Normal file
|
@ -0,0 +1,83 @@
|
|||
name: "In-place git pull from master"
|
||||
|
||||
# any branch is useful for testing before a PR is submitted
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
withpluginsLinux:
|
||||
# run on pushes to any branch
|
||||
# run on PRs from external forks
|
||||
if: |
|
||||
(github.event_name != 'pull_request')
|
||||
|| (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
|
||||
name: Linux with Plugins
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node: [10, 12, 14, 15]
|
||||
|
||||
steps:
|
||||
- name: Checkout master repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install Etherpad plugins
|
||||
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
|
||||
# https://github.com/npm/cli/issues/2199
|
||||
run: >
|
||||
npm install --no-save --legacy-peer-deps
|
||||
ep_align
|
||||
ep_author_hover
|
||||
ep_cursortrace
|
||||
ep_font_size
|
||||
ep_hash_auth
|
||||
ep_headings2
|
||||
ep_image_upload
|
||||
ep_markdown
|
||||
ep_readonly_guest
|
||||
ep_set_title_on_pad
|
||||
ep_spellcheck
|
||||
ep_subscript_and_superscript
|
||||
ep_table_of_contents
|
||||
|
||||
# This must be run after installing the plugins, otherwise npm will try to
|
||||
# hoist common dependencies by removing them from src/node_modules and
|
||||
# installing them in the top-level node_modules. As of v6.14.10, npm's hoist
|
||||
# logic appears to be buggy, because it sometimes removes dependencies from
|
||||
# src/node_modules but fails to add them to the top-level node_modules. Even
|
||||
# if npm correctly hoists the dependencies, the hoisting seems to confuse
|
||||
# tools such as `npm outdated`, `npm update`, and some ESLint rules.
|
||||
- name: Install all dependencies and symlink for ep_etherpad-lite
|
||||
run: src/bin/installDeps.sh
|
||||
|
||||
- name: Run the backend tests
|
||||
run: cd src && npm test
|
||||
|
||||
- name: Git fetch
|
||||
run: git fetch
|
||||
|
||||
- name: Checkout this branch over master
|
||||
run: git checkout "${GITHUB_SHA}"
|
||||
|
||||
- name: Install all dependencies and symlink for ep_etherpad-lite
|
||||
run: src/bin/installDeps.sh
|
||||
|
||||
- name: Run the backend tests
|
||||
run: cd src && npm test
|
||||
|
||||
- name: Install Cypress
|
||||
run: npm install cypress -g
|
||||
|
||||
- name: Run Etherpad & Test Frontend
|
||||
run: |
|
||||
node src/node/server.js &
|
||||
curl --connect-timeout 10 --max-time 20 --retry 5 --retry-delay 10 --retry-max-time 60 --retry-connrefused http://127.0.0.1:9001/p/test
|
||||
cd src/tests/frontend
|
||||
cypress run --spec cypress/integration/test.js --config-file cypress/cypress.json
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -1,11 +1,27 @@
|
|||
# 1.8.12
|
||||
|
||||
Special mention: Thanks to Sauce Labs for additional testing tunnels to help us grow! :)
|
||||
|
||||
### Security patches
|
||||
|
||||
* Fixed a regression in v1.8.11 which caused some pad names to cause Etherpad to restart.
|
||||
|
||||
### Notable fixes
|
||||
|
||||
* Fixed a bug in the `dirty` database driver that sometimes caused Node.js to
|
||||
crash during shutdown and lose buffered database writes.
|
||||
* Fixed a regression in v1.8.8 that caused "Uncaught TypeError: Cannot read
|
||||
property '0' of undefined" with some plugins (#4885)
|
||||
* Less warnings in server console for supported element types on import.
|
||||
* Support Azure and other network share installations by using a
|
||||
more truthful relative path.
|
||||
|
||||
### Notable enhancements
|
||||
|
||||
* Dependency updates
|
||||
* Various Docker deployment improvements
|
||||
* Various new translations
|
||||
* Improvement of rendering of plugin hook list and error message handling
|
||||
|
||||
# 1.8.11
|
||||
|
||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -40,9 +40,20 @@ ENV NODE_ENV=production
|
|||
#
|
||||
# Running as non-root enables running this image in platforms like OpenShift
|
||||
# that do not allow images running as root.
|
||||
RUN useradd --uid 5001 --create-home etherpad
|
||||
#
|
||||
# If any of the following args are set to the empty string, default
|
||||
# values will be chosen.
|
||||
ARG EP_HOME=
|
||||
ARG EP_UID=5001
|
||||
ARG EP_GID=0
|
||||
ARG EP_SHELL=
|
||||
RUN groupadd --system ${EP_GID:+--gid "${EP_GID}" --non-unique} etherpad && \
|
||||
useradd --system ${EP_UID:+--uid "${EP_UID}" --non-unique} --gid etherpad \
|
||||
${EP_HOME:+--home-dir "${EP_HOME}"} --create-home \
|
||||
${EP_SHELL:+--shell "${EP_SHELL}"} etherpad
|
||||
|
||||
RUN mkdir /opt/etherpad-lite && chown etherpad:0 /opt/etherpad-lite
|
||||
ARG EP_DIR=/opt/etherpad-lite
|
||||
RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
|
||||
|
||||
# install abiword for DOC/PDF/ODT export
|
||||
RUN [ -z "${INSTALL_ABIWORD}" ] || (apt update && apt -y install abiword && apt clean && rm -rf /var/lib/apt/lists/*)
|
||||
|
@ -53,24 +64,20 @@ RUN [ -z "${INSTALL_SOFFICE}" ] || (apt update && mkdir -p /usr/share/man/man1 &
|
|||
|
||||
USER etherpad
|
||||
|
||||
WORKDIR /opt/etherpad-lite
|
||||
WORKDIR "${EP_DIR}"
|
||||
|
||||
COPY --chown=etherpad:0 ./ ./
|
||||
COPY --chown=etherpad:etherpad ./ ./
|
||||
|
||||
# install node dependencies for Etherpad
|
||||
RUN src/bin/installDeps.sh && \
|
||||
rm -rf ~/.npm/_cacache
|
||||
|
||||
# Install the plugins, if ETHERPAD_PLUGINS is not empty.
|
||||
#
|
||||
# Bash trick: in the for loop ${ETHERPAD_PLUGINS} is NOT quoted, in order to be
|
||||
# able to split at spaces.
|
||||
RUN for PLUGIN_NAME in ${ETHERPAD_PLUGINS}; do npm install "${PLUGIN_NAME}" || exit 1; done
|
||||
RUN [ -z "${ETHERPAD_PLUGINS}" ] || npm install ${ETHERPAD_PLUGINS}
|
||||
|
||||
# Copy the configuration file.
|
||||
COPY --chown=etherpad:0 ./settings.json.docker /opt/etherpad-lite/settings.json
|
||||
COPY --chown=etherpad:etherpad ./settings.json.docker "${EP_DIR}"/settings.json
|
||||
|
||||
# Fix permissions for root group
|
||||
# Fix group permissions
|
||||
RUN chmod -R g=u .
|
||||
|
||||
EXPOSE 9001
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
"Upwinxp"
|
||||
]
|
||||
},
|
||||
"admin_plugins.description": "Opis",
|
||||
"admin_plugins.last-update": "Zadnja posodobitev",
|
||||
"admin_plugins.name": "Ime",
|
||||
"admin_plugins.version": "Različica",
|
||||
"admin_settings": "Nastavitve",
|
||||
"admin_settings.current_save.value": "Shrani nastavitve",
|
||||
"index.newPad": "Nov dokument",
|
||||
"index.createOpenPad": "ali pa ustvari/odpri dokument z imenom:",
|
||||
"pad.toolbar.bold.title": "Krepko (Ctrl-B)",
|
||||
|
|
|
@ -26,8 +26,8 @@ exports.expressCreateServer = (hookName, args, cb) => {
|
|||
epVersion,
|
||||
installedPlugins: `<pre>${plugins.formatPlugins().replace(/, /g, '\n')}</pre>`,
|
||||
installedParts: `<pre>${plugins.formatParts()}</pre>`,
|
||||
installedServerHooks: `<div>${plugins.formatHooks()}</div>`,
|
||||
installedClientHooks: `<div>${plugins.formatHooks('client_hooks')}</div>`,
|
||||
installedServerHooks: `<div>${plugins.formatHooks('hooks', true)}</div>`,
|
||||
installedClientHooks: `<div>${plugins.formatHooks('client_hooks', true)}</div>`,
|
||||
latestVersion: UpdateCheck.getLatestVersion(),
|
||||
req,
|
||||
}));
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -144,7 +144,7 @@ exports.start = async () => {
|
|||
.join(', ');
|
||||
logger.info(`Installed plugins: ${installedPlugins}`);
|
||||
logger.debug(`Installed parts:\n${plugins.formatParts()}`);
|
||||
logger.debug(`Installed hooks:\n${plugins.formatHooks()}`);
|
||||
logger.debug(`Installed server-side hooks:\n${plugins.formatHooks('hooks', false)}`);
|
||||
await hooks.aCallAll('loadSettings', {settings});
|
||||
await hooks.aCallAll('createServer');
|
||||
} catch (err) {
|
||||
|
|
|
@ -18,15 +18,14 @@
|
|||
|
||||
const db = require('../db/DB');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const supportedElems = require('../../static/js/contentcollector').supportedElems;
|
||||
|
||||
exports.setPadRaw = (padId, r) => {
|
||||
const records = JSON.parse(r);
|
||||
|
||||
const blockElems = ['div', 'br', 'p', 'pre', 'li', 'author', 'lmkr', 'insertorder'];
|
||||
|
||||
// get supported block Elements from plugins, we will use this later.
|
||||
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
|
||||
blockElems.push(element);
|
||||
supportedElems.push(element);
|
||||
});
|
||||
|
||||
Object.keys(records).forEach(async (key) => {
|
||||
|
@ -65,7 +64,7 @@ exports.setPadRaw = (padId, r) => {
|
|||
if (value.pool) {
|
||||
for (const attrib of Object.keys(value.pool.numToAttrib)) {
|
||||
const attribName = value.pool.numToAttrib[attrib][0];
|
||||
if (blockElems.indexOf(attribName) === -1) {
|
||||
if (supportedElems.indexOf(attribName) === -1) {
|
||||
console.warn('Plugin missing: ' +
|
||||
`You might want to install a plugin to support this node name: ${attribName}`);
|
||||
}
|
||||
|
|
10
src/package-lock.json
generated
10
src/package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ep_etherpad-lite",
|
||||
"version": "1.8.11",
|
||||
"version": "1.8.12",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -7539,11 +7539,11 @@
|
|||
}
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.19.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
|
||||
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
|
||||
"version": "1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
|
||||
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
|
||||
"requires": {
|
||||
"is-core-module": "^2.1.0",
|
||||
"is-core-module": "^2.2.0",
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"rehype": "^10.0.0",
|
||||
"rehype-minify-whitespace": "^4.0.5",
|
||||
"request": "2.88.2",
|
||||
"resolve": "1.19.0",
|
||||
"resolve": "1.20.0",
|
||||
"security": "1.0.0",
|
||||
"semver": "5.7.1",
|
||||
"socket.io": "^2.4.1",
|
||||
|
@ -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.11",
|
||||
"version": "1.8.12",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html:not(.inner-editor), html:not(.inner-editor) body {
|
||||
html.pad, html.pad body {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
|
|
|
@ -28,88 +28,19 @@ const hooks = require('./pluginfw/hooks');
|
|||
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 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);
|
||||
});
|
||||
};
|
||||
|
||||
const pollCondition = async (predicate, cleanups, pollPeriod, timeout) => {
|
||||
let done = false;
|
||||
cleanups.push(() => { done = true; });
|
||||
// Pause a tick to give the predicate a chance to become true before adding latency.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const start = Date.now();
|
||||
while (!done && !predicate()) {
|
||||
if (Date.now() - start > timeout) throw new Error('timeout');
|
||||
await new Promise((resolve) => setTimeout(resolve, pollPeriod));
|
||||
debugLog('Ace2Editor.init() polling');
|
||||
}
|
||||
if (!done) debugLog('Ace2Editor.init() poll condition became true');
|
||||
};
|
||||
|
||||
// Resolves when the frame's document is ready to be mutated:
|
||||
// - Firefox seems to replace the frame's contentWindow.document object with a different object
|
||||
// after the frame is created so we need to wait for the window's load event before continuing.
|
||||
// - Chrome doesn't need any waiting (not even next tick), but on Windows it never seems to fire
|
||||
// any events. Eventually the document's readyState becomes 'complete' (even though it never
|
||||
// fires a readystatechange event), so this function waits for that to happen to avoid returning
|
||||
// too soon on Firefox.
|
||||
// - Safari behaves like Chrome.
|
||||
// I'm not sure how other browsers behave, 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'),
|
||||
// If all else fails, poll.
|
||||
pollCondition(() => doc().readyState === 'complete', cleanups, 10, 5000),
|
||||
]);
|
||||
} finally {
|
||||
for (const cleanup of cleanups) cleanup();
|
||||
}
|
||||
};
|
||||
const scriptTag =
|
||||
(source) => `<script type="text/javascript">\n${source.replace(/<\//g, '<\\/')}</script>`;
|
||||
|
||||
const Ace2Editor = function () {
|
||||
let info = {editor: this};
|
||||
window.ace2EditorInfo = info; // Make it accessible to iframes.
|
||||
let loaded = false;
|
||||
|
||||
let actionsPendingInit = [];
|
||||
|
@ -178,19 +109,16 @@ const Ace2Editor = function () {
|
|||
// returns array of {error: <browser Error object>, time: +new Date()}
|
||||
this.getUnhandledErrors = () => loaded ? info.ace_getUnhandledErrors() : [];
|
||||
|
||||
const addStyleTagsFor = (doc, files) => {
|
||||
const pushStyleTagsFor = (buffer, files) => {
|
||||
for (const file of files) {
|
||||
const link = doc.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = absUrl(encodeURI(file));
|
||||
doc.head.appendChild(link);
|
||||
buffer.push(`<link rel="stylesheet" type="text/css" href="${absUrl(encodeURI(file))}"/>`);
|
||||
}
|
||||
};
|
||||
|
||||
this.destroy = pendingInit(() => {
|
||||
info.ace_dispose();
|
||||
info.frame.parentNode.removeChild(info.frame);
|
||||
delete window.ace2EditorInfo;
|
||||
info = null; // prevent IE 6 closure memory leaks
|
||||
});
|
||||
|
||||
|
@ -207,128 +135,109 @@ const Ace2Editor = function () {
|
|||
`../static/skins/${clientVars.skinName}/pad.css?v=${clientVars.randomVersionString}`,
|
||||
];
|
||||
|
||||
const skinVariants = clientVars.skinVariants.split(' ').filter((x) => x !== '');
|
||||
const doctype = '<!doctype html>';
|
||||
|
||||
const outerFrame = document.createElement('iframe');
|
||||
const iframeHTML = [];
|
||||
|
||||
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="outer-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>',
|
||||
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.frameBorder = 0; // for IE
|
||||
outerFrame.title = 'Ether';
|
||||
info.frame = outerFrame;
|
||||
document.getElementById(containerId).appendChild(outerFrame);
|
||||
const outerWindow = outerFrame.contentWindow;
|
||||
|
||||
// For some unknown reason Firefox replaces outerWindow.document with a new Document object some
|
||||
// time between running the above code and firing the outerWindow load event. Work around it by
|
||||
// waiting until the load event fires before mutating the Document object.
|
||||
const editorDocument = outerFrame.contentWindow.document;
|
||||
|
||||
debugLog('Ace2Editor.init() waiting for outer frame');
|
||||
await frameReady(outerFrame);
|
||||
debugLog('Ace2Editor.init() outer frame ready');
|
||||
|
||||
// This must be done after the Window's load event. See above comment.
|
||||
const outerDocument = outerWindow.document;
|
||||
|
||||
// <html> tag
|
||||
outerDocument.documentElement.classList.add('inner-editor', 'outerdoc', ...skinVariants);
|
||||
|
||||
// <head> tag
|
||||
addStyleTagsFor(outerDocument, includedCSS);
|
||||
const outerStyle = outerDocument.createElement('style');
|
||||
outerStyle.type = 'text/css';
|
||||
outerStyle.title = 'dynamicsyntax';
|
||||
outerDocument.head.appendChild(outerStyle);
|
||||
const link = outerDocument.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = 'data:text/css,';
|
||||
outerDocument.head.appendChild(link);
|
||||
|
||||
// <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
|
||||
innerFrame.ace_outerWin = outerWindow;
|
||||
outerDocument.body.insertBefore(innerFrame, outerDocument.body.firstChild);
|
||||
const innerWindow = innerFrame.contentWindow;
|
||||
|
||||
// Wait before mutating the inner document. See above comment recarding outerWindow load.
|
||||
debugLog('Ace2Editor.init() waiting for inner frame');
|
||||
await frameReady(innerFrame);
|
||||
debugLog('Ace2Editor.init() inner frame ready');
|
||||
|
||||
// This must be done after the Window's load event. See above comment.
|
||||
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 new Promise((resolve, reject) => innerWindow.Ace2Inner.init(
|
||||
info, (err) => err != null ? reject(err) : resolve()));
|
||||
debugLog('Ace2Editor.init() Ace2Inner.init() returned');
|
||||
await new Promise((resolve, reject) => {
|
||||
info.onEditorReady = (err) => err != null ? reject(err) : resolve();
|
||||
editorDocument.open();
|
||||
editorDocument.write(outerHTML.join(''));
|
||||
editorDocument.close();
|
||||
});
|
||||
loaded = true;
|
||||
doActionsPendingInit();
|
||||
debugLog('Ace2Editor.init() done');
|
||||
|
|
|
@ -55,6 +55,28 @@ const getAttribute = (n, a) => {
|
|||
if (n.attribs != null) return n.attribs[a];
|
||||
return null;
|
||||
};
|
||||
// supportedElems are Supported natively within Etherpad and don't require a plugin
|
||||
const supportedElems = [
|
||||
'author',
|
||||
'b',
|
||||
'bold',
|
||||
'br',
|
||||
'div',
|
||||
'font',
|
||||
'i',
|
||||
'insertorder',
|
||||
'italic',
|
||||
'li',
|
||||
'lmkr',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'strong',
|
||||
's',
|
||||
'span',
|
||||
'u',
|
||||
'ul',
|
||||
];
|
||||
|
||||
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
|
||||
const _blockElems = {
|
||||
|
@ -66,6 +88,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
|
||||
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
|
||||
_blockElems[element] = 1;
|
||||
supportedElems.push(element);
|
||||
});
|
||||
|
||||
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
|
||||
|
@ -315,9 +338,11 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
const localAttribs = state.localAttribs;
|
||||
state.localAttribs = null;
|
||||
const isBlock = isBlockElement(node);
|
||||
if (!isBlock && node.name && (node.name !== 'body') && (node.name !== 'br')) {
|
||||
console.warn('Plugin missing: ' +
|
||||
if (!isBlock && node.name && (node.name !== 'body')) {
|
||||
if (supportedElems.indexOf(node.name) === -1) {
|
||||
console.warn('Plugin missing: ' +
|
||||
`You might want to install a plugin to support this node name: ${node.name}`);
|
||||
}
|
||||
}
|
||||
const isEmpty = _isEmpty(node, state);
|
||||
if (isBlock) _ensureColumnZero(state);
|
||||
|
@ -701,3 +726,4 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
|
||||
exports.sanitizeUnicode = sanitizeUnicode;
|
||||
exports.makeContentCollector = makeContentCollector;
|
||||
exports.supportedElems = supportedElems;
|
||||
|
|
|
@ -28,16 +28,48 @@ exports.formatPlugins = () => Object.keys(defs.plugins).join(', ');
|
|||
|
||||
exports.formatParts = () => defs.parts.map((part) => part.full_name).join('\n');
|
||||
|
||||
exports.formatHooks = (hookSetName) => {
|
||||
const res = [];
|
||||
const hooks = pluginUtils.extractHooks(defs.parts, hookSetName || 'hooks');
|
||||
for (const registeredHooks of Object.values(hooks)) {
|
||||
for (const hook of registeredHooks) {
|
||||
res.push(`<dt>${hook.hook_name}</dt><dd>${hook.hook_fn_name} ` +
|
||||
`from ${hook.part.full_name}</dd>`);
|
||||
exports.formatHooks = (hookSetName, html) => {
|
||||
let hooks = new Map();
|
||||
for (const [pluginName, def] of Object.entries(defs.plugins)) {
|
||||
for (const part of def.parts) {
|
||||
for (const [hookName, hookFnName] of Object.entries(part[hookSetName] || {})) {
|
||||
let hookEntry = hooks.get(hookName);
|
||||
if (!hookEntry) {
|
||||
hookEntry = new Map();
|
||||
hooks.set(hookName, hookEntry);
|
||||
}
|
||||
let pluginEntry = hookEntry.get(pluginName);
|
||||
if (!pluginEntry) {
|
||||
pluginEntry = new Map();
|
||||
hookEntry.set(pluginName, pluginEntry);
|
||||
}
|
||||
pluginEntry.set(part.name, hookFnName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return `<dl>${res.join('\n')}</dl>`;
|
||||
const lines = [];
|
||||
const sortStringKeys = (a, b) => String(a[0]).localeCompare(b[0]);
|
||||
if (html) lines.push('<dl>');
|
||||
hooks = new Map([...hooks].sort(sortStringKeys));
|
||||
for (const [hookName, hookEntry] of hooks) {
|
||||
lines.push(html ? ` <dt>${hookName}:</dt><dd><dl>` : ` ${hookName}:`);
|
||||
const sortedHookEntry = new Map([...hookEntry].sort(sortStringKeys));
|
||||
hooks.set(hookName, sortedHookEntry);
|
||||
for (const [pluginName, pluginEntry] of sortedHookEntry) {
|
||||
lines.push(html ? ` <dt>${pluginName}:</dt><dd><dl>` : ` ${pluginName}:`);
|
||||
const sortedPluginEntry = new Map([...pluginEntry].sort(sortStringKeys));
|
||||
sortedHookEntry.set(pluginName, sortedPluginEntry);
|
||||
for (const [partName, hookFnName] of sortedPluginEntry) {
|
||||
lines.push(html
|
||||
? ` <dt>${partName}:</dt><dd>${hookFnName}</dd>`
|
||||
: ` ${partName}: ${hookFnName}`);
|
||||
}
|
||||
if (html) lines.push(' </dl></dd>');
|
||||
}
|
||||
if (html) lines.push(' </dl></dd>');
|
||||
}
|
||||
if (html) lines.push('</dl>');
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
const callInit = async () => {
|
||||
|
|
|
@ -55,9 +55,10 @@ const extractHooks = (parts, hookSetName, normalizer) => {
|
|||
try {
|
||||
hookFn = loadFn(hookFnName, hookName);
|
||||
if (!hookFn) throw new Error('Not a function');
|
||||
} catch (exc) {
|
||||
console.error(`Failed to load '${hookFnName}' for ` +
|
||||
`'${part.full_name}/${hookSetName}/${hookName}': ${exc.toString()}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to load hook function "${hookFnName}" for plugin "${part.plugin}" ` +
|
||||
`part "${part.name}" hook set "${hookSetName}" hook "${hookName}": ` +
|
||||
`${err.stack || err}`);
|
||||
}
|
||||
if (hookFn) {
|
||||
if (hooks[hookName] == null) hooks[hookName] = [];
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/* intentionally empty */
|
|
@ -7,7 +7,7 @@
|
|||
<!doctype html>
|
||||
|
||||
<% e.begin_block("htmlHead"); %>
|
||||
<html class="<%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
|
||||
<html class="pad <%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
|
||||
<% e.end_block(); %>
|
||||
|
||||
<title><%=settings.title%></title>
|
||||
|
|
|
@ -10,6 +10,7 @@ Cypress.Commands.add('iframe', {prevSubject: 'element'},
|
|||
describe(__filename, () => {
|
||||
it('Pad content exists', () => {
|
||||
cy.visit('http://127.0.0.1:9001/p/test');
|
||||
cy.wait(10000); // wait for Minified JS to be built...
|
||||
cy.get('iframe[name="ace_outer"]', {timeout: 10000}).iframe()
|
||||
.find('.line-number:first')
|
||||
.should('have.text', '1');
|
||||
|
|
Loading…
Reference in a new issue