Move all files to esm

This commit is contained in:
SamTV12345 2023-06-25 00:05:01 +02:00
parent 237f7242ec
commit 76a6f665a4
No known key found for this signature in database
GPG key ID: E63EEC7466038043
87 changed files with 23693 additions and 30732 deletions

1
node_modules/ep_etherpad-lite generated vendored
View file

@ -1 +0,0 @@
../src

View file

@ -4,7 +4,15 @@
*/
import AttributeMap from '../../static/js/AttributeMap';
import {applyToAText, copyAText, makeAText} from '../../static/js/Changeset';
import {
applyToAText, checkRep,
copyAText, deserializeOps,
makeAText,
makeSplice,
opsFromAText,
pack,
smartOpAssembler, unpack
} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {AttributePool} from '../../static/js/AttributePool';
import {Stream} from '../utils/Stream';
@ -266,7 +274,7 @@ export class Pad {
(!ins && start > 0 && orig[start - 1] === '\n');
if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
const changeset = makeSplice(orig, start, ndel, ins);
await this.appendRevision(changeset, authorId);
}
@ -366,7 +374,7 @@ export class Pad {
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = cleanText(context.content);
}
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
const firstChangeset = makeSplice('\n', 0, 0, text);
await this.appendRevision(firstChangeset, authorId);
}
await aCallAll('padLoad', {pad: this});
@ -490,8 +498,8 @@ export class Pad {
const oldAText = this.atext;
// based on Changeset.makeSplice
const assem = Changeset.smartOpAssembler();
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
const assem = smartOpAssembler();
for (const op of opsFromAText(oldAText)) assem.append(op);
assem.endDocument();
// although we have instantiated the dstPad with '\n', an additional '\n' is
@ -503,7 +511,7 @@ export class Pad {
// create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
const changeset = pack(oldLength, newLength, assem.toString(), newText);
dstPad.appendRevision(changeset, authorId);
await aCallAll('padCopy', {
@ -677,7 +685,7 @@ export class Pad {
}
})
.batch(100).buffer(99);
let atext = Changeset.makeAText('\n');
let atext = makeAText('\n');
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
try {
assert(authorId != null);
@ -688,10 +696,10 @@ export class Pad {
assert(timestamp > 0);
assert(changeset != null);
assert.equal(typeof changeset, 'string');
Changeset.checkRep(changeset);
const unpacked = Changeset.unpack(changeset);
checkRep(changeset);
const unpacked = unpack(changeset);
let text = atext.text;
for (const op of Changeset.deserializeOps(unpacked.ops)) {
for (const op of deserializeOps(unpacked.ops)) {
if (['=', '-'].includes(op.opcode)) {
assert(text.length >= op.chars);
const consumed = text.slice(0, op.chars);
@ -702,7 +710,7 @@ export class Pad {
}
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
}
atext = Changeset.applyToAText(changeset, atext, pool);
atext = applyToAText(changeset, atext, pool);
if (isKeyRev) assert.deepEqual(keyAText, atext);
} catch (err) {
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;

1553
src/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,18 @@
import AttributeMap from "./AttributeMap.js";
import * as Changeset from "./Changeset.js";
import * as ChangesetUtils from "./ChangesetUtils.js";
import * as attributes from "./attributes.js";
import * as _ from "./underscore.js";
'use strict';
const AttributeMap = require('./AttributeMap');
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const attributes = require('./attributes');
const _ = require('./underscore');
const lineMarkerAttribute = 'lmkr';
// Some of these attributes are kept for compatibility purposes.
// Not sure if we need all of them
const DEFAULT_LINE_ATTRIBUTES = ['author', 'lmkr', 'insertorder', 'start'];
// If one of these attributes are set to the first character of a
// line it is considered as a line attribute marker i.e. attributes
// set on this marker are applied to the whole line.
// The list attribute is only maintained for compatibility reasons
const lineAttributes = [lineMarkerAttribute, 'list'];
/*
The Attribute manager builds changesets based on a document
representation for setting and removing range or line-based attributes.
@ -32,32 +27,25 @@ const lineAttributes = [lineMarkerAttribute, 'list'];
- an Attribute pool `apool`
- a SkipList `lines` containing the text lines of the document.
*/
const AttributeManager = function (rep, applyChangesetCallback) {
this.rep = rep;
this.applyChangesetCallback = applyChangesetCallback;
this.author = '';
// If the first char in a line has one of the following attributes
// it will be considered as a line marker
};
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
AttributeManager.lineAttributes = lineAttributes;
AttributeManager.prototype = _(AttributeManager.prototype).extend({
applyChangeset(changeset) {
if (!this.applyChangesetCallback) return changeset;
if (!this.applyChangesetCallback)
return changeset;
const cs = changeset.toString();
if (!Changeset.isIdentity(cs)) {
this.applyChangesetCallback(cs);
}
return changeset;
},
/*
Sets attributes on a range
@param start [row, col] tuple pointing to the start of the range
@ -65,14 +53,17 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
@param attribs: an array of attributes
*/
setAttributesOnRange(start, end, attribs) {
if (start[0] < 0) throw new RangeError('selection start line number is negative');
if (start[1] < 0) throw new RangeError('selection start column number is negative');
if (end[0] < 0) throw new RangeError('selection end line number is negative');
if (end[1] < 0) throw new RangeError('selection end column number is negative');
if (start[0] < 0)
throw new RangeError('selection start line number is negative');
if (start[1] < 0)
throw new RangeError('selection start column number is negative');
if (end[0] < 0)
throw new RangeError('selection end line number is negative');
if (end[1] < 0)
throw new RangeError('selection end column number is negative');
if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) {
throw new RangeError('selection ends before it starts');
}
// instead of applying the attributes to the whole range at once, we need to apply them
// line by line, to be able to disregard the "*" used as line marker. For more details,
// see https://github.com/ether/etherpad-lite/issues/2772
@ -80,43 +71,42 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
for (let row = start[0]; row <= end[0]; row++) {
const [startCol, endCol] = this._findRowRange(row, start, end);
const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs);
// compose changesets of all rows into a single changeset
// as the range might not be continuous
// due to the presence of line markers on the rows
if (allChangesets) {
allChangesets = Changeset.compose(
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
} else {
allChangesets = Changeset.compose(allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
}
else {
allChangesets = rowChangeset;
}
}
return this.applyChangeset(allChangesets);
},
_findRowRange(row, start, end) {
if (row < start[0] || row > end[0]) throw new RangeError(`line ${row} not in selection`);
if (row >= this.rep.lines.length()) throw new RangeError(`selected line ${row} does not exist`);
if (row < start[0] || row > end[0])
throw new RangeError(`line ${row} not in selection`);
if (row >= this.rep.lines.length())
throw new RangeError(`selected line ${row} does not exist`);
// Subtract 1 for the end-of-line '\n' (it is never selected).
const lineLength =
this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1;
const lineLength = this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1;
const markerWidth = this.lineHasMarker(row) ? 1 : 0;
if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`);
if (start[1] < 0) throw new RangeError('selection starts at negative column');
if (lineLength - markerWidth < 0)
throw new Error(`line ${row} has negative length`);
if (start[1] < 0)
throw new RangeError('selection starts at negative column');
const startCol = Math.max(markerWidth, row === start[0] ? start[1] : 0);
if (startCol > lineLength) throw new RangeError('selection starts after line end');
if (end[1] < 0) throw new RangeError('selection ends at negative column');
if (startCol > lineLength)
throw new RangeError('selection starts after line end');
if (end[1] < 0)
throw new RangeError('selection ends at negative column');
const endCol = Math.max(markerWidth, row === end[0] ? end[1] : lineLength);
if (endCol > lineLength) throw new RangeError('selection ends after line end');
if (startCol > endCol) throw new RangeError('selection ends before it starts');
if (endCol > lineLength)
throw new RangeError('selection ends after line end');
if (startCol > endCol)
throw new RangeError('selection ends before it starts');
return [startCol, endCol];
},
/**
* Sets attributes on a range, by line
* @param row the row where range is
@ -127,20 +117,16 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
const builder = Changeset.builder(this.rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
ChangesetUtils.buildKeepRange(
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
ChangesetUtils.buildKeepRange(this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
return builder;
},
/*
Returns if the line already has a line marker
@param lineNum: the number of the line
*/
lineHasMarker(lineNum) {
return lineAttributes.find(
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
return lineAttributes.find((attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
},
/*
Gets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@ -149,12 +135,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributeOnLine(lineNum, attributeName) {
// get `attributeName` attribute of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return '';
if (!aline)
return '';
const [op] = Changeset.deserializeOps(aline);
if (op == null) return '';
if (op == null)
return '';
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
},
/*
Gets all attributes on a line
@param lineNum: the number of the line to get the attribute for
@ -162,12 +149,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributesOnLine(lineNum) {
// get attributes of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return [];
if (!aline)
return [];
const [op] = Changeset.deserializeOps(aline);
if (op == null) return [];
if (op == null)
return [];
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
},
/*
Gets a given attribute on a selection
@param attributeName
@ -176,7 +164,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
*/
getAttributeOnSelection(attributeName, prevChar) {
const rep = this.rep;
if (!(rep.selStart && rep.selEnd)) return;
if (!(rep.selStart && rep.selEnd))
return;
// If we're looking for the caret attribute not the selection
// has the user already got a selection or is this purely a caret location?
const isNotSelection = (rep.selStart[0] === rep.selEnd[0] && rep.selEnd[1] === rep.selStart[1]);
@ -188,38 +177,29 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
}
}
}
const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString();
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
const hasIt = (attribs) => withItRegex.test(attribs);
const rangeHasAttrib = (selStart, selEnd) => {
// if range is collapsed -> no attribs in range
if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false;
if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0])
return false;
if (selStart[0] !== selEnd[0]) { // -> More than one line selected
// from selStart to the end of the first line
let hasAttrib = rangeHasAttrib(
selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
let hasAttrib = rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
// for all lines in between
for (let n = selStart[0] + 1; n < selEnd[0]; n++) {
hasAttrib = hasAttrib && rangeHasAttrib([n, 0], [n, rep.lines.atIndex(n).text.length]);
}
// for the last, potentially partial, line
hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]);
return hasAttrib;
}
// Logic tells us we now have a range on a single line
const lineNum = selStart[0];
const start = selStart[1];
const end = selEnd[1];
let hasAttrib = true;
let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
const opStartInLine = indexIntoLine;
@ -234,12 +214,10 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
}
indexIntoLine = opEndInLine;
}
return hasAttrib;
};
return rangeHasAttrib(rep.selStart, rep.selEnd);
},
/*
Gets all attributes at a position containing line number and column
@param lineNumber starting with zero
@ -250,22 +228,19 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributesOnPosition(lineNumber, column) {
// get all attributes of the line
const aline = this.rep.alines[lineNumber];
if (!aline) {
return [];
}
// we need to sum up how much characters each operations take until the wanted position
let currentPointer = 0;
for (const currentOperation of Changeset.deserializeOps(aline)) {
currentPointer += currentOperation.chars;
if (currentPointer <= column) continue;
if (currentPointer <= column)
continue;
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
}
return [];
},
/*
Gets all attributes at caret position
if the user selected a range, the start of the selection is taken
@ -275,7 +250,6 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
getAttributesOnCaret() {
return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]);
},
/*
Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@ -287,14 +261,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
let loc = [0, 0];
const builder = Changeset.builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum);
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
if (hasMarker) {
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
[attributeName, attributeValue],
], this.rep.apool);
} else {
}
else {
// add a line marker
builder.insert('*', [
['author', this.author],
@ -303,10 +276,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
[attributeName, attributeValue],
], this.rep.apool);
}
return this.applyChangeset(builder);
},
/**
* Removes a specified attribute on a line
* @param lineNum the number of the affected line
@ -317,38 +288,32 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
const builder = Changeset.builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum);
let found = false;
const attribs = this.getAttributesOnLine(lineNum).map((attrib) => {
if (attrib[0] === attributeName && (!attributeValue || attrib[0] === attributeValue)) {
found = true;
return [attrib[0], ''];
} else if (attrib[0] === 'author') {
}
else if (attrib[0] === 'author') {
// update last author to make changes to line attributes on this line
return [attrib[0], this.author];
}
return attrib;
});
if (!found) {
return;
}
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
const countAttribsWithMarker = _.chain(attribs).filter((a) => !!a[1])
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
// if we have marker and any of attributes don't need to have marker. we need delete it
if (hasMarker && !countAttribsWithMarker) {
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
} else {
ChangesetUtils.buildKeepRange(
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
}
else {
ChangesetUtils.buildKeepRange(this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
}
return this.applyChangeset(builder);
},
/*
Toggles a line attribute for the specified line number
If a line attribute with the specified name exists with any value it will be removed
@ -362,15 +327,13 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
? this.removeAttributeOnLine(lineNum, attributeName)
: this.setAttributeOnLine(lineNum, attributeName, attributeValue);
},
hasAttributeOnSelectionOrCaretPosition(attributeName) {
const hasSelection = (
(this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])
);
const hasSelection = ((this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1]));
let hasAttrib;
if (hasSelection) {
hasAttrib = this.getAttributeOnSelection(attributeName);
} else {
}
else {
const attributesOnCaretPosition = this.getAttributesOnCaret();
const allAttribs = [].concat(...attributesOnCaretPosition); // flatten
hasAttrib = allAttribs.includes(attributeName);
@ -378,5 +341,4 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({
return hasAttrib;
},
});
module.exports = AttributeManager;
export default AttributeManager;

View file

@ -1,13 +1,10 @@
import * as attributes from "./attributes.js";
'use strict';
const attributes = require('./attributes');
/**
* A `[key, value]` pair of strings describing a text attribute.
*
* @typedef {[string, string]} Attribute
*/
/**
* A concatenated sequence of zero or more attribute identifiers, each one represented by an
* asterisk followed by a base-36 encoded attribute number.
@ -16,7 +13,6 @@ const attributes = require('./attributes');
*
* @typedef {string} AttributeString
*/
/**
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
*/
@ -31,7 +27,6 @@ class AttributeMap extends Map {
static fromString(str, pool) {
return new AttributeMap(pool).updateFromString(str);
}
/**
* @param {AttributePool} pool - Attribute pool.
*/
@ -40,7 +35,6 @@ class AttributeMap extends Map {
/** @public */
this.pool = pool;
}
/**
* @param {string} k - Attribute name.
* @param {string} v - Attribute value.
@ -52,11 +46,9 @@ class AttributeMap extends Map {
this.pool.putAttrib([k, v]);
return super.set(k, v);
}
toString() {
return attributes.attribsToString(attributes.sort([...this]), this.pool);
}
/**
* @param {Iterable<Attribute>} entries - [key, value] pairs to insert into this map.
* @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the
@ -69,13 +61,13 @@ class AttributeMap extends Map {
v = v == null ? '' : String(v);
if (!v && emptyValueIsDelete) {
this.delete(k);
} else {
}
else {
this.set(k, v);
}
}
return this;
}
/**
* @param {AttributeString} str - The attribute string identifying the attributes to insert into
* this map.
@ -87,5 +79,4 @@ class AttributeMap extends Map {
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
}
}
module.exports = AttributeMap;
export default AttributeMap;

View file

@ -1,52 +1,28 @@
'use strict';
/**
* This module contains several helper Functions to build Changesets
* based on a SkipList
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.buildRemoveRange = (rep, builder, start, end) => {
export const buildRemoveRange = (rep, builder, start, end) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0]) {
builder.remove(endLineOffset - startLineOffset - start[1], end[0] - start[0]);
builder.remove(end[1]);
} else {
}
else {
builder.remove(end[1] - start[1]);
}
};
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
export const buildKeepRange = (rep, builder, start, end, attribs, pool) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
if (end[0] > start[0]) {
builder.keep(endLineOffset - startLineOffset - start[1], end[0] - start[0], attribs, pool);
builder.keep(end[1], 0, attribs, pool);
} else {
}
else {
builder.keep(end[1] - start[1], 0, attribs, pool);
}
};
exports.buildKeepToStartOfRange = (rep, builder, start) => {
export const buildKeepToStartOfRange = (rep, builder, start) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]);
builder.keep(start[1]);
};

View file

@ -1,7 +1,6 @@
import { padutils } from "./pad_utils.js";
'use strict';
const {padutils: {warnDeprecated}} = require('./pad_utils');
const { padutils: { warnDeprecated } } = { padutils };
/**
* Represents a chat message stored in the database and transmitted among users. Plugins can extend
* the object with additional properties.
@ -13,13 +12,14 @@ class ChatMessage {
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept
// the old names in case the db record was written by an older version of Etherpad.
obj = Object.assign({}, obj); // Don't mutate the caller's object.
if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId;
if ('userId' in obj && !('authorId' in obj))
obj.authorId = obj.userId;
delete obj.userId;
if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName;
if ('userName' in obj && !('displayName' in obj))
obj.displayName = obj.userName;
delete obj.userName;
return Object.assign(new ChatMessage(), obj);
}
/**
* @param {?string} [text] - Initial value of the `text` property.
* @param {?string} [authorId] - Initial value of the `authorId` property.
@ -32,21 +32,18 @@ class ChatMessage {
* @type {?string}
*/
this.text = text;
/**
* The user's author ID.
*
* @type {?string}
*/
this.authorId = authorId;
/**
* The message's timestamp, as milliseconds since epoch.
*
* @type {?number}
*/
this.time = time;
/**
* The user's display name.
*
@ -54,7 +51,6 @@ class ChatMessage {
*/
this.displayName = null;
}
/**
* Alias of `authorId`, for compatibility with old plugins.
*
@ -69,7 +65,6 @@ class ChatMessage {
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
this.authorId = val;
}
/**
* Alias of `displayName`, for compatibility with old plugins.
*
@ -84,15 +79,13 @@ class ChatMessage {
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
this.displayName = val;
}
// TODO: Delete this method once users are unlikely to roll back to a version of Etherpad that
// doesn't support authorId and displayName.
toJSON() {
const {authorId, displayName, ...obj} = this;
const { authorId, displayName, ...obj } = this;
obj.userId = authorId;
obj.userName = displayName;
return obj;
}
}
module.exports = ChatMessage;
export default ChatMessage;

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,12 @@
// Autosize 1.13 - jQuery plugin for textareas
// (c) 2012 Jack Moore - jacklmoore.com
// license: www.opensource.org/licenses/mit-license.php
(function ($) {
var
defaults = {
var defaults = {
className: 'autosizejs',
append: "",
callback: false
},
hidden = 'hidden',
borderBox = 'border-box',
lineHeight = 'lineHeight',
copy = '<textarea tabindex="-1" style="position:absolute; top:-9999px; left:-9999px; right:auto; bottom:auto; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
}, hidden = 'hidden', borderBox = 'border-box', lineHeight = 'lineHeight', copy = '<textarea tabindex="-1" style="position:absolute; top:-9999px; left:-9999px; right:auto; bottom:auto; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden;"/>',
// line-height is omitted because IE7/IE8 doesn't return the correct value.
copyStyle = [
'fontFamily',
@ -23,53 +17,31 @@
'textTransform',
'wordSpacing',
'textIndent'
],
oninput = 'oninput',
onpropertychange = 'onpropertychange',
test = $(copy)[0];
], oninput = 'oninput', onpropertychange = 'onpropertychange', test = $(copy)[0];
// For testing support in old FireFox
test.setAttribute(oninput, "return");
if ($.isFunction(test[oninput]) || onpropertychange in test) {
// test that line-height can be accurately copied to avoid
// incorrect value reporting in old IE and old Opera
$(test).css(lineHeight, '99px');
if ($(test).css(lineHeight) === '99px') {
copyStyle.push(lineHeight);
}
$.fn.autosize = function (options) {
options = $.extend({}, defaults, options || {});
return this.each(function () {
var
ta = this,
$ta = $(ta),
mirror,
minHeight = $ta.height(),
maxHeight = parseInt($ta.css('maxHeight'), 10),
active,
i = copyStyle.length,
resize,
boxOffset = 0,
value = ta.value,
callback = $.isFunction(options.callback);
if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox){
var ta = this, $ta = $(ta), mirror, minHeight = $ta.height(), maxHeight = parseInt($ta.css('maxHeight'), 10), active, i = copyStyle.length, resize, boxOffset = 0, value = ta.value, callback = $.isFunction(options.callback);
if ($ta.css('box-sizing') === borderBox || $ta.css('-moz-box-sizing') === borderBox || $ta.css('-webkit-box-sizing') === borderBox) {
boxOffset = $ta.outerHeight() - $ta.height();
}
if ($ta.data('mirror') || $ta.data('ismirror')) {
// if autosize has already been applied, exit.
// if autosize is being applied to a mirror element, exit.
return;
} else {
}
else {
mirror = $(copy).data('ismirror', true).addClass(options.className)[0];
resize = $ta.css('resize') === 'none' ? 'none' : 'horizontal';
$ta.data('mirror', $(mirror)).css({
overflow: hidden,
overflowY: hidden,
@ -77,51 +49,43 @@
resize: resize
});
}
// Opera returns '-1px' when max-height is set to 'none'.
maxHeight = maxHeight && maxHeight > 0 ? maxHeight : 9e4;
// Using mainly bare JS in this function because it is going
// to fire very often while typing, and needs to very efficient.
function adjust() {
var height, overflow, original;
// the active flag keeps IE from tripping all over itself. Otherwise
// actions in the adjust function will cause IE to call adjust again.
if (!active) {
active = true;
mirror.value = ta.value + options.append;
mirror.style.overflowY = ta.style.overflowY;
original = parseInt(ta.style.height,10);
original = parseInt(ta.style.height, 10);
// Update the width in case the original textarea width has changed
mirror.style.width = $ta.css('width');
// Needed for IE to reliably return the correct scrollHeight
mirror.scrollTop = 0;
// Set a very high value for scrollTop to be sure the
// mirror is scrolled all the way to the bottom.
mirror.scrollTop = 9e4;
height = mirror.scrollTop;
overflow = hidden;
if (height > maxHeight) {
height = maxHeight;
overflow = 'scroll';
} else if (height < minHeight) {
}
else if (height < minHeight) {
height = minHeight;
}
height += boxOffset;
ta.style.overflowY = overflow;
if (original !== height) {
ta.style.height = height + 'px';
if (callback) {
options.callback.call(ta);
}
}
// This small timeout gives IE a chance to draw it's scrollbar
// before adjust can be run again (prevents an infinite loop).
setTimeout(function () {
@ -129,7 +93,6 @@
}, 1);
}
}
// mirror is a duplicate textarea located off-screen that
// is automatically updated to contain the same text as the
// original textarea. mirror always has a height of 0.
@ -138,43 +101,39 @@
while (i--) {
mirror.style[copyStyle[i]] = $ta.css(copyStyle[i]);
}
$('body').append(mirror);
if (onpropertychange in ta) {
if (oninput in ta) {
// Detects IE9. IE9 does not fire onpropertychange or oninput for deletions,
// so binding to onkeyup to catch most of those occassions. There is no way that I
// know of to detect something like 'cut' in IE9.
ta[oninput] = ta.onkeyup = adjust;
} else {
}
else {
// IE7 / IE8
ta[onpropertychange] = adjust;
}
} else {
}
else {
// Modern Browsers
ta[oninput] = adjust;
// The textarea overflow is now hidden. But Chrome doesn't reflow the text after the scrollbars are removed.
// This is a hack to get Chrome to reflow it's text.
ta.value = '';
ta.value = value;
}
$(window).resize(adjust);
// Allow for manual triggering if needed.
$ta.bind('autosize', adjust);
// Call adjust in case the textarea already contains text.
adjust();
});
};
} else {
}
else {
// Makes no changes for older browsers (FireFox3- and Safari4-)
$.fn.autosize = function (callback) {
return this;
};
}
}(jQuery));

View file

@ -2,35 +2,24 @@
v0.1 (c) Kyle Simpson
MIT License
*/
(function(global){
(function (global) {
if (typeof global.JSON == "undefined" || !global.JSON) {
global.JSON = {};
}
global.JSON.minify = function(json) {
var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
in_string = false,
in_multiline_comment = false,
in_singleline_comment = false,
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
;
global.JSON.minify = function (json) {
var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g, in_string = false, in_multiline_comment = false, in_singleline_comment = false, tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc;
tokenizer.lastIndex = 0;
while (tmp = tokenizer.exec(json)) {
lc = RegExp.leftContext;
rc = RegExp.rightContext;
if (!in_multiline_comment && !in_singleline_comment) {
tmp2 = lc.substring(from);
if (!in_string) {
tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
tmp2 = tmp2.replace(/(\n|\r|\s)*/g, "");
}
new_str[ns++] = tmp2;
}
from = tokenizer.lastIndex;
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
tmp2 = lc.match(/(\\)*$/);
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string

View file

@ -1,15 +1,13 @@
'use strict';
/* global socketio */
$(document).ready(() => {
const socket = socketio.connect('..', '/pluginfw/installer');
socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
if (reason === 'io server disconnect') socket.connect();
if (reason === 'io server disconnect')
socket.connect();
});
const search = (searchTerm, limit) => {
if (search.searchTerm !== searchTerm) {
search.offset = 0;
@ -26,7 +24,6 @@ $(document).ready(() => {
sortDir: search.sortDir,
});
search.offset += limit;
$('#search-progress').show();
search.messages.show('fetching');
search.searching = true;
@ -36,8 +33,8 @@ $(document).ready(() => {
search.limit = 999;
search.results = [];
search.sortBy = 'name';
search.sortDir = /* DESC?*/true;
search.end = true;// have we received all results already?
search.sortDir = /* DESC?*/ true;
search.end = true; // have we received all results already?
search.messages = {
show: (msg) => {
// $('.search-results .messages').show()
@ -50,7 +47,6 @@ $(document).ready(() => {
$(`.search-results .messages .${msg} *`).hide();
},
};
const installed = {
progress: {
show: (plugin, msg) => {
@ -77,11 +73,9 @@ $(document).ready(() => {
},
list: [],
};
const displayPluginList = (plugins, container, template) => {
plugins.forEach((plugin) => {
const row = template.clone();
for (const attr in plugin) {
if (attr === 'name') { // Hack to rewrite URLS into name
const link = $('<a>')
@ -91,7 +85,8 @@ $(document).ready(() => {
.attr('target', '_blank')
.text(plugin.name.substr(3));
row.find('.name').append(link);
} else {
}
else {
row.find(`.${attr}`).text(plugin[attr]);
}
}
@ -102,23 +97,21 @@ $(document).ready(() => {
});
updateHandlers();
};
const sortPluginList = (plugins, property, /* ASC?*/dir) => plugins.sort((a, b) => {
if (a[property] < b[property]) return dir ? -1 : 1;
if (a[property] > b[property]) return dir ? 1 : -1;
const sortPluginList = (plugins, property, /* ASC?*/ dir) => plugins.sort((a, b) => {
if (a[property] < b[property])
return dir ? -1 : 1;
if (a[property] > b[property])
return dir ? 1 : -1;
// a must be equal to b
return 0;
});
const updateHandlers = () => {
// Search
$('#search-query').unbind('keyup').keyup(() => {
search($('#search-query').val());
});
// Prevent form submit
$('#search-query').parent().bind('submit', () => false);
// update & install
$('.do-install, .do-update').unbind('click').click(function (e) {
const $row = $(e.target).closest('tr');
@ -126,13 +119,13 @@ $(document).ready(() => {
if ($(this).hasClass('do-install')) {
$row.remove().appendTo('#installed-plugins');
installed.progress.show(plugin, 'Installing');
} else {
}
else {
installed.progress.show(plugin, 'Updating');
}
socket.emit('install', plugin);
installed.messages.hide('nothing-installed');
});
// uninstall
$('.do-uninstall').unbind('click').click((e) => {
const $row = $(e.target).closest('tr');
@ -141,7 +134,6 @@ $(document).ready(() => {
installed.progress.show(pluginName, 'Uninstalling');
installed.list = installed.list.filter((plugin) => plugin.name !== pluginName);
});
// Sort
$('.sort.up').unbind('click').click(function () {
search.sortBy = $(this).attr('data-label').toLowerCase();
@ -158,19 +150,17 @@ $(document).ready(() => {
search.results = [];
});
};
socket.on('results:search', (data) => {
if (!data.results.length) search.end = true;
if (data.query.offset === 0) search.results = [];
if (!data.results.length)
search.end = true;
if (data.query.offset === 0)
search.results = [];
search.messages.hide('nothing-found');
search.messages.hide('fetching');
$('#search-query').removeAttr('disabled');
console.log('got search results', data);
// add to results
search.results = search.results.concat(data.results);
// Update sorting head
$('.sort')
.removeClass('up down')
@ -178,54 +168,46 @@ $(document).ready(() => {
$(`.search-results thead th[data-label=${data.query.sortBy}]`)
.removeClass('none')
.addClass(data.query.sortDir ? 'up' : 'down');
// re-render search results
const searchWidget = $('.search-results');
searchWidget.find('.results *').remove();
if (search.results.length > 0) {
displayPluginList(
search.results, searchWidget.find('.results'), searchWidget.find('.template tr'));
} else {
displayPluginList(search.results, searchWidget.find('.results'), searchWidget.find('.template tr'));
}
else {
search.messages.show('nothing-found');
}
search.messages.hide('fetching');
$('#search-progress').hide();
search.searching = false;
});
socket.on('results:installed', (data) => {
installed.messages.hide('fetching');
installed.messages.hide('nothing-installed');
installed.list = data.installed;
sortPluginList(installed.list, 'name', /* ASC?*/true);
sortPluginList(installed.list, 'name', /* ASC?*/ true);
// filter out epl
installed.list = installed.list.filter((plugin) => plugin.name !== 'ep_etherpad-lite');
// remove all installed plugins (leave plugins that are still being installed)
installed.list.forEach((plugin) => {
$(`#installed-plugins .${plugin.name}`).remove();
});
if (installed.list.length > 0) {
displayPluginList(installed.list, $('#installed-plugins'), $('#installed-plugin-template'));
socket.emit('checkUpdates');
} else {
}
else {
installed.messages.show('nothing-installed');
}
});
socket.on('results:updatable', (data) => {
data.updatable.forEach((pluginName) => {
const actions = $(`#installed-plugins > tr.${pluginName} .actions`);
actions.find('.do-update').remove();
actions.append(
$('<input>').addClass('do-update').attr('type', 'button').attr('value', 'Update'));
actions.append($('<input>').addClass('do-update').attr('type', 'button').attr('value', 'Update'));
});
updateHandlers();
});
socket.on('finished:install', (data) => {
if (data.error) {
if (data.code === 'EPEERINVALID') {
@ -234,38 +216,30 @@ $(document).ready(() => {
alert(`An error occurred while installing ${data.plugin} \n${data.error}`);
$(`#installed-plugins .${data.plugin}`).remove();
}
socket.emit('getInstalled');
// update search results
search.offset = 0;
search(search.searchTerm, search.results.length);
search.results = [];
});
socket.on('finished:uninstall', (data) => {
if (data.error) {
alert(`An error occurred while uninstalling the ${data.plugin} \n${data.error}`);
}
// remove plugin from installed list
$(`#installed-plugins .${data.plugin}`).remove();
socket.emit('getInstalled');
// update search results
search.offset = 0;
search(search.searchTerm, search.results.length);
search.results = [];
});
socket.on('connect', () => {
updateHandlers();
socket.emit('getInstalled');
search.searchTerm = null;
search($('#search-query').val());
});
// check for updates every 5mins
setInterval(() => {
socket.emit('checkUpdates');

View file

@ -1,18 +1,15 @@
'use strict';
$(document).ready(() => {
const socket = window.socketio.connect('..', '/settings');
socket.on('connect', () => {
socket.emit('load');
});
socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
if (reason === 'io server disconnect') socket.connect();
if (reason === 'io server disconnect')
socket.connect();
});
socket.on('settings', (settings) => {
/* Check whether the settings.json is authorized to be viewed */
if (settings.results === 'NOT_ALLOWED') {
@ -21,49 +18,46 @@ $(document).ready(() => {
$('.err-message').html('Settings json is not authorized to be viewed in Admin page!!');
return;
}
/* Check to make sure the JSON is clean before proceeding */
if (isJSONClean(settings.results)) {
$('.settings').append(settings.results);
$('.settings').focus();
$('.settings').autosize();
} else {
}
else {
alert('Invalid JSON');
}
});
/* When the admin clicks save Settings check the JSON then send the JSON back to the server */
$('#saveSettings').on('click', () => {
const editedSettings = $('.settings').val();
if (isJSONClean(editedSettings)) {
// JSON is clean so emit it to the server
socket.emit('saveSettings', $('.settings').val());
} else {
}
else {
alert('Invalid JSON');
$('.settings').focus();
}
});
/* Tell Etherpad Server to restart */
$('#restartEtherpad').on('click', () => {
socket.emit('restartServer');
});
socket.on('saveprogress', (progress) => {
$('#response').show();
$('#response').text(progress);
$('#response').fadeOut('slow');
});
});
const isJSONClean = (data) => {
let cleanSettings = JSON.minify(data);
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
try {
return typeof jQuery.parseJSON(cleanSettings) === 'object';
} catch (e) {
}
catch (e) {
return false; // the JSON failed to be parsed
}
};

View file

@ -1,56 +1,23 @@
'use strict';
// Low-level utilities for manipulating attribute strings. For a high-level API, see AttributeMap.
/**
* A `[key, value]` pair of strings describing a text attribute.
*
* @typedef {[string, string]} Attribute
*/
/**
* A concatenated sequence of zero or more attribute identifiers, each one represented by an
* asterisk followed by a base-36 encoded attribute number.
*
* Examples: '', '*0', '*3*j*z*1q'
*
* @typedef {string} AttributeString
*/
/**
* Converts an attribute string into a sequence of attribute identifier numbers.
*
* WARNING: This only works on attribute strings. It does NOT work on serialized operations or
* changesets.
*
* @param {AttributeString} str - Attribute string.
* @yields {number} The attribute numbers (to look up in the associated pool), in the order they
* appear in `str`.
* @returns {Generator<number>}
*/
exports.decodeAttribString = function* (str) {
const checkAttribNum = (n) => {
if (typeof n !== 'number')
throw new TypeError(`not a number: ${n}`);
if (n < 0)
throw new Error(`attribute number is negative: ${n}`);
if (n !== Math.trunc(n))
throw new Error(`attribute number is not an integer: ${n}`);
};
export const decodeAttribString = function* (str) {
const re = /\*([0-9a-z]+)|./gy;
let match;
while ((match = re.exec(str)) != null) {
const [m, n] = match;
if (n == null) throw new Error(`invalid character in attribute string: ${m}`);
if (n == null)
throw new Error(`invalid character in attribute string: ${m}`);
yield Number.parseInt(n, 36);
}
};
const checkAttribNum = (n) => {
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
};
/**
* Inverse of `decodeAttribString`.
*
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
* @returns {AttributeString}
*/
exports.encodeAttribString = (attribNums) => {
export const encodeAttribString = (attribNums) => {
let str = '';
for (const n of attribNums) {
checkAttribNum(n);
@ -58,73 +25,21 @@ exports.encodeAttribString = (attribNums) => {
}
return str;
};
/**
* Converts a sequence of attribute numbers into a sequence of attributes.
*
* @param {Iterable<number>} attribNums - Attribute numbers to look up in the pool.
* @param {AttributePool} pool - Attribute pool.
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
* @returns {Generator<Attribute>}
*/
exports.attribsFromNums = function* (attribNums, pool) {
export const attribsFromNums = function* (attribNums, pool) {
for (const n of attribNums) {
checkAttribNum(n);
const attrib = pool.getAttrib(n);
if (attrib == null) throw new Error(`attribute ${n} does not exist in pool`);
if (attrib == null)
throw new Error(`attribute ${n} does not exist in pool`);
yield attrib;
}
};
/**
* Inverse of `attribsFromNums`.
*
* @param {Iterable<Attribute>} attribs - Attributes. Any attributes not already in `pool` are
* inserted into `pool`. No checking is performed to ensure that the attributes are in the
* canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if
* required.)
* @param {AttributePool} pool - Attribute pool.
* @yields {number} The attribute number of each attribute in `attribs`, in order.
* @returns {Generator<number>}
*/
exports.attribsToNums = function* (attribs, pool) {
for (const attrib of attribs) yield pool.putAttrib(attrib);
export const attribsToNums = function* (attribs, pool) {
for (const attrib of attribs)
yield pool.putAttrib(attrib);
};
/**
* Convenience function that is equivalent to `attribsFromNums(decodeAttribString(str), pool)`.
*
* WARNING: This only works on attribute strings. It does NOT work on serialized operations or
* changesets.
*
* @param {AttributeString} str - Attribute string.
* @param {AttributePool} pool - Attribute pool.
* @yields {Attribute} The attributes identified in `str`, in order.
* @returns {Generator<Attribute>}
*/
exports.attribsFromString = function* (str, pool) {
export const attribsFromString = function* (str, pool) {
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
};
/**
* Inverse of `attribsFromString`.
*
* @param {Iterable<Attribute>} attribs - Attributes. The attributes to insert into the pool (if
* necessary) and encode. No checking is performed to ensure that the attributes are in the
* canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if
* required.)
* @param {AttributePool} pool - Attribute pool.
* @returns {AttributeString}
*/
exports.attribsToString =
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
/**
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
* unspecified.
*
* @param {Attribute[]} attribs - Attributes to sort in place.
* @returns {Attribute[]} `attribs` (for chaining).
*/
exports.sort =
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
export const attribsToString = (attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
export const sort = (attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));

View file

@ -1,23 +1,17 @@
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
/* Copyright 2021 Richard Hansen <rhansen@rhansen.org> */
'use strict';
// Set up an error handler to display errors that happen during page load. This handler will be
// overridden with a nicer handler by setupGlobalExceptionHandler() in pad_utils.js.
(() => {
const originalHandler = window.onerror;
window.onerror = (...args) => {
const [msg, url, line, col, err] = args;
// Purge the existing HTML and styles for a consistent view.
document.body.textContent = '';
for (const el of document.querySelectorAll('head style, head link[rel="stylesheet"]')) {
el.remove();
}
const box = document.body;
box.textContent = '';
const summary = document.createElement('p');
@ -40,9 +34,7 @@
const stack = document.createElement('pre');
stackBlock.appendChild(stack);
stack.appendChild(document.createTextNode(err.stack || err.toString()));
if (typeof originalHandler === 'function') originalHandler(...args);
if (typeof originalHandler === 'function')
originalHandler(...args);
};
})();
// @license-end

View file

@ -1,11 +1,18 @@
import { makeCSSManager as makeCSSManager$0 } from "./cssmanager.js";
import { domline as domline$0 } from "./domline.js";
import AttribPool from "./AttributePool.js";
import * as Changeset from "./Changeset.js";
import * as attributes from "./attributes.js";
import { linestylefilter as linestylefilter$0 } from "./linestylefilter.js";
import { colorutils as colorutils$0 } from "./colorutils.js";
import * as _ from "./underscore.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -21,43 +28,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset');
const attributes = require('./attributes');
const linestylefilter = require('./linestylefilter').linestylefilter;
const colorutils = require('./colorutils').colorutils;
const _ = require('./underscore');
const hooks = require('./pluginfw/hooks');
const makeCSSManager = { makeCSSManager: makeCSSManager$0 }.makeCSSManager;
const domline = { domline: domline$0 }.domline;
const linestylefilter = { linestylefilter: linestylefilter$0 }.linestylefilter;
const colorutils = { colorutils: colorutils$0 }.colorutils;
// These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate.
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {
let goToRevisionIfEnabledCount = 0;
let changesetLoader = undefined;
const debugLog = (...args) => {
try {
if (window.console) console.log(...args);
} catch (e) {
if (window.console) console.log('error printing: ', e);
if (window.console)
console.log(...args);
}
catch (e) {
if (window.console)
console.log('error printing: ', e);
}
};
const padContents = {
currentRevision: clientVars.collab_client_vars.rev,
currentTime: clientVars.collab_client_vars.time,
currentLines:
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
currentDivs: null,
// to be filled in once the dom loads
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
alines: Changeset.splitAttributionLines(
clientVars.collab_client_vars.initialAttributedText.attribs,
clientVars.collab_client_vars.initialAttributedText.text),
alines: Changeset.splitAttributionLines(clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text),
// generates a jquery element containing HTML for a line
lineToElement(line, aline) {
const element = document.createElement('div');
@ -70,38 +67,32 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
element.id = Math.random();
return $(element);
},
// splice the lines
splice(start, numRemoved, ...newLines) {
// remove spliced-out lines from DOM
for (let i = start; i < start + numRemoved && i < this.currentDivs.length; i++) {
this.currentDivs[i].remove();
}
// remove spliced-out line divs from currentDivs array
this.currentDivs.splice(start, numRemoved);
const newDivs = [];
for (let i = 0; i < newLines.length; i++) {
newDivs.push(this.lineToElement(newLines[i], this.alines[start + i]));
}
// grab the div just before the first one
let startDiv = this.currentDivs[start - 1] || null;
// insert the div elements into the correct place, in the correct order
for (let i = 0; i < newDivs.length; i++) {
if (startDiv) {
startDiv.after(newDivs[i]);
} else {
}
else {
$('#innerdocbody').prepend(newDivs[i]);
}
startDiv = newDivs[i];
}
// insert new divs into currentDivs array
this.currentDivs.splice(start, 0, ...newDivs);
// call currentLines.splice, to keep the currentLines array up to date
this.currentLines.splice(start, numRemoved, ...newLines);
},
@ -113,36 +104,35 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
length() {
return this.currentLines.length;
},
getActiveAuthors() {
const authorIds = new Set();
for (const aline of this.alines) {
for (const op of Changeset.deserializeOps(aline)) {
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
if (k !== 'author') continue;
if (v) authorIds.add(v);
if (k !== 'author')
continue;
if (v)
authorIds.add(v);
}
}
}
return [...authorIds].sort();
},
};
const applyChangeset = (changeset, revision, preventSliderMovement, timeDelta) => {
// disable the next 'gotorevision' call handled by a timeslider update
if (!preventSliderMovement) {
goToRevisionIfEnabledCount++;
BroadcastSlider.setSliderPosition(revision);
}
const oldAlines = padContents.alines.slice();
try {
// must mutate attribution lines before text lines
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
} catch (e) {
}
catch (e) {
debugLog(e);
}
// scroll to the area that is changed before the lines are mutated
if ($('#options-followContents').is(':checked') ||
$('#options-followContents').prop('checked')) {
@ -163,7 +153,6 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops);
lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
}
const goToLineNumber = (lineNumber) => {
// Sets the Y scrolling of the browser to go to this line
const line = $('#innerdocbody').find(`div:nth-child(${lineNumber + 1})`);
@ -171,48 +160,41 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
const ecb = document.getElementById('editorcontainerbox');
// Chrome 55 - 59 bugfix
if (ecb.scrollTo) {
ecb.scrollTo({top: newY, behavior: 'auto'});
} else {
ecb.scrollTo({ top: newY, behavior: 'auto' });
}
else {
$('#editorcontainerbox').scrollTop(newY);
}
};
goToLineNumber(lineChanged);
}
Changeset.mutateTextLines(changeset, padContents);
padContents.currentRevision = revision;
padContents.currentTime += timeDelta * 1000;
updateTimer();
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);
BroadcastSlider.setAuthors(authors);
};
const loadedNewChangeset = (changesetForward, changesetBackward, revision, timeDelta) => {
const revisionInfo = window.revisionInfo;
const broadcasting = (BroadcastSlider.getSliderPosition() === revisionInfo.latest);
revisionInfo.addChangeset(
revision, revision + 1, changesetForward, changesetBackward, timeDelta);
revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta);
BroadcastSlider.setSliderLength(revisionInfo.latest);
if (broadcasting) applyChangeset(changesetForward, revision + 1, false, timeDelta);
if (broadcasting)
applyChangeset(changesetForward, revision + 1, false, timeDelta);
};
/*
At this point, we must be certain that the changeset really does map from
the current revision to the specified revision. Any mistakes here will
cause the whole slider to get out of sync.
*/
const updateTimer = () => {
const zpad = (str, length) => {
str = `${str}`;
while (str.length < length) str = `0${str}`;
while (str.length < length)
str = `0${str}`;
return str;
};
const date = new Date(padContents.currentTime);
const dateFormat = () => {
const month = zpad(date.getMonth() + 1, 2);
@ -230,8 +212,6 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
seconds,
}));
};
$('#timer').html(dateFormat());
const revisionDate = html10n.get('timeslider.saved', {
day: date.getDate(),
@ -253,17 +233,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
});
$('#revision_date').html(revisionDate);
};
updateTimer();
const goToRevision = (newRevision) => {
padContents.targetRevision = newRevision;
const path = window.revisionInfo.getPath(padContents.currentRevision, newRevision);
hooks.aCallAll('goToRevisionEvent', {
rev: newRevision,
});
if (path.status === 'complete') {
const cs = path.changesets;
let changeset = cs[0];
@ -272,52 +248,46 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i];
}
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
} else if (path.status === 'partial') {
if (changeset)
applyChangeset(changeset, path.rev, true, timeDelta);
}
else if (path.status === 'partial') {
// callback is called after changeset information is pulled from server
// this may never get called, if the changeset has already been loaded
const update = (start, end) => {
// if we've called goToRevision in the time since, don't goToRevision
goToRevision(padContents.targetRevision);
};
// do our best with what we have...
const cs = path.changesets;
let changeset = cs[0];
let timeDelta = path.times[0];
for (let i = 1; i < cs.length; i++) {
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i];
}
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
if (changeset)
applyChangeset(changeset, path.rev, true, timeDelta);
// Loading changeset history for new revision
loadChangesetsForRevision(newRevision, update);
// Loading changeset history for old revision (to make diff between old and new revision)
loadChangesetsForRevision(padContents.currentRevision - 1);
}
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);
BroadcastSlider.setAuthors(authors);
};
const loadChangesetsForRevision = (revision, callback) => {
if (BroadcastSlider.getSliderLength() > 10000) {
const start = (Math.floor((revision) / 10000) * 10000); // revision 0 to 10
changesetLoader.queueUp(start, 100);
}
if (BroadcastSlider.getSliderLength() > 1000) {
const start = (Math.floor((revision) / 1000) * 1000); // (start from -1, go to 19) + 1
changesetLoader.queueUp(start, 10);
}
const start = (Math.floor((revision) / 100) * 100);
changesetLoader.queueUp(start, 1, callback);
};
changesetLoader = {
running: false,
resolved: [],
@ -326,7 +296,8 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
requestQueue3: [],
reqCallbacks: [],
queueUp(revision, width, callback) {
if (revision < 0) revision = 0;
if (revision < 0)
revision = 0;
// if(this.requestQueue.indexOf(revision) != -1)
// return; // already in the queue.
if (this.resolved.indexOf(`${revision}_${width}`) !== -1) {
@ -334,13 +305,10 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
return;
}
this.resolved.push(`${revision}_${width}`);
const requestQueue =
width === 1 ? this.requestQueue3
const requestQueue = width === 1 ? this.requestQueue3
: width === 10 ? this.requestQueue2
: this.requestQueue1;
requestQueue.push(
{
requestQueue.push({
rev: revision,
res: width,
callback,
@ -351,29 +319,24 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
}
},
loadFromQueue() {
const requestQueue =
this.requestQueue1.length > 0 ? this.requestQueue1
const requestQueue = this.requestQueue1.length > 0 ? this.requestQueue1
: this.requestQueue2.length > 0 ? this.requestQueue2
: this.requestQueue3.length > 0 ? this.requestQueue3
: null;
if (!requestQueue) {
this.running = false;
return;
}
const request = requestQueue.pop();
const granularity = request.res;
const callback = request.callback;
const start = request.rev;
const requestID = Math.floor(Math.random() * 100000);
sendSocketMsg('CHANGESET_REQ', {
start,
granularity,
requestID,
});
this.reqCallbacks[requestID] = callback;
},
handleSocketResponse(message) {
@ -381,7 +344,6 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
const granularity = message.data.granularity;
const callback = this.reqCallbacks[message.data.requestID];
delete this.reqCallbacks[message.data.requestID];
this.handleResponse(message.data, start, granularity, callback);
setTimeout(() => this.loadFromQueue(), 10);
},
@ -390,52 +352,46 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
for (let i = 0; i < data.forwardsChangesets.length; i++) {
const astart = start + i * granularity - 1; // rev -1 is a blank single line
let aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
if (aend > data.actualEndNum - 1)
aend = data.actualEndNum - 1;
// debugLog("adding changeset:", astart, aend);
const forwardcs =
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
const backwardcs =
Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
const forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
const backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
}
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
if (callback)
callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
},
handleMessageFromServer(obj) {
if (obj.type === 'COLLABROOM') {
obj = obj.data;
if (obj.type === 'NEW_CHANGES') {
const changeset = Changeset.moveOpsToNewPool(
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
let changesetBack = Changeset.inverse(
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
changesetBack = Changeset.moveOpsToNewPool(
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
const changeset = Changeset.moveOpsToNewPool(obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
let changesetBack = Changeset.inverse(obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
changesetBack = Changeset.moveOpsToNewPool(changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
} else if (obj.type === 'NEW_AUTHORDATA') {
}
else if (obj.type === 'NEW_AUTHORDATA') {
const authorMap = {};
authorMap[obj.author] = obj.data;
receiveAuthorData(authorMap);
const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]);
BroadcastSlider.setAuthors(authors);
} else if (obj.type === 'NEW_SAVEDREV') {
}
else if (obj.type === 'NEW_SAVEDREV') {
const savedRev = obj.savedRev;
BroadcastSlider.addSavedRevision(savedRev.revNum, savedRev);
}
hooks.callAll(`handleClientTimesliderMessage_${obj.type}`, {payload: obj});
} else if (obj.type === 'CHANGESET_REQ') {
hooks.callAll(`handleClientTimesliderMessage_${obj.type}`, { payload: obj });
}
else if (obj.type === 'CHANGESET_REQ') {
this.handleSocketResponse(obj);
} else {
}
else {
debugLog(`Unknown message type: ${obj.type}`);
}
},
};
// to start upon window load, just push a function onto this array
// window['onloadFuncts'].push(setUpSocket);
// window['onloadFuncts'].push(function ()
@ -449,22 +405,19 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
$('#innerdocbody').append(div);
}
});
// this is necessary to keep infinite loops of events firing,
// since goToRevision changes the slider position
const goToRevisionIfEnabled = (...args) => {
if (goToRevisionIfEnabledCount > 0) {
goToRevisionIfEnabledCount--;
} else {
}
else {
goToRevision(...args);
}
};
BroadcastSlider.onSlider(goToRevisionIfEnabled);
const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet);
const authorData = {};
const receiveAuthorData = (newAuthorData) => {
for (const [author, data] of Object.entries(newAuthorData)) {
const bgcolor = typeof data.colorId === 'number'
@ -478,10 +431,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
authorData[author] = data;
}
};
receiveAuthorData(clientVars.collab_client_vars.historicalAuthorData);
return changesetLoader;
};
exports.loadBroadcastJS = loadBroadcastJS;
export { loadBroadcastJS };

View file

@ -1,11 +1,9 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -24,13 +22,11 @@
// revision info is a skip list whos entries represent a particular revision
// of the document. These revisions are connected together by various
// changesets, or deltas, between any two revisions.
const loadBroadcastRevisionsJS = () => {
function Revision(revNum) {
this.rev = revNum;
this.changesets = [];
}
Revision.prototype.addChangeset = function (destIndex, changeset, timeDelta) {
const changesetWrapper = {
deltaRev: destIndex - this.rev,
@ -40,7 +36,6 @@ const loadBroadcastRevisionsJS = () => {
this.changesets.push(changesetWrapper);
this.changesets.sort((a, b) => (b.deltaRev - a.deltaRev));
};
const revisionInfo = {};
revisionInfo.addChangeset = function (fromIndex, toIndex, changeset, backChangeset, timeDelta) {
const startRevision = this[fromIndex] || this.createNew(fromIndex);
@ -48,18 +43,14 @@ const loadBroadcastRevisionsJS = () => {
startRevision.addChangeset(toIndex, changeset, timeDelta);
endRevision.addChangeset(fromIndex, backChangeset, -1 * timeDelta);
};
revisionInfo.latest = clientVars.collab_client_vars.rev || -1;
revisionInfo.createNew = function (index) {
this[index] = new Revision(index);
if (index > this.latest) {
this.latest = index;
}
return this[index];
};
// assuming that there is a path from fromIndex to toIndex, and that the links
// are laid out in a skip-list format
revisionInfo.getPath = function (fromIndex, toIndex) {
@ -72,16 +63,12 @@ const loadBroadcastRevisionsJS = () => {
while (((elem.rev < toIndex) && !reverse) || ((elem.rev > toIndex) && reverse)) {
let couldNotContinue = false;
const oldRev = elem.rev;
for (let i = reverse ? elem.changesets.length - 1 : 0;
reverse ? i >= 0 : i < elem.changesets.length;
i += reverse ? -1 : 1) {
for (let i = reverse ? elem.changesets.length - 1 : 0; reverse ? i >= 0 : i < elem.changesets.length; i += reverse ? -1 : 1) {
if (((elem.changesets[i].deltaRev < 0) && !reverse) ||
((elem.changesets[i].deltaRev > 0) && reverse)) {
couldNotContinue = true;
break;
}
if (((elem.rev + elem.changesets[i].deltaRev <= toIndex) && !reverse) ||
((elem.rev + elem.changesets[i].deltaRev >= toIndex) && reverse)) {
const topush = elem.changesets[i];
@ -92,14 +79,13 @@ const loadBroadcastRevisionsJS = () => {
break;
}
}
if (couldNotContinue || oldRev === elem.rev) break;
if (couldNotContinue || oldRev === elem.rev)
break;
}
}
let status = 'partial';
if (elem.rev === toIndex) status = 'complete';
if (elem.rev === toIndex)
status = 'complete';
return {
fromRev: fromIndex,
rev: elem.rev,
@ -111,5 +97,4 @@ const loadBroadcastRevisionsJS = () => {
};
window.revisionInfo = revisionInfo;
};
exports.loadBroadcastRevisionsJS = loadBroadcastRevisionsJS;
export { loadBroadcastRevisionsJS };

View file

@ -1,121 +1,76 @@
import * as _ from "./underscore.js";
import { padmodals as padmodals$0 } from "./pad_modals.js";
import { colorutils as colorutils$0 } from "./colorutils.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate.
const _ = require('./underscore');
const padmodals = require('./pad_modals').padmodals;
const colorutils = require('./colorutils').colorutils;
const padmodals = { padmodals: padmodals$0 }.padmodals;
const colorutils = { colorutils: colorutils$0 }.colorutils;
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let BroadcastSlider;
// Hack to ensure timeslider i18n values are in
$("[data-key='timeslider_returnToPad'] > a > span").html(
html10n.get('timeslider.toolbar.returnbutton'));
(() => { // wrap this code in its own namespace
$("[data-key='timeslider_returnToPad'] > a > span").html(html10n.get('timeslider.toolbar.returnbutton'));
(() => {
let sliderLength = 1000;
let sliderPos = 0;
let sliderActive = false;
const slidercallbacks = [];
const savedRevisions = [];
let sliderPlaying = false;
const _callSliderCallbacks = (newval) => {
sliderPos = newval;
for (let i = 0; i < slidercallbacks.length; i++) {
slidercallbacks[i](newval);
}
};
const updateSliderElements = () => {
for (let i = 0; i < savedRevisions.length; i++) {
const position = parseInt(savedRevisions[i].attr('pos'));
savedRevisions[i].css(
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
savedRevisions[i].css('left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
}
$('#ui-slider-handle').css(
'left', sliderPos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
$('#ui-slider-handle').css('left', sliderPos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
};
const addSavedRevision = (position, info) => {
const newSavedRevision = $('<div></div>');
newSavedRevision.addClass('star');
newSavedRevision.attr('pos', position);
newSavedRevision.css(
'left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
newSavedRevision.css('left', (position * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0)) - 1);
$('#ui-slider-bar').append(newSavedRevision);
newSavedRevision.mouseup((evt) => {
BroadcastSlider.setSliderPosition(position);
});
savedRevisions.push(newSavedRevision);
};
/* Begin small 'API' */
const onSlider = (callback) => {
slidercallbacks.push(callback);
};
const getSliderPosition = () => sliderPos;
const setSliderPosition = (newpos) => {
newpos = Number(newpos);
if (newpos < 0 || newpos > sliderLength) return;
if (newpos < 0 || newpos > sliderLength)
return;
if (!newpos) {
newpos = 0; // stops it from displaying NaN if newpos isn't set
}
window.location.hash = `#${newpos}`;
$('#ui-slider-handle').css(
'left', newpos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
$('#ui-slider-handle').css('left', newpos * ($('#ui-slider-bar').width() - 2) / (sliderLength * 1.0));
$('a.tlink').map(function () {
$(this).attr('href', $(this).attr('thref').replace('%revision%', newpos));
});
$('#revision_label').html(html10n.get('timeslider.version', {version: newpos}));
$('#revision_label').html(html10n.get('timeslider.version', { version: newpos }));
$('#leftstar, #leftstep').toggleClass('disabled', newpos === 0);
$('#rightstar, #rightstep').toggleClass('disabled', newpos === sliderLength);
sliderPos = newpos;
_callSliderCallbacks(newpos);
};
const getSliderLength = () => sliderLength;
const setSliderLength = (newlength) => {
sliderLength = newlength;
updateSliderElements();
};
// just take over the whole slider screen with a reconnect message
const showReconnectUI = () => {
padmodals.showModal('disconnected');
};
const setAuthors = (authors) => {
const authorsList = $('#authorsList');
authorsList.empty();
@ -126,36 +81,37 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
if (author) {
const authorColor = clientVars.colorPalette[author.colorId] || author.colorId;
if (author.name) {
if (numNamed !== 0) authorsList.append(', ');
const textColor =
colorutils.textColorFromBackgroundColor(authorColor, clientVars.skinName);
if (numNamed !== 0)
authorsList.append(', ');
const textColor = colorutils.textColorFromBackgroundColor(authorColor, clientVars.skinName);
$('<span />')
.text(author.name || 'unnamed')
.css('background-color', authorColor)
.css('color', textColor)
.addClass('author')
.appendTo(authorsList);
numNamed++;
} else {
}
else {
numAnonymous++;
if (authorColor) colorsAnonymous.push(authorColor);
if (authorColor)
colorsAnonymous.push(authorColor);
}
}
});
if (numAnonymous > 0) {
const anonymousAuthorString = html10n.get('timeslider.unnamedauthors', {num: numAnonymous});
const anonymousAuthorString = html10n.get('timeslider.unnamedauthors', { num: numAnonymous });
if (numNamed !== 0) {
authorsList.append(` + ${anonymousAuthorString}`);
} else {
}
else {
authorsList.append(anonymousAuthorString);
}
if (colorsAnonymous.length > 0) {
authorsList.append(' (');
_.each(colorsAnonymous, (color, i) => {
if (i > 0) authorsList.append(' ');
if (i > 0)
authorsList.append(' ');
$('<span>&nbsp;</span>')
.css('background-color', color)
.addClass('author author-anonymous')
@ -168,7 +124,6 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
authorsList.append(html10n.get('timeslider.toolbar.authorsList'));
}
};
const playButtonUpdater = () => {
if (sliderPlaying) {
if (getSliderPosition() + 1 > sliderLength) {
@ -177,23 +132,21 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
return;
}
setSliderPosition(getSliderPosition() + 1);
setTimeout(playButtonUpdater, 100);
}
};
const playpause = () => {
$('#playpause_button_icon').toggleClass('pause');
if (!sliderPlaying) {
if (getSliderPosition() === sliderLength) setSliderPosition(0);
if (getSliderPosition() === sliderLength)
setSliderPosition(0);
sliderPlaying = true;
playButtonUpdater();
} else {
}
else {
sliderPlaying = false;
}
};
BroadcastSlider = {
onSlider,
getSliderPosition,
@ -206,41 +159,41 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
showReconnectUI,
setAuthors,
};
// assign event handlers to html UI elements after page load
fireWhenAllScriptsAreLoaded.push(() => {
$(document).keyup((e) => {
if (!e) e = window.event;
if (!e)
e = window.event;
const code = e.keyCode || e.which;
if (code === 37) { // left
if (e.shiftKey) {
$('#leftstar').click();
} else {
}
else {
$('#leftstep').click();
}
} else if (code === 39) { // right
}
else if (code === 39) { // right
if (e.shiftKey) {
$('#rightstar').click();
} else {
}
else {
$('#rightstep').click();
}
} else if (code === 32) { // spacebar
}
else if (code === 32) { // spacebar
$('#playpause_button_icon').trigger('click');
}
});
// Resize
$(window).resize(() => {
updateSliderElements();
});
// Slider click
$('#ui-slider-bar').mousedown((evt) => {
$('#ui-slider-handle').css('left', (evt.clientX - $('#ui-slider-bar').offset().left));
$('#ui-slider-handle').trigger(evt);
});
// Slider dragging
$('#ui-slider-handle').mousedown(function (evt) {
this.startLoc = evt.clientX;
@ -249,37 +202,41 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
$(document).mousemove((evt2) => {
$(this).css('pointer', 'move');
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
if (newloc < 0) newloc = 0;
if (newloc < 0)
newloc = 0;
const maxPos = $('#ui-slider-bar').width() - 2;
if (newloc > maxPos) newloc = maxPos;
if (newloc > maxPos)
newloc = maxPos;
const version = Math.floor(newloc * sliderLength / maxPos);
$('#revision_label').html(html10n.get('timeslider.version', {version}));
$('#revision_label').html(html10n.get('timeslider.version', { version }));
$(this).css('left', newloc);
if (getSliderPosition() !== version) _callSliderCallbacks(version);
if (getSliderPosition() !== version)
_callSliderCallbacks(version);
});
$(document).mouseup((evt2) => {
$(document).unbind('mousemove');
$(document).unbind('mouseup');
sliderActive = false;
let newloc = this.currentLoc + (evt2.clientX - this.startLoc);
if (newloc < 0) newloc = 0;
if (newloc < 0)
newloc = 0;
const maxPos = $('#ui-slider-bar').width() - 2;
if (newloc > maxPos) newloc = maxPos;
if (newloc > maxPos)
newloc = maxPos;
$(this).css('left', newloc);
setSliderPosition(Math.floor(newloc * sliderLength / maxPos));
if (parseInt($(this).css('left')) < 2) {
$(this).css('left', '2px');
} else {
}
else {
this.currentLoc = parseInt($(this).css('left'));
}
});
});
// play/pause toggling
$('#playpause_button_icon').click((evt) => {
BroadcastSlider.playpause();
});
// next/prev saved revision and changeset
$('.stepper').click(function (evt) {
switch ($(this).attr('id')) {
@ -293,7 +250,8 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let nextStar = 0; // default to first revision in document
for (let i = 0; i < savedRevisions.length; i++) {
const pos = parseInt(savedRevisions[i].attr('pos'));
if (pos < getSliderPosition() && nextStar < pos) nextStar = pos;
if (pos < getSliderPosition() && nextStar < pos)
nextStar = pos;
}
setSliderPosition(nextStar);
break;
@ -302,17 +260,16 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let nextStar = sliderLength; // default to last revision in document
for (let i = 0; i < savedRevisions.length; i++) {
const pos = parseInt(savedRevisions[i].attr('pos'));
if (pos > getSliderPosition() && nextStar > pos) nextStar = pos;
if (pos > getSliderPosition() && nextStar > pos)
nextStar = pos;
}
setSliderPosition(nextStar);
break;
}
}
});
if (clientVars) {
$('#timeslider-wrapper').show();
if (window.location.hash.length > 1) {
const hashRev = Number(window.location.hash.substr(1));
if (!isNaN(hashRev)) {
@ -320,23 +277,17 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
setTimeout(() => { setSliderPosition(hashRev); }, 1);
}
}
setSliderLength(clientVars.collab_client_vars.rev);
setSliderPosition(clientVars.collab_client_vars.rev);
_.each(clientVars.savedRevisions, (revision) => {
addSavedRevision(revision.revNum, revision);
});
}
});
})();
BroadcastSlider.onSlider((loc) => {
$('#viewlatest').html(
`${loc === BroadcastSlider.getSliderLength() ? 'Viewing' : 'View'} latest content`);
$('#viewlatest').html(`${loc === BroadcastSlider.getSliderLength() ? 'Viewing' : 'View'} latest content`);
});
return BroadcastSlider;
};
exports.loadBroadcastSliderJS = loadBroadcastSliderJS;
export { loadBroadcastSliderJS };

View file

@ -1,11 +1,129 @@
'use strict';
// One rep.line(div) can be broken in more than one line in the browser.
// This function is useful to get the caret position of the line as
// is represented by the browser
exports.getPosition = () => {
const createSelectionRange = (range) => {
const clonedRange = range.cloneRange();
// we set the selection start and end to avoid error when user selects a text bigger than
// the viewport height and uses the arrow keys to expand the selection. In this particular
// case is necessary to know where the selections ends because both edges of the selection
// is out of the viewport but we only use the end of it to calculate if it needs to scroll
clonedRange.setStart(range.endContainer, range.endOffset);
clonedRange.setEnd(range.endContainer, range.endOffset);
return clonedRange;
};
const getPositionOfRepLineAtOffset = (node, offset) => {
// it is not a text node, so we cannot make a selection
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
return getPositionOfElementOrSelection(node);
}
while (node.length === 0 && node.nextSibling) {
node = node.nextSibling;
}
const newRange = new Range();
newRange.setStart(node, offset);
newRange.setEnd(node, offset);
const linePosition = getPositionOfElementOrSelection(newRange);
newRange.detach(); // performance sake
return linePosition;
};
const getPositionOfElementOrSelection = (element) => {
const rect = element.getBoundingClientRect();
const linePosition = {
bottom: rect.bottom,
height: rect.height,
top: rect.top,
};
return linePosition;
};
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const firstRootNode = getFirstRootChildNode(lineNode);
// to get the position of the node we get the position of the first char
const positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1);
return positionOfFirstRootNode.top === caretLineTop;
};
// find the first root node, usually it is a text node
const getFirstRootChildNode = (node) => {
if (!node.firstChild) {
return node;
}
else {
return getFirstRootChildNode(node.firstChild);
}
};
const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we get the position of the line in the last char of it
const lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition;
};
const getLastRootChildNode = (node) => {
if (!node.lastChild) {
return {
node,
length: node.length,
};
}
else {
return getLastRootChildNode(node.lastChild);
}
};
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we take a rep line and get the position of the last char of it
const lastRootChildNodePosition = getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition.top === caretLineTop;
};
const getPreviousVisibleLine = (line, rep) => {
const firstLineOfPad = 0;
if (line <= firstLineOfPad) {
return firstLineOfPad;
}
else if (isLineVisible(line, rep)) {
return line;
}
else {
return getPreviousVisibleLine(line - 1, rep);
}
};
const getNextVisibleLine = (line, rep) => {
const lastLineOfThePad = rep.lines.length() - 1;
if (line >= lastLineOfThePad) {
return lastLineOfThePad;
}
else if (isLineVisible(line, rep)) {
return line;
}
else {
return getNextVisibleLine(line + 1, rep);
}
};
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const firstRootChildNode = getFirstRootChildNode(lineNode);
// we can get the position of the line, getting the position of the first char of the rep line
const firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1);
return firstRootChildNodePosition;
};
const getSelectionRange = () => {
if (!window.getSelection) {
return;
}
const selection = window.getSelection();
if (selection && selection.type !== 'None' && selection.rangeCount > 0) {
return selection.getRangeAt(0);
}
else {
return null;
}
};
export const getPosition = () => {
const range = getSelectionRange();
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') return null;
if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody')
return null;
// When there's a <br> or any element that has no height, we can't get the dimension of the
// element where the caret is. As we can't get the element height, we create a text node to get
// the dimensions on the position.
@ -17,187 +135,31 @@ exports.getPosition = () => {
shadowCaret.remove();
return line;
};
const createSelectionRange = (range) => {
const clonedRange = range.cloneRange();
// we set the selection start and end to avoid error when user selects a text bigger than
// the viewport height and uses the arrow keys to expand the selection. In this particular
// case is necessary to know where the selections ends because both edges of the selection
// is out of the viewport but we only use the end of it to calculate if it needs to scroll
clonedRange.setStart(range.endContainer, range.endOffset);
clonedRange.setEnd(range.endContainer, range.endOffset);
return clonedRange;
};
const getPositionOfRepLineAtOffset = (node, offset) => {
// it is not a text node, so we cannot make a selection
if (node.tagName === 'BR' || node.tagName === 'EMPTY') {
return getPositionOfElementOrSelection(node);
}
while (node.length === 0 && node.nextSibling) {
node = node.nextSibling;
}
const newRange = new Range();
newRange.setStart(node, offset);
newRange.setEnd(node, offset);
const linePosition = getPositionOfElementOrSelection(newRange);
newRange.detach(); // performance sake
return linePosition;
};
const getPositionOfElementOrSelection = (element) => {
const rect = element.getBoundingClientRect();
const linePosition = {
bottom: rect.bottom,
height: rect.height,
top: rect.top,
};
return linePosition;
};
// here we have two possibilities:
// [1] the line before the caret line has the same type, so both of them has the same margin,
// padding height, etc. So, we can use the caret line to make calculation necessary to know
// where is the top of the previous line
// [2] the line before is part of another rep line. It's possible this line has different margins
// height. So we have to get the exactly position of the line
exports.getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
export const getPositionTopOfPreviousBrowserLine = (caretLinePosition, rep) => {
let previousLineTop = caretLinePosition.top - caretLinePosition.height; // [1]
const isCaretLineFirstBrowserLine = caretLineIsFirstBrowserLine(caretLinePosition.top, rep);
// the caret is in the beginning of a rep line, so the previous browser line
// is the last line browser line of the a rep line
if (isCaretLineFirstBrowserLine) { // [2]
const lineBeforeCaretLine = rep.selStart[0] - 1;
const firstLineVisibleBeforeCaretLine = getPreviousVisibleLine(lineBeforeCaretLine, rep);
const linePosition =
getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep);
const linePosition = getDimensionOfLastBrowserLineOfRepLine(firstLineVisibleBeforeCaretLine, rep);
previousLineTop = linePosition.top;
}
return previousLineTop;
};
const caretLineIsFirstBrowserLine = (caretLineTop, rep) => {
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const firstRootNode = getFirstRootChildNode(lineNode);
// to get the position of the node we get the position of the first char
const positionOfFirstRootNode = getPositionOfRepLineAtOffset(firstRootNode, 1);
return positionOfFirstRootNode.top === caretLineTop;
};
// find the first root node, usually it is a text node
const getFirstRootChildNode = (node) => {
if (!node.firstChild) {
return node;
} else {
return getFirstRootChildNode(node.firstChild);
}
};
const getDimensionOfLastBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we get the position of the line in the last char of it
const lastRootChildNodePosition =
getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition;
};
const getLastRootChildNode = (node) => {
if (!node.lastChild) {
return {
node,
length: node.length,
};
} else {
return getLastRootChildNode(node.lastChild);
}
};
// here we have two possibilities:
// [1] The next line is part of the same rep line of the caret line, so we have the same dimensions.
// So, we can use the caret line to calculate the bottom of the line.
// [2] the next line is part of another rep line.
// It's possible this line has different dimensions, so we have to get the exactly dimension of it
exports.getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
export const getBottomOfNextBrowserLine = (caretLinePosition, rep) => {
let nextLineBottom = caretLinePosition.bottom + caretLinePosition.height; // [1]
const isCaretLineLastBrowserLine =
caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
const isCaretLineLastBrowserLine = caretLineIsLastBrowserLineOfRepLine(caretLinePosition.top, rep);
// the caret is at the end of a rep line, so we can get the next browser line dimension
// using the position of the first char of the next rep line
if (isCaretLineLastBrowserLine) { // [2]
const nextLineAfterCaretLine = rep.selStart[0] + 1;
const firstNextLineVisibleAfterCaretLine = getNextVisibleLine(nextLineAfterCaretLine, rep);
const linePosition =
getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep);
const linePosition = getDimensionOfFirstBrowserLineOfRepLine(firstNextLineVisibleAfterCaretLine, rep);
nextLineBottom = linePosition.bottom;
}
return nextLineBottom;
};
const caretLineIsLastBrowserLineOfRepLine = (caretLineTop, rep) => {
const caretRepLine = rep.selStart[0];
const lineNode = rep.lines.atIndex(caretRepLine).lineNode;
const lastRootChildNode = getLastRootChildNode(lineNode);
// we take a rep line and get the position of the last char of it
const lastRootChildNodePosition =
getPositionOfRepLineAtOffset(lastRootChildNode.node, lastRootChildNode.length);
return lastRootChildNodePosition.top === caretLineTop;
};
const getPreviousVisibleLine = (line, rep) => {
const firstLineOfPad = 0;
if (line <= firstLineOfPad) {
return firstLineOfPad;
} else if (isLineVisible(line, rep)) {
return line;
} else {
return getPreviousVisibleLine(line - 1, rep);
}
};
exports.getPreviousVisibleLine = getPreviousVisibleLine;
const getNextVisibleLine = (line, rep) => {
const lastLineOfThePad = rep.lines.length() - 1;
if (line >= lastLineOfThePad) {
return lastLineOfThePad;
} else if (isLineVisible(line, rep)) {
return line;
} else {
return getNextVisibleLine(line + 1, rep);
}
};
exports.getNextVisibleLine = getNextVisibleLine;
const isLineVisible = (line, rep) => rep.lines.atIndex(line).lineNode.offsetHeight > 0;
const getDimensionOfFirstBrowserLineOfRepLine = (line, rep) => {
const lineNode = rep.lines.atIndex(line).lineNode;
const firstRootChildNode = getFirstRootChildNode(lineNode);
// we can get the position of the line, getting the position of the first char of the rep line
const firstRootChildNodePosition = getPositionOfRepLineAtOffset(firstRootChildNode, 1);
return firstRootChildNodePosition;
};
const getSelectionRange = () => {
if (!window.getSelection) {
return;
}
const selection = window.getSelection();
if (selection && selection.type !== 'None' && selection.rangeCount > 0) {
return selection.getRangeAt(0);
} else {
return null;
}
};
export { getPreviousVisibleLine };
export { getNextVisibleLine };

View file

@ -1,31 +1,7 @@
import AttributeMap from "./AttributeMap.js";
import AttributePool from "./AttributePool.js";
import * as Changeset from "./Changeset.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const AttributeMap = require('./AttributeMap');
const AttributePool = require('./AttributePool');
const Changeset = require('./Changeset');
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// latest official text from server
let baseAText = Changeset.makeAText('\n');
@ -40,11 +16,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// unset in a "finally" block. When set to true, the setter
// takes change of userChangeset.
let applyingNonUserChanges = false;
let changeCallback = null;
let changeCallbackTimeout = null;
const setChangeCallbackTimeout = () => {
// can call this multiple times per call-stack, because
// we only schedule a call to changeCallback if it exists
@ -53,15 +26,16 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
changeCallbackTimeout = scheduler.setTimeout(() => {
try {
changeCallback();
} catch (pseudoError) {
}
catch (pseudoError) {
// as empty as my soul
} finally {
}
finally {
changeCallbackTimeout = null;
}
}, 0);
}
};
let self;
return self = {
isTracking: () => tracking,
@ -81,49 +55,47 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
applyingNonUserChanges = true;
try {
callbacks.setDocumentAttributedText(atext);
} finally {
}
finally {
applyingNonUserChanges = false;
}
});
},
composeUserChangeset: (c) => {
if (!tracking) return;
if (applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return;
if (!tracking)
return;
if (applyingNonUserChanges)
return;
if (Changeset.isIdentity(c))
return;
userChangeset = Changeset.compose(userChangeset, c, apool);
setChangeCallbackTimeout();
},
applyChangesToBase: (c, optAuthor, apoolJsonObj) => {
if (!tracking) return;
if (!tracking)
return;
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
}
baseAText = Changeset.applyToAText(c, baseAText, apool);
let c2 = c;
if (submittedChangeset) {
const oldSubmittedChangeset = submittedChangeset;
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
}
const preferInsertingAfterUserChanges = true;
const oldUserChangeset = userChangeset;
userChangeset = Changeset.follow(
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
userChangeset = Changeset.follow(c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
applyingNonUserChanges = true;
try {
callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret);
} finally {
}
finally {
applyingNonUserChanges = false;
}
});
@ -136,15 +108,14 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// submission must have been canceled, prepare new changeset
// that includes old submittedChangeset
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
} else {
}
else {
// Get my authorID
const authorId = parent.parent.pad.myUserInfo.userId;
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
// text was copied from another author.
const cs = Changeset.unpack(userChangeset);
const assem = Changeset.mergingOpAssembler();
for (const op of Changeset.deserializeOps(cs.ops)) {
if (op.opcode === '+') {
const attribs = AttributeMap.fromString(op.attribs, apool);
@ -159,16 +130,15 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
assem.endDocument();
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
Changeset.checkRep(userChangeset);
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
else toSubmit = userChangeset;
if (Changeset.isIdentity(userChangeset))
toSubmit = null;
else
toSubmit = userChangeset;
}
let cs = null;
if (toSubmit) {
submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
cs = toSubmit;
}
let wireApool = null;
@ -177,7 +147,6 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
wireApool = forWire.pool.toJsonable();
cs = forWire.translated;
}
const data = {
changeset: cs,
apool: wireApool,
@ -199,5 +168,4 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
};
};
exports.makeChangesetTracker = makeChangesetTracker;
export { makeChangesetTracker };

View file

@ -1,31 +1,16 @@
import ChatMessage from "./ChatMessage.js";
import { padutils as padutils$0 } from "./pad_utils.js";
import { padcookie as padcookie$0 } from "./pad_cookie.js";
import Tinycon from "tinycon/tinycon";
import * as hooks from "./pluginfw/hooks.js";
import { padeditor as padeditor$0 } from "./pad_editor.js";
'use strict';
/**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const ChatMessage = require('./ChatMessage');
const padutils = require('./pad_utils').padutils;
const padcookie = require('./pad_cookie').padcookie;
const Tinycon = require('tinycon/tinycon');
const hooks = require('./pluginfw/hooks');
const padeditor = require('./pad_editor').padeditor;
const padutils = { padutils: padutils$0 }.padutils;
const padcookie = { padcookie: padcookie$0 }.padcookie;
const padeditor = { padeditor: padeditor$0 }.padeditor;
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
exports.chat = (() => {
export const chat = (() => {
let isStuck = false;
let userAndChat = false;
let chatMentions = 0;
@ -58,7 +43,6 @@ exports.chat = (() => {
$('#chatbox, .sticky-container').toggleClass('stickyChat', isStuck);
$('#chatbox').css('display', 'flex');
}, 0);
padcookie.setPref('chatAlwaysVisible', isStuck);
$('#options-stickychat').prop('checked', isStuck);
},
@ -70,7 +54,8 @@ exports.chat = (() => {
$('#options-chatandusers').prop('checked', true);
$('#options-stickychat').prop('disabled', 'disabled');
userAndChat = true;
} else {
}
else {
$('#options-stickychat').prop('disabled', false);
userAndChat = false;
}
@ -84,7 +69,8 @@ exports.chat = (() => {
if ($('#options-stickychat').prop('checked')) {
this.stickToScreen();
$('#options-stickychat').prop('checked', false);
} else {
}
else {
$('#chatcounter').text('0');
$('#chaticon').addClass('visible');
$('#chatbox').removeClass('visible');
@ -96,26 +82,24 @@ exports.chat = (() => {
this.lastMessage.position().top < ($('#chattext').outerHeight() + 20)) {
// if we use a slow animate here we can have a race condition
// when a users focus can not be moved away from the last message recieved.
$('#chattext').animate(
{scrollTop: $('#chattext')[0].scrollHeight},
{duration: 400, queue: false});
$('#chattext').animate({ scrollTop: $('#chattext')[0].scrollHeight }, { duration: 400, queue: false });
this.lastMessage = $('#chattext > p').eq(-1);
}
}
},
async send() {
const text = $('#chatinput').val();
if (text.replace(/\s+/, '').length === 0) return;
if (text.replace(/\s+/, '').length === 0)
return;
const message = new ChatMessage(text);
await hooks.aCallAll('chatSendMessage', Object.freeze({message}));
this._pad.collabClient.sendMessage({type: 'CHAT_MESSAGE', message});
await hooks.aCallAll('chatSendMessage', Object.freeze({ message }));
this._pad.collabClient.sendMessage({ type: 'CHAT_MESSAGE', message });
$('#chatinput').val('');
},
async addMessage(msg, increment, isHistoryAdd) {
msg = ChatMessage.fromObject(msg);
// correct the time
msg.time += this._pad.clientTimeOffset;
if (!msg.authorId) {
/*
* If, for a bug or a database corruption, the message coming from the
@ -123,16 +107,14 @@ exports.chat = (() => {
* let's be defensive and replace it with "unknown".
*/
msg.authorId = 'unknown';
console.warn(
'The "authorId" field of a chat message coming from the server was not present. ' +
console.warn('The "authorId" field of a chat message coming from the server was not present. ' +
'Replacing with "unknown". This may be a bug or a database corruption.');
}
const authorClass = (authorId) => `author-${authorId.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-';
if (c === '.')
return '-';
return `z${c.charCodeAt(0)}z`;
})}`;
// the hook args
const ctx = {
authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'),
@ -145,32 +127,28 @@ exports.chat = (() => {
timeStr: (() => {
let minutes = `${new Date(msg.time).getMinutes()}`;
let hours = `${new Date(msg.time).getHours()}`;
if (minutes.length === 1) minutes = `0${minutes}`;
if (hours.length === 1) hours = `0${hours}`;
if (minutes.length === 1)
minutes = `0${minutes}`;
if (hours.length === 1)
hours = `0${hours}`;
return `${hours}:${minutes}`;
})(),
duration: 4000,
};
// is the users focus already in the chatbox?
const alreadyFocused = $('#chatinput').is(':focus');
// does the user already have the chatbox open?
const chatOpen = $('#chatbox').hasClass('visible');
// does this message contain this user's name? (is the current user mentioned?)
const wasMentioned =
msg.authorId !== window.clientVars.userId &&
const wasMentioned = msg.authorId !== window.clientVars.userId &&
ctx.authorName !== html10n.get('pad.userlist.unnamed') &&
normalize(ctx.text).includes(normalize(ctx.authorName));
// If the user was mentioned, make the message sticky
if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) {
chatMentions++;
Tinycon.setBubble(chatMentions);
ctx.sticky = true;
}
await hooks.aCallAll('chatNewMessage', ctx);
const cls = authorClass(ctx.author);
const chatMsg = ctx.rendered != null ? $(ctx.rendered) : $('<p>')
@ -187,17 +165,17 @@ exports.chat = (() => {
// ctx.text was HTML-escaped before calling the hook. Hook functions are trusted to not
// introduce an XSS vulnerability by adding unescaped user input.
.append($('<div>').html(ctx.text).contents());
if (isHistoryAdd) chatMsg.insertAfter('#chatloadmessagesbutton');
else $('#chattext').append(chatMsg);
if (isHistoryAdd)
chatMsg.insertAfter('#chatloadmessagesbutton');
else
$('#chattext').append(chatMsg);
chatMsg.each((i, e) => html10n.translateElement(html10n.translations, e));
// should we increment the counter??
if (increment && !isHistoryAdd) {
// Update the counter of unread messages
let count = Number($('#chatcounter').text());
count++;
$('#chatcounter').text(count);
if (!chatOpen && ctx.duration > 0) {
const text = $('<p>')
.append($('<span>').addClass('author-name').text(ctx.authorName))
@ -214,7 +192,8 @@ exports.chat = (() => {
});
}
}
if (!isHistoryAdd) this.scrollDown();
if (!isHistoryAdd)
this.scrollDown();
},
init(pad) {
this._pad = pad;
@ -234,7 +213,6 @@ exports.chat = (() => {
chatMentions = 0;
Tinycon.setBubble(0);
});
const self = this;
$('body:not(#chatinput)').on('keypress', function (evt) {
if (evt.altKey && evt.which === 67) {
@ -245,7 +223,6 @@ exports.chat = (() => {
evt.preventDefault();
}
});
$('#chatinput').keypress((evt) => {
// if the user typed enter, fire the send
if (evt.key === 'Enter' && !evt.shiftKey) {
@ -253,20 +230,16 @@ exports.chat = (() => {
this.send();
}
});
// initial messages are loaded in pad.js' _afterHandshake
$('#chatcounter').text(0);
$('#chatloadmessagesbutton').click(() => {
const start = Math.max(this.historyPointer - 20, 0);
const end = this.historyPointer;
if (start === end) return; // nothing to load
if (start === end)
return; // nothing to load
$('#chatloadmessagesbutton').css('display', 'none');
$('#chatloadmessagesball').css('display', 'block');
pad.collabClient.sendMessage({type: 'GET_CHAT_MESSAGES', start, end});
pad.collabClient.sendMessage({ type: 'GET_CHAT_MESSAGES', start, end });
this.historyPointer = start;
});
},

View file

@ -1,11 +1,12 @@
import { chat as chat$0 } from "./chat.js";
import * as hooks from "./pluginfw/hooks.js";
import browser from "./vendors/browser.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -21,23 +22,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const chat = require('./chat').chat;
const hooks = require('./pluginfw/hooks');
const browser = require('./vendors/browser');
const chat = { chat: chat$0 }.chat;
// Dependency fill on init. This exists for `pad.socket` only.
// TODO: bind directly to the socket.
let pad = undefined;
const getSocket = () => pad && pad.socket;
/** Call this when the document is ready, and a new Ace2Editor() has been created and inited.
ACE's ready callback does not need to have fired yet.
"serverVars" are from calling doc.getCollabClientVars() on the server. */
const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) => {
const editor = ace2editor;
pad = _pad; // Inject pad to avoid a circular dependency.
let rev = serverVars.rev;
let committing = false;
let stateMessage;
@ -45,23 +40,20 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
let lastCommitTime = 0;
let initialStartConnectTime = 0;
let commitDelay = 500;
const userId = initialUserInfo.userId;
// var socket;
const userSet = {}; // userId -> userInfo
userSet[userId] = initialUserInfo;
let isPendingRevision = false;
const callbacks = {
onUserJoin: () => {},
onUserLeave: () => {},
onUpdateUserInfo: () => {},
onChannelStateChange: () => {},
onClientMessage: () => {},
onInternalAction: () => {},
onConnectionTrouble: () => {},
onServerMessage: () => {},
onUserJoin: () => { },
onUserLeave: () => { },
onUpdateUserInfo: () => { },
onChannelStateChange: () => { },
onClientMessage: () => { },
onInternalAction: () => { },
onConnectionTrouble: () => { },
onServerMessage: () => { },
};
if (browser.firefox) {
// Prevent "escape" from taking effect and canceling a comet connection;
@ -72,7 +64,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
}
});
}
const handleUserChanges = () => {
if (editor.getInInternationalComposition()) {
// handleUserChanges() will be called again once composition ends so there's no need to set up
@ -83,32 +74,32 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
if ((!getSocket()) || channelState === 'CONNECTING') {
if (channelState === 'CONNECTING' && (now - initialStartConnectTime) > 20000) {
setChannelState('DISCONNECTED', 'initsocketfail');
} else {
}
else {
// check again in a bit
setTimeout(handleUserChanges, 1000);
}
return;
}
if (committing) {
if (now - lastCommitTime > 20000) {
// a commit is taking too long
setChannelState('DISCONNECTED', 'slowcommit');
} else if (now - lastCommitTime > 5000) {
}
else if (now - lastCommitTime > 5000) {
callbacks.onConnectionTrouble('SLOW');
} else {
}
else {
// run again in a few seconds, to detect a disconnect
setTimeout(handleUserChanges, 3000);
}
return;
}
const earliestCommit = lastCommitTime + commitDelay;
if (now < earliestCommit) {
setTimeout(handleUserChanges, earliestCommit - now);
return;
}
let sentMessage = false;
// Check if there are any pending revisions to be received from server.
// Allow only if there are no pending revisions to be received from server
@ -127,65 +118,60 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
sentMessage = true;
callbacks.onInternalAction('commitPerformed');
}
} else {
}
else {
// run again in a few seconds, to check if there was a reconnection attempt
setTimeout(handleUserChanges, 3000);
}
if (sentMessage) {
// run again in a few seconds, to detect a disconnect
setTimeout(handleUserChanges, 3000);
}
};
const acceptCommit = () => {
editor.applyPreparedChangesetToBase();
setStateIdle();
try {
callbacks.onInternalAction('commitAcceptedByServer');
callbacks.onConnectionTrouble('OK');
} catch (err) { /* intentionally ignored */ }
}
catch (err) { /* intentionally ignored */ }
handleUserChanges();
};
const setUpSocket = () => {
setChannelState('CONNECTED');
doDeferredActions();
initialStartConnectTime = Date.now();
};
const sendMessage = (msg) => {
getSocket().json.send(
{
getSocket().json.send({
type: 'COLLABROOM',
component: 'pad',
data: msg,
});
};
const serverMessageTaskQueue = new class {
constructor() {
this._promiseChain = Promise.resolve();
}
async enqueue(fn) {
const taskPromise = this._promiseChain.then(fn);
// Use .catch() to prevent rejections from halting the queue.
this._promiseChain = taskPromise.catch(() => {});
this._promiseChain = taskPromise.catch(() => { });
// Do NOT do `return await this._promiseChain;` because the caller would not see an error if
// fn() throws/rejects (due to the .catch() added above).
return await taskPromise;
}
}();
const handleMessageFromServer = (evt) => {
if (!getSocket()) return;
if (!evt.data) return;
if (!getSocket())
return;
if (!evt.data)
return;
const wrapper = evt;
if (wrapper.type !== 'COLLABROOM' && wrapper.type !== 'CUSTOM') return;
if (wrapper.type !== 'COLLABROOM' && wrapper.type !== 'CUSTOM')
return;
const msg = wrapper.data;
if (msg.type === 'NEW_CHANGES') {
serverMessageTaskQueue.enqueue(async () => {
// Avoid updating the DOM while the user is composing a character. Notes about this `await`:
@ -196,7 +182,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
// possible, that the chances are so small or the consequences so minor that it's not
// worth addressing).
await editor.getInInternationalComposition();
const {newRev, changeset, author = '', apool} = msg;
const { newRev, changeset, author = '', apool } = msg;
if (newRev !== (rev + 1)) {
window.console.warn(`bad message revision on NEW_CHANGES: ${newRev} not ${rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_newchanges");
@ -205,9 +191,10 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
rev = newRev;
editor.applyChangesToBase(changeset, author, apool);
});
} else if (msg.type === 'ACCEPT_COMMIT') {
}
else if (msg.type === 'ACCEPT_COMMIT') {
serverMessageTaskQueue.enqueue(() => {
const {newRev} = msg;
const { newRev } = msg;
// newRev will equal rev if the changeset has no net effect (identity changeset, removing
// and re-adding the same characters with the same attributes, or retransmission of an
// already applied changeset).
@ -219,7 +206,8 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
rev = newRev;
acceptCommit();
});
} else if (msg.type === 'CLIENT_RECONNECT') {
}
else if (msg.type === 'CLIENT_RECONNECT') {
// Server sends a CLIENT_RECONNECT message when there is a client reconnect.
// Server also returns all pending revisions along with this CLIENT_RECONNECT message
serverMessageTaskQueue.enqueue(() => {
@ -228,7 +216,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
setIsPendingRevision(false);
return;
}
const {headRev, newRev, changeset, author = '', apool} = msg;
const { headRev, newRev, changeset, author = '', apool } = msg;
if (newRev !== (rev + 1)) {
window.console.warn(`bad message revision on CLIENT_RECONNECT: ${newRev} not ${rev + 1}`);
// setChannelState("DISCONNECTED", "badmessage_acceptcommit");
@ -237,7 +225,8 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
rev = newRev;
if (author === pad.getUserId()) {
acceptCommit();
} else {
}
else {
editor.applyChangesToBase(changeset, author, apool);
}
if (newRev === headRev) {
@ -245,18 +234,21 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
setIsPendingRevision(false);
}
});
} else if (msg.type === 'USER_NEWINFO') {
}
else if (msg.type === 'USER_NEWINFO') {
const userInfo = msg.userInfo;
const id = userInfo.userId;
if (userSet[id]) {
userSet[id] = userInfo;
callbacks.onUpdateUserInfo(userInfo);
} else {
}
else {
userSet[id] = userInfo;
callbacks.onUserJoin(userInfo);
}
tellAceActiveAuthorInfo(userInfo);
} else if (msg.type === 'USER_LEAVE') {
}
else if (msg.type === 'USER_LEAVE') {
const userInfo = msg.userInfo;
const id = userInfo.userId;
if (userSet[id]) {
@ -264,11 +256,14 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
fadeAceAuthorInfo(userInfo);
callbacks.onUserLeave(userInfo);
}
} else if (msg.type === 'CLIENT_MESSAGE') {
}
else if (msg.type === 'CLIENT_MESSAGE') {
callbacks.onClientMessage(msg.payload);
} else if (msg.type === 'CHAT_MESSAGE') {
}
else if (msg.type === 'CHAT_MESSAGE') {
chat.addMessage(msg.message, true, false);
} else if (msg.type === 'CHAT_MESSAGES') {
}
else if (msg.type === 'CHAT_MESSAGES') {
for (let i = msg.messages.length - 1; i >= 0; i--) {
chat.addMessage(msg.messages[i], true, true);
}
@ -277,19 +272,17 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
chat.gotInitalMessages = true;
chat.historyPointer = clientVars.chatHead - msg.messages.length;
}
// messages are loaded, so hide the loading-ball
$('#chatloadmessagesball').css('display', 'none');
// there are less than 100 messages or we reached the top
if (chat.historyPointer <= 0) {
$('#chatloadmessagesbutton').css('display', 'none');
} else {
}
else {
// there are still more messages, re-show the load-button
$('#chatloadmessagesbutton').css('display', 'block');
}
}
// HACKISH: User messages do not have "payload" but "userInfo", so that all
// "handleClientMessage_USER_" hooks would work, populate payload
// FIXME: USER_* messages to have "payload" property instead of "userInfo",
@ -298,51 +291,45 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
msg.payload = msg.userInfo;
}
// Similar for NEW_CHANGES
if (msg.type === 'NEW_CHANGES') msg.payload = msg;
hooks.callAll(`handleClientMessage_${msg.type}`, {payload: msg.payload});
if (msg.type === 'NEW_CHANGES')
msg.payload = msg;
hooks.callAll(`handleClientMessage_${msg.type}`, { payload: msg.payload });
};
const updateUserInfo = (userInfo) => {
userInfo.userId = userId;
userSet[userId] = userInfo;
tellAceActiveAuthorInfo(userInfo);
if (!getSocket()) return;
sendMessage(
{
if (!getSocket())
return;
sendMessage({
type: 'USERINFO_UPDATE',
userInfo,
});
};
const tellAceActiveAuthorInfo = (userInfo) => {
tellAceAuthorInfo(userInfo.userId, userInfo.colorId);
};
const tellAceAuthorInfo = (userId, colorId, inactive) => {
if (typeof colorId === 'number') {
colorId = clientVars.colorPalette[colorId];
}
const cssColor = colorId;
if (inactive) {
editor.setAuthorInfo(userId, {
bgcolor: cssColor,
fade: 0.5,
});
} else {
}
else {
editor.setAuthorInfo(userId, {
bgcolor: cssColor,
});
}
};
const fadeAceAuthorInfo = (userInfo) => {
tellAceAuthorInfo(userInfo.userId, userInfo.colorId, true);
};
const getConnectedUsers = () => valuesArray(userSet);
const tellAceAboutHistoricalAuthors = (hadata) => {
for (const [author, data] of Object.entries(hadata)) {
if (!userSet[author]) {
@ -350,14 +337,12 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
}
}
};
const setChannelState = (newChannelState, moreInfo) => {
if (newChannelState !== channelState) {
channelState = newChannelState;
callbacks.onChannelStateChange(channelState, moreInfo);
}
};
const valuesArray = (obj) => {
const array = [];
$.each(obj, (k, v) => {
@ -365,11 +350,9 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
});
return array;
};
// We need to present a working interface even before the socket
// is connected for the first time.
let deferredActions = [];
const defer = (func, tag) => function (...args) {
const action = () => {
func.call(this, ...args);
@ -377,34 +360,31 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
action.tag = tag;
if (channelState === 'CONNECTING') {
deferredActions.push(action);
} else {
}
else {
action();
}
};
const doDeferredActions = (tag) => {
const newArray = [];
for (let i = 0; i < deferredActions.length; i++) {
const a = deferredActions[i];
if ((!tag) || (tag === a.tag)) {
a();
} else {
}
else {
newArray.push(a);
}
}
deferredActions = newArray;
};
const sendClientMessage = (msg) => {
sendMessage(
{
sendMessage({
type: 'CLIENT_MESSAGE',
payload: msg,
});
};
const getCurrentRevisionNumber = () => rev;
const getMissedChanges = () => {
const obj = {};
obj.userInfo = userSet[userId];
@ -421,24 +401,19 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
}
return obj;
};
const setStateIdle = () => {
committing = false;
callbacks.onInternalAction('newlyIdle');
schedulePerhapsCallIdleFuncs();
};
const setIsPendingRevision = (value) => {
isPendingRevision = value;
};
const idleFuncs = [];
const callWhenNotCommitting = (func) => {
idleFuncs.push(func);
schedulePerhapsCallIdleFuncs();
};
const schedulePerhapsCallIdleFuncs = () => {
setTimeout(() => {
if (!committing) {
@ -449,7 +424,6 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
}
}, 0);
};
const self = {
setOnUserJoin: (cb) => {
callbacks.onUserJoin = cb;
@ -487,16 +461,12 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad)
set commitDelay(ms) { commitDelay = ms; },
get commitDelay() { return commitDelay; },
};
tellAceAboutHistoricalAuthors(serverVars.historicalAuthorData);
tellAceActiveAuthorInfo(initialUserInfo);
editor.setProperty('userAuthor', userId);
editor.setBaseAttributedText(serverVars.initialAttributedText, serverVars.apool);
editor.setUserChangeNotificationCallback(handleUserChanges);
setUpSocket();
return self;
};
exports.getCollabClient = getCollabClient;
export { getCollabClient };

View file

@ -1,11 +1,9 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js
// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS
/**
@ -23,17 +21,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const colorutils = {};
// Check that a given value is a css hex color value, e.g.
// "#ffffff" or "#fff"
colorutils.isCssHex = (cssColor) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor);
// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0]
colorutils.css2triple = (cssColor) => {
const sixHex = colorutils.css2sixhex(cssColor);
const hexToFloat = (hh) => Number(`0x${hh}`) / 255;
return [
hexToFloat(sixHex.substr(0, 2)),
@ -41,7 +35,6 @@ colorutils.css2triple = (cssColor) => {
hexToFloat(sixHex.substr(4, 2)),
];
};
// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff"
colorutils.css2sixhex = (cssColor) => {
let h = /[0-9a-fA-F]+/.exec(cssColor)[0];
@ -53,7 +46,6 @@ colorutils.css2sixhex = (cssColor) => {
}
return h;
};
// [1.0, 1.0, 1.0] -> "#ffffff"
colorutils.triple2css = (triple) => {
const floatToHex = (n) => {
@ -62,8 +54,6 @@ colorutils.triple2css = (triple) => {
};
return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`;
};
colorutils.clamp = (v, bot, top) => v < bot ? bot : (v > top ? top : v);
colorutils.min3 = (a, b, c) => (a < b) ? (a < c ? a : c) : (b < c ? b : c);
colorutils.max3 = (a, b, c) => (a > b) ? (a > c ? a : c) : (b > c ? b : c);
@ -71,37 +61,31 @@ colorutils.colorMin = (c) => colorutils.min3(c[0], c[1], c[2]);
colorutils.colorMax = (c) => colorutils.max3(c[0], c[1], c[2]);
colorutils.scale = (v, bot, top) => colorutils.clamp(bot + v * (top - bot), 0, 1);
colorutils.unscale = (v, bot, top) => colorutils.clamp((v - bot) / (top - bot), 0, 1);
colorutils.scaleColor = (c, bot, top) => [
colorutils.scale(c[0], bot, top),
colorutils.scale(c[1], bot, top),
colorutils.scale(c[2], bot, top),
];
colorutils.unscaleColor = (c, bot, top) => [
colorutils.unscale(c[0], bot, top),
colorutils.unscale(c[1], bot, top),
colorutils.unscale(c[2], bot, top),
];
// rule of thumb for RGB brightness; 1.0 is white
colorutils.luminosity = (c) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11;
colorutils.saturate = (c) => {
const min = colorutils.colorMin(c);
const max = colorutils.colorMax(c);
if (max - min <= 0) return [1.0, 1.0, 1.0];
if (max - min <= 0)
return [1.0, 1.0, 1.0];
return colorutils.unscaleColor(c, min, max);
};
colorutils.blend = (c1, c2, t) => [
colorutils.scale(t, c1[0], c2[0]),
colorutils.scale(t, c1[1], c2[1]),
colorutils.scale(t, c1[2], c2[2]),
];
colorutils.invert = (c) => [1 - c[0], 1 - c[1], 1 - c[2]];
colorutils.complementary = (c) => {
const inv = colorutils.invert(c);
return [
@ -110,12 +94,9 @@ colorutils.complementary = (c) => {
(inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11),
];
};
colorutils.textColorFromBackgroundColor = (bgcolor, skinName) => {
const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff';
const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222';
return colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5 ? white : black;
};
exports.colorutils = colorutils;
export { colorutils };

View file

@ -1,10 +1,13 @@
import AttributeMap from "./AttributeMap.js";
import UNorm from "unorm";
import * as Changeset from "./Changeset.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins");
@ -23,14 +26,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const _MAX_LIST_LEVEL = 16;
const AttributeMap = require('./AttributeMap');
const UNorm = require('unorm');
const Changeset = require('./Changeset');
const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s);
const tagName = (n) => n.tagName && n.tagName.toLowerCase();
// supportedElems are Supported natively within Etherpad and don't require a plugin
@ -55,7 +51,6 @@ const supportedElems = new Set([
'u',
'ul',
]);
const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => {
const _blockElems = {
div: 1,
@ -63,22 +58,16 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
pre: 1,
li: 1,
};
hooks.callAll('ccRegisterBlockElements').forEach((element) => {
_blockElems[element] = 1;
supportedElems.add(element);
});
const isBlockElement = (n) => !!_blockElems[tagName(n) || ''];
const textify = (str) => sanitizeUnicode(
str.replace(/(\n | \n)/g, ' ')
const textify = (str) => sanitizeUnicode(str.replace(/(\n | \n)/g, ' ')
.replace(/[\n\r ]/g, ' ')
.replace(/\xa0/g, ' ')
.replace(/\t/g, ' '));
const getAssoc = (node, name) => node[`_magicdom_${name}`];
const lines = (() => {
const textArray = [];
const attribsArray = [];
@ -113,7 +102,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
return self;
})();
const cc = {};
const _ensureColumnZero = (state) => {
if (!lines.atColumnZero()) {
cc.startNewLine(state);
@ -124,7 +112,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
let selEnd = [-1, -1];
const _isEmpty = (node, state) => {
// consider clean blank lines pasted in IE to be empty
if (node.childNodes.length === 0) return true;
if (node.childNodes.length === 0)
return true;
if (node.childNodes.length === 1 &&
getAssoc(node, 'shouldBeEmpty') &&
node.innerHTML === '&nbsp;' &&
@ -138,7 +127,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
}
return false;
};
const _pointHere = (charsAfter, state) => {
const ln = lines.length() - 1;
let chr = lines.textOfLine(ln).length;
@ -148,11 +136,10 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
chr += charsAfter;
return [ln, chr];
};
const _reachBlockPoint = (nd, idx, state) => {
if (nd.nodeType !== nd.TEXT_NODE) _reachPoint(nd, idx, state);
if (nd.nodeType !== nd.TEXT_NODE)
_reachPoint(nd, idx, state);
};
const _reachPoint = (nd, idx, state) => {
if (startPoint && nd === startPoint.node && startPoint.index === idx) {
selStart = _pointHere(0, state);
@ -170,7 +157,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cc.incrementAttrib = (state, attribName) => {
if (!state.attribs[attribName]) {
state.attribs[attribName] = 1;
} else {
}
else {
state.attribs[attribName]++;
}
_recalcAttribString(state);
@ -179,9 +167,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
state.attribs[attribName]--;
_recalcAttribString(state);
};
const _enterList = (state, listType) => {
if (!listType) return;
if (!listType)
return;
const oldListType = state.lineAttributes.list;
if (listType !== 'none') {
state.listNesting = (state.listNesting || 0) + 1;
@ -190,29 +178,28 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
state.start = (state.start || 0) + 1;
}
}
if (listType === 'none') {
delete state.lineAttributes.list;
} else {
}
else {
state.lineAttributes.list = listType;
}
_recalcAttribString(state);
return oldListType;
};
const _exitList = (state, oldListType) => {
if (state.lineAttributes.list) {
state.listNesting--;
}
if (oldListType && oldListType !== 'none') {
state.lineAttributes.list = oldListType;
} else {
}
else {
delete state.lineAttributes.list;
delete state.lineAttributes.start;
}
_recalcAttribString(state);
};
const _enterAuthor = (state, author) => {
const oldAuthor = state.author;
state.authorLevel = (state.authorLevel || 0) + 1;
@ -220,17 +207,16 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
_recalcAttribString(state);
return oldAuthor;
};
const _exitAuthor = (state, oldAuthor) => {
state.authorLevel--;
state.author = oldAuthor;
_recalcAttribString(state);
};
const _recalcAttribString = (state) => {
const attribs = new AttributeMap(apool);
for (const [a, count] of Object.entries(state.attribs)) {
if (!count) continue;
if (!count)
continue;
// The following splitting of the attribute name is a workaround
// to enable the content collector to store key-value attributes
// see https://github.com/ether/etherpad-lite/issues/2567 for more information
@ -239,15 +225,16 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// TODO: This approach doesn't support changing existing values: if both 'foo::bar' and
// 'foo::baz' are in state.attribs then the last one encountered while iterating will win.
const ATTRIBUTE_SPLIT_STRING = '::';
// see if attributeString is splittable
const attributeSplits = a.split(ATTRIBUTE_SPLIT_STRING);
if (attributeSplits.length > 1) {
// the attribute name follows the convention key::value
// so save it as a key value attribute
const [k, v] = attributeSplits;
if (v) attribs.set(k, v);
} else {
if (v)
attribs.set(k, v);
}
else {
// the "normal" case, the attribute is just a switch
// so set it true
attribs.set(a, 'true');
@ -257,12 +244,12 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
if (apool.putAttrib(['author', state.author], true) >= 0) {
// require that author already be in pool
// (don't add authors from other documents, etc.)
if (state.author) attribs.set('author', state.author);
if (state.author)
attribs.set('author', state.author);
}
}
state.attribString = attribs.toString();
};
const _produceLineAttributesMarker = (state) => {
// TODO: This has to go to AttributeManager.
const attribs = new AttributeMap(apool)
@ -299,11 +286,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
let unsupportedElements = null;
if (!state) {
state = {
flags: { /* name -> nesting counter*/
},
flags: { /* name -> nesting counter*/},
localAttribs: null,
attribs: { /* name -> nesting counter*/
},
attribs: { /* name -> nesting counter*/},
attribString: '',
// lineAttributes maintain a map from attributes to attribute values set on a line
lineAttributes: {
@ -320,23 +305,22 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
state.localAttribs = null;
const isBlock = isBlockElement(node);
if (!isBlock && node.name && (node.name !== 'body')) {
if (!supportedElems.has(node.name)) state.unsupportedElements.add(node.name);
if (!supportedElems.has(node.name))
state.unsupportedElements.add(node.name);
}
const isEmpty = _isEmpty(node, state);
if (isBlock) _ensureColumnZero(state);
if (isBlock)
_ensureColumnZero(state);
const startLine = lines.length() - 1;
_reachBlockPoint(node, 0, state);
if (node.nodeType === node.TEXT_NODE) {
const tname = node.parentNode.getAttribute('name');
const context = {cc: this, state, tname, node, text: node.nodeValue};
const context = { cc: this, state, tname, node, text: node.nodeValue };
// Hook functions may either return a string (deprecated) or modify context.text. If any hook
// function modifies context.text then all returned strings are ignored. If no hook functions
// modify context.text, the first hook function to return a string wins.
const [hookTxt] =
hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string');
const [hookTxt] = hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string');
let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text;
let rest = '';
let x = 0; // offset into original text
if (txt.length === 0) {
@ -354,7 +338,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
consumed = firstLine.length + 1;
rest = txt.substring(consumed);
txt = firstLine;
} else { /* will only run this loop body once */
}
else { /* will only run this loop body once */
}
if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) {
selStart = _pointHere(startPoint.index - x, state);
@ -385,9 +370,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cc.startNewLine(state);
}
}
} else if (node.nodeType === node.ELEMENT_NODE) {
}
else if (node.nodeType === node.ELEMENT_NODE) {
const tname = tagName(node) || '';
if (tname === 'img') {
hooks.callAll('collectContentImage', {
cc,
@ -397,11 +382,11 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cls: null,
node,
});
} else {
}
else {
// THIS SEEMS VERY HACKY! -- Please submit a better fix!
delete state.lineAttributes.img;
}
if (tname === 'br') {
this.breakLine = true;
const tvalue = node.getAttribute('value');
@ -416,24 +401,25 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
if (startNewLine) {
cc.startNewLine(state);
}
} else if (tname === 'script' || tname === 'style') {
}
else if (tname === 'script' || tname === 'style') {
// ignore
} else if (!isEmpty) {
}
else if (!isEmpty) {
let styl = node.getAttribute('style');
let cls = node.getAttribute('class');
let isPre = (tname === 'pre');
if ((!isPre) && abrowser && abrowser.safari) {
isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl));
}
if (isPre) cc.incrementFlag(state, 'preMode');
if (isPre)
cc.incrementFlag(state, 'preMode');
let oldListTypeOrNull = null;
let oldAuthorOrNull = null;
// LibreOffice Writer puts in weird items during import or copy/paste, we should drop them.
if (cls === 'Numbering_20_Symbols' || cls === 'Bullet_20_Symbols') {
styl = null;
cls = null;
// We have to return here but this could break things in the future,
// for now it shows how to fix the problem
return;
@ -473,32 +459,40 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// check if we find a better hint within the node's children
if (!rr && !type) {
for (const child of node.childNodes) {
if (tagName(child) !== 'ul') continue;
if (tagName(child) !== 'ul')
continue;
type = child.getAttribute('class');
if (type) break;
if (type)
break;
}
}
if (rr && rr[1]) {
type = rr[1];
} else {
}
else {
if (tname === 'ul') {
const cls = node.getAttribute('class');
if ((type && type.match('indent')) || (cls && cls.match('indent'))) {
type = 'indent';
} else {
}
else {
type = 'bullet';
}
} else {
}
else {
type = 'number';
}
type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1));
}
oldListTypeOrNull = (_enterList(state, type) || 'none');
} else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) {
}
else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) {
// This has undesirable behavior in Chrome but is right in other browsers.
// See https://github.com/ether/etherpad-lite/issues/2412 for reasoning
if (!abrowser.chrome) oldListTypeOrNull = (_enterList(state, undefined) || 'none');
} else if (tname === 'li') {
if (!abrowser.chrome)
oldListTypeOrNull = (_enterList(state, undefined) || 'none');
}
else if (tname === 'li') {
state.lineAttributes.start = state.start || 0;
_recalcAttribString(state);
if (state.lineAttributes.list.indexOf('number') !== -1) {
@ -533,7 +527,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// But I don't know a way to do that because we're only aware of the previous Line
// As the concept of parent's doesn't exist when processing each domline...
}
} else {
}
else {
// Below needs more testin if it's neccesary as _exitList should take care of this.
// delete state.start;
// delete state.listNesting;
@ -553,11 +548,9 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
}
}
}
for (const c of node.childNodes) {
cc.collectContent(c, state);
}
if (collectStyles) {
hooks.callAll('collectContentPost', {
cc,
@ -567,8 +560,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
cls,
});
}
if (isPre) cc.decrementFlag(state, 'preMode');
if (isPre)
cc.decrementFlag(state, 'preMode');
if (state.localAttribs) {
for (let i = 0; i < state.localAttribs.length; i++) {
cc.decrementAttrib(state, state.localAttribs[i]);
@ -587,14 +580,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
if (lines.length() - 1 === startLine) {
// added additional check to resolve https://github.com/JohnMcLear/ep_copy_paste_images/issues/20
// this does mean that images etc can't be pasted on lists but imho that's fine
// If we're doing an export event we need to start a new lines
// Export events don't have window available.
// commented out to solve #2412 - https://github.com/ether/etherpad-lite/issues/2412
if ((state.lineAttributes && !state.lineAttributes.list) || typeof window === 'undefined') {
cc.startNewLine(state);
}
} else {
}
else {
_ensureColumnZero(state);
}
}
@ -618,23 +611,18 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// each returns [line, char] or [-1,-1]
const getSelectionStart = () => selStart;
const getSelectionEnd = () => selEnd;
// returns array of strings for lines found, last entry will be "" if
// last line is complete (i.e. if a following span should be on a new line).
// can be called at any point
cc.getLines = () => lines.textLines();
cc.finish = () => {
lines.flush();
const lineAttribs = lines.attribLines();
const lineStrings = cc.getLines();
lineStrings.length--;
lineAttribs.length--;
const ss = getSelectionStart();
const se = getSelectionEnd();
const fixLongLines = () => {
// design mode does not deal with with really long lines!
const lineLimit = 2000; // chars
@ -660,14 +648,15 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
newStrings.push(oldString);
newAttribStrings.push(oldAttribString);
}
const fixLineNumber = (lineChar) => {
if (lineChar[0] < 0) return;
if (lineChar[0] < 0)
return;
let n = lineChar[0];
let c = lineChar[1];
if (n > i) {
n += (newStrings.length - 1);
} else if (n === i) {
}
else if (n === i) {
let a = 0;
while (c > newStrings[a].length) {
c -= newStrings[a].length;
@ -692,7 +681,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
};
};
const wrapData = fixLongLines();
return {
selStart: ss,
selEnd: se,
@ -702,10 +690,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
lineAttribs,
};
};
return cc;
};
exports.sanitizeUnicode = sanitizeUnicode;
exports.makeContentCollector = makeContentCollector;
exports.supportedElems = supportedElems;
export { sanitizeUnicode };
export { makeContentCollector };
export { supportedElems };

View file

@ -1,41 +1,19 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.makeCSSManager = (browserSheet) => {
export const makeCSSManager = (browserSheet) => {
const browserRules = () => (browserSheet.cssRules || browserSheet.rules);
const browserDeleteRule = (i) => {
if (browserSheet.deleteRule) browserSheet.deleteRule(i);
else browserSheet.removeRule(i);
if (browserSheet.deleteRule)
browserSheet.deleteRule(i);
else
browserSheet.removeRule(i);
};
const browserInsertRule = (i, selector) => {
if (browserSheet.insertRule) browserSheet.insertRule(`${selector} {}`, i);
else browserSheet.addRule(selector, null, i);
if (browserSheet.insertRule)
browserSheet.insertRule(`${selector} {}`, i);
else
browserSheet.addRule(selector, null, i);
};
const selectorList = [];
const indexOfSelector = (selector) => {
for (let i = 0; i < selectorList.length; i++) {
if (selectorList[i] === selector) {
@ -44,7 +22,6 @@ exports.makeCSSManager = (browserSheet) => {
}
return -1;
};
const selectorStyle = (selector) => {
let i = indexOfSelector(selector);
if (i < 0) {
@ -55,7 +32,6 @@ exports.makeCSSManager = (browserSheet) => {
}
return browserRules().item(i).style;
};
const removeSelectorStyle = (selector) => {
const i = indexOfSelector(selector);
if (i >= 0) {
@ -63,7 +39,6 @@ exports.makeCSSManager = (browserSheet) => {
selectorList.splice(i, 1);
}
};
return {
selectorStyle,
removeSelectorStyle,

View file

@ -1,36 +1,11 @@
import * as Security from "./security.js";
import * as hooks from "./pluginfw/hooks.js";
import * as _ from "./underscore.js";
import { lineAttributeMarker as lineAttributeMarker$0 } from "./linestylefilter.js";
'use strict';
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
// %APPJET%: import("etherpad.admin.plugins");
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// requires: top
// requires: plugins
// requires: undefined
const Security = require('./security');
const hooks = require('./pluginfw/hooks');
const _ = require('./underscore');
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
const noop = () => {};
const lineAttributeMarker = { lineAttributeMarker: lineAttributeMarker$0 }.lineAttributeMarker;
const noop = () => { };
const domline = {};
domline.addToLineClass = (lineClass, cls) => {
// an "empty span" at any point can be used to add classes to
// the line, using line:className. otherwise, we ignore
@ -43,7 +18,6 @@ domline.addToLineClass = (lineClass, cls) => {
});
return lineClass;
};
// if "document" is falsy we don't create a DOM node, just
// an object with innerHTML and className
domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
@ -56,37 +30,32 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
finishUpdate: noop,
lineMarker: 0,
};
const document = optDocument;
if (document) {
result.node = document.createElement('div');
// JAWS and NVDA screen reader compatibility. Only needed if in a real browser.
result.node.setAttribute('aria-live', 'assertive');
} else {
}
else {
result.node = {
innerHTML: '',
className: '',
};
}
let html = [];
let preHtml = '';
let postHtml = '';
let curHTML = null;
const processSpaces = (s) => domline.processSpaces(s, doesWrap);
const perTextNodeProcess = (doesWrap ? _.identity : processSpaces);
const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity);
let lineClass = 'ace-line';
result.appendSpan = (txt, cls) => {
let processedMarker = false;
// Handle lineAttributeMarker, if present
if (cls.indexOf(lineAttributeMarker) >= 0) {
let listType = /(?:^| )list:(\S+)/.exec(cls);
const start = /(?:^| )start:(\S+)/.exec(cls);
_.map(hooks.callAll('aceDomLinePreProcessLineAttributes', {
domline,
cls,
@ -101,7 +70,8 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
if (listType.indexOf('number') < 0) {
preHtml += `<ul class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
postHtml = `</li></ul>${postHtml}`;
} else {
}
else {
if (start) { // is it a start of a list with more than one item in?
if (Number.parseInt(start[1]) === 1) { // if its the first one at this level?
// Add start class to DIV node
@ -109,7 +79,8 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
}
preHtml +=
`<ol start=${start[1]} class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
} else {
}
else {
// Handles pasted contents into existing lists
preHtml += `<ol class="list-${Security.escapeHTMLAttribute(listType)}"><li>`;
}
@ -141,15 +112,14 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
}
if (cls.indexOf('tag') >= 0) {
cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => {
if (!simpleTags) simpleTags = [];
if (!simpleTags)
simpleTags = [];
simpleTags.push(tag.toLowerCase());
return space + tag;
});
}
let extraOpenTags = '';
let extraCloseTags = '';
_.map(hooks.callAll('aceCreateDomLine', {
domline,
cls,
@ -158,10 +128,10 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
extraOpenTags += modifier.extraOpenTags;
extraCloseTags = modifier.extraCloseTags + extraCloseTags;
});
if ((!txt) && cls) {
lineClass = domline.addToLineClass(lineClass, cls);
} else if (txt) {
}
else if (txt) {
if (href) {
const urn_schemes = new RegExp('^(about|geo|mailto|tel):');
// if the url doesn't include a protocol prefix, assume http
@ -186,13 +156,7 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
simpleTags.reverse();
extraCloseTags = `</${simpleTags.join('></')}>${extraCloseTags}`;
}
html.push(
'<span class="', Security.escapeHTMLAttribute(cls || ''),
'">',
extraOpenTags,
perTextNodeProcess(Security.escapeHTML(txt)),
extraCloseTags,
'</span>');
html.push('<span class="', Security.escapeHTMLAttribute(cls || ''), '">', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, '</span>');
}
};
result.clearSpans = () => {
@ -200,13 +164,13 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
lineClass = 'ace-line';
result.lineMarker = 0;
};
const writeHTML = () => {
let newHTML = perHtmlLineProcess(html.join(''));
if (!newHTML) {
if ((!document) || (!optBrowser)) {
newHTML += '&nbsp;';
} else {
}
else {
newHTML += '<br/>';
}
}
@ -218,8 +182,8 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
curHTML = newHTML;
result.node.innerHTML = curHTML;
}
if (lineClass != null) result.node.className = lineClass;
if (lineClass != null)
result.node.className = lineClass;
hooks.callAll('acePostWriteDomLineHTML', {
node: result.node,
});
@ -228,7 +192,6 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => {
result.finishUpdate = writeHTML;
return result;
};
domline.processSpaces = (s, doesWrap) => {
if (s.indexOf('<') < 0 && !doesWrap) {
// short-cut
@ -246,10 +209,12 @@ domline.processSpaces = (s, doesWrap) => {
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
if (p === ' ') {
if (endOfLine || beforeSpace) parts[i] = '&nbsp;';
if (endOfLine || beforeSpace)
parts[i] = '&nbsp;';
endOfLine = false;
beforeSpace = true;
} else if (p.charAt(0) !== '<') {
}
else if (p.charAt(0) !== '<') {
endOfLine = false;
beforeSpace = false;
}
@ -260,11 +225,13 @@ domline.processSpaces = (s, doesWrap) => {
if (p === ' ') {
parts[i] = '&nbsp;';
break;
} else if (p.charAt(0) !== '<') {
}
else if (p.charAt(0) !== '<') {
break;
}
}
} else {
}
else {
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (p === ' ') {
@ -274,5 +241,4 @@ domline.processSpaces = (s, doesWrap) => {
}
return parts.join('');
};
exports.domline = domline;
export { domline };

View file

@ -1,5 +1,4 @@
'use strict';
/* eslint-disable-next-line max-len */
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
/**
@ -18,7 +17,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const randomPadName = () => {
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
// using the PRNG below
@ -39,24 +37,21 @@ const randomPadName = () => {
}
return randomstring;
};
$(() => {
$('#go2Name').submit(() => {
const padname = $('#padname').val();
if (padname.length > 0) {
window.location = `p/${encodeURIComponent(padname.trim())}`;
} else {
}
else {
alert('Please enter a name');
}
return false;
});
$('#button').click(() => {
window.location = `p/${randomPadName()}`;
});
// start the custom js
if (typeof window.customStart === 'function') window.customStart();
if (typeof window.customStart === 'function')
window.customStart();
});
// @license-end

View file

@ -1,14 +1,12 @@
'use strict';
((document) => {
// Set language for l10n
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) language = language[1];
if (language)
language = language[1];
html10n.bind('indexed', () => {
html10n.localize([language, navigator.language, navigator.userLanguage, 'en']);
});
html10n.bind('localized', () => {
document.documentElement.lang = html10n.getLanguage();
document.documentElement.dir = html10n.getDirection();

View file

@ -1,57 +1,23 @@
import * as Changeset from "./Changeset.js";
import * as attributes from "./attributes.js";
import * as hooks from "./pluginfw/hooks.js";
import AttributeManager from "./AttributeManager.js";
import { padutils as padutils$0 } from "./pad_utils.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.linestylefilter
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins");
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// requires: easysync2.Changeset
// requires: top
// requires: plugins
// requires: undefined
const Changeset = require('./Changeset');
const attributes = require('./attributes');
const hooks = require('./pluginfw/hooks');
const linestylefilter = {};
const AttributeManager = require('./AttributeManager');
const padutils = require('./pad_utils').padutils;
const padutils = { padutils: padutils$0 }.padutils;
linestylefilter.ATTRIB_CLASSES = {
bold: 'tag:b',
italic: 'tag:i',
underline: 'tag:u',
strikethrough: 'tag:s',
};
const lineAttributeMarker = 'lineAttribMarker';
exports.lineAttributeMarker = lineAttributeMarker;
linestylefilter.getAuthorClassName = (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-';
if (c === '.')
return '-';
return `z${c.charCodeAt(0)}z`;
})}`;
// lineLength is without newline; aline includes newline,
// but may be falsy if lineLength == 0
linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool) => {
@ -59,56 +25,55 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
for (const attribClasses of hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES)) {
Object.assign(linestylefilter.ATTRIB_CLASSES, attribClasses);
}
if (lineLength === 0) return textAndClassFunc;
if (lineLength === 0)
return textAndClassFunc;
const nextAfterAuthorColors = textAndClassFunc;
const authorColorFunc = (() => {
const lineEnd = lineLength;
let curIndex = 0;
let extraClasses;
let leftInAuthor;
const attribsToClasses = (attribs) => {
let classes = '';
let isLineAttribMarker = false;
for (const [key, value] of attributes.attribsFromString(attribs, apool)) {
if (!key || !value) continue;
if (!key || !value)
continue;
if (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) {
isLineAttribMarker = true;
}
if (key === 'author') {
classes += ` ${linestylefilter.getAuthorClassName(value)}`;
} else if (key === 'list') {
}
else if (key === 'list') {
classes += ` list:${value}`;
} else if (key === 'start') {
}
else if (key === 'start') {
// Needed to introduce the correct Ordered list item start number on import
classes += ` start:${value}`;
} else if (linestylefilter.ATTRIB_CLASSES[key]) {
}
else if (linestylefilter.ATTRIB_CLASSES[key]) {
classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`;
} else {
const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value});
}
else {
const results = hooks.callAll('aceAttribsToClasses', { linestylefilter, key, value });
classes += ` ${results.join(' ')}`;
}
}
if (isLineAttribMarker) classes += ` ${lineAttributeMarker}`;
if (isLineAttribMarker)
classes += ` ${lineAttributeMarker}`;
return classes.substring(1);
};
const attrOps = Changeset.deserializeOps(aline);
let attrOpsNext = attrOps.next();
let nextOp, nextOpClasses;
const goNextOp = () => {
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
if (!attrOpsNext.done)
attrOpsNext = attrOps.next();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
};
goNextOp();
const nextClasses = () => {
if (curIndex < lineEnd) {
extraClasses = nextOpClasses;
@ -121,7 +86,6 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
}
};
nextClasses();
return (txt, cls) => {
const disableAuthColorForThisLine = hooks.callAll('disableAuthorColorsForThisLine', {
linestylefilter,
@ -152,7 +116,6 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
})();
return authorColorFunc;
};
linestylefilter.getAtSignSplitterFilter = (lineText, textAndClassFunc) => {
const at = /@/g;
at.lastIndex = 0;
@ -164,12 +127,10 @@ linestylefilter.getAtSignSplitterFilter = (lineText, textAndClassFunc) => {
}
splitPoints.push(execResult.index);
}
if (!splitPoints) return textAndClassFunc;
if (!splitPoints)
return textAndClassFunc;
return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints);
};
linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc) => {
regExp.lastIndex = 0;
let regExpMatchs = null;
@ -185,9 +146,8 @@ linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc)
regExpMatchs.push([startIndex, regExpMatch]);
splitPoints.push(startIndex, startIndex + regExpMatch.length);
}
if (!regExpMatchs) return textAndClassFunc;
if (!regExpMatchs)
return textAndClassFunc;
const regExpMatchForIndex = (idx) => {
for (let k = 0; k < regExpMatchs.length; k++) {
const u = regExpMatchs[k];
@ -197,7 +157,6 @@ linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc)
}
return false;
};
const handleRegExpMatchsAfterSplit = (() => {
let curIndex = 0;
return (txt, cls) => {
@ -211,29 +170,24 @@ linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc)
curIndex += txtlen;
};
})();
return linestylefilter.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints);
};
linestylefilter.getURLFilter = linestylefilter.getRegexpFilter(padutils.urlRegex, 'url');
linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => {
let nextPointIndex = 0;
let idx = 0;
// don't split at 0
while (splitPointsOpt &&
nextPointIndex < splitPointsOpt.length &&
splitPointsOpt[nextPointIndex] === 0) {
nextPointIndex++;
}
const spanHandler = (txt, cls) => {
if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) {
func(txt, cls);
idx += txt.length;
} else {
}
else {
const splitPoints = splitPointsOpt;
const pointLocInSpan = splitPoints[nextPointIndex] - idx;
const txtlen = txt.length;
@ -243,7 +197,8 @@ linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => {
if (pointLocInSpan === txtlen) {
nextPointIndex++;
}
} else {
}
else {
if (pointLocInSpan > 0) {
func(txt.substring(0, pointLocInSpan), cls);
idx += pointLocInSpan;
@ -256,10 +211,8 @@ linestylefilter.textAndClassFuncSplitter = (func, splitPointsOpt) => {
};
return spanHandler;
};
linestylefilter.getFilterStack = (lineText, textAndClassFunc, abrowser) => {
let func = linestylefilter.getURLFilter(lineText, textAndClassFunc);
const hookFilters = hooks.callAll('aceGetFilterStack', {
linestylefilter,
browser: abrowser,
@ -267,10 +220,8 @@ linestylefilter.getFilterStack = (lineText, textAndClassFunc, abrowser) => {
hookFilters.map((hookFilter) => {
func = hookFilter(lineText, func);
});
return func;
};
// domLineObj is like that returned by domline.createDomLine
linestylefilter.populateDomLine = (textLine, aline, apool, domLineObj) => {
// remove final newline from text if any
@ -278,14 +229,12 @@ linestylefilter.populateDomLine = (textLine, aline, apool, domLineObj) => {
if (text.slice(-1) === '\n') {
text = text.substring(0, text.length - 1);
}
const textAndClassFunc = (tokenText, tokenClass) => {
domLineObj.appendSpan(tokenText, tokenClass);
};
let func = linestylefilter.getFilterStack(text, textAndClassFunc);
func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool);
func(text, '');
};
exports.linestylefilter = linestylefilter;
export { lineAttributeMarker };
export { linestylefilter };

View file

@ -1,11 +1,26 @@
import "./vendors/jquery.js";
import "./vendors/farbtastic.js";
import "./vendors/gritter.js";
import * as padUtils from "./pad_utils.js";
import { chat as chat$0 } from "./chat.js";
import { getCollabClient as getCollabClient$0 } from "./collab_client.js";
import { padconnectionstatus as padconnectionstatus$0 } from "./pad_connectionstatus.js";
import { padcookie as padcookie$0 } from "./pad_cookie.js";
import { padeditbar as padeditbar$0 } from "./pad_editbar.js";
import { padeditor as padeditor$0 } from "./pad_editor.js";
import { padimpexp as padimpexp$0 } from "./pad_impexp.js";
import { padmodals as padmodals$0 } from "./pad_modals.js";
import * as padsavedrevs from "./pad_savedrevs.js";
import { paduserlist as paduserlist$0 } from "./pad_userlist.js";
import { colorutils as colorutils$0 } from "./colorutils.js";
import * as socketio from "./socketio.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
@ -21,33 +36,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
let socket;
// These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins.
require('./vendors/jquery');
require('./vendors/farbtastic');
require('./vendors/gritter');
const Cookies = require('./pad_utils').Cookies;
const chat = require('./chat').chat;
const getCollabClient = require('./collab_client').getCollabClient;
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
const padcookie = require('./pad_cookie').padcookie;
const padeditbar = require('./pad_editbar').padeditbar;
const padeditor = require('./pad_editor').padeditor;
const padimpexp = require('./pad_impexp').padimpexp;
const padmodals = require('./pad_modals').padmodals;
const padsavedrevs = require('./pad_savedrevs');
const paduserlist = require('./pad_userlist').paduserlist;
const padutils = require('./pad_utils').padutils;
const colorutils = require('./colorutils').colorutils;
const randomString = require('./pad_utils').randomString;
const socketio = require('./socketio');
const hooks = require('./pluginfw/hooks');
const Cookies = { Cookies: padUtils }.Cookies;
const chat = { chat: chat$0 }.chat;
const getCollabClient = { getCollabClient: getCollabClient$0 }.getCollabClient;
const padconnectionstatus = { padconnectionstatus: padconnectionstatus$0 }.padconnectionstatus;
const padcookie = { padcookie: padcookie$0 }.padcookie;
const padeditbar = { padeditbar: padeditbar$0 }.padeditbar;
const padeditor = { padeditor: padeditor$0 }.padeditor;
const padimpexp = { padimpexp: padimpexp$0 }.padimpexp;
const padmodals = { padmodals: padmodals$0 }.padmodals;
const paduserlist = { paduserlist: paduserlist$0 }.paduserlist;
const padutils = { padutils: padUtils }.padutils;
const colorutils = { colorutils: colorutils$0 }.colorutils;
const randomString = { randomString: padUtils }.randomString;
// This array represents all GET-parameters which can be used to change a setting.
// name: the parameter-name, eg `?noColors=true` => `noColors`
// checkVal: the callback is only executed when
@ -122,7 +124,8 @@ const getParameters = [
name: 'alwaysShowChat',
checkVal: 'true',
callback: (val) => {
if (!settings.hideChat) chat.stickToScreen();
if (!settings.hideChat)
chat.stickToScreen();
},
},
{
@ -141,18 +144,17 @@ const getParameters = [
},
},
];
const getParams = () => {
// Tries server enforced options first..
for (const setting of getParameters) {
let value = clientVars.padOptions[setting.name];
if (value == null) continue;
if (value == null)
continue;
value = value.toString();
if (value === setting.checkVal || setting.checkVal == null) {
setting.callback(value);
}
}
// Then URL applied stuff
const params = getUrlVars();
for (const setting of getParameters) {
@ -162,26 +164,21 @@ const getParams = () => {
}
}
};
const getUrlVars = () => new URL(window.location.href).searchParams;
const sendClientReady = (isReconnect) => {
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId);
if (!isReconnect) {
const titleArray = document.title.split('|');
const title = titleArray[titleArray.length - 1];
document.title = `${padId.replace(/_+/g, ' ')} | ${title}`;
}
let token = Cookies.get('token');
if (token == null || !padutils.isValidAuthorToken(token)) {
token = padutils.generateAuthorToken();
Cookies.set('token', token, {expires: 60});
Cookies.set('token', token, { expires: 60 });
}
// If known, propagate the display name and color to the server in the CLIENT_READY message. This
// allows the server to include the values in its reply CLIENT_VARS message (which avoids
// initialization race conditions) and in the USER_NEWINFO messages sent to the other users on the
@ -191,7 +188,6 @@ const sendClientReady = (isReconnect) => {
colorId: params.get('userColor'),
name: params.get('userName'),
};
const msg = {
component: 'pad',
type: 'CLIENT_READY',
@ -200,36 +196,30 @@ const sendClientReady = (isReconnect) => {
token,
userInfo,
};
// this is a reconnect, lets tell the server our revisionnumber
if (isReconnect) {
msg.client_rev = pad.collabClient.getCurrentRevisionNumber();
msg.reconnect = true;
}
socket.json.send(msg);
};
const handshake = async () => {
let receivedClientVars = false;
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId);
// padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear
// to the proxy/gateway/whatever that this is a pad connection and should be treated as such
socket = pad.socket = socketio.connect(exports.baseURL, '/', {
query: {padId},
query: { padId },
reconnectionAttempts: 5,
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
});
socket.once('connect', () => {
sendClientReady(false);
});
socket.on('reconnect', () => {
// pad.collabClient might be null if the hanshake failed (or it never got that far).
if (pad.collabClient != null) {
@ -237,7 +227,6 @@ const handshake = async () => {
}
sendClientReady(receivedClientVars);
});
const socketReconnecting = () => {
// pad.collabClient might be null if the hanshake failed (or it never got that far).
if (pad.collabClient != null) {
@ -246,26 +235,24 @@ const handshake = async () => {
pad.collabClient.setChannelState('RECONNECTING');
}
};
socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
if (reason !== 'io server disconnect') return;
if (reason !== 'io server disconnect')
return;
socketReconnecting();
socket.connect();
});
socket.on('reconnecting', socketReconnecting);
socket.on('reconnect_failed', (error) => {
// pad.collabClient might be null if the hanshake failed (or it never got that far).
if (pad.collabClient != null) {
pad.collabClient.setChannelState('DISCONNECTED', 'reconnect_timeout');
} else {
}
else {
throw new Error('Reconnect timed out');
}
});
socket.on('error', (error) => {
// pad.collabClient might be null if the error occurred before the hanshake completed.
if (pad.collabClient != null) {
@ -276,47 +263,45 @@ const handshake = async () => {
// addressed by reconnection logic, so throwing an exception each time there's a socket.io error
// just annoys users and fills logs.
});
socket.on('message', (obj) => {
// the access was not granted, give the user a message
if (obj.accessStatus) {
if (obj.accessStatus === 'deny') {
$('#loading').hide();
$('#permissionDenied').show();
if (receivedClientVars) {
// got kicked
$('#editorcontainer').hide();
$('#editorloadingbox').show();
}
}
} else if (!receivedClientVars && obj.type === 'CLIENT_VARS') {
}
else if (!receivedClientVars && obj.type === 'CLIENT_VARS') {
receivedClientVars = true;
window.clientVars = obj.data;
if (window.clientVars.sessionRefreshInterval) {
const ping =
() => $.ajax('../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
const ping = () => $.ajax('../_extendExpressSessionLifetime', { method: 'PUT' }).catch(() => { });
setInterval(ping, window.clientVars.sessionRefreshInterval);
}
} else if (obj.disconnect) {
}
else if (obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect();
// block user from making any change to the pad
padeditor.disable();
padeditbar.disable();
padimpexp.disable();
return;
} else {
}
else {
pad._messageQ.enqueue(obj);
}
});
await Promise.all([
new Promise((resolve) => {
const h = (obj) => {
if (obj.accessStatus || obj.type !== 'CLIENT_VARS') return;
if (obj.accessStatus || obj.type !== 'CLIENT_VARS')
return;
socket.off('message', h);
resolve();
};
@ -326,32 +311,31 @@ const handshake = async () => {
// the hook must first be promoted to officially supported by deleting the leading underscore
// from the name, adding documentation to `doc/api/hooks_client-side.md`, and deleting this
// comment.
hooks.aCallAll('_socketCreated', {socket}),
hooks.aCallAll('_socketCreated', { socket }),
]);
};
/** Defers message handling until setCollabClient() is called with a non-null value. */
class MessageQueue {
constructor() {
this._q = [];
this._cc = null;
}
setCollabClient(cc) {
this._cc = cc;
this.enqueue(); // Flush.
}
enqueue(...msgs) {
if (this._cc == null) {
this._q.push(...msgs);
} else {
while (this._q.length > 0) this._cc.handleMessageFromServer(this._q.shift());
for (const msg of msgs) this._cc.handleMessageFromServer(msg);
}
else {
while (this._q.length > 0)
this._cc.handleMessageFromServer(this._q.shift());
for (const msg of msgs)
this._cc.handleMessageFromServer(msg);
}
}
}
const pad = {
// don't access these directly from outside this file, except
// for debugging
@ -362,7 +346,6 @@ const pad = {
clientTimeOffset: null,
padOptions: {},
_messageQ: new MessageQueue(),
// these don't require init; clientVars should all go through here
getPadId: () => clientVars.padId,
getClientIp: () => clientVars.clientIp,
@ -374,16 +357,15 @@ const pad = {
sendClientMessage: (msg) => {
pad.collabClient.sendClientMessage(msg);
},
init() {
padutils.setupGlobalExceptionHandler();
// $(handler), $().ready(handler), $.wait($.ready).then(handler), etc. don't work if handler is
// an async function for some bizarre reason, so the async function is wrapped in a non-async
// function.
$(() => (async () => {
if (window.customStart != null) window.customStart();
$('#colorpicker').farbtastic({callback: '#mycolorpickerpreview', width: 220});
if (window.customStart != null)
window.customStart();
$('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220 });
$('#readonlyinput').on('click', () => { padeditbar.setEmbedLinks(); });
padcookie.init();
await handshake();
@ -395,18 +377,15 @@ const pad = {
// initialize the chat
chat.init(this);
getParams();
padcookie.init(); // initialize the cookies
pad.initTime = +(new Date());
pad.padOptions = clientVars.initialOptions;
pad.myUserInfo = {
userId: clientVars.userId,
name: clientVars.userName,
ip: pad.getClientIp(),
colorId: clientVars.userColor,
};
const postAceInit = () => {
padeditbar.init();
setTimeout(() => {
@ -433,7 +412,6 @@ const pad = {
}
pad.changeViewOption('padFontFamily', padcookie.getPref('padFontFamily'));
$('#viewfontmenu').val(padcookie.getPref('padFontFamily')).niceSelect('update');
// Prevent sticky chat or chat and users to be checked for mobiles
const checkChatAndUsersVisibility = (x) => {
if (x.matches) { // If media query matches
@ -444,12 +422,9 @@ const pad = {
const mobileMatch = window.matchMedia('(max-width: 800px)');
mobileMatch.addListener(checkChatAndUsersVisibility); // check if window resized
setTimeout(() => { checkChatAndUsersVisibility(mobileMatch); }, 0); // check now after load
$('#editorcontainer').addClass('initialized');
hooks.aCallAll('postAceInit', {ace: padeditor.ace, clientVars, pad});
hooks.aCallAll('postAceInit', { ace: padeditor.ace, clientVars, pad });
};
// order of inits is important here:
padimpexp.init(this);
padsavedrevs.init(this);
@ -457,10 +432,7 @@ const pad = {
paduserlist.init(pad.myUserInfo, this);
padconnectionstatus.init();
padmodals.init(this);
pad.collabClient = getCollabClient(
padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo,
{colorPalette: pad.getColorPalette()}, pad);
pad.collabClient = getCollabClient(padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, { colorPalette: pad.getColorPalette() }, pad);
this._messageQ.setCollabClient(this.collabClient);
pad.collabClient.setOnUserJoin(pad.handleUserJoin);
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
@ -468,17 +440,16 @@ const pad = {
pad.collabClient.setOnClientMessage(pad.handleClientMessage);
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
pad.collabClient.setOnInternalAction(pad.handleCollabAction);
// load initial chat-messages
if (clientVars.chatHead !== -1) {
const chatHead = clientVars.chatHead;
const start = Math.max(chatHead - 100, 0);
pad.collabClient.sendMessage({type: 'GET_CHAT_MESSAGES', start, end: chatHead});
} else {
pad.collabClient.sendMessage({ type: 'GET_CHAT_MESSAGES', start, end: chatHead });
}
else {
// there are no messages
$('#chatloadmessagesbutton').css('display', 'none');
}
if (window.clientVars.readonly) {
chat.hide();
$('#myusernameedit').attr('disabled', true);
@ -486,29 +457,26 @@ const pad = {
$('#chaticon').hide();
$('#options-chatandusers').parent().hide();
$('#options-stickychat').parent().hide();
} else if (!settings.hideChat) { $('#chaticon').show(); }
}
else if (!settings.hideChat) {
$('#chaticon').show();
}
$('body').addClass(window.clientVars.readonly ? 'readonly' : 'readwrite');
padeditor.ace.callWithAce((ace) => {
ace.ace_setEditable(!window.clientVars.readonly);
});
// If the LineNumbersDisabled value is set to true then we need to hide the Line Numbers
if (settings.LineNumbersDisabled === true) {
this.changeViewOption('showLineNumbers', false);
}
// If the noColors value is set to true then we need to
// hide the background colors on the ace spans
if (settings.noColors === true) {
this.changeViewOption('noColors', true);
}
if (settings.rtlIsTrue === true) {
this.changeViewOption('rtlIsTrue', true);
}
// If the Monospacefont value is set to true then change it to monospace.
if (settings.useMonospaceFontGlobal === true) {
this.changeViewOption('padFontFamily', 'RobotoMono');
@ -528,7 +496,6 @@ const pad = {
paduserlist.setMyUserInfo(this.myUserInfo);
}
},
dispose: () => {
padeditor.dispose();
},
@ -544,8 +511,7 @@ const pad = {
const options = {};
options[key] = value;
pad.handleOptionsChange(options);
pad.collabClient.sendClientMessage(
{
pad.collabClient.sendClientMessage({
type: 'padoptions',
options,
changedBy: pad.myUserInfo.name || 'unnamed',
@ -575,8 +541,7 @@ const pad = {
// caller shouldn't mutate the object
getPadOptions: () => pad.padOptions,
suggestUserName: (userId, name) => {
pad.collabClient.sendClientMessage(
{
pad.collabClient.sendClientMessage({
type: 'suggestUserName',
unnamedId: userId,
newName: name,
@ -597,11 +562,14 @@ const pad = {
pad.notifyChangeName(msg.newName);
paduserlist.setMyUserInfo(pad.myUserInfo);
}
} else if (msg.type === 'newRevisionList') {
}
else if (msg.type === 'newRevisionList') {
padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'revisionLabel') {
}
else if (msg.type === 'revisionLabel') {
padsavedrevs.newRevisionList(msg.revisionList);
} else if (msg.type === 'padoptions') {
}
else if (msg.type === 'padoptions') {
const opts = msg.options;
pad.handleOptionsChange(opts);
}
@ -614,26 +582,25 @@ const pad = {
padeditbar.enable();
padimpexp.enable();
padconnectionstatus.connected();
} else if (newState === 'RECONNECTING') {
}
else if (newState === 'RECONNECTING') {
padeditor.disable();
padeditbar.disable();
padimpexp.disable();
padconnectionstatus.reconnecting();
} else if (newState === 'DISCONNECTED') {
}
else if (newState === 'DISCONNECTED') {
pad.diagnosticInfo.disconnectedMessage = message;
pad.diagnosticInfo.padId = pad.getPadId();
pad.diagnosticInfo.socket = {};
// we filter non objects from the socket object and put them in the diagnosticInfo
// this ensures we have no cyclic data - this allows us to stringify the data
for (const [i, value] of Object.entries(socket.socket || {})) {
const type = typeof value;
if (type === 'string' || type === 'number') {
pad.diagnosticInfo.socket[i] = value;
}
}
pad.asyncSendDiagnosticInfo();
if (typeof window.ajlog === 'string') {
window.ajlog += (`Disconnected: ${message}\n`);
@ -641,7 +608,6 @@ const pad = {
padeditor.disable();
padeditbar.disable();
padimpexp.disable();
padconnectionstatus.disconnected(message);
}
const newFullyConnected = !!padconnectionstatus.isFullyConnected();
@ -662,7 +628,8 @@ const pad = {
if (chatVisCookie) { // if the cookie is set for chat always visible
chat.stickToScreen(true); // stick it to the screen
$('#options-stickychat').prop('checked', true); // set the checkbox to on
} else {
}
else {
$('#options-stickychat').prop('checked', false); // set the checkbox for off
}
},
@ -671,7 +638,8 @@ const pad = {
if (chatAUVisCookie) { // if the cookie is set for chat always visible
chat.chatAndUsers(true); // stick it to the screen
$('#options-chatandusers').prop('checked', true); // set the checkbox to on
} else {
}
else {
$('#options-chatandusers').prop('checked', false); // set the checkbox for off
}
},
@ -680,28 +648,29 @@ const pad = {
if (authColCookie) {
pad.changeViewOption('showAuthorColors', true);
$('#options-colorscheck').prop('checked', true);
} else {
}
else {
$('#options-colorscheck').prop('checked', false);
}
},
handleCollabAction: (action) => {
if (action === 'commitPerformed') {
padeditbar.setSyncStatus('syncing');
} else if (action === 'newlyIdle') {
}
else if (action === 'newlyIdle') {
padeditbar.setSyncStatus('done');
}
},
asyncSendDiagnosticInfo: () => {
window.setTimeout(() => {
$.ajax(
{
$.ajax({
type: 'post',
url: 'ep/pad/connection-diagnostic-info',
data: {
diagnosticInfo: JSON.stringify(pad.diagnosticInfo),
},
success: () => {},
error: () => {},
success: () => { },
error: () => { },
});
}, 0);
},
@ -723,14 +692,13 @@ const pad = {
window.setTimeout(() => {
pad.addHistoricalAuthors(data);
}, 1000);
} else {
}
else {
pad.collabClient.addHistoricalAuthors(data);
}
},
};
const init = () => pad.init();
const settings = {
LineNumbersDisabled: false,
noColors: false,
@ -739,12 +707,10 @@ const settings = {
globalUserColor: false,
rtlIsTrue: false,
};
pad.settings = settings;
exports.baseURL = '';
exports.settings = settings;
exports.randomString = randomString;
exports.getParams = getParams;
exports.pad = pad;
exports.init = init;
export const baseURL = '';
export { settings };
export { randomString };
export { getParams };
export { pad };
export { init };

View file

@ -1,61 +1,34 @@
'use strict';
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
createCountDownElementsIfNecessary($modal);
const timer = createTimerForModal($modal, pad);
$modal.find('#cancelreconnect').one('click', () => {
timer.cancel();
disableAutomaticReconnection($modal);
});
enableAutomaticReconnection($modal);
}
};
const createCountDownElementsIfNecessary = ($modal) => {
const elementsDoNotExist = $modal.find('#cancelreconnect').length === 0;
if (elementsDoNotExist) {
const $defaultMessage = $modal.find('#defaulttext');
const $reconnectButton = $modal.find('#forcereconnect');
// create extra DOM elements, if they don't exist
const $reconnectTimerMessage =
$('<p>')
const $reconnectTimerMessage = $('<p>')
.addClass('reconnecttimer')
.append(
$('<span>')
.append($('<span>')
.attr('data-l10n-id', 'pad.modals.reconnecttimer')
.text('Trying to reconnect in'))
.append(' ')
.append(
$('<span>')
.append($('<span>')
.addClass('timetoexpire'));
const $cancelReconnect =
$('<button>')
const $cancelReconnect = $('<button>')
.attr('id', 'cancelreconnect')
.attr('data-l10n-id', 'pad.modals.cancel')
.text('Cancel');
localize($reconnectTimerMessage);
localize($cancelReconnect);
$reconnectTimerMessage.insertAfter($defaultMessage);
$cancelReconnect.insertAfter($reconnectButton);
}
};
const localize = ($element) => {
html10n.translateElement(html10n.translations, $element.get(0));
};
const createTimerForModal = ($modal, pad) => {
const timeUntilReconnection =
clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry();
const timeUntilReconnection = clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry();
const timer = new CountDownTimer(timeUntilReconnection);
timer.onTick((minutes, seconds) => {
updateCountDownTimerMessage($modal, minutes, seconds);
}).onExpire(() => {
@ -63,14 +36,13 @@ const createTimerForModal = ($modal, pad) => {
if (wasANetworkError) {
// cannot simply reconnect, client is having issues to establish connection to server
waitUntilClientCanConnectToServerAndThen(() => { forceReconnection($modal); }, pad);
} else {
}
else {
forceReconnection($modal);
}
}).start();
return timer;
};
const disableAutomaticReconnection = ($modal) => {
toggleAutomaticReconnectionOption($modal, true);
};
@ -81,12 +53,10 @@ const toggleAutomaticReconnectionOption = ($modal, disableAutomaticReconnect) =>
$modal.find('#cancelreconnect, .reconnecttimer').toggleClass('hidden', disableAutomaticReconnect);
$modal.find('#defaulttext').toggleClass('hidden', !disableAutomaticReconnect);
};
const waitUntilClientCanConnectToServerAndThen = (callback, pad) => {
whenConnectionIsRestablishedWithServer(callback, pad);
pad.socket.connect();
};
const whenConnectionIsRestablishedWithServer = (callback, pad) => {
// only add listener for the first try, don't need to add another listener
// on every unsuccessful try
@ -94,32 +64,25 @@ const whenConnectionIsRestablishedWithServer = (callback, pad) => {
pad.socket.once('connect', callback);
}
};
const forceReconnection = ($modal) => {
$modal.find('#forcereconnect').click();
};
const updateCountDownTimerMessage = ($modal, minutes, seconds) => {
minutes = minutes < 10 ? `0${minutes}` : minutes;
seconds = seconds < 10 ? `0${seconds}` : seconds;
$modal.find('.timetoexpire').text(`${minutes}:${seconds}`);
};
// store number of tries to reconnect to server, in order to increase time to wait
// until next try
const reconnectionTries = {
counter: 0,
nextTry() {
// double the time to try to reconnect on every time reconnection fails
const nextCounterFactor = 2 ** this.counter;
this.counter++;
return nextCounterFactor;
},
};
// Timer based on http://stackoverflow.com/a/20618517.
// duration: how many **seconds** until the timer ends
// granularity (optional): how many **milliseconds**
@ -128,11 +91,9 @@ const CountDownTimer = function (duration, granularity) {
this.duration = duration;
this.granularity = granularity || 1000;
this.running = false;
this.onTickCallbacks = [];
this.onExpireCallbacks = [];
};
CountDownTimer.prototype.start = function () {
if (this.running) {
return;
@ -143,11 +104,11 @@ CountDownTimer.prototype.start = function () {
let diff;
const timer = () => {
diff = that.duration - Math.floor((Date.now() - start) / 1000);
if (diff > 0) {
that.timeoutId = setTimeout(timer, that.granularity);
that.tick(diff);
} else {
}
else {
that.running = false;
that.tick(0);
that.expire();
@ -155,7 +116,6 @@ CountDownTimer.prototype.start = function () {
};
timer();
};
CountDownTimer.prototype.tick = function (diff) {
const obj = CountDownTimer.parse(diff);
this.onTickCallbacks.forEach(function (callback) {
@ -167,28 +127,35 @@ CountDownTimer.prototype.expire = function () {
callback.call(this);
}, this);
};
CountDownTimer.prototype.onTick = function (callback) {
if (typeof callback === 'function') {
this.onTickCallbacks.push(callback);
}
return this;
};
CountDownTimer.prototype.onExpire = function (callback) {
if (typeof callback === 'function') {
this.onExpireCallbacks.push(callback);
}
return this;
};
CountDownTimer.prototype.cancel = function () {
this.running = false;
clearTimeout(this.timeoutId);
return this;
};
CountDownTimer.parse = (seconds) => ({
minutes: (seconds / 60) | 0,
seconds: (seconds % 60) | 0,
});
export const showCountDownTimerToReconnectOnModal = ($modal, pad) => {
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {
createCountDownElementsIfNecessary($modal);
const timer = createTimerForModal($modal, pad);
$modal.find('#cancelreconnect').one('click', () => {
timer.cancel();
disableAutomaticReconnection($modal);
});
enableAutomaticReconnection($modal);
}
};

View file

@ -1,11 +1,10 @@
import { padmodals as padmodals$0 } from "./pad_modals.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -21,14 +20,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const padmodals = require('./pad_modals').padmodals;
const padmodals = { padmodals: padmodals$0 }.padmodals;
const padconnectionstatus = (() => {
let status = {
what: 'connecting',
};
const self = {
init: () => {
$('button#forcereconnect').click(() => {
@ -46,18 +42,16 @@ const padconnectionstatus = (() => {
status = {
what: 'reconnecting',
};
padmodals.showModal('reconnecting');
padmodals.showOverlay();
},
disconnected: (msg) => {
if (status.what === 'disconnected') return;
if (status.what === 'disconnected')
return;
status = {
what: 'disconnected',
why: msg,
};
// These message IDs correspond to localized strings that are presented to the user. If a new
// message ID is added here then a new div must be added to src/templates/pad.html and the
// corresponding l10n IDs must be added to the language files in src/locales.
@ -79,7 +73,6 @@ const padconnectionstatus = (() => {
// Fall back to a generic message.
k = 'disconnected';
}
padmodals.showModal(k);
padmodals.showOverlay();
},
@ -88,5 +81,4 @@ const padconnectionstatus = (() => {
};
return self;
})();
exports.padconnectionstatus = padconnectionstatus;
export { padconnectionstatus };

View file

@ -1,5 +1,5 @@
import * as padUtils from "./pad_utils.js";
'use strict';
/**
* Copyright 2009 Google Inc.
*
@ -15,14 +15,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const Cookies = require('./pad_utils').Cookies;
exports.padcookie = new class {
const Cookies = { Cookies: padUtils }.Cookies;
export const padcookie = new class {
constructor() {
this.cookieName_ = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp';
}
init() {
const prefs = this.readPrefs_() || {};
delete prefs.userId;
@ -39,31 +36,28 @@ exports.padcookie = new class {
});
}
}
readPrefs_() {
try {
const json = Cookies.get(this.cookieName_);
if (json == null) return null;
if (json == null)
return null;
return JSON.parse(json);
} catch (e) {
}
catch (e) {
return null;
}
}
writePrefs_(prefs) {
Cookies.set(this.cookieName_, JSON.stringify(prefs), {expires: 365 * 100});
Cookies.set(this.cookieName_, JSON.stringify(prefs), { expires: 365 * 100 });
}
getPref(prefName) {
return this.readPrefs_()[prefName];
}
setPref(prefName, value) {
const prefs = this.readPrefs_();
prefs[prefName] = value;
this.writePrefs_(prefs);
}
clear() {
this.writePrefs_({});
}

View file

@ -1,68 +1,39 @@
import browser from "./vendors/browser.js";
import * as hooks from "./pluginfw/hooks.js";
import { padutils as padutils$0 } from "./pad_utils.js";
import { padeditor as padeditor$0 } from "./pad_editor.js";
import * as padsavedrevs from "./pad_savedrevs.js";
import _ from "underscore";
import "./vendors/nice-select.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const browser = require('./vendors/browser');
const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils;
const padeditor = require('./pad_editor').padeditor;
const padsavedrevs = require('./pad_savedrevs');
const _ = require('underscore');
require('./vendors/nice-select');
const padutils = { padutils: padutils$0 }.padutils;
const padeditor = { padeditor: padeditor$0 }.padeditor;
class ToolbarItem {
constructor(element) {
this.$el = element;
}
getCommand() {
return this.$el.attr('data-key');
}
getValue() {
if (this.isSelect()) {
return this.$el.find('select').val();
}
}
setValue(val) {
if (this.isSelect()) {
return this.$el.find('select').val(val);
}
}
getType() {
return this.$el.attr('data-type');
}
isSelect() {
return this.getType() === 'select';
}
isButton() {
return this.getType() === 'button';
}
bind(callback) {
if (this.isButton()) {
this.$el.click((event) => {
@ -70,14 +41,14 @@ class ToolbarItem {
callback(this.getCommand(), this);
event.preventDefault();
});
} else if (this.isSelect()) {
}
else if (this.isSelect()) {
this.$el.find('select').change(() => {
callback(this.getCommand(), this);
});
}
}
}
const syncAnimation = (() => {
const SYNCING = -100;
const DONE = 100;
@ -90,19 +61,22 @@ const syncAnimation = (() => {
const animator = padutils.makeAnimationScheduler(() => {
if (state === SYNCING || state === DONE) {
return false;
} else if (state >= T_GONE) {
}
else if (state >= T_GONE) {
state = DONE;
$('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'none');
return false;
} else if (state < 0) {
}
else if (state < 0) {
state += step;
if (state >= 0) {
$('#syncstatussyncing').css('display', 'none');
$('#syncstatusdone').css('display', 'block').css('opacity', 1);
}
return true;
} else {
}
else {
state += step;
if (state >= T_FADE) {
$('#syncstatusdone').css('opacity', (T_GONE - state) / (T_GONE - T_FADE));
@ -122,14 +96,12 @@ const syncAnimation = (() => {
},
};
})();
exports.padeditbar = new class {
export const padeditbar = new class {
constructor() {
this._editbarPosition = 0;
this.commands = {};
this.dropdowns = [];
}
init() {
$('#editbar .editbarbutton').attr('unselectable', 'on'); // for IE
this.enable();
@ -139,24 +111,19 @@ exports.padeditbar = new class {
this.triggerCommand(command, item);
});
});
$('body:not(#editorcontainerbox)').on('keydown', (evt) => {
this._bodyKeyEvent(evt);
});
$('.show-more-icon-btn').click(() => {
$('.toolbar').toggleClass('full-icons');
});
this.checkAllIconsAreDisplayedInToolbar();
$(window).resize(_.debounce(() => this.checkAllIconsAreDisplayedInToolbar(), 100));
this._registerDefaultCommands();
hooks.callAll('postToolbarInit', {
toolbar: this,
ace: padeditor.ace,
});
/*
* On safari, the dropdown in the toolbar gets hidden because of toolbar
* overflow:hidden property. This is a bug from Safari: any children with
@ -166,7 +133,6 @@ exports.padeditbar = new class {
if (!browser.safari) {
$('select').niceSelect();
}
// When editor is scrolled, we add a class to style the editbar differently
$('iframe[name="ace_outer"]').contents().scroll((ev) => {
$('#editbar').toggleClass('editor-scrolled', $(ev.currentTarget).scrollTop() > 2);
@ -201,9 +167,9 @@ exports.padeditbar = new class {
if (this.isEnabled() && this.commands[cmd]) {
this.commands[cmd](cmd, padeditor.ace, item);
}
if (padeditor.ace) padeditor.ace.focus();
if (padeditor.ace)
padeditor.ace.focus();
}
// cb is deprecated (this function is synchronous so a callback is unnecessary).
toggleDropDown(moduleName, cb = null) {
let cbErr = null;
@ -212,51 +178,54 @@ exports.padeditbar = new class {
if (moduleName === 'users' && $('#users').hasClass('stickyUsers')) {
return;
}
$('.nice-select').removeClass('open');
$('.toolbar-popup').removeClass('popup-show');
// hide all modules and remove highlighting of all buttons
if (moduleName === 'none') {
for (const thisModuleName of this.dropdowns) {
// skip the userlist
if (thisModuleName === 'users') continue;
if (thisModuleName === 'users')
continue;
const module = $(`#${thisModuleName}`);
// skip any "force reconnect" message
const isAForceReconnectMessage = module.find('button#forcereconnect:visible').length > 0;
if (isAForceReconnectMessage) continue;
if (isAForceReconnectMessage)
continue;
if (module.hasClass('popup-show')) {
$(`li[data-key=${thisModuleName}] > a`).removeClass('selected');
module.removeClass('popup-show');
}
}
} else {
}
else {
// hide all modules that are not selected and remove highlighting
// respectively add highlighting to the corresponding button
for (const thisModuleName of this.dropdowns) {
const module = $(`#${thisModuleName}`);
if (module.hasClass('popup-show')) {
$(`li[data-key=${thisModuleName}] > a`).removeClass('selected');
module.removeClass('popup-show');
} else if (thisModuleName === moduleName) {
}
else if (thisModuleName === moduleName) {
$(`li[data-key=${thisModuleName}] > a`).addClass('selected');
module.addClass('popup-show');
}
}
}
} catch (err) {
}
catch (err) {
cbErr = err || new Error(err);
} finally {
if (cb) Promise.resolve().then(() => cb(cbErr));
}
finally {
if (cb)
Promise.resolve().then(() => cb(cbErr));
}
}
setSyncStatus(status) {
if (status === 'syncing') {
syncAnimation.syncing();
} else if (status === 'done') {
}
else if (status === 'done') {
syncAnimation.done();
}
}
@ -264,7 +233,6 @@ exports.padeditbar = new class {
const padUrl = window.location.href.split('?')[0];
const params = '?showControls=true&showChat=true&showLineNumbers=true&useMonospaceFont=false';
const props = 'width="100%" height="600" frameborder="0"';
if ($('#readonlyinput').is(':checked')) {
const urlParts = padUrl.split('/');
urlParts.pop();
@ -272,7 +240,8 @@ exports.padeditbar = new class {
$('#embedinput')
.val(`<iframe name="embed_readonly" src="${readonlyLink}${params}" ${props}></iframe>`);
$('#linkinput').val(readonlyLink);
} else {
}
else {
$('#embedinput')
.val(`<iframe name="embed_readwrite" src="${padUrl}${params}" ${props}></iframe>`);
$('#linkinput').val(padUrl);
@ -283,7 +252,6 @@ exports.padeditbar = new class {
$('.toolbar').removeClass('cropped');
$('body').removeClass('mobile-layout');
const menuLeft = $('.toolbar .menu_left')[0];
// this is approximate, we cannot measure it because on mobile
// Layout it takes the full width on the bottom of the page
const menuRightWidth = 280;
@ -295,7 +263,6 @@ exports.padeditbar = new class {
$('.toolbar').addClass('cropped');
}
}
_bodyKeyEvent(evt) {
// If the event is Alt F9 or Escape & we're already in the editbar menu
// Send the users focus back to the pad
@ -311,15 +278,16 @@ exports.padeditbar = new class {
if (typeof pad === 'undefined') {
// Timeslider probably..
$('#editorcontainerbox').focus(); // Focus back onto the pad
} else {
}
else {
padeditor.ace.focus(); // Sends focus back to pad
// The above focus doesn't always work in FF, you have to hit enter afterwards
evt.preventDefault();
}
} else {
}
else {
// Focus on the editbar :)
const firstEditbarElement = parent.parent.$('#editbar button').first();
$(evt.currentTarget).blur();
firstEditbarElement.focus();
evt.preventDefault();
@ -328,47 +296,44 @@ exports.padeditbar = new class {
// Are we in the toolbar??
if ($(':focus').parents('.toolbar').length === 1) {
// On arrow keys go to next/previous button item in editbar
if (evt.keyCode !== 39 && evt.keyCode !== 37) return;
if (evt.keyCode !== 39 && evt.keyCode !== 37)
return;
// Get all the focusable items in the editbar
const focusItems = $('#editbar').find('button, select');
// On left arrow move to next button in editbar
if (evt.keyCode === 37) {
// If a dropdown is visible or we're in an input don't move to the next button
if ($('.popup').is(':visible') || evt.target.localName === 'input') return;
if ($('.popup').is(':visible') || evt.target.localName === 'input')
return;
this._editbarPosition--;
// Allow focus to shift back to end of row and start of row
if (this._editbarPosition === -1) this._editbarPosition = focusItems.length - 1;
if (this._editbarPosition === -1)
this._editbarPosition = focusItems.length - 1;
$(focusItems[this._editbarPosition]).focus();
}
// On right arrow move to next button in editbar
if (evt.keyCode === 39) {
// If a dropdown is visible or we're in an input don't move to the next button
if ($('.popup').is(':visible') || evt.target.localName === 'input') return;
if ($('.popup').is(':visible') || evt.target.localName === 'input')
return;
this._editbarPosition++;
// Allow focus to shift back to end of row and start of row
if (this._editbarPosition >= focusItems.length) this._editbarPosition = 0;
if (this._editbarPosition >= focusItems.length)
this._editbarPosition = 0;
$(focusItems[this._editbarPosition]).focus();
}
}
}
_registerDefaultCommands() {
this.registerDropdownCommand('showusers', 'users');
this.registerDropdownCommand('settings');
this.registerDropdownCommand('connectivity');
this.registerDropdownCommand('import_export');
this.registerDropdownCommand('embed');
this.registerCommand('settings', () => {
this.toggleDropDown('settings');
$('#options-stickychat').focus();
});
this.registerCommand('import_export', () => {
this.toggleDropDown('import_export');
// If Import file input exists then focus on it..
@ -376,30 +341,26 @@ exports.padeditbar = new class {
setTimeout(() => {
$('#importfileinput').focus();
}, 100);
} else {
}
else {
$('.exportlink').first().focus();
}
});
this.registerCommand('showusers', () => {
this.toggleDropDown('users');
$('#myusernameedit').focus();
});
this.registerCommand('embed', () => {
this.setEmbedLinks();
this.toggleDropDown('embed');
$('#linkinput').focus().select();
});
this.registerCommand('savedRevision', () => {
padsavedrevs.saveNow();
});
this.registerCommand('showTimeSlider', () => {
document.location = `${document.location.pathname}/timeslider`;
});
const aceAttributeCommand = (cmd, ace) => {
ace.ace_toggleAttributeOnSelection(cmd);
};
@ -407,33 +368,26 @@ exports.padeditbar = new class {
this.registerAceCommand('italic', aceAttributeCommand);
this.registerAceCommand('underline', aceAttributeCommand);
this.registerAceCommand('strikethrough', aceAttributeCommand);
this.registerAceCommand('undo', (cmd, ace) => {
ace.ace_doUndoRedo(cmd);
});
this.registerAceCommand('redo', (cmd, ace) => {
ace.ace_doUndoRedo(cmd);
});
this.registerAceCommand('insertunorderedlist', (cmd, ace) => {
ace.ace_doInsertUnorderedList();
});
this.registerAceCommand('insertorderedlist', (cmd, ace) => {
ace.ace_doInsertOrderedList();
});
this.registerAceCommand('indent', (cmd, ace) => {
if (!ace.ace_doIndentOutdent(false)) {
ace.ace_doInsertUnorderedList();
}
});
this.registerAceCommand('outdent', (cmd, ace) => {
ace.ace_doIndentOutdent(true);
});
this.registerAceCommand('clearauthorship', (cmd, ace) => {
// If we have the whole document selected IE control A has been hit
const rep = ace.ace_getRep();
@ -454,7 +408,6 @@ exports.padeditbar = new class {
* This does make wonder if it's worth having a checkbox to avoid being
* prompted again but that's probably overkill for this contribution.
*/
// if we don't have any text selected, we have a caret or we have already said to prompt
if ((!(rep.selStart && rep.selEnd)) || ace.ace_isCaret() || doPrompt) {
if (window.confirm(html10n.get('pad.editbar.clearcolors'))) {
@ -462,17 +415,17 @@ exports.padeditbar = new class {
['author', ''],
]);
}
} else {
}
else {
ace.ace_setAttributeOnSelection('author', '');
}
});
this.registerCommand('timeslider_returnToPad', (cmd) => {
if (document.referrer.length > 0 &&
document.referrer.substring(document.referrer.lastIndexOf('/') - 1,
document.referrer.lastIndexOf('/')) === 'p') {
document.referrer.substring(document.referrer.lastIndexOf('/') - 1, document.referrer.lastIndexOf('/')) === 'p') {
document.location = document.referrer;
} else {
}
else {
document.location = document.location.href
.substring(0, document.location.href.lastIndexOf('/'));
}

View file

@ -1,10 +1,11 @@
import * as padUtils from "./pad_utils.js";
import { padcookie as padcookie$0 } from "./pad_cookie.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -20,16 +21,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const Cookies = require('./pad_utils').Cookies;
const padcookie = require('./pad_cookie').padcookie;
const padutils = require('./pad_utils').padutils;
const Cookies = { Cookies: padUtils }.Cookies;
const padcookie = { padcookie: padcookie$0 }.padcookie;
const padutils = { padutils: padUtils }.padutils;
const padeditor = (() => {
let Ace2Editor = undefined;
let pad = undefined;
let settings = undefined;
const self = {
ace: null,
// this is accessed directly from other files
@ -59,13 +57,11 @@ const padeditor = (() => {
padutils.bindCheckboxChange($('#options-linenoscheck'), () => {
pad.changeViewOption('showLineNumbers', padutils.getCheckbox($('#options-linenoscheck')));
});
// Author colors
padutils.bindCheckboxChange($('#options-colorscheck'), () => {
padcookie.setPref('showAuthorshipColors', padutils.getCheckbox('#options-colorscheck'));
pad.changeViewOption('showAuthorColors', padutils.getCheckbox('#options-colorscheck'));
});
// Right to left
padutils.bindCheckboxChange($('#options-rtlcheck'), () => {
pad.changeViewOption('rtlIsTrue', padutils.getCheckbox($('#options-rtlcheck')));
@ -74,17 +70,14 @@ const padeditor = (() => {
pad.changeViewOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
padutils.setCheckbox($('#options-rtlcheck'), ('rtl' === html10n.getDirection()));
});
// font family change
$('#viewfontmenu').change(() => {
pad.changeViewOption('padFontFamily', $('#viewfontmenu').val());
});
// Language
html10n.bind('localized', () => {
$('#languagemenu').val(html10n.getLanguage());
// translate the value of 'unnamed' and 'Enter your name' textboxes in the userlist
// this does not interfere with html10n's normal value-setting because
// html10n just ingores <input>s
// also, a value which has been set by the user will be not overwritten
@ -108,32 +101,28 @@ const padeditor = (() => {
setViewOptions: (newOptions) => {
const getOption = (key, defaultValue) => {
const value = String(newOptions[key]);
if (value === 'true') return true;
if (value === 'false') return false;
if (value === 'true')
return true;
if (value === 'false')
return false;
return defaultValue;
};
let v;
v = getOption('rtlIsTrue', ('rtl' === html10n.getDirection()));
self.ace.setProperty('rtlIsTrue', v);
padutils.setCheckbox($('#options-rtlcheck'), v);
v = getOption('showLineNumbers', true);
self.ace.setProperty('showslinenumbers', v);
padutils.setCheckbox($('#options-linenoscheck'), v);
v = getOption('showAuthorColors', true);
self.ace.setProperty('showsauthorcolors', v);
$('#chattext').toggleClass('authorColors', v);
$('iframe[name="ace_outer"]').contents().find('#sidedivinner').toggleClass('authorColors', v);
padutils.setCheckbox($('#options-colorscheck'), v);
// Override from parameters if true
if (settings.noColors !== false) {
self.ace.setProperty('showsauthorcolors', !settings.noColors);
}
self.ace.setProperty('textface', newOptions.padFontFamily || '');
},
dispose: () => {
@ -159,10 +148,7 @@ const padeditor = (() => {
};
return self;
})();
exports.padeditor = padeditor;
exports.focusOnLine = (ace) => {
export const focusOnLine = (ace) => {
// If a number is in the URI IE #L124 go to that line number
const lineNumber = window.location.hash.substr(1);
if (lineNumber) {
@ -182,8 +168,8 @@ exports.focusOnLine = (ace) => {
}
const $outerdocHTML = $('iframe[name="ace_outer"]').contents()
.find('#outerdocbody').parent();
$outerdoc.css({top: `${offsetTop}px`}); // Chrome
$outerdocHTML.animate({scrollTop: offsetTop}); // needed for FF
$outerdoc.css({ top: `${offsetTop}px` }); // Chrome
$outerdocHTML.animate({ scrollTop: offsetTop }); // needed for FF
const node = line[0];
ace.callWithAce((ace) => {
const selection = {
@ -208,3 +194,4 @@ exports.focusOnLine = (ace) => {
}
// End of setSelection / set Y position of editor
};
export { padeditor };

View file

@ -1,11 +1,9 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -21,10 +19,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const padimpexp = (() => {
let pad;
// /// import
const addImportFrames = () => {
$('#import .importframe').remove();
@ -34,24 +30,23 @@ const padimpexp = (() => {
.addClass('importframe');
$('#import').append(iframe);
};
const fileInputUpdated = () => {
$('#importsubmitinput').addClass('throbbold');
$('#importformfilediv').addClass('importformenabled');
$('#importsubmitinput').removeAttr('disabled');
$('#importmessagefail').fadeOut('fast');
};
const fileInputSubmit = function (e) {
e.preventDefault();
$('#importmessagefail').fadeOut('fast');
if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) return;
$('#importsubmitinput').attr({disabled: true}).val(html10n.get('pad.impexp.importing'));
window.setTimeout(() => $('#importfileinput').attr({disabled: true}), 0);
if (!window.confirm(html10n.get('pad.impexp.confirmimport')))
return;
$('#importsubmitinput').attr({ disabled: true }).val(html10n.get('pad.impexp.importing'));
window.setTimeout(() => $('#importfileinput').attr({ disabled: true }), 0);
$('#importarrow').stop(true, true).hide();
$('#importstatusball').show();
(async () => {
const {code, message, data: {directDatabaseAccess} = {}} = await $.ajax({
const { code, message, data: { directDatabaseAccess } = {} } = await $.ajax({
url: `${window.location.href.split('?')[0].split('#')[0]}/import`,
method: 'POST',
data: new FormData(this),
@ -60,14 +55,17 @@ const padimpexp = (() => {
dataType: 'json',
timeout: 25000,
}).catch((err) => {
if (err.responseJSON) return err.responseJSON;
return {code: 2, message: 'Unknown import error'};
if (err.responseJSON)
return err.responseJSON;
return { code: 2, message: 'Unknown import error' };
});
if (code !== 0) {
importErrorMessage(message);
} else {
}
else {
$('#import_export').removeClass('popup-show');
if (directDatabaseAccess) window.location.reload();
if (directDatabaseAccess)
window.location.reload();
}
$('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton'));
window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0);
@ -75,7 +73,6 @@ const padimpexp = (() => {
addImportFrames();
})();
};
const importErrorMessage = (status) => {
const known = [
'convertFailed',
@ -85,7 +82,6 @@ const padimpexp = (() => {
'permission',
];
const msg = html10n.get(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`);
const showError = (fade) => {
const popup = $('#importmessagefail').empty()
.append($('<strong>')
@ -94,73 +90,67 @@ const padimpexp = (() => {
.append(document.createTextNode(msg));
popup[(fade ? 'fadeIn' : 'show')]();
};
if ($('#importexport .importmessage').is(':visible')) {
$('#importmessagesuccess').fadeOut('fast');
$('#importmessagefail').fadeOut('fast', () => showError(true));
} else {
}
else {
showError();
}
};
// /// export
function cantExport() {
let type = $(this);
if (type.hasClass('exporthrefpdf')) {
type = 'PDF';
} else if (type.hasClass('exporthrefdoc')) {
}
else if (type.hasClass('exporthrefdoc')) {
type = 'Microsoft Word';
} else if (type.hasClass('exporthrefodt')) {
}
else if (type.hasClass('exporthrefodt')) {
type = 'OpenDocument';
} else {
}
else {
type = 'this file';
}
alert(html10n.get('pad.impexp.exportdisabled', {type}));
alert(html10n.get('pad.impexp.exportdisabled', { type }));
return false;
}
// ///
const self = {
init: (_pad) => {
pad = _pad;
// get /p/padname
// if /p/ isn't available due to a rewrite we use the clientVars padId
const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || clientVars.padId;
// i10l buttom import
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
html10n.bind('localized', () => {
$('#importsubmitinput').val(html10n.get('pad.impexp.importbutton'));
});
// build the export links
$('#exporthtmla').attr('href', `${padRootPath}/export/html`);
$('#exportetherpada').attr('href', `${padRootPath}/export/etherpad`);
$('#exportplaina').attr('href', `${padRootPath}/export/txt`);
// hide stuff thats not avaible if abiword/soffice is disabled
if (clientVars.exportAvailable === 'no') {
$('#exportworda').remove();
$('#exportpdfa').remove();
$('#exportopena').remove();
$('#importmessageabiword').show();
} else if (clientVars.exportAvailable === 'withoutPDF') {
}
else if (clientVars.exportAvailable === 'withoutPDF') {
$('#exportpdfa').remove();
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
$('#importexport').css({height: '142px'});
$('#importexportline').css({height: '142px'});
} else {
$('#importexport').css({ height: '142px' });
$('#importexportline').css({ height: '142px' });
}
else {
$('#exportworda').attr('href', `${padRootPath}/export/doc`);
$('#exportpdfa').attr('href', `${padRootPath}/export/pdf`);
$('#exportopena').attr('href', `${padRootPath}/export/odt`);
}
addImportFrames();
$('#importfileinput').change(fileInputUpdated);
$('#importform').unbind('submit').submit(fileInputSubmit);
@ -179,5 +169,4 @@ const padimpexp = (() => {
};
return self;
})();
exports.padimpexp = padimpexp;
export { padimpexp };

View file

@ -1,11 +1,11 @@
import { padeditbar as padeditbar$0 } from "./pad_editbar.js";
import * as automaticReconnect from "./pad_automatic_reconnect.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -21,10 +21,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const padeditbar = require('./pad_editbar').padeditbar;
const automaticReconnect = require('./pad_automatic_reconnect');
const padeditbar = { padeditbar: padeditbar$0 }.padeditbar;
const padmodals = (() => {
let pad = undefined;
const self = {
@ -35,10 +32,8 @@ const padmodals = (() => {
padeditbar.toggleDropDown('none');
$('#connectivity .visible').removeClass('visible');
$(`#connectivity .${messageId}`).addClass('visible');
const $modal = $(`#connectivity .${messageId}`);
automaticReconnect.showCountDownTimerToReconnectOnModal($modal, pad);
padeditbar.toggleDropDown('connectivity');
},
showOverlay: () => {
@ -51,5 +46,4 @@ const padmodals = (() => {
};
return self;
})();
exports.padmodals = padmodals;
export { padmodals };

View file

@ -1,5 +1,4 @@
'use strict';
/**
* Copyright 2012 Peter 'Pita' Martischka
*
@ -15,11 +14,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
let pad;
exports.saveNow = () => {
pad.collabClient.sendMessage({type: 'SAVE_REVISION'});
export const saveNow = () => {
pad.collabClient.sendMessage({ type: 'SAVE_REVISION' });
$.gritter.add({
// (string | mandatory) the heading of the notification
title: html10n.get('pad.savedrevs.marked'),
@ -32,7 +29,6 @@ exports.saveNow = () => {
class_name: 'saved-revision',
});
};
exports.init = (_pad) => {
export const init = (_pad) => {
pad = _pad;
};

View file

@ -1,5 +1,6 @@
import { padutils as padutils$0 } from "./pad_utils.js";
import * as hooks from "./pluginfw/hooks.js";
'use strict';
/**
* Copyright 2009 Google Inc.
*
@ -15,21 +16,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const padutils = require('./pad_utils').padutils;
const hooks = require('./pluginfw/hooks');
const padutils = { padutils: padutils$0 }.padutils;
let myUserInfo = {};
let colorPickerOpen = false;
let colorPickerSetup = false;
const paduserlist = (() => {
const rowManager = (() => {
// The row manager handles rendering rows of the user list and animating
// their insertion, removal, and reordering. It manipulates TD height
// and TD opacity.
const nextRowId = () => `usertr${nextRowId.counter++}`;
nextRowId.counter = 1;
// objects are shared; fields are "domId","data","animationStep"
@ -38,7 +33,6 @@ const paduserlist = (() => {
const rowsPresent = []; // in order
const ANIMATION_START = -12; // just starting to fade in
const ANIMATION_END = 12; // just finishing fading out
const animateStep = () => {
// animation must be symmetrical
for (let i = rowsFadingIn.length - 1; i >= 0; i--) { // backwards to allow removal
@ -49,13 +43,16 @@ const paduserlist = (() => {
const baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step <= -OPACITY_STEPS) {
node.find('td').height(animHeight);
} else if (step === -OPACITY_STEPS + 1) {
}
else if (step === -OPACITY_STEPS + 1) {
node.empty().append(createUserRowTds(animHeight, row.data))
.find('td').css('opacity', baseOpacity * 1 / OPACITY_STEPS);
} else if (step < 0) {
}
else if (step < 0) {
node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - (-step)) / OPACITY_STEPS)
.height(animHeight);
} else if (step === 0) {
}
else if (step === 0) {
// set HTML in case modified during animation
node.empty().append(createUserRowTds(animHeight, row.data))
.find('td').css('opacity', baseOpacity * 1).height(animHeight);
@ -71,48 +68,45 @@ const paduserlist = (() => {
if (step < OPACITY_STEPS) {
node.find('td').css('opacity', baseOpacity * (OPACITY_STEPS - step) / OPACITY_STEPS)
.height(animHeight);
} else if (step === OPACITY_STEPS) {
}
else if (step === OPACITY_STEPS) {
node.empty().append(createEmptyRowTds(animHeight));
} else if (step <= ANIMATION_END) {
}
else if (step <= ANIMATION_END) {
node.find('td').height(animHeight);
} else {
}
else {
rowsFadingOut.splice(i, 1); // remove from set
node.remove();
}
}
handleOtherUserInputs();
return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
};
const getAnimationHeight = (step, power) => {
let a = Math.abs(step / 12);
if (power === 2) a **= 2;
else if (power === 3) a **= 3;
else if (power === 4) a **= 4;
else if (power >= 5) a **= 5;
if (power === 2)
a **= 2;
else if (power === 3)
a **= 3;
else if (power === 4)
a **= 4;
else if (power >= 5)
a **= 5;
return Math.round(26 * (1 - a));
};
const OPACITY_STEPS = 6;
const ANIMATION_STEP_TIME = 20;
const LOWER_FRAMERATE_FACTOR = 2;
const {scheduleAnimation} =
padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR);
const { scheduleAnimation } = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR);
const NUMCOLS = 4;
// we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
// IE's poor handling when manipulating the DOM directly.
const createEmptyRowTds = (height) => $('<td>')
.attr('colspan', NUMCOLS)
.css('border', 0)
.css('height', `${height}px`);
const isNameEditable = (data) => (!data.name) && (data.status !== 'Disconnected');
const replaceUserRowContents = (tr, height, data) => {
const tds = createUserRowTds(height, data);
if (isNameEditable(data) && tr.find('td.usertdname input:enabled').length > 0) {
@ -121,29 +115,32 @@ const paduserlist = (() => {
const oldTd = $(tr.find('td').get(i));
if (!oldTd.hasClass('usertdname')) {
oldTd.replaceWith(td);
} else {
}
else {
// Prevent leak. I'm not 100% confident that this is necessary, but it shouldn't hurt.
$(td).remove();
}
});
} else {
}
else {
tr.empty().append(tds);
}
return tr;
};
const createUserRowTds = (height, data) => {
let name;
if (data.name) {
name = document.createTextNode(data.name);
} else {
}
else {
name = $('<input>')
.attr('data-l10n-id', 'pad.userlist.unnamed')
.attr('type', 'text')
.addClass('editempty')
.addClass('newinput')
.attr('value', html10n.get('pad.userlist.unnamed'));
if (isNameEditable(data)) name.attr('disabled', 'disabled');
if (isNameEditable(data))
name.attr('disabled', 'disabled');
}
return $()
.add($('<td>')
@ -162,22 +159,19 @@ const paduserlist = (() => {
.addClass('activity')
.text(data.activity));
};
const createRow = (id, contents, authorId) => $('<tr>')
.attr('data-authorId', authorId)
.attr('id', id)
.append(contents);
const rowNode = (row) => $(`#${row.domId}`);
const handleRowData = (row) => {
if (row.data && row.data.status === 'Disconnected') {
row.opacity = 0.5;
} else {
}
else {
delete row.opacity;
}
};
const handleOtherUserInputs = () => {
// handle 'INPUT' elements for naming other unnamed users
$('#otheruserstable input.newinput').each(function () {
@ -192,14 +186,10 @@ const paduserlist = (() => {
}
}).removeClass('newinput');
};
// animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
const insertRow = (position, data, animationPower) => {
position = Math.max(0, Math.min(rowsPresent.length, position));
animationPower = (animationPower === undefined ? 4 : animationPower);
const domId = nextRowId();
const row = {
data,
@ -208,33 +198,30 @@ const paduserlist = (() => {
animationPower,
};
const authorId = data.id;
handleRowData(row);
rowsPresent.splice(position, 0, row);
let tr;
if (animationPower === 0) {
tr = createRow(domId, createUserRowTds(getAnimationHeight(0), data), authorId);
row.animationStep = 0;
} else {
}
else {
rowsFadingIn.push(row);
tr = createRow(domId, createEmptyRowTds(getAnimationHeight(ANIMATION_START)), authorId);
}
$('table#otheruserstable').show();
if (position === 0) {
$('table#otheruserstable').prepend(tr);
} else {
}
else {
rowNode(rowsPresent[position - 1]).after(tr);
}
if (animationPower !== 0) {
scheduleAnimation();
}
handleOtherUserInputs();
return row;
};
const updateRow = (position, data) => {
const row = rowsPresent[position];
if (row) {
@ -250,7 +237,6 @@ const paduserlist = (() => {
}
}
};
const removeRow = (position, animationPower) => {
animationPower = (animationPower === undefined ? 4 : animationPower);
const row = rowsPresent[position];
@ -258,7 +244,8 @@ const paduserlist = (() => {
rowsPresent.splice(position, 1); // remove
if (animationPower === 0) {
rowNode(row).remove();
} else {
}
else {
row.animationStep = -row.animationStep; // use symmetry
row.animationPower = animationPower;
rowsFadingOut.push(row);
@ -269,10 +256,7 @@ const paduserlist = (() => {
$('table#otheruserstable').hide();
}
};
// newPosition is position after the row has been removed
const moveRow = (oldPosition, newPosition, animationPower) => {
animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
const row = rowsPresent[oldPosition];
@ -282,7 +266,6 @@ const paduserlist = (() => {
insertRow(newPosition, rowData, animationPower);
}
};
const self = {
insertRow,
removeRow,
@ -293,26 +276,26 @@ const paduserlist = (() => {
})(); // //////// rowManager
const otherUsersInfo = [];
const otherUsersData = [];
const rowManagerMakeNameEditor = (jnode, userId) => {
setUpEditable(jnode, () => {
const existingIndex = findExistingIndex(userId);
if (existingIndex >= 0) {
return otherUsersInfo[existingIndex].name || '';
} else {
}
else {
return '';
}
}, (newName) => {
if (!newName) {
jnode.addClass('editempty');
jnode.val(html10n.get('pad.userlist.unnamed'));
} else {
}
else {
jnode.attr('disabled', 'disabled');
pad.suggestUserName(userId, newName);
}
});
};
const findExistingIndex = (userId) => {
let existingIndex = -1;
for (let i = 0; i < otherUsersInfo.length; i++) {
@ -323,7 +306,6 @@ const paduserlist = (() => {
}
return existingIndex;
};
const setUpEditable = (jqueryNode, valueGetter, valueSetter) => {
jqueryNode.bind('focus', (evt) => {
const oldValue = valueGetter();
@ -343,20 +325,15 @@ const paduserlist = (() => {
});
jqueryNode.removeAttr('disabled').addClass('editable');
};
let pad = undefined;
const self = {
init: (myInitialUserInfo, _pad) => {
pad = _pad;
self.setMyUserInfo(myInitialUserInfo);
if ($('#online_count').length === 0) {
$('#editbar [data-key=showusers] > a').append('<span id="online_count">1</span>');
}
$('#otheruserstable tr').remove();
$('#myusernameedit').addClass('myusernameedithoverable');
setUpEditable($('#myusernameedit'), () => myUserInfo.name || '', (newValue) => {
myUserInfo.name = newValue;
@ -367,7 +344,6 @@ const paduserlist = (() => {
self.renderMyUserInfo();
}, 0);
});
// color picker
$('#myswatchbox').click(showColorPicker);
$('#mycolorpicker .pickerswatchouter').click(function () {
@ -394,17 +370,15 @@ const paduserlist = (() => {
users: () => {
// Returns an object of users who have been on this pad
const userList = self.usersOnline();
// Now we add historical authors
const historical = clientVars.collab_client_vars.historicalAuthorData;
for (const [key, {userId}] of Object.entries(historical)) {
for (const [key, { userId }] of Object.entries(historical)) {
// Check we don't already have this author in our array
let exists = false;
userList.forEach((user) => {
if (user.userId === userId) exists = true;
if (user.userId === userId)
exists = true;
});
if (exists === false) {
userList.push(historical[key]);
}
@ -416,10 +390,7 @@ const paduserlist = (() => {
if (typeof info.colorId === 'number') {
info.colorId = clientVars.colorPalette[info.colorId];
}
myUserInfo = $.extend(
{}, info);
myUserInfo = $.extend({}, info);
self.renderMyUserInfo();
},
userJoinOrUpdate: (info) => {
@ -427,11 +398,9 @@ const paduserlist = (() => {
// not sure how this would happen
return;
}
hooks.callAll('userJoinOrUpdate', {
userInfo: info,
});
const userData = {};
userData.color = typeof info.colorId === 'number'
? clientVars.colorPalette[info.colorId] : info.colorId;
@ -439,9 +408,7 @@ const paduserlist = (() => {
userData.status = '';
userData.activity = '';
userData.id = info.userId;
const existingIndex = findExistingIndex(info.userId);
let numUsersBesides = otherUsersInfo.length;
if (existingIndex >= 0) {
numUsersBesides--;
@ -458,14 +425,14 @@ const paduserlist = (() => {
const idThis = info.userId;
return (nameN > nameThis) || (nameN === nameThis && idN > idThis);
});
if (existingIndex >= 0) {
// update
if (existingIndex === newIndex) {
otherUsersInfo[existingIndex] = info;
otherUsersData[existingIndex] = userData;
rowManager.updateRow(existingIndex, userData);
} else {
}
else {
otherUsersInfo.splice(existingIndex, 1);
otherUsersData.splice(existingIndex, 1);
otherUsersInfo.splice(newIndex, 0, info);
@ -473,12 +440,12 @@ const paduserlist = (() => {
rowManager.updateRow(existingIndex, userData);
rowManager.moveRow(existingIndex, newIndex);
}
} else {
}
else {
otherUsersInfo.splice(newIndex, 0, info);
otherUsersData.splice(newIndex, 0, userData);
rowManager.insertRow(newIndex, userData);
}
self.updateNumberOfOnlineUsers();
},
updateNumberOfOnlineUsers: () => {
@ -488,9 +455,7 @@ const paduserlist = (() => {
online++;
}
}
$('#online_count').text(online);
return online;
},
userLeave: (info) => {
@ -523,30 +488,28 @@ const paduserlist = (() => {
}, 8000); // how long to wait
userData.leaveTimer = thisLeaveTimer;
}
self.updateNumberOfOnlineUsers();
},
renderMyUserInfo: () => {
if (myUserInfo.name) {
$('#myusernameedit').removeClass('editempty').val(myUserInfo.name);
} else {
}
else {
$('#myusernameedit').attr('placeholder', html10n.get('pad.userlist.entername'));
}
if (colorPickerOpen) {
$('#myswatchbox').addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable');
} else {
}
else {
$('#myswatchbox').addClass('myswatchboxhoverable').removeClass('myswatchboxunhoverable');
}
$('#myswatch').css({'background-color': myUserInfo.colorId});
$('li[data-key=showusers] > a').css({'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}`});
$('#myswatch').css({ 'background-color': myUserInfo.colorId });
$('li[data-key=showusers] > a').css({ 'box-shadow': `inset 0 0 30px ${myUserInfo.colorId}` });
},
};
return self;
})();
const getColorPickerSwatchIndex = (jnode) => $('#colorpickerswatches li').index(jnode);
const closeColorPicker = (accept) => {
if (accept) {
let newColor = $('#mycolorpickerpreview').css('background-color');
@ -556,55 +519,46 @@ const closeColorPicker = (accept) => {
delete (parts[0]);
for (let i = 1; i <= 3; ++i) {
parts[i] = parseInt(parts[i]).toString(16);
if (parts[i].length === 1) parts[i] = `0${parts[i]}`;
if (parts[i].length === 1)
parts[i] = `0${parts[i]}`;
}
newColor = `#${parts.join('')}`; // "0070ff"
}
myUserInfo.colorId = newColor;
pad.notifyChangeColor(newColor);
paduserlist.renderMyUserInfo();
} else {
}
else {
// pad.notifyChangeColor(previousColorId);
// paduserlist.renderMyUserInfo();
}
colorPickerOpen = false;
$('#mycolorpicker').removeClass('popup-show');
};
const showColorPicker = () => {
$.farbtastic('#colorpicker').setColor(myUserInfo.colorId);
if (!colorPickerOpen) {
const palette = pad.getColorPalette();
if (!colorPickerSetup) {
const colorsList = $('#colorpickerswatches');
for (let i = 0; i < palette.length; i++) {
const li = $('<li>', {
style: `background: ${palette[i]};`,
});
li.appendTo(colorsList);
li.bind('click', (event) => {
$('#colorpickerswatches li').removeClass('picked');
$(event.target).addClass('picked');
const newColorId = getColorPickerSwatchIndex($('#colorpickerswatches .picked'));
pad.notifyChangeColor(newColorId);
});
}
colorPickerSetup = true;
}
$('#mycolorpicker').addClass('popup-show');
colorPickerOpen = true;
$('#colorpickerswatches li').removeClass('picked');
$($('#colorpickerswatches li')[myUserInfo.colorId]).addClass('picked'); // seems weird
}
};
exports.paduserlist = paduserlist;
export { paduserlist };

View file

@ -1,32 +1,6 @@
import * as pluginUtils from "./shared.js";
import * as defs from "./plugin_defs.js";
'use strict';
const pluginUtils = require('./shared');
const defs = require('./plugin_defs');
exports.baseURL = '';
exports.ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
exports.update = (cb) => {
// It appears that this response (see #620) may interrupt the current thread
// of execution on Firefox. This schedules the response in the run-loop,
// which appears to fix the issue.
const callback = () => setTimeout(cb, 0);
jQuery.getJSON(
`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`
).done((data) => {
defs.plugins = data.plugins;
defs.parts = data.parts;
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');
defs.loaded = true;
callback();
}).fail((err) => {
console.error(`Failed to load plugin-definitions: ${err}`);
callback();
});
};
const adoptPluginsFromAncestorsOf = (frame) => {
// Bind plugins with parent;
let parentRequire = null;
@ -37,22 +11,42 @@ const adoptPluginsFromAncestorsOf = (frame) => {
break;
}
}
} catch (error) {
}
catch (error) {
// Silence (this can only be a XDomain issue).
console.error(error);
}
if (!parentRequire) throw new Error('Parent plugins could not be found.');
if (!parentRequire)
throw new Error('Parent plugins could not be found.');
const ancestorPluginDefs = parentRequire('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
defs.hooks = ancestorPluginDefs.hooks;
defs.loaded = ancestorPluginDefs.loaded;
defs.parts = ancestorPluginDefs.parts;
defs.plugins = ancestorPluginDefs.plugins;
const ancestorPlugins = parentRequire('ep_etherpad-lite/static/js/pluginfw/client_plugins');
exports.baseURL = ancestorPlugins.baseURL;
exports.ensure = ancestorPlugins.ensure;
exports.update = ancestorPlugins.update;
ancestorPlugins.baseURL;
ancestorPlugins.ensure;
ancestorPlugins.update;
};
exports.adoptPluginsFromAncestorsOf = adoptPluginsFromAncestorsOf;
export const baseURL = '';
export const ensure = (cb) => !defs.loaded ? exports.update(cb) : cb();
export const update = (cb) => {
// It appears that this response (see #620) may interrupt the current thread
// of execution on Firefox. This schedules the response in the run-loop,
// which appears to fix the issue.
const callback = () => setTimeout(cb, 0);
jQuery.getJSON(`${exports.baseURL}pluginfw/plugin-definitions.json?v=${clientVars.randomVersionString}`).done((data) => {
defs.plugins = data.plugins;
defs.parts = data.parts;
defs.hooks = pluginUtils.extractHooks(defs.parts, 'client_hooks');
defs.loaded = true;
callback();
}).fail((err) => {
console.error(`Failed to load plugin-definitions: ${err}`);
callback();
});
};
export { adoptPluginsFromAncestorsOf as baseURL };
export { adoptPluginsFromAncestorsOf as ensure };
export { adoptPluginsFromAncestorsOf as update };
export { adoptPluginsFromAncestorsOf };

View file

@ -2,4 +2,4 @@
// Provides a require'able version of jQuery without leaking $ and jQuery;
window.$ = require('./vendors/jquery');
const jq = window.$.noConflict(true);
exports.jQuery = exports.$ = jq;
export { jq as $ };

View file

@ -1,36 +1,23 @@
import * as caretPosition from "./caretPosition.js";
'use strict';
/*
This file handles scroll on edition or when user presses arrow keys.
In this file we have two representations of line (browser and rep line).
Rep Line = a line in the way is represented by Etherpad(rep) (each <div> is a line)
Browser Line = each vertical line. A <div> can be break into more than one
browser line.
*/
const caretPosition = require('./caretPosition');
function Scroll(outerWin) {
// scroll settings
this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport;
// DOM reference
this.outerWin = outerWin;
this.doc = this.outerWin.document;
this.rootDocument = parent.parent.document;
}
Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary =
function (rep, isScrollableEvent, innerHeight) {
// are we placing the caret on the line at the bottom of viewport?
// And if so, do we need to scroll the editor, as defined on the settings.json?
const shouldScrollWhenCaretIsAtBottomOfViewport =
this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
const shouldScrollWhenCaretIsAtBottomOfViewport = this.scrollSettings.scrollWhenCaretIsInTheLastLineOfViewport;
if (shouldScrollWhenCaretIsAtBottomOfViewport) {
// avoid scrolling when selection includes multiple lines --
// user can potentially be selecting more lines
// than it fits on viewport
const multipleLinesSelected = rep.selStart[0] !== rep.selEnd[0];
// avoid scrolling when pad loads
if (isScrollableEvent && !multipleLinesSelected && this._isCaretAtTheBottomOfViewport(rep)) {
// when scrollWhenFocusLineIsOutOfViewport.percentage is 0, pixelsToScroll is 0
@ -39,21 +26,19 @@ Scroll.prototype.scrollWhenCaretIsInTheLastLineOfViewportWhenNecessary =
}
}
};
Scroll.prototype.scrollWhenPressArrowKeys = function (arrowUp, rep, innerHeight) {
// if percentageScrollArrowUp is 0, let the scroll to be handled as default, put the previous
// rep line on the top of the viewport
if (this._arrowUpWasPressedInTheFirstLineOfTheViewport(arrowUp, rep)) {
const pixelsToScroll = this._getPixelsToScrollWhenUserPressesArrowUp(innerHeight);
// by default, the browser scrolls to the middle of the viewport. To avoid the twist made
// when we apply a second scroll, we made it immediately (without animation)
this._scrollYPageWithoutAnimation(-pixelsToScroll);
} else {
}
else {
this.scrollNodeVerticallyIntoView(rep, innerHeight);
}
};
// Some plugins might set a minimum height to the editor (ex: ep_page_view), so checking
// if (caretLine() === rep.lines.length() - 1) is not enough. We need to check if there are
// other lines after caretLine(), and all of them are out of viewport.
@ -65,10 +50,8 @@ Scroll.prototype._isCaretAtTheBottomOfViewport = function (rep) {
const caretLine = rep.selStart[0];
const lineAfterCaretLine = caretLine + 1;
const firstLineVisibleAfterCaretLine = caretPosition.getNextVisibleLine(lineAfterCaretLine, rep);
const caretLineIsPartiallyVisibleOnViewport =
this._isLinePartiallyVisibleOnViewport(caretLine, rep);
const lineAfterCaretLineIsPartiallyVisibleOnViewport =
this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
const caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
const lineAfterCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleAfterCaretLine, rep);
if (caretLineIsPartiallyVisibleOnViewport || lineAfterCaretLineIsPartiallyVisibleOnViewport) {
// check if the caret is in the bottom of the viewport
const caretLinePosition = caretPosition.getPosition();
@ -79,7 +62,6 @@ Scroll.prototype._isCaretAtTheBottomOfViewport = function (rep) {
}
return false;
};
Scroll.prototype._isLinePartiallyVisibleOnViewport = function (lineNumber, rep) {
const lineNode = rep.lines.atIndex(lineNumber);
const linePosition = this._getLineEntryTopBottom(lineNode);
@ -88,48 +70,40 @@ Scroll.prototype._isLinePartiallyVisibleOnViewport = function (lineNumber, rep)
const viewport = this._getViewPortTopBottom();
const viewportBottom = viewport.bottom;
const viewportTop = viewport.top;
const topOfLineIsAboveOfViewportBottom = lineTop < viewportBottom;
const bottomOfLineIsOnOrBelowOfViewportBottom = lineBottom >= viewportBottom;
const topOfLineIsBelowViewportTop = lineTop >= viewportTop;
const topOfLineIsAboveViewportBottom = lineTop <= viewportBottom;
const bottomOfLineIsAboveViewportBottom = lineBottom <= viewportBottom;
const bottomOfLineIsBelowViewportTop = lineBottom >= viewportTop;
return (topOfLineIsAboveOfViewportBottom && bottomOfLineIsOnOrBelowOfViewportBottom) ||
(topOfLineIsBelowViewportTop && topOfLineIsAboveViewportBottom) ||
(bottomOfLineIsAboveViewportBottom && bottomOfLineIsBelowViewportTop);
};
Scroll.prototype._getViewPortTopBottom = function () {
const theTop = this.getScrollY();
const doc = this.doc;
const height = doc.documentElement.clientHeight; // includes padding
// we have to get the exactly height of the viewport.
// So it has to subtract all the values which changes
// the viewport height (E.g. padding, position top)
const viewportExtraSpacesAndPosition =
this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
const viewportExtraSpacesAndPosition = this._getEditorPositionTop() + this._getPaddingTopAddedWhenPageViewIsEnable();
return {
top: theTop,
bottom: (theTop + height - viewportExtraSpacesAndPosition),
};
};
Scroll.prototype._getEditorPositionTop = function () {
const editor = parent.document.getElementsByTagName('iframe');
const editorPositionTop = editor[0].offsetTop;
return editorPositionTop;
};
// ep_page_view adds padding-top, which makes the viewport smaller
Scroll.prototype._getPaddingTopAddedWhenPageViewIsEnable = function () {
const aceOuter = this.rootDocument.getElementsByName('ace_outer');
const aceOuterPaddingTop = parseInt($(aceOuter).css('padding-top'));
return aceOuterPaddingTop;
};
Scroll.prototype._getScrollXY = function () {
const win = this.outerWin;
const odoc = this.doc;
@ -147,36 +121,27 @@ Scroll.prototype._getScrollXY = function () {
};
}
};
Scroll.prototype.getScrollX = function () {
return this._getScrollXY().x;
};
Scroll.prototype.getScrollY = function () {
return this._getScrollXY().y;
};
Scroll.prototype.setScrollX = function (x) {
this.outerWin.scrollTo(x, this.getScrollY());
};
Scroll.prototype.setScrollY = function (y) {
this.outerWin.scrollTo(this.getScrollX(), y);
};
Scroll.prototype.setScrollXY = function (x, y) {
this.outerWin.scrollTo(x, y);
};
Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) {
const caretLine = rep.selStart[0];
const linePrevCaretLine = caretLine - 1;
const firstLineVisibleBeforeCaretLine =
caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep);
const caretLineIsPartiallyVisibleOnViewport =
this._isLinePartiallyVisibleOnViewport(caretLine, rep);
const lineBeforeCaretLineIsPartiallyVisibleOnViewport =
this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
const firstLineVisibleBeforeCaretLine = caretPosition.getPreviousVisibleLine(linePrevCaretLine, rep);
const caretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(caretLine, rep);
const lineBeforeCaretLineIsPartiallyVisibleOnViewport = this._isLinePartiallyVisibleOnViewport(firstLineVisibleBeforeCaretLine, rep);
if (caretLineIsPartiallyVisibleOnViewport || lineBeforeCaretLineIsPartiallyVisibleOnViewport) {
const caretLinePosition = caretPosition.getPosition(); // get the position of the browser line
const viewportPosition = this._getViewPortTopBottom();
@ -184,8 +149,7 @@ Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) {
const viewportBottom = viewportPosition.bottom;
const caretLineIsBelowViewportTop = caretLinePosition.bottom >= viewportTop;
const caretLineIsAboveViewportBottom = caretLinePosition.top < viewportBottom;
const caretLineIsInsideOfViewport =
caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
const caretLineIsInsideOfViewport = caretLineIsBelowViewportTop && caretLineIsAboveViewportBottom;
if (caretLineIsInsideOfViewport) {
const prevLineTop = caretPosition.getPositionTopOfPreviousBrowserLine(caretLinePosition, rep);
const previousLineIsAboveViewportTop = prevLineTop < viewportTop;
@ -194,7 +158,6 @@ Scroll.prototype._isCaretAtTheTopOfViewport = function (rep) {
}
return false;
};
// By default, when user makes an edition in a line out of viewport, this line goes
// to the edge of viewport. This function gets the extra pixels necessary to get the
// caret line in a position X relative to Y% viewport.
@ -207,7 +170,6 @@ Scroll.prototype._getPixelsRelativeToPercentageOfViewport =
}
return pixels;
};
// we use different percentages when change selection. It depends on if it is
// either above the top or below the bottom of the page
Scroll.prototype._getPercentageToScroll = function (aboveOfViewport) {
@ -217,7 +179,6 @@ Scroll.prototype._getPercentageToScroll = function (aboveOfViewport) {
}
return percentageToScroll;
};
Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeight) {
let pixels = 0;
const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
@ -226,35 +187,28 @@ Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeigh
}
return pixels;
};
Scroll.prototype._scrollYPage = function (pixelsToScroll) {
const durationOfAnimationToShowFocusline = this.scrollSettings.duration;
if (durationOfAnimationToShowFocusline) {
this._scrollYPageWithAnimation(pixelsToScroll, durationOfAnimationToShowFocusline);
} else {
}
else {
this._scrollYPageWithoutAnimation(pixelsToScroll);
}
};
Scroll.prototype._scrollYPageWithoutAnimation = function (pixelsToScroll) {
this.outerWin.scrollBy(0, pixelsToScroll);
};
Scroll.prototype._scrollYPageWithAnimation =
function (pixelsToScroll, durationOfAnimationToShowFocusline) {
const outerDocBody = this.doc.getElementById('outerdocbody');
// it works on later versions of Chrome
const $outerDocBody = $(outerDocBody);
this._triggerScrollWithAnimation(
$outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
this._triggerScrollWithAnimation($outerDocBody, pixelsToScroll, durationOfAnimationToShowFocusline);
// it works on Firefox and earlier versions of Chrome
const $outerDocBodyParent = $outerDocBody.parent();
this._triggerScrollWithAnimation(
$outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
this._triggerScrollWithAnimation($outerDocBodyParent, pixelsToScroll, durationOfAnimationToShowFocusline);
};
// using a custom queue and clearing it, we avoid creating a queue of scroll animations.
// So if this function is called twice quickly, only the last one runs.
Scroll.prototype._triggerScrollWithAnimation =
@ -268,14 +222,12 @@ Scroll.prototype._triggerScrollWithAnimation =
queue: 'scrollanimation',
}).dequeue('scrollanimation');
};
// scrollAmountWhenFocusLineIsOutOfViewport is set to 0 (default), scroll it the minimum distance
// needed to be completely in view. If the value is greater than 0 and less than or equal to 1,
// besides of scrolling the minimum needed to be visible, it scrolls additionally
// (viewport height * scrollAmountWhenFocusLineIsOutOfViewport) pixels
Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
const viewport = this._getViewPortTopBottom();
// when the selection changes outside of the viewport the browser automatically scrolls the line
// to inside of the viewport. Tested on IE, Firefox, Chrome in releases from 2015 until now
// So, when the line scrolled gets outside of the viewport we let the browser handle it.
@ -286,10 +238,10 @@ Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
const caretIsAboveOfViewport = distanceOfTopOfViewport < 0;
const caretIsBelowOfViewport = distanceOfBottomOfViewport < 0;
if (caretIsAboveOfViewport) {
const pixelsToScroll =
distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
const pixelsToScroll = distanceOfTopOfViewport - this._getPixelsRelativeToPercentageOfViewport(innerHeight, true);
this._scrollYPage(pixelsToScroll);
} else if (caretIsBelowOfViewport) {
}
else if (caretIsBelowOfViewport) {
// setTimeout is required here as line might not be fully rendered onto the pad
setTimeout(() => {
const outer = window.parent;
@ -302,17 +254,14 @@ Scroll.prototype.scrollNodeVerticallyIntoView = function (rep, innerHeight) {
}
}
};
Scroll.prototype._partOfRepLineIsOutOfViewport = function (viewportPosition, rep) {
const focusLine = (rep.selFocusAtStart ? rep.selStart[0] : rep.selEnd[0]);
const line = rep.lines.atIndex(focusLine);
const linePosition = this._getLineEntryTopBottom(line);
const lineIsAboveOfViewport = linePosition.top < viewportPosition.top;
const lineIsBelowOfViewport = linePosition.bottom > viewportPosition.bottom;
return lineIsBelowOfViewport || lineIsAboveOfViewport;
};
Scroll.prototype._getLineEntryTopBottom = function (entry, destObj) {
const dom = entry.lineNode;
const top = dom.offsetTop;
@ -322,12 +271,10 @@ Scroll.prototype._getLineEntryTopBottom = function (entry, destObj) {
obj.bottom = (top + height);
return obj;
};
Scroll.prototype._arrowUpWasPressedInTheFirstLineOfTheViewport = function (arrowUp, rep) {
const percentageScrollArrowUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp;
return percentageScrollArrowUp && arrowUp && this._isCaretAtTheTopOfViewport(rep);
};
Scroll.prototype.getVisibleLineRange = function (rep) {
const viewport = this._getViewPortTopBottom();
// console.log("viewport top/bottom: %o", viewport);
@ -338,14 +285,13 @@ Scroll.prototype.getVisibleLineRange = function (rep) {
// the viewport. That is the first line that is below the viewport bottom.
// So the line that is in the bottom of the viewport is the very previous one.
let end = rep.lines.search((e) => self._getLineEntryTopBottom(e, obj).top >= viewport.bottom);
if (end < start) end = start; // unlikely
if (end < start)
end = start; // unlikely
// top.console.log(start+","+(end -1));
return [start, end - 1];
};
Scroll.prototype.getVisibleCharRange = function (rep) {
const lineRange = this.getVisibleLineRange(rep);
return [rep.lines.offsetOfIndex(lineRange[0]), rep.lines.offsetOfIndex(lineRange[1])];
};
exports.init = (outerWin) => new Scroll(outerWin);
export const init = (outerWin) => new Scroll(outerWin);

View file

@ -1,19 +1,2 @@
'use strict';
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = require('security');
export * from "security";

View file

@ -1,12 +1,9 @@
'use strict';
// Specific hash to display the skin variants builder popup
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
$('#skin-variants').addClass('popup-show');
const containers = ['editor', 'background', 'toolbar'];
const colors = ['super-light', 'light', 'dark', 'super-dark'];
// add corresponding classes when config change
const updateSkinVariantsClasses = () => {
const domsToUpdate = [
@ -19,20 +16,16 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
domsToUpdate.forEach((el) => { el.removeClass(`${color}-${container}`); });
});
});
domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); });
const newClasses = [];
$('select.skin-variant-color').each(function () {
newClasses.push(`${$(this).val()}-${$(this).data('container')}`);
});
if ($('#skin-variant-full-width').is(':checked')) newClasses.push('full-width-editor');
if ($('#skin-variant-full-width').is(':checked'))
newClasses.push('full-width-editor');
domsToUpdate.forEach((el) => { el.addClass(newClasses.join(' ')); });
$('#skin-variants-result').val(`"skinVariants": "${newClasses.join(' ')}",`);
};
// run on init
const updateCheckboxFromSkinClasses = () => {
$('html').attr('class').split(' ').forEach((classItem) => {
@ -42,14 +35,11 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
$(`.skin-variant-color[data-container="${container}"`).val(color);
}
});
$('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor'));
};
$('.skin-variant').change(() => {
updateSkinVariantsClasses();
});
updateCheckboxFromSkinClasses();
updateSkinVariantsClasses();
}

View file

@ -1,11 +1,9 @@
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
@ -21,9 +19,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const _entryWidth = (e) => (e && e.width) || 0;
class Node {
constructor(entry, levels = 0, downSkips = 1, downSkipWidths = 0) {
this.key = entry != null ? entry.key : null;
@ -34,7 +30,6 @@ class Node {
this.downSkips = Array(levels).fill(downSkips);
this.downSkipWidths = Array(levels).fill(downSkipWidths);
}
propagateWidthChange() {
const oldWidth = this.downSkipWidths[0];
const newWidth = _entryWidth(this.entry);
@ -51,7 +46,6 @@ class Node {
return widthChange;
}
}
// A "point" object at index x allows modifications immediately after the first x elements of the
// skiplist, such as multiple inserts or deletes. After an insert or delete using point P, the point
// is still valid and points to the same index in the skiplist. Other operations with other points
@ -89,24 +83,21 @@ class Point {
this.nodes = nodes;
this.widthSkips = widthSkips;
}
toString() {
return `Point(${this.loc})`;
}
insert(entry) {
if (entry.key == null) throw new Error('entry.key must not be null');
if (entry.key == null)
throw new Error('entry.key must not be null');
if (this._skipList.containsKey(entry.key)) {
throw new Error(`an entry with key ${entry.key} already exists`);
}
const newNode = new Node(entry);
const pNodes = this.nodes;
const pIdxs = this.idxs;
const pLoc = this.loc;
const widthLoc = this.widthSkips[0] + this.nodes[0].downSkipWidths[0];
const newWidth = _entryWidth(entry);
// The new node will have at least level 1
// With a proability of 0.01^(n-1) the nodes level will be >= n
while (newNode.levels === 0 || Math.random() < 0.01) {
@ -149,7 +140,6 @@ class Point {
this._skipList._keyToNodeMap.set(newNode.key, newNode);
this._skipList._totalWidth += newWidth;
}
delete() {
const elem = this.nodes[0].downPtrs[0];
const elemWidth = _entryWidth(elem.entry);
@ -163,7 +153,8 @@ class Point {
up.downSkips[i] = totalSkip;
const totalWidthSkip = up.downSkipWidths[i] + elem.downSkipWidths[i] - elemWidth;
up.downSkipWidths[i] = totalWidthSkip;
} else {
}
else {
const up = this.nodes[i];
up.downSkips[i]--;
up.downSkipWidths[i] -= elemWidth;
@ -172,12 +163,10 @@ class Point {
this._skipList._keyToNodeMap.delete(elem.key);
this._skipList._totalWidth -= elemWidth;
}
getNode() {
return this.nodes[0].downPtrs[0];
}
}
/**
* The skip-list contains "entries", JavaScript objects that each must have a unique "key"
* property that is a string.
@ -192,7 +181,6 @@ class SkipList {
this._start.downPtrs[0] = this._end;
this._end.upPtrs[0] = this._start;
}
_getNodeAtOffset(targetOffset) {
let i = 0;
let n = this._start;
@ -204,25 +192,26 @@ class SkipList {
}
lvl--;
}
if (n === this._start) return (this._start.downPtrs[0] || null);
if (n === this._start)
return (this._start.downPtrs[0] || null);
if (n === this._end) {
return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null;
}
return n;
}
_getNodeIndex(node, byWidth) {
let dist = (byWidth ? 0 : -1);
let n = node;
while (n !== this._start) {
const lvl = n.levels - 1;
n = n.upPtrs[lvl];
if (byWidth) dist += n.downSkipWidths[lvl];
else dist += n.downSkips[lvl];
if (byWidth)
dist += n.downSkipWidths[lvl];
else
dist += n.downSkips[lvl];
}
return dist;
}
// Returns index of first entry such that entryFunc(entry) is truthy,
// or length() if no such entry. Assumes all falsy entries come before
// all truthy entries.
@ -230,13 +219,14 @@ class SkipList {
let low = this._start;
let lvl = this._start.levels - 1;
let lowIndex = -1;
const f = (node) => {
if (node === this._start) return false;
else if (node === this._end) return true;
else return entryFunc(node.entry);
if (node === this._start)
return false;
else if (node === this._end)
return true;
else
return entryFunc(node.entry);
};
while (lvl >= 0) {
let nextLow = low.downPtrs[lvl];
while (!f(nextLow)) {
@ -248,50 +238,56 @@ class SkipList {
}
return lowIndex + 1;
}
length() { return this._keyToNodeMap.size; }
atIndex(i) {
if (i < 0) console.warn(`atIndex(${i})`);
if (i >= this._keyToNodeMap.size) console.warn(`atIndex(${i}>=${this._keyToNodeMap.size})`);
if (i < 0)
console.warn(`atIndex(${i})`);
if (i >= this._keyToNodeMap.size)
console.warn(`atIndex(${i}>=${this._keyToNodeMap.size})`);
return (new Point(this, i)).getNode().entry;
}
// differs from Array.splice() in that new elements are in an array, not varargs
splice(start, deleteCount, newEntryArray) {
if (start < 0) console.warn(`splice(${start}, ...)`);
if (start < 0)
console.warn(`splice(${start}, ...)`);
if (start + deleteCount > this._keyToNodeMap.size) {
console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._keyToNodeMap.size}`);
console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._keyToNodeMap.size);
console.trace();
}
if (!newEntryArray) newEntryArray = [];
if (!newEntryArray)
newEntryArray = [];
const pt = new Point(this, start);
for (let i = 0; i < deleteCount; i++) pt.delete();
for (let i = 0; i < deleteCount; i++)
pt.delete();
for (let i = (newEntryArray.length - 1); i >= 0; i--) {
const entry = newEntryArray[i];
pt.insert(entry);
}
}
next(entry) { return this._keyToNodeMap.get(entry.key).downPtrs[0].entry || null; }
prev(entry) { return this._keyToNodeMap.get(entry.key).upPtrs[0].entry || null; }
push(entry) { this.splice(this._keyToNodeMap.size, 0, [entry]); }
slice(start, end) {
// act like Array.slice()
if (start === undefined) start = 0;
else if (start < 0) start += this._keyToNodeMap.size;
if (end === undefined) end = this._keyToNodeMap.size;
else if (end < 0) end += this._keyToNodeMap.size;
if (start < 0) start = 0;
if (start > this._keyToNodeMap.size) start = this._keyToNodeMap.size;
if (end < 0) end = 0;
if (end > this._keyToNodeMap.size) end = this._keyToNodeMap.size;
if (end <= start) return [];
if (start === undefined)
start = 0;
else if (start < 0)
start += this._keyToNodeMap.size;
if (end === undefined)
end = this._keyToNodeMap.size;
else if (end < 0)
end += this._keyToNodeMap.size;
if (start < 0)
start = 0;
if (start > this._keyToNodeMap.size)
start = this._keyToNodeMap.size;
if (end < 0)
end = 0;
if (end > this._keyToNodeMap.size)
end = this._keyToNodeMap.size;
if (end <= start)
return [];
let n = this.atIndex(start);
const array = [n];
for (let i = 1; i < (end - start); i++) {
@ -300,7 +296,6 @@ class SkipList {
}
return array;
}
atKey(key) { return this._keyToNodeMap.get(key).entry; }
indexOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key)); }
indexOfEntry(entry) { return this.indexOfKey(entry.key); }
@ -316,15 +311,18 @@ class SkipList {
}
totalWidth() { return this._totalWidth; }
offsetOfIndex(i) {
if (i < 0) return 0;
if (i >= this._keyToNodeMap.size) return this._totalWidth;
if (i < 0)
return 0;
if (i >= this._keyToNodeMap.size)
return this._totalWidth;
return this.offsetOfEntry(this.atIndex(i));
}
indexOfOffset(offset) {
if (offset <= 0) return 0;
if (offset >= this._totalWidth) return this._keyToNodeMap.size;
if (offset <= 0)
return 0;
if (offset >= this._totalWidth)
return this._keyToNodeMap.size;
return this.indexOfEntry(this.atOffset(offset));
}
}
module.exports = SkipList;
export default SkipList;

View file

@ -1,5 +1,4 @@
'use strict';
/**
* Creates a socket.io connection.
* @param etherpadBaseUrl - Etherpad URL. If relative, it is assumed to be relative to
@ -19,11 +18,11 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
const baseUrl = new URL(etherpadBaseUrl, window.location);
const socketioUrl = new URL('socket.io', baseUrl);
const namespaceUrl = new URL(namespace, new URL('/', baseUrl));
return io(namespaceUrl.href, Object.assign({path: socketioUrl.pathname}, options));
return io(namespaceUrl.href, Object.assign({ path: socketioUrl.pathname }, options));
};
if (typeof exports === 'object') {
exports.connect = connect;
} else {
window.socketio = {connect};
}
else {
window.socketio = { connect };
}
export { connect };

View file

@ -1,102 +1,69 @@
import "./vendors/jquery.js";
import * as padUtils from "./pad_utils.js";
import * as hooks from "./pluginfw/hooks.js";
import * as socketio from "./socketio.js";
import { loadBroadcastRevisionsJS } from "./broadcast_revisions.js";
import { padimpexp } from "./pad_impexp.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins.
require('./vendors/jquery');
const Cookies = require('./pad_utils').Cookies;
const randomString = require('./pad_utils').randomString;
const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils;
const socketio = require('./socketio');
const Cookies = { Cookies: padUtils }.Cookies;
const randomString = { randomString: padUtils }.randomString;
const padutils = { padutils: padUtils }.padutils;
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
const init = () => {
padutils.setupGlobalExceptionHandler();
$(document).ready(() => {
// start the custom js
if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef
if (typeof customStart === 'function')
customStart(); // eslint-disable-line no-undef
// get the padId out of the url
const urlParts = document.location.pathname.split('/');
padId = decodeURIComponent(urlParts[urlParts.length - 2]);
// set the title
document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`;
// ensure we have a token
token = Cookies.get('token');
if (token == null) {
token = `t.${randomString()}`;
Cookies.set('token', token, {expires: 60});
Cookies.set('token', token, { expires: 60 });
}
socket = socketio.connect(exports.baseURL, '/', {query: {padId}});
socket = socketio.connect(exports.baseURL, '/', { query: { padId } });
// send the ready message once we're connected
socket.on('connect', () => {
sendSocketMsg('CLIENT_READY', {});
});
socket.on('disconnect', (reason) => {
BroadcastSlider.showReconnectUI();
// The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect".
if (reason === 'io server disconnect') socket.connect();
if (reason === 'io server disconnect')
socket.connect();
});
// route the incoming messages
socket.on('message', (message) => {
if (message.type === 'CLIENT_VARS') {
handleClientVars(message);
} else if (message.accessStatus) {
}
else if (message.accessStatus) {
$('body').html('<h2>You have no permission to access this pad</h2>');
} else if (message.type === 'CHANGESET_REQ' || message.type === 'COLLABROOM') {
}
else if (message.type === 'CHANGESET_REQ' || message.type === 'COLLABROOM') {
changesetLoader.handleMessageFromServer(message);
}
});
// get all the export links
exportLinks = $('#export > .exportlink');
$('button#forcereconnect').click(() => {
window.location.reload();
});
exports.socket = socket; // make the socket available
exports.BroadcastSlider = BroadcastSlider; // Make the slider available
socket; // make the socket available
BroadcastSlider; // Make the slider available
hooks.aCallAll('postTimesliderInit');
});
};
// sends a message over the socket
const sendSocketMsg = (type, data) => {
socket.json.send({
component: 'pad', // FIXME: Remove this stupidity!
component: 'pad',
type,
data,
padId,
@ -104,33 +71,24 @@ const sendSocketMsg = (type, data) => {
sessionID: Cookies.get('sessionID'),
});
};
const fireWhenAllScriptsAreLoaded = [];
const handleClientVars = (message) => {
// save the client Vars
window.clientVars = message.data;
if (window.clientVars.sessionRefreshInterval) {
const ping =
() => $.ajax('../../_extendExpressSessionLifetime', {method: 'PUT'}).catch(() => {});
const ping = () => $.ajax('../../_extendExpressSessionLifetime', { method: 'PUT' }).catch(() => { });
setInterval(ping, window.clientVars.sessionRefreshInterval);
}
// load all script that doesn't work without the clientVars
BroadcastSlider = require('./broadcast_slider')
.loadBroadcastSliderJS(fireWhenAllScriptsAreLoaded);
require('./broadcast_revisions').loadBroadcastRevisionsJS();
({ loadBroadcastRevisionsJS }.loadBroadcastRevisionsJS());
changesetLoader = require('./broadcast')
.loadBroadcastJS(socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider);
// initialize export ui
require('./pad_impexp').padimpexp.init();
({ padimpexp }.padimpexp.init());
// Create a base URI used for timeslider exports
const baseURI = document.location.pathname;
// change export urls when the slider moves
BroadcastSlider.onSlider((revno) => {
// exportLinks is a jQuery Array, so .each is allowed.
@ -146,23 +104,21 @@ const handleClientVars = (message) => {
}
});
});
// fire all start functions of these scripts, formerly fired with window.load
for (let i = 0; i < fireWhenAllScriptsAreLoaded.length; i++) {
fireWhenAllScriptsAreLoaded[i]();
}
$('#ui-slider-handle').css('left', $('#ui-slider-bar').width() - 2);
// Translate some strings where we only want to set the title not the actual values
$('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause'));
$('#leftstep').attr('title', html10n.get('timeslider.backRevision'));
$('#rightstep').attr('title', html10n.get('timeslider.forwardRevision'));
// font family change
$('#viewfontmenu').change(function () {
$('#innerdocbody').css('font-family', $(this).val() || '');
});
};
exports.baseURL = '';
exports.init = init;
export const baseURL = '';
export { init as socket };
export { init as BroadcastSlider };
export { init };

View file

@ -1,3 +1,2 @@
'use strict';
module.exports = require('underscore');
export { default } from "underscore";

View file

@ -1,30 +1,6 @@
import * as Changeset from "./Changeset.js";
import * as _ from "./underscore.js";
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const Changeset = require('./Changeset');
const _ = require('./underscore');
const undoModule = (() => {
const stack = (() => {
const stackElements = [];
@ -34,43 +10,36 @@ const undoModule = (() => {
// 2) { elementType: EXTERNAL_CHANGE, changeset: <changeset> }
// invariant: no two consecutive EXTERNAL_CHANGEs
let numUndoableEvents = 0;
const UNDOABLE_EVENT = 'undoableEvent';
const EXTERNAL_CHANGE = 'externalChange';
const clearStack = () => {
stackElements.length = 0;
stackElements.push(
{
stackElements.push({
elementType: UNDOABLE_EVENT,
eventType: 'bottom',
});
numUndoableEvents = 1;
};
clearStack();
const pushEvent = (event) => {
const e = _.extend(
{}, event);
const e = _.extend({}, event);
e.elementType = UNDOABLE_EVENT;
stackElements.push(e);
numUndoableEvents++;
};
const pushExternalChange = (cs) => {
const idx = stackElements.length - 1;
if (stackElements[idx].elementType === EXTERNAL_CHANGE) {
stackElements[idx].changeset =
Changeset.compose(stackElements[idx].changeset, cs, getAPool());
} else {
stackElements.push(
{
}
else {
stackElements.push({
elementType: EXTERNAL_CHANGE,
changeset: cs,
});
}
};
const _exposeEvent = (nthFromTop) => {
// precond: 0 <= nthFromTop < numUndoableEvents
const targetIndex = stackElements.length - 1 - nthFromTop;
@ -101,27 +70,24 @@ const undoModule = (() => {
stackElements.splice(idx - 2, 1);
idx--;
}
} else {
}
else {
idx--;
}
}
};
const getNthFromTop = (n) => {
// precond: 0 <= n < numEvents()
_exposeEvent(n);
return stackElements[stackElements.length - 1 - n];
};
const numEvents = () => numUndoableEvents;
const popEvent = () => {
// precond: numEvents() > 0
_exposeEvent(0);
numUndoableEvents--;
return stackElements.pop();
};
return {
numEvents,
popEvent,
@ -131,15 +97,12 @@ const undoModule = (() => {
getNthFromTop,
};
})();
// invariant: stack always has at least one undoable event
let undoPtr = 0; // zero-index from top of stack, 0 == top
const clearHistory = () => {
stack.clearStack();
undoPtr = 0;
};
const _charOccurrences = (str, c) => {
let i = 0;
let count = 0;
@ -152,13 +115,12 @@ const undoModule = (() => {
}
return count;
};
const _opcodeOccurrences = (cs, opcode) => _charOccurrences(Changeset.unpack(cs).ops, opcode);
const _mergeChangesets = (cs1, cs2) => {
if (!cs1) return cs2;
if (!cs2) return cs1;
if (!cs1)
return cs2;
if (!cs2)
return cs1;
// Rough heuristic for whether changesets should be considered one action:
// each does exactly one insertion, no dels, and the composition does also; or
// each does exactly one deletion, no ins, and the composition does also.
@ -176,7 +138,8 @@ const undoModule = (() => {
if (plusCount3 === 1 && minusCount3 === 0) {
return merge;
}
} else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) {
}
else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) {
const merge = Changeset.compose(cs1, cs2, getAPool());
const plusCount3 = _opcodeOccurrences(merge, '+');
const minusCount3 = _opcodeOccurrences(merge, '-');
@ -186,10 +149,8 @@ const undoModule = (() => {
}
return null;
};
const reportEvent = (event) => {
const topEvent = stack.getNthFromTop(0);
const applySelectionToTop = () => {
if ((typeof event.selStart) === 'number') {
topEvent.selStart = event.selStart;
@ -197,10 +158,10 @@ const undoModule = (() => {
topEvent.selFocusAtStart = event.selFocusAtStart;
}
};
if ((!event.backset) || Changeset.isIdentity(event.backset)) {
applySelectionToTop();
} else {
}
else {
let merged = false;
if (topEvent.eventType === event.eventType) {
const merge = _mergeChangesets(event.backset, topEvent.backset);
@ -224,17 +185,16 @@ const undoModule = (() => {
undoPtr = 0;
}
};
const reportExternalChange = (changeset) => {
if (changeset && !Changeset.isIdentity(changeset)) {
stack.pushExternalChange(changeset);
}
};
const _getSelectionInfo = (event) => {
if ((typeof event.selStart) !== 'number') {
return null;
} else {
}
else {
return {
selStart: event.selStart,
selEnd: event.selEnd,
@ -242,13 +202,11 @@ const undoModule = (() => {
};
}
};
// For "undo" and "redo", the change event must be returned
// by eventFunc and NOT reported through the normal mechanism.
// "eventFunc" should take a changeset and an optional selection info object,
// or can be called with no arguments to mean that no undo is possible.
// "eventFunc" will be called exactly once.
const performUndo = (eventFunc) => {
if (undoPtr < stack.numEvents() - 1) {
const backsetEvent = stack.getNthFromTop(undoPtr);
@ -256,9 +214,11 @@ const undoModule = (() => {
const undoEvent = eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.pushEvent(undoEvent);
undoPtr += 2;
} else { eventFunc(); }
}
else {
eventFunc();
}
};
const performRedo = (eventFunc) => {
if (undoPtr >= 2) {
const backsetEvent = stack.getNthFromTop(0);
@ -266,11 +226,12 @@ const undoModule = (() => {
eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.popEvent();
undoPtr -= 2;
} else { eventFunc(); }
}
else {
eventFunc();
}
};
const getAPool = () => undoModule.apool;
return {
clearHistory,
reportEvent,
@ -281,5 +242,4 @@ const undoModule = (() => {
apool: null,
}; // apool is filled in by caller
})();
exports.undoModule = undoModule;
export { undoModule };

View file

@ -1,198 +1,179 @@
// WARNING: This file may have been modified from original.
// TODO: Check requirement of this file, this afaik was to cover weird edge cases
// that have probably been fixed in browsers.
/*!
* Bowser - a browser detector
* https://github.com/ded/bowser
* MIT License | (c) Dustin Diaz 2015
*/
!function (name, definition) {
if (typeof module != 'undefined' && module.exports) module.exports = definition()
else if (typeof define == 'function' && define.amd) define(definition)
else this[name] = definition()
if (typeof module != 'undefined' && module.exports)
;
else if (typeof define == 'function' && define.amd)
define(definition);
else
this[name] = definition();
}('bowser', function () {
/**
* See useragents.js for examples of navigator.userAgent
*/
var t = true
var t = true;
function detect(ua) {
function getFirstMatch(regex) {
var match = ua.match(regex);
return (match && match.length > 1 && match[1]) || '';
}
function getSecondMatch(regex) {
var match = ua.match(regex);
return (match && match.length > 1 && match[2]) || '';
}
var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase()
, likeAndroid = /like android/i.test(ua)
, android = !likeAndroid && /android/i.test(ua)
, chromeos = /CrOS/.test(ua)
, silk = /silk/i.test(ua)
, sailfish = /sailfish/i.test(ua)
, tizen = /tizen/i.test(ua)
, webos = /(web|hpw)os/i.test(ua)
, windowsphone = /windows phone/i.test(ua)
, windows = !windowsphone && /windows/i.test(ua)
, mac = !iosdevice && !silk && /macintosh/i.test(ua)
, linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua)
, edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i)
, versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i)
, tablet = /tablet/i.test(ua)
, mobile = !tablet && /[^-]mobi/i.test(ua)
, result
var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase(), likeAndroid = /like android/i.test(ua), android = !likeAndroid && /android/i.test(ua), chromeos = /CrOS/.test(ua), silk = /silk/i.test(ua), sailfish = /sailfish/i.test(ua), tizen = /tizen/i.test(ua), webos = /(web|hpw)os/i.test(ua), windowsphone = /windows phone/i.test(ua), windows = !windowsphone && /windows/i.test(ua), mac = !iosdevice && !silk && /macintosh/i.test(ua), linux = !android && !sailfish && !tizen && !webos && /linux/i.test(ua), edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i), versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i), tablet = /tablet/i.test(ua), mobile = !tablet && /[^-]mobi/i.test(ua), result;
if (/opera|opr/i.test(ua)) {
result = {
name: 'Opera'
, opera: t
, version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)
}
name: 'Opera',
opera: t,
version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)
};
}
else if (/yabrowser/i.test(ua)) {
result = {
name: 'Yandex Browser'
, yandexbrowser: t
, version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)
}
name: 'Yandex Browser',
yandexbrowser: t,
version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)
};
}
else if (windowsphone) {
result = {
name: 'Windows Phone'
, windowsphone: t
}
name: 'Windows Phone',
windowsphone: t
};
if (edgeVersion) {
result.msedge = t
result.version = edgeVersion
result.msedge = t;
result.version = edgeVersion;
}
else {
result.msie = t
result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i)
result.msie = t;
result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i);
}
}
else if (/msie|trident/i.test(ua)) {
result = {
name: 'Internet Explorer'
, msie: t
, version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i)
name: 'Internet Explorer',
msie: t,
version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i)
};
}
} else if (chromeos) {
else if (chromeos) {
result = {
name: 'Chrome'
, chromeos: t
, chromeBook: t
, chrome: t
, version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
name: 'Chrome',
chromeos: t,
chromeBook: t,
chrome: t,
version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
};
}
} else if (/chrome.+? edge/i.test(ua)) {
else if (/chrome.+? edge/i.test(ua)) {
result = {
name: 'Microsoft Edge'
, msedge: t
, version: edgeVersion
}
name: 'Microsoft Edge',
msedge: t,
version: edgeVersion
};
}
else if (/chrome|crios|crmo/i.test(ua)) {
result = {
name: 'Chrome'
, chrome: t
, version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
}
name: 'Chrome',
chrome: t,
version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
};
}
else if (iosdevice) {
result = {
name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'
}
name: iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'
};
// WTF: version is not part of user agent in web apps
if (versionIdentifier) {
result.version = versionIdentifier
result.version = versionIdentifier;
}
}
else if (sailfish) {
result = {
name: 'Sailfish'
, sailfish: t
, version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i)
}
name: 'Sailfish',
sailfish: t,
version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i)
};
}
else if (/seamonkey\//i.test(ua)) {
result = {
name: 'SeaMonkey'
, seamonkey: t
, version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i)
}
name: 'SeaMonkey',
seamonkey: t,
version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i)
};
}
else if (/firefox|iceweasel/i.test(ua)) {
result = {
name: 'Firefox'
, firefox: t
, version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)
}
name: 'Firefox',
firefox: t,
version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)
};
if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) {
result.firefoxos = t
result.firefoxos = t;
}
}
else if (silk) {
result = {
name: 'Amazon Silk'
, silk: t
, version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
}
name: 'Amazon Silk',
silk: t,
version: getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
};
}
else if (android) {
result = {
name: 'Android'
, version: versionIdentifier
}
name: 'Android',
version: versionIdentifier
};
}
else if (/phantom/i.test(ua)) {
result = {
name: 'PhantomJS'
, phantom: t
, version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
}
name: 'PhantomJS',
phantom: t,
version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
};
}
else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) {
result = {
name: 'BlackBerry'
, blackberry: t
, version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i)
}
name: 'BlackBerry',
blackberry: t,
version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i)
};
}
else if (webos) {
result = {
name: 'WebOS'
, webos: t
, version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)
name: 'WebOS',
webos: t,
version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)
};
/touchpad\//i.test(ua) && (result.touchpad = t)
/touchpad\//i.test(ua) && (result.touchpad = t);
}
else if (/bada/i.test(ua)) {
result = {
name: 'Bada'
, bada: t
, version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i)
name: 'Bada',
bada: t,
version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i)
};
}
else if (tizen) {
result = {
name: 'Tizen'
, tizen: t
, version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
name: 'Tizen',
tizen: t,
version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
};
}
else if (/safari/i.test(ua)) {
result = {
name: 'Safari'
, safari: t
, version: versionIdentifier
}
name: 'Safari',
safari: t,
version: versionIdentifier
};
}
else {
result = {
@ -200,64 +181,71 @@
version: getSecondMatch(/^(.*)\/(.*) /)
};
}
// set webkit or gecko flag for browsers based on these engines
if (!result.msedge && /(apple)?webkit/i.test(ua)) {
result.name = result.name || "Webkit"
result.webkit = t
result.name = result.name || "Webkit";
result.webkit = t;
if (!result.version && versionIdentifier) {
result.version = versionIdentifier
result.version = versionIdentifier;
}
} else if (!result.opera && /gecko\//i.test(ua)) {
result.name = result.name || "Gecko"
result.gecko = t
result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i)
}
else if (!result.opera && /gecko\//i.test(ua)) {
result.name = result.name || "Gecko";
result.gecko = t;
result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i);
}
// set OS flags for platforms that have multiple browsers
if (!result.msedge && (android || result.silk)) {
result.android = t
} else if (iosdevice) {
result[iosdevice] = t
result.ios = t
} else if (windows) {
result.windows = t
} else if (mac) {
result.mac = t
} else if (linux) {
result.linux = t
result.android = t;
}
else if (iosdevice) {
result[iosdevice] = t;
result.ios = t;
}
else if (windows) {
result.windows = t;
}
else if (mac) {
result.mac = t;
}
else if (linux) {
result.linux = t;
}
// OS version extraction
var osVersion = '';
if (result.windowsphone) {
osVersion = getFirstMatch(/windows phone (?:os)?\s?(\d+(\.\d+)*)/i);
} else if (iosdevice) {
}
else if (iosdevice) {
osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i);
osVersion = osVersion.replace(/[_\s]/g, '.');
} else if (android) {
}
else if (android) {
osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i);
} else if (result.webos) {
}
else if (result.webos) {
osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i);
} else if (result.blackberry) {
}
else if (result.blackberry) {
osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i);
} else if (result.bada) {
}
else if (result.bada) {
osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i);
} else if (result.tizen) {
}
else if (result.tizen) {
osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i);
}
if (osVersion) {
result.osversion = osVersion;
}
// device type extraction
var osMajorVersion = osVersion.split('.')[0];
if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) {
result.tablet = t
} else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) {
result.mobile = t
result.tablet = t;
}
else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada) {
result.mobile = t;
}
// Graded Browser Support
// http://developer.yahoo.com/yui/articles/gbs
if (result.msedge ||
@ -268,8 +256,7 @@
(result.safari && result.version >= 6) ||
(result.opera && result.version >= 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] >= 6) ||
(result.blackberry && result.version >= 10.1)
) {
(result.blackberry && result.version >= 10.1)) {
result.a = t;
}
else if ((result.msie && result.version < 10) ||
@ -277,34 +264,31 @@
(result.firefox && result.version < 20.0) ||
(result.safari && result.version < 6) ||
(result.opera && result.version < 10.0) ||
(result.ios && result.osversion && result.osversion.split(".")[0] < 6)
) {
result.c = t
} else result.x = t
return result
(result.ios && result.osversion && result.osversion.split(".")[0] < 6)) {
result.c = t;
}
var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '')
else
result.x = t;
return result;
}
var bowser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '');
bowser.test = function (browserList) {
for (var i = 0; i < browserList.length; ++i) {
var browserItem = browserList[i];
if (typeof browserItem=== 'string') {
if (typeof browserItem === 'string') {
if (browserItem in bowser) {
return true;
}
}
}
return false;
}
};
/*
* Set our detect method to the main bowser object so we can
* reuse it to test other user agents.
* This is needed to implement future tests.
*/
bowser._detect = detect;
return bowser
return bowser;
});
export default definition();

View file

@ -1,6 +1,5 @@
// WARNING: This file has been modified from original.
// TODO: Replace with https://github.com/Simonwep/pickr
// Farbtastic 2.0 alpha
// Original can be found at:
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/src/farbtastic.js
@ -8,25 +7,19 @@
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
(function ($) {
var __debug = false;
var __factor = 1;
$.fn.farbtastic = function (options) {
var __debug = false;
var __factor = 1;
$.fn.farbtastic = function (options) {
$.farbtastic(this, options);
return this;
};
$.farbtastic = function (container, options) {
};
$.farbtastic = function (container, options) {
var container = $(container)[0];
return container.farbtastic || (container.farbtastic = new $._farbtastic(container, options));
}
$._farbtastic = function (container, options) {
};
$._farbtastic = function (container, options) {
var fb = this;
/////////////////////////////////////////////////////
/**
* Link to the given element(s) or callback.
*/
@ -35,10 +28,8 @@ $._farbtastic = function (container, options) {
if (typeof fb.callback == 'object') {
$(fb.callback).unbind('keyup', fb.updateValue);
}
// Reset color
fb.color = null;
// Bind callback or elements
if (typeof callback == 'function') {
fb.callback = callback;
@ -51,13 +42,12 @@ $._farbtastic = function (container, options) {
}
}
return this;
}
};
fb.updateValue = function (event) {
if (this.value && this.value != fb.color) {
fb.setColor(this.value);
}
}
};
/**
* Change color with HTML syntax #123456
*/
@ -70,52 +60,42 @@ $._farbtastic = function (container, options) {
fb.updateDisplay();
}
return this;
}
};
/**
* Change color with HSL triplet [0..1, 0..1, 0..1]
*/
fb.setHSL = function (hsl) {
fb.hsl = hsl;
var convertedHSL = [hsl[0]]
convertedHSL[1] = hsl[1]*__factor+((1-__factor)/2);
convertedHSL[2] = hsl[2]*__factor+((1-__factor)/2);
var convertedHSL = [hsl[0]];
convertedHSL[1] = hsl[1] * __factor + ((1 - __factor) / 2);
convertedHSL[2] = hsl[2] * __factor + ((1 - __factor) / 2);
fb.rgb = fb.HSLToRGB(convertedHSL);
fb.color = fb.pack(fb.rgb);
fb.updateDisplay();
return this;
}
};
/////////////////////////////////////////////////////
/**
* Initialize the color picker widget.
*/
fb.initWidget = function () {
// Insert markup and size accordingly.
var dim = {
width: options.width,
height: options.width
};
$(container)
.html(
'<div class="farbtastic" style="position: relative">' +
.html('<div class="farbtastic" style="position: relative">' +
'<div class="farbtastic-solid"></div>' +
'<canvas class="farbtastic-mask"></canvas>' +
'<canvas class="farbtastic-overlay"></canvas>' +
'</div>'
)
'</div>')
.find('*').attr(dim).css(dim).end()
.find('div>*').css('position', 'absolute');
// IE Fix: Recreate canvas elements with doc.createElement and excanvas.
browser.msie && $('canvas', container).each(function () {
// Fetch info.
var attr = { 'class': $(this).attr('class'), style: this.getAttribute('style') },
e = document.createElement('canvas');
var attr = { 'class': $(this).attr('class'), style: this.getAttribute('style') }, e = document.createElement('canvas');
// Replace element.
$(this).before($(e).attr(attr)).remove();
// Init with explorerCanvas.
@ -124,7 +104,6 @@ $._farbtastic = function (container, options) {
$(e).attr(dim).css(dim).css('position', 'absolute')
.find('*').attr(dim).css(dim);
});
// Determine layout
fb.radius = (options.width - options.wheelWidth) / 2 - 1;
fb.square = Math.floor((fb.radius - options.wheelWidth / 2) * 0.7) - 1;
@ -136,7 +115,6 @@ $._farbtastic = function (container, options) {
left: fb.mid - fb.square,
top: fb.mid - fb.square
});
// Set up drawing context.
fb.cnvMask = $('.farbtastic-mask', container);
fb.ctxMask = fb.cnvMask[0].getContext('2d');
@ -144,12 +122,10 @@ $._farbtastic = function (container, options) {
fb.ctxOverlay = fb.cnvOverlay[0].getContext('2d');
fb.ctxMask.translate(fb.mid, fb.mid);
fb.ctxOverlay.translate(fb.mid, fb.mid);
// Draw widget base layers.
fb.drawCircle();
fb.drawMask();
}
};
/**
* Draw the color wheel.
*/
@ -157,19 +133,14 @@ $._farbtastic = function (container, options) {
var tm = +(new Date());
// Draw a hue circle with a bunch of gradient-stroked beziers.
// Have to use beziers, as gradient-stroked arcs don't work.
var n = 24,
r = fb.radius,
w = options.wheelWidth,
nudge = 8 / r / n * Math.PI, // Fudge factor for seams.
m = fb.ctxMask,
angle1 = 0, color1, d1;
var n = 24, r = fb.radius, w = options.wheelWidth, nudge = 8 / r / n * Math.PI, // Fudge factor for seams.
m = fb.ctxMask, angle1 = 0, color1, d1;
m.save();
m.lineWidth = w / r;
m.scale(r, r);
// Each segment goes from angle1 to angle2.
for (var i = 0; i <= n; ++i) {
var d2 = i / n,
angle2 = d2 * Math.PI * 2,
var d2 = i / n, angle2 = d2 * Math.PI * 2,
// Endpoints
x1 = Math.sin(angle1), y1 = -Math.cos(angle1);
x2 = Math.sin(angle2), y2 = -Math.cos(angle2),
@ -213,18 +184,18 @@ $._farbtastic = function (container, options) {
}
}
// Prevent seams where curves join.
angle1 = angle2 - nudge; color1 = color2; d1 = d2;
angle1 = angle2 - nudge;
color1 = color2;
d1 = d2;
}
m.restore();
__debug && $('body').append('<div>drawCircle '+ (+(new Date()) - tm) +'ms');
__debug && $('body').append('<div>drawCircle ' + (+(new Date()) - tm) + 'ms');
};
/**
* Draw the saturation/luminance mask.
*/
fb.drawMask = function () {
var tm = +(new Date());
// Iterate over sat/lum space and calculate appropriate mask pixel values.
var size = fb.square * 2, sq = fb.square;
function calculateMask(sizex, sizey, outputPixel) {
@ -236,15 +207,12 @@ $._farbtastic = function (container, options) {
// From sat/lum to alpha and color (grayscale)
var a = 1 - 2 * Math.min(l * s, (1 - l) * s);
var c = (a > 0) ? ((2 * l - 1 + a) * .5 / a) : 0;
a = a*__factor+(1-__factor)/2;
c = c*__factor+(1-__factor)/2;
a = a * __factor + (1 - __factor) / 2;
c = c * __factor + (1 - __factor) / 2;
outputPixel(x, y, c, a);
}
}
}
// Method #1: direct pixel access (new Canvas).
if (fb.ctxMask.getImageData) {
// Create half-resolution buffer.
@ -253,13 +221,11 @@ $._farbtastic = function (container, options) {
buffer.width = buffer.height = sz + 1;
var ctx = buffer.getContext('2d');
var frame = ctx.getImageData(0, 0, sz + 1, sz + 1);
var i = 0;
calculateMask(sz, sz, function (x, y, c, a) {
frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255;
frame.data[i++] = a * 255;
});
ctx.putImageData(frame, 0, 0);
fb.ctxMask.drawImage(buffer, 0, 0, sz + 1, sz + 1, -sq, -sq, sq * 2, sq * 2);
}
@ -269,7 +235,7 @@ $._farbtastic = function (container, options) {
var sz = Math.floor(size / 2);
calculateMask(sz, sz, function (x, y, c, a) {
c = Math.round(c * 255);
fb.ctxMask.fillStyle = 'rgba(' + c + ', ' + c + ', ' + c + ', ' + a +')';
fb.ctxMask.fillStyle = 'rgba(' + c + ', ' + c + ', ' + c + ', ' + a + ')';
fb.ctxMask.fillRect(x * 2 - sq - 1, y * 2 - sq - 1, 2, 2);
});
}
@ -287,15 +253,10 @@ $._farbtastic = function (container, options) {
a = Math.round(a * 255);
// We can only start outputting gradients once we have two rows of pixels.
if (y > 0) {
var c_last = cache_last[x][0],
a_last = cache_last[x][1],
color1 = fb.packDX(c_last, a_last),
color2 = fb.packDX(c, a),
y1 = Math.round(fb.mid + ((y - 1) * .333 - 1) * sq),
y2 = Math.round(fb.mid + (y * .333 - 1) * sq);
var c_last = cache_last[x][0], a_last = cache_last[x][1], color1 = fb.packDX(c_last, a_last), color2 = fb.packDX(c, a), y1 = Math.round(fb.mid + ((y - 1) * .333 - 1) * sq), y2 = Math.round(fb.mid + (y * .333 - 1) * sq);
$('<div>').css({
position: 'absolute',
filter: "progid:DXImageTransform.Microsoft.Gradient(StartColorStr="+ color1 +", EndColorStr="+ color2 +", GradientType=0)",
filter: "progid:DXImageTransform.Microsoft.Gradient(StartColorStr=" + color1 + ", EndColorStr=" + color2 + ", GradientType=0)",
top: y1,
height: y2 - y1,
// Avoid right-edge sticking out.
@ -306,27 +267,21 @@ $._farbtastic = function (container, options) {
cache.push([c, a]);
});
}
__debug && $('body').append('<div>drawMask '+ (+(new Date()) - tm) +'ms');
}
__debug && $('body').append('<div>drawMask ' + (+(new Date()) - tm) + 'ms');
};
/**
* Draw the selection markers.
*/
fb.drawMarkers = function () {
// Determine marker dimensions
var sz = options.width;
var angle = fb.hsl[0] * 6.28,
x1 = Math.sin(angle) * fb.radius,
y1 = -Math.cos(angle) * fb.radius,
x2 = 2 * fb.square * (.5 - fb.hsl[1]),
y2 = 2 * fb.square * (.5 - fb.hsl[2]);
var angle = fb.hsl[0] * 6.28, x1 = Math.sin(angle) * fb.radius, y1 = -Math.cos(angle) * fb.radius, x2 = 2 * fb.square * (.5 - fb.hsl[1]), y2 = 2 * fb.square * (.5 - fb.hsl[2]);
var circles = [
{ x: x1, y: y1, r: fb.markerSize + 1, c: 'rgb(0,0,0,.4)', lw: 2 },
{ x: x1, y: y1, r: fb.markerSize, c: '#fff', lw: 2 },
{ x: x2, y: y2, r: fb.markerSize + 1, c: 'rgb(0,0,0,.4)', lw: 2 },
{ x: x2, y: y2, r: fb.markerSize, c: '#fff', lw: 2 },
];
// Update the overlay canvas.
fb.ctxOverlay.clearRect(-fb.mid, -fb.mid, sz, sz);
for (i in circles) {
@ -337,21 +292,17 @@ $._farbtastic = function (container, options) {
fb.ctxOverlay.arc(c.x, c.y, c.r, 0, Math.PI * 2, true);
fb.ctxOverlay.stroke();
}
}
};
/**
* Update the markers and styles
*/
fb.updateDisplay = function () {
// Determine whether labels/markers should invert.
fb.invert = (fb.rgb[0] * 0.3 + fb.rgb[1] * .59 + fb.rgb[2] * .11) <= 0.6;
// Update the solid background fill.
fb.solidFill.css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
// Draw markers
fb.drawMarkers();
// Linked elements or callback
if (typeof fb.callback == 'object') {
// Set background/foreground color
@ -359,9 +310,8 @@ $._farbtastic = function (container, options) {
backgroundColor: fb.color,
color: fb.invert ? '#fff' : '#000'
});
// Change linked value
$(fb.callback).each(function() {
$(fb.callback).each(function () {
if ((typeof this.value == 'string') && this.value != fb.color) {
this.value = fb.color;
}
@ -370,8 +320,7 @@ $._farbtastic = function (container, options) {
else if (typeof fb.callback == 'function') {
fb.callback.call(fb, fb.color);
}
}
};
/**
* Helper for returning coordinates relative to the center.
*/
@ -380,8 +329,7 @@ $._farbtastic = function (container, options) {
x: event.pageX - fb.offset.left - fb.mid,
y: event.pageY - fb.offset.top - fb.mid
};
}
};
/**
* Mousedown handler
*/
@ -391,26 +339,21 @@ $._farbtastic = function (container, options) {
$(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
$._farbtastic.dragging = true;
}
// Update the stored offset for the widget.
fb.offset = $(container).offset();
// Check which area is being dragged
var pos = fb.widgetCoords(event);
fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) > (fb.square + 2);
// Process
fb.mousemove(event);
return false;
}
};
/**
* Mousemove handler
*/
fb.mousemove = function (event) {
// Get coordinates relative to color picker center
var pos = fb.widgetCoords(event);
// Set new HSL parameters
if (fb.circleDrag) {
var hue = Math.atan2(pos.x, -pos.y) / 6.28;
@ -422,8 +365,7 @@ $._farbtastic = function (container, options) {
fb.setHSL([fb.hsl[0], sat, lum]);
}
return false;
}
};
/**
* Mouseup handler
*/
@ -432,39 +374,34 @@ $._farbtastic = function (container, options) {
$(document).unbind('mousemove', fb.mousemove);
$(document).unbind('mouseup', fb.mouseup);
$._farbtastic.dragging = false;
}
};
/* Various color utility functions */
fb.dec2hex = function (x) {
return (x < 16 ? '0' : '') + x.toString(16);
}
};
fb.packDX = function (c, a) {
return '#' + fb.dec2hex(a) + fb.dec2hex(c) + fb.dec2hex(c) + fb.dec2hex(c);
};
fb.pack = function (rgb) {
var r = Math.round(rgb[0] * 255);
var g = Math.round(rgb[1] * 255);
var b = Math.round(rgb[2] * 255);
return '#' + fb.dec2hex(r) + fb.dec2hex(g) + fb.dec2hex(b);
};
fb.unpack = function (color) {
if (color.length == 7) {
function x(i) {
return parseInt(color.substring(i, i + 2), 16) / 255;
}
return [ x(1), x(3), x(5) ];
return [x(1), x(3), x(5)];
}
else if (color.length == 4) {
function x(i) {
return parseInt(color.substring(i, i + 1), 16) / 15;
}
return [ x(1), x(2), x(3) ];
return [x(1), x(2), x(3)];
}
};
fb.HSLToRGB = function (hsl) {
var m1, m2, r, g, b;
var h = hsl[0], s = hsl[1], l = hsl[2];
@ -476,35 +413,32 @@ $._farbtastic = function (container, options) {
this.hueToRGB(m1, m2, h - 0.33333)
];
};
fb.hueToRGB = function (m1, m2, h) {
h = (h + 1) % 1;
if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
if (h * 2 < 1) return m2;
if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
if (h * 6 < 1)
return m1 + (m2 - m1) * h * 6;
if (h * 2 < 1)
return m2;
if (h * 3 < 2)
return m1 + (m2 - m1) * (0.66666 - h) * 6;
return m1;
};
fb.RGBToHSL = function (rgb) {
var r = rgb[0], g = rgb[1], b = rgb[2],
min = Math.min(r, g, b),
max = Math.max(r, g, b),
delta = max - min,
h = 0,
s = 0,
l = (min + max) / 2;
var r = rgb[0], g = rgb[1], b = rgb[2], min = Math.min(r, g, b), max = Math.max(r, g, b), delta = max - min, h = 0, s = 0, l = (min + max) / 2;
if (l > 0 && l < 1) {
s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
}
if (delta > 0) {
if (max == r && max != g) h += (g - b) / delta;
if (max == g && max != b) h += (2 + (b - r) / delta);
if (max == b && max != r) h += (4 + (r - g) / delta);
if (max == r && max != g)
h += (g - b) / delta;
if (max == g && max != b)
h += (2 + (b - r) / delta);
if (max == b && max != r)
h += (4 + (r - g) / delta);
h /= 6;
}
return [h, s, l];
};
// Parse options.
if (!options.callback) {
options = { callback: options };
@ -514,19 +448,15 @@ $._farbtastic = function (container, options) {
wheelWidth: (options.width || 300) / 10,
callback: null
}, options);
// Initialize.
fb.initWidget();
// Install mousedown handler (the others are set on the document on-demand)
$('canvas.farbtastic-overlay', container).mousedown(fb.mousedown);
// Set linked elements/callback
if (options.callback) {
fb.linkTo(options.callback);
}
// Set to gray.
fb.setColor('#808080');
}
};
})(jQuery);

View file

@ -1,5 +1,4 @@
// WARNING: This file has been modified from the Original
/*
* Gritter for jQuery
* http://www.boedesign.com/
@ -16,66 +15,55 @@
* notification title and text, and to treat plain strings as text instead of HTML (to avoid XSS
* vunlerabilities).
*/
(function($){
(function ($) {
/**
* Set it up as an object under the jQuery namespace
*/
$.gritter = {};
/**
* Set up global options that the user can over-ride
*/
$.gritter.options = {
position: '',
class_name: '', // could be set to 'gritter-light' to use white notifications
class_name: '',
time: 3000 // hang on the screen for...
}
};
/**
* Add a gritter notification to the screen
* @see Gritter#add();
*/
$.gritter.add = function(params){
$.gritter.add = function (params) {
try {
return Gritter.add(params || {});
} catch(e) {
}
catch (e) {
var err = 'Gritter Error: ' + e;
(typeof(console) != 'undefined' && console.error) ?
(typeof (console) != 'undefined' && console.error) ?
console.error(err, params) :
alert(err);
}
}
};
/**
* Remove a gritter notification from the screen
* @see Gritter#removeSpecific();
*/
$.gritter.remove = function(id, params){
$.gritter.remove = function (id, params) {
Gritter.removeSpecific(id.split('gritter-item-')[1], params || {});
}
};
/**
* Remove all notifications
* @see Gritter#stop();
*/
$.gritter.removeAll = function(params){
$.gritter.removeAll = function (params) {
Gritter.stop(params || {});
}
};
/**
* Big fat Gritter object
* @constructor (not really since its object literal)
*/
var Gritter = {
// Public - options to over-ride with $.gritter.options in "add"
time: '',
// Private - no touchy the private parts
_custom_timer: 0,
_item_count: 0,
@ -89,111 +77,82 @@
.append($('<div>').addClass('gritter-content'))
.append($('<div>').addClass('gritter-close')
.append($('<i>').addClass('buttonicon buttonicon-times'))))),
/**
* Add a gritter notification to the screen
* @param {Object} params The object that contains all the options for drawing the notification
* @return {Integer} The specific numeric id to that gritter notification
*/
add: function(params){
add: function (params) {
// Handle straight text
if(typeof(params) == 'string'){
params = {text:params};
if (typeof (params) == 'string') {
params = { text: params };
}
// We might have some issues if we don't have a title or text!
if(!params.text){
if (!params.text) {
throw 'You must supply "text" parameter.';
}
// Check the options and set them once
if(!this._is_setup){
if (!this._is_setup) {
this._runSetup();
}
// Basics
var title = params.title,
text = params.text,
image = params.image || '',
position = params.position || 'top',
sticky = params.sticky || false,
item_class = params.class_name || $.gritter.options.class_name,
time_alive = params.time || '';
var title = params.title, text = params.text, image = params.image || '', position = params.position || 'top', sticky = params.sticky || false, item_class = params.class_name || $.gritter.options.class_name, time_alive = params.time || '';
this._verifyWrapper();
if (sticky) {
item_class += " sticky";
}
this._item_count++;
var number = this._item_count;
// Assign callbacks
$(['before_open', 'after_open', 'before_close', 'after_close']).each(function(i, val){
Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function(){}
$(['before_open', 'after_open', 'before_close', 'after_close']).each(function (i, val) {
Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function () { };
});
// Reset
this._custom_timer = 0;
// A custom fade time set
if(time_alive){
if (time_alive) {
this._custom_timer = time_alive;
}
// String replacements on the template
if(title){
title = this._tpl_title.clone().append(
typeof title === 'string' ? document.createTextNode(title) : title);
}else{
if (title) {
title = this._tpl_title.clone().append(typeof title === 'string' ? document.createTextNode(title) : title);
}
else {
title = '';
}
const tmp = this._tpl_item.clone();
tmp.attr('id', `gritter-item-${number}`);
tmp.addClass(item_class);
tmp.find('.gritter-content')
.append(title)
.append(typeof text === 'string' ? $('<p>').text(text) : text);
// If it's false, don't show another gritter message
if(this['_before_open_' + number]() === false){
if (this['_before_open_' + number]() === false) {
return false;
}
if (['top', 'bottom'].indexOf(position) == -1) {
position = 'top';
}
$('#gritter-container.' + position).append(tmp);
var item = $('#gritter-item-' + this._item_count);
setTimeout(function() { item.addClass('popup-show'); }, 0);
setTimeout(function () { item.addClass('popup-show'); }, 0);
Gritter['_after_open_' + number](item);
if(!sticky){
if (!sticky) {
this._setFadeTimer(item, number);
// Bind the hover/unhover states
$(item).on('mouseenter', function(event) {
$(item).on('mouseenter', function (event) {
Gritter._restoreItemIfFading($(this), number);
});
$(item).on('mouseleave', function(event) {
$(item).on('mouseleave', function (event) {
Gritter._setFadeTimer($(this), number);
});
}
// Clicking (X) makes the perdy thing close
$(item).find('.gritter-close').click(function(){
$(item).find('.gritter-close').click(function () {
Gritter.removeSpecific(number, {}, null, true);
});
return number;
},
/**
* If we don't have any more gritter notifications, get rid of the wrapper using this check
* @private
@ -201,20 +160,17 @@
* @param {Object} e The jQuery element that we're going to perform the remove() action on
* @param {Boolean} manual_close Did we close the gritter dialog with the (X) button
*/
_countRemoveWrapper: function(unique_id, e, manual_close){
_countRemoveWrapper: function (unique_id, e, manual_close) {
// Remove it then run the callback function
e.remove();
this['_after_close_' + unique_id](e, manual_close);
// Remove container if empty
$('#gritter-container').each(function() {
$('#gritter-container').each(function () {
if ($(this).find('.gritter-item').length == 0) {
$(this).remove();
}
})
});
},
/**
* Fade out an element after it's been on the screen for x amount of time
* @private
@ -223,34 +179,24 @@
* @param {Object} params An optional list of params.
* @param {Boolean} unbind_events Unbind the mouseenter/mouseleave events if they click (X)
*/
_fade: function(e, unique_id, params, unbind_events){
var params = params || {},
fade = (typeof(params.fade) != 'undefined') ? params.fade : true,
manual_close = unbind_events;
_fade: function (e, unique_id, params, unbind_events) {
var params = params || {}, fade = (typeof (params.fade) != 'undefined') ? params.fade : true, manual_close = unbind_events;
this['_before_close_' + unique_id](e, manual_close);
// If this is true, then we are coming from clicking the (X)
if(unbind_events){
if (unbind_events) {
e.unbind('mouseenter mouseleave');
}
// Fade it out or remove it
if(fade){
if (fade) {
e.removeClass('popup-show');
setTimeout(function() {
setTimeout(function () {
Gritter._countRemoveWrapper(unique_id, e, manual_close);
}, 300)
}, 300);
}
else {
this._countRemoveWrapper(unique_id, e);
}
},
/**
* Remove a specific notification based on an ID
* @param {Integer} unique_id The ID used to delete a specific notification
@ -258,100 +204,78 @@
* @param {Object} e The jQuery element that we're "fading" then removing
* @param {Boolean} unbind_events If we clicked on the (X) we set this to true to unbind mouseenter/mouseleave
*/
removeSpecific: function(unique_id, params, e, unbind_events){
if(!e){
removeSpecific: function (unique_id, params, e, unbind_events) {
if (!e) {
var e = $('#gritter-item-' + unique_id);
}
// We set the fourth param to let the _fade function know to
// unbind the "mouseleave" event. Once you click (X) there's no going back!
this._fade(e, unique_id, params || {}, unbind_events);
},
/**
* If the item is fading out and we hover over it, restore it!
* @private
* @param {Object} e The HTML element to remove
* @param {Integer} unique_id The ID of the element
*/
_restoreItemIfFading: function(e, unique_id){
_restoreItemIfFading: function (e, unique_id) {
clearTimeout(this['_int_id_' + unique_id]);
e.stop().css({ opacity: '', height: '' });
},
/**
* Setup the global options - only once
* @private
*/
_runSetup: function(){
for(opt in $.gritter.options){
_runSetup: function () {
for (opt in $.gritter.options) {
this[opt] = $.gritter.options[opt];
}
this._is_setup = 1;
},
/**
* Set the notification to fade out after a certain amount of time
* @private
* @param {Object} item The HTML element we're dealing with
* @param {Integer} unique_id The ID of the element
*/
_setFadeTimer: function(item, unique_id){
_setFadeTimer: function (item, unique_id) {
var timer_str = (this._custom_timer) ? this._custom_timer : this.time;
this['_int_id_' + unique_id] = setTimeout(function(){
this['_int_id_' + unique_id] = setTimeout(function () {
Gritter._fade(item, unique_id);
}, timer_str);
},
/**
* Bring everything to a halt
* @param {Object} params A list of callback functions to pass when all notifications are removed
*/
stop: function(params){
stop: function (params) {
// callbacks (if passed)
var before_close = ($.isFunction(params.before_close)) ? params.before_close : function(){};
var after_close = ($.isFunction(params.after_close)) ? params.after_close : function(){};
var before_close = ($.isFunction(params.before_close)) ? params.before_close : function () { };
var after_close = ($.isFunction(params.after_close)) ? params.after_close : function () { };
var wrap = $('#gritter-container');
before_close(wrap);
wrap.fadeOut(function(){
wrap.fadeOut(function () {
$(this).remove();
after_close();
});
},
/**
* A check to make sure we have something to wrap our notices with
* @private
*/
_verifyWrapper: function(){
_verifyWrapper: function () {
if ($('#gritter-container.top').length === 0) {
$('#editorcontainerbox').append(this._tpl_wrap_top);
}
if ($('#gritter-container.bottom').length === 0) {
$('#editorcontainerbox').append(this._tpl_wrap_bottom);
}
}
}
};
})(jQuery);
// For Emacs:
// Local Variables:
// tab-width: 2
// indent-tabs-mode: t
// End:
// vi: ts=2:noet:sw=2

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,30 @@
// WARNING: This file has been modified from the Original
// TODO: Nice Select seems relatively abandoned, we should consider other options.
/* jQuery Nice Select - v1.1.0
https://github.com/hernansartorio/jquery-nice-select
Made by Hernán Sartorio */
(function($) {
$.fn.niceSelect = function(method) {
(function ($) {
$.fn.niceSelect = function (method) {
// Methods
if (typeof method == 'string') {
if (method == 'update') {
this.each(function() {
this.each(function () {
var $select = $(this);
var $dropdown = $(this).next('.nice-select');
var open = $dropdown.hasClass('open');
if ($dropdown.length) {
$dropdown.remove();
create_nice_select($select);
if (open) {
$select.next().trigger('click');
}
}
});
} else if (method == 'destroy') {
this.each(function() {
}
else if (method == 'destroy') {
this.each(function () {
var $select = $(this);
var $dropdown = $(this).next('.nice-select');
if ($dropdown.length) {
$dropdown.remove();
$select.css('display', '');
@ -39,67 +33,52 @@
if ($('.nice-select').length == 0) {
$(document).off('.nice_select');
}
} else {
console.log('Method "' + method + '" does not exist.')
}
else {
console.log('Method "' + method + '" does not exist.');
}
return this;
}
// Hide native select
this.hide();
// Create custom markup
this.each(function() {
this.each(function () {
var $select = $(this);
if (!$select.next().hasClass('nice-select')) {
create_nice_select($select);
}
});
function create_nice_select($select) {
$select.after($('<div></div>')
.addClass('nice-select')
.addClass($select.attr('class') || '')
.addClass($select.attr('disabled') ? 'disabled' : '')
.attr('tabindex', $select.attr('disabled') ? null : '0')
.html('<span class="current"></span><ul class="list thin-scrollbar"></ul>')
);
.html('<span class="current"></span><ul class="list thin-scrollbar"></ul>'));
var $dropdown = $select.next();
var $options = $select.find('option');
var $selected = $select.find('option:selected');
$dropdown.find('.current').html($selected.data('display') || $selected.text());
$options.each(function(i) {
$options.each(function (i) {
var $option = $(this);
var display = $option.data('display');
$dropdown.find('ul').append($('<li></li>')
.attr('data-value', $option.val())
.attr('data-display', (display || null))
.addClass('option' +
($option.is(':selected') ? ' selected' : '') +
($option.is(':disabled') ? ' disabled' : ''))
.html($option.text())
);
.html($option.text()));
});
}
/* Event listeners */
// Unbind existing events in case that the plugin has been initialized before
$(document).off('.nice_select');
// Open/close
$(document).on('click.nice_select', '.nice-select', function(event) {
$(document).on('click.nice_select', '.nice-select', function (event) {
var $dropdown = $(this);
$('.nice-select').not($dropdown).removeClass('open');
$dropdown.toggleClass('open');
if ($dropdown.hasClass('open')) {
$dropdown.find('.option');
$dropdown.find('.focus').removeClass('focus');
@ -109,7 +88,6 @@
$dropdown.find('.list').css('top', $dropdown.offset().top + $dropdown.outerHeight());
$dropdown.find('.list').css('min-width', $dropdown.outerWidth() + 'px');
}
$listHeight = $dropdown.find('.list').outerHeight();
$top = $dropdown.parent().offset().top;
$bottom = $('body').height() - $top;
@ -117,55 +95,52 @@
if ($maxListHeight < 200) {
$dropdown.addClass('reverse');
$maxListHeight = 250;
} else {
$dropdown.removeClass('reverse')
}
else {
$dropdown.removeClass('reverse');
}
$dropdown.find('.list').css('max-height', $maxListHeight + 'px');
} else {
}
else {
$dropdown.focus();
}
});
// Close when clicking outside
$(document).on('click.nice_select', function(event) {
$(document).on('click.nice_select', function (event) {
if ($(event.target).closest('.nice-select').length === 0) {
$('.nice-select').removeClass('open').find('.option');
}
});
// Option click
$(document).on('click.nice_select', '.nice-select .option:not(.disabled)', function(event) {
$(document).on('click.nice_select', '.nice-select .option:not(.disabled)', function (event) {
var $option = $(this);
var $dropdown = $option.closest('.nice-select');
$dropdown.find('.selected').removeClass('selected');
$option.addClass('selected');
var text = $option.data('display') || $option.text();
$dropdown.find('.current').text(text);
$dropdown.prev('select').val($option.data('value')).trigger('change');
});
// Keyboard events
$(document).on('keydown.nice_select', '.nice-select', function(event) {
$(document).on('keydown.nice_select', '.nice-select', function (event) {
var $dropdown = $(this);
var $focused_option = $($dropdown.find('.focus') || $dropdown.find('.list .option.selected'));
// Space or Enter
if (event.keyCode == 32 || event.keyCode == 13) {
if ($dropdown.hasClass('open')) {
$focused_option.trigger('click');
} else {
}
else {
$dropdown.trigger('click');
}
return false;
// Down
} else if (event.keyCode == 40) {
}
else if (event.keyCode == 40) {
if (!$dropdown.hasClass('open')) {
$dropdown.trigger('click');
} else {
}
else {
var $next = $focused_option.nextAll('.option:not(.disabled)').first();
if ($next.length > 0) {
$dropdown.find('.focus').removeClass('focus');
@ -174,10 +149,12 @@
}
return false;
// Up
} else if (event.keyCode == 38) {
}
else if (event.keyCode == 38) {
if (!$dropdown.hasClass('open')) {
$dropdown.trigger('click');
} else {
}
else {
var $prev = $focused_option.prevAll('.option:not(.disabled)').first();
if ($prev.length > 0) {
$dropdown.find('.focus').removeClass('focus');
@ -186,27 +163,25 @@
}
return false;
// Esc
} else if (event.keyCode == 27) {
}
else if (event.keyCode == 27) {
if ($dropdown.hasClass('open')) {
$dropdown.trigger('click');
}
// Tab
} else if (event.keyCode == 9) {
}
else if (event.keyCode == 9) {
if ($dropdown.hasClass('open')) {
return false;
}
}
});
// Detect CSS pointer-events support, for IE <= 10. From Modernizr.
var style = document.createElement('a').style;
style.cssText = 'pointer-events:auto';
if (style.pointerEvents !== 'auto') {
$('html').addClass('no-csspointerevents');
}
return this;
};
}(jQuery));

View file

@ -1,5 +1,4 @@
'use strict';
window.customStart = () => {
// define your javascript here
// jquery is available - except index.js

View file

@ -1,5 +1,4 @@
'use strict';
window.customStart = () => {
$('#pad_title').show();
$('.buttonicon').mousedown(function () { $(this).parent().addClass('pressed'); });

View file

@ -1,4 +1,3 @@
'use strict';
window.customStart = () => {
};

View file

@ -1,5 +1,4 @@
'use strict';
window.customStart = () => {
// define your javascript here
// jquery is available - except index.js

View file

@ -1,5 +1,4 @@
'use strict';
window.customStart = () => {
// define your javascript here
// jquery is available - except index.js

View file

@ -1,5 +1,4 @@
'use strict';
window.customStart = () => {
// define your javascript here
// jquery is available - except index.js

View file

@ -1,32 +1,26 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as exportEtherpad from "../../../node/utils/ExportEtherpad.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
'use strict';
const assert = require('assert').strict;
const common = require('../common');
const exportEtherpad = require('../../../node/utils/ExportEtherpad');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
const assert = assert$0.strict;
describe(__filename, function () {
let padId;
beforeEach(async function () {
padId = common.randomString();
assert(!await padManager.doesPadExist(padId));
});
describe('exportEtherpadAdditionalContent', function () {
let hookBackup;
before(async function () {
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
plugins.hooks.exportEtherpadAdditionalContent = [{hook_fn: () => ['custom']}];
plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }];
});
after(async function () {
plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
});
it('exports custom records', async function () {
const pad = await padManager.getPad(padId);
await pad.db.set(`custom:${padId}`, 'a');
@ -37,7 +31,6 @@ describe(__filename, function () {
assert.equal(data[`custom:${padId}:`], 'b');
assert.equal(data[`custom:${padId}:foo`], 'c');
});
it('export from read-only pad uses read-only ID', async function () {
const pad = await padManager.getPad(padId);
const readOnlyId = await readOnlyManager.getReadOnlyId(padId);
@ -52,7 +45,6 @@ describe(__filename, function () {
assert(!(`custom:${padId}:` in data));
assert(!(`custom:${padId}:foo` in data));
});
it('does not export records from pad with similar ID', async function () {
const pad = await padManager.getPad(padId);
await pad.db.set(`custom:${padId}x`, 'a');

View file

@ -1,18 +1,16 @@
import assert$0 from "assert";
import * as authorManager from "../../../node/db/AuthorManager.js";
import * as db from "../../../node/db/DB.js";
import * as importEtherpad from "../../../node/utils/ImportEtherpad.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as padUtils from "../../../static/js/pad_utils.js";
'use strict';
const assert = require('assert').strict;
const authorManager = require('../../../node/db/AuthorManager');
const db = require('../../../node/db/DB');
const importEtherpad = require('../../../node/utils/ImportEtherpad');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const {randomString} = require('../../../static/js/pad_utils');
const assert = assert$0.strict;
const { randomString } = padUtils;
describe(__filename, function () {
let padId;
const makeAuthorId = () => `a.${randomString(16)}`;
const makeExport = (authorId) => ({
'pad:testing': {
atext: {
@ -48,12 +46,10 @@ describe(__filename, function () {
},
},
});
beforeEach(async function () {
padId = randomString(10);
assert(!await padManager.doesPadExist(padId));
});
it('unknown db records are ignored', async function () {
const badKey = `maliciousDbKey${randomString(10)}`;
await importEtherpad.setPadRaw(padId, JSON.stringify({
@ -62,7 +58,6 @@ describe(__filename, function () {
}));
assert(await db.get(badKey) == null);
});
it('changes are all or nothing', async function () {
const authorId = makeAuthorId();
const data = makeExport(authorId);
@ -72,11 +67,9 @@ describe(__filename, function () {
assert(!await authorManager.doesAuthorExist(authorId));
assert(!await padManager.doesPadExist(padId));
});
describe('author pad IDs', function () {
let existingAuthorId;
let newAuthorId;
beforeEach(async function () {
existingAuthorId = (await authorManager.createAuthor('existing')).authorID;
assert(await authorManager.doesAuthorExist(existingAuthorId));
@ -85,7 +78,6 @@ describe(__filename, function () {
assert.notEqual(newAuthorId, existingAuthorId);
assert(!await authorManager.doesAuthorExist(newAuthorId));
});
it('author does not yet exist', async function () {
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
assert(await authorManager.doesAuthorExist(newAuthorId));
@ -94,7 +86,6 @@ describe(__filename, function () {
assert.equal(author.colorId, '#000000');
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
});
it('author already exists, no pads', async function () {
newAuthorId = existingAuthorId;
await importEtherpad.setPadRaw(padId, JSON.stringify(makeExport(newAuthorId)));
@ -104,7 +95,6 @@ describe(__filename, function () {
assert.notEqual(author.colorId, '#000000');
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
});
it('author already exists, on different pad', async function () {
const otherPadId = randomString(10);
await authorManager.addPad(existingAuthorId, otherPadId);
@ -114,11 +104,8 @@ describe(__filename, function () {
const author = await authorManager.getAuthor(newAuthorId);
assert.equal(author.name, 'existing');
assert.notEqual(author.colorId, '#000000');
assert.deepEqual(
(await authorManager.listPadsOfAuthor(newAuthorId)).padIDs.sort(),
[otherPadId, padId].sort());
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs.sort(), [otherPadId, padId].sort());
});
it('author already exists, on same pad', async function () {
await authorManager.addPad(existingAuthorId, padId);
newAuthorId = existingAuthorId;
@ -130,7 +117,6 @@ describe(__filename, function () {
assert.deepEqual((await authorManager.listPadsOfAuthor(newAuthorId)).padIDs, [padId]);
});
});
describe('enforces consistent pad ID', function () {
it('pad record has different pad ID', async function () {
const data = makeExport(makeAuthorId());
@ -138,14 +124,12 @@ describe(__filename, function () {
delete data['pad:testing'];
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
});
it('globalAuthor record has different pad ID', async function () {
const authorId = makeAuthorId();
const data = makeExport(authorId);
data[`globalAuthor:${authorId}`].padIDs = 'differentPadId';
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
});
it('pad rev record has different pad ID', async function () {
const data = makeExport(makeAuthorId());
data['pad:differentPadId:revs:0'] = data['pad:testing:revs:0'];
@ -153,34 +137,28 @@ describe(__filename, function () {
assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify(data)), /unexpected pad ID/);
});
});
describe('order of records does not matter', function () {
for (const perm of [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]]) {
it(JSON.stringify(perm), async function () {
const authorId = makeAuthorId();
const records = Object.entries(makeExport(authorId));
assert.equal(records.length, 3);
await importEtherpad.setPadRaw(
padId, JSON.stringify(Object.fromEntries(perm.map((i) => records[i]))));
await importEtherpad.setPadRaw(padId, JSON.stringify(Object.fromEntries(perm.map((i) => records[i]))));
assert.deepEqual((await authorManager.listPadsOfAuthor(authorId)).padIDs, [padId]);
const pad = await padManager.getPad(padId);
assert.equal(pad.text(), 'foo\n');
});
}
});
describe('exportEtherpadAdditionalContent', function () {
let hookBackup;
before(async function () {
hookBackup = plugins.hooks.exportEtherpadAdditionalContent || [];
plugins.hooks.exportEtherpadAdditionalContent = [{hook_fn: () => ['custom']}];
plugins.hooks.exportEtherpadAdditionalContent = [{ hook_fn: () => ['custom'] }];
});
after(async function () {
plugins.hooks.exportEtherpadAdditionalContent = hookBackup;
});
it('imports from custom prefix', async function () {
await importEtherpad.setPadRaw(padId, JSON.stringify({
...makeExport(makeAuthorId()),
@ -191,7 +169,6 @@ describe(__filename, function () {
assert.equal(await pad.db.get(`custom:${padId}`), 'a');
assert.equal(await pad.db.get(`custom:${padId}:foo`), 'b');
});
it('rejects records for pad with similar ID', async function () {
await assert.rejects(importEtherpad.setPadRaw(padId, JSON.stringify({
...makeExport(makeAuthorId()),

View file

@ -1,37 +1,33 @@
import * as Pad from "../../../node/db/Pad.js";
import assert$0 from "assert";
import * as authorManager from "../../../node/db/AuthorManager.js";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as settings from "../../../node/utils/Settings.js";
'use strict';
const Pad = require('../../../node/db/Pad');
const assert = require('assert').strict;
const authorManager = require('../../../node/db/AuthorManager');
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const settings = require('../../../node/utils/Settings');
const assert = assert$0.strict;
describe(__filename, function () {
const backups = {};
let pad;
let padId;
before(async function () {
backups.hooks = {
padDefaultContent: plugins.hooks.padDefaultContent,
};
backups.defaultPadText = settings.defaultPadText;
});
beforeEach(async function () {
backups.hooks.padDefaultContent = [];
padId = common.randomString();
assert(!(await padManager.doesPadExist(padId)));
});
afterEach(async function () {
Object.assign(plugins.hooks, backups.hooks);
if (pad != null) await pad.remove();
if (pad != null)
await pad.remove();
pad = null;
});
describe('cleanText', function () {
const testCases = [
['', ''],
@ -49,84 +45,74 @@ describe(__filename, function () {
});
}
});
describe('padDefaultContent hook', function () {
it('runs when a pad is created without specific text', async function () {
const p = new Promise((resolve) => {
plugins.hooks.padDefaultContent.push({hook_fn: () => resolve()});
plugins.hooks.padDefaultContent.push({ hook_fn: () => resolve() });
});
pad = await padManager.getPad(padId);
await p;
});
it('not run if pad is created with specific text', async function () {
plugins.hooks.padDefaultContent.push(
{hook_fn: () => { throw new Error('should not be called'); }});
plugins.hooks.padDefaultContent.push({ hook_fn: () => { throw new Error('should not be called'); } });
pad = await padManager.getPad(padId, '');
});
it('defaults to settings.defaultPadText', async function () {
const p = new Promise((resolve, reject) => {
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => {
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => {
try {
assert.equal(ctx.type, 'text');
assert.equal(ctx.content, settings.defaultPadText);
} catch (err) {
}
catch (err) {
return reject(err);
}
resolve();
}});
} });
});
pad = await padManager.getPad(padId);
await p;
});
it('passes the pad object', async function () {
const gotP = new Promise((resolve) => {
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, {pad}) => resolve(pad)});
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { pad }) => resolve(pad) });
});
pad = await padManager.getPad(padId);
assert.equal(await gotP, pad);
});
it('passes empty authorId if not provided', async function () {
const gotP = new Promise((resolve) => {
plugins.hooks.padDefaultContent.push(
{hook_fn: async (hookName, {authorId}) => resolve(authorId)});
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { authorId }) => resolve(authorId) });
});
pad = await padManager.getPad(padId);
assert.equal(await gotP, '');
});
it('passes provided authorId', async function () {
const want = await authorManager.getAuthor4Token(`t.${padId}`);
const gotP = new Promise((resolve) => {
plugins.hooks.padDefaultContent.push(
{hook_fn: async (hookName, {authorId}) => resolve(authorId)});
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, { authorId }) => resolve(authorId) });
});
pad = await padManager.getPad(padId, null, want);
assert.equal(await gotP, want);
});
it('uses provided content', async function () {
const want = 'hello world';
assert.notEqual(want, settings.defaultPadText);
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => {
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => {
ctx.type = 'text';
ctx.content = want;
}});
} });
pad = await padManager.getPad(padId);
assert.equal(pad.text(), `${want}\n`);
});
it('cleans provided content', async function () {
const input = 'foo\r\nbar\r\tbaz';
const want = 'foo\nbar\n baz';
assert.notEqual(want, settings.defaultPadText);
plugins.hooks.padDefaultContent.push({hook_fn: async (hookName, ctx) => {
plugins.hooks.padDefaultContent.push({ hook_fn: async (hookName, ctx) => {
ctx.type = 'text';
ctx.content = input;
}});
} });
pad = await padManager.getPad(padId);
assert.equal(pad.text(), `${want}\n`);
});

View file

@ -1,123 +1,107 @@
import SessionStore from "../../../node/db/SessionStore.js";
import assert$0 from "assert";
import * as common from "../common.js";
import * as db from "../../../node/db/DB.js";
import util from "util";
'use strict';
const SessionStore = require('../../../node/db/SessionStore');
const assert = require('assert').strict;
const common = require('../common');
const db = require('../../../node/db/DB');
const util = require('util');
const assert = assert$0.strict;
describe(__filename, function () {
let ss;
let sid;
const set = async (sess) => await util.promisify(ss.set).call(ss, sid, sess);
const get = async () => await util.promisify(ss.get).call(ss, sid);
const destroy = async () => await util.promisify(ss.destroy).call(ss, sid);
const touch = async (sess) => await util.promisify(ss.touch).call(ss, sid, sess);
before(async function () {
await common.init();
});
beforeEach(async function () {
ss = new SessionStore();
sid = common.randomString();
});
afterEach(async function () {
if (ss != null) {
if (sid != null) await destroy();
if (sid != null)
await destroy();
ss.shutdown();
}
sid = null;
ss = null;
});
describe('set', function () {
it('set of null is a no-op', async function () {
await set(null);
assert(await db.get(`sessionstorage:${sid}`) == null);
});
it('set of non-expiring session', async function () {
const sess = {foo: 'bar', baz: {asdf: 'jkl;'}};
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
await set(sess);
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
});
it('set of session that expires', async function () {
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess);
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
await new Promise((resolve) => setTimeout(resolve, 110));
// Writing should start a timeout.
assert(await db.get(`sessionstorage:${sid}`) == null);
});
it('set of already expired session', async function () {
const sess = {foo: 'bar', cookie: {expires: new Date(1)}};
const sess = { foo: 'bar', cookie: { expires: new Date(1) } };
await set(sess);
// No record should have been created.
assert(await db.get(`sessionstorage:${sid}`) == null);
});
it('switch from non-expiring to expiring', async function () {
const sess = {foo: 'bar'};
const sess = { foo: 'bar' };
await set(sess);
const sess2 = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
const sess2 = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess2);
await new Promise((resolve) => setTimeout(resolve, 110));
assert(await db.get(`sessionstorage:${sid}`) == null);
});
it('switch from expiring to non-expiring', async function () {
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess);
const sess2 = {foo: 'bar'};
const sess2 = { foo: 'bar' };
await set(sess2);
await new Promise((resolve) => setTimeout(resolve, 110));
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
});
});
describe('get', function () {
it('get of non-existent entry', async function () {
assert(await get() == null);
});
it('set+get round trip', async function () {
const sess = {foo: 'bar', baz: {asdf: 'jkl;'}};
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
await set(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
});
it('get of record from previous run (no expiration)', async function () {
const sess = {foo: 'bar', baz: {asdf: 'jkl;'}};
const sess = { foo: 'bar', baz: { asdf: 'jkl;' } };
await db.set(`sessionstorage:${sid}`, sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
});
it('get of record from previous run (not yet expired)', async function () {
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await db.set(`sessionstorage:${sid}`, sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
await new Promise((resolve) => setTimeout(resolve, 110));
// Reading should start a timeout.
assert(await db.get(`sessionstorage:${sid}`) == null);
});
it('get of record from previous run (already expired)', async function () {
const sess = {foo: 'bar', cookie: {expires: new Date(1)}};
const sess = { foo: 'bar', cookie: { expires: new Date(1) } };
await db.set(`sessionstorage:${sid}`, sess);
assert(await get() == null);
assert(await db.get(`sessionstorage:${sid}`) == null);
});
it('external expiration update is picked up', async function () {
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
const sess2 = {...sess, cookie: {expires: new Date(Date.now() + 200)}};
const sess2 = { ...sess, cookie: { expires: new Date(Date.now() + 200) } };
await db.set(`sessionstorage:${sid}`, sess2);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
await new Promise((resolve) => setTimeout(resolve, 110));
@ -125,10 +109,9 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
});
});
describe('shutdown', function () {
it('shutdown cancels timeouts', async function () {
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 100)}};
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 100) } };
await set(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
ss.shutdown();
@ -137,95 +120,84 @@ describe(__filename, function () {
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
});
});
describe('destroy', function () {
it('destroy deletes the database record', async function () {
const sess = {cookie: {expires: new Date(Date.now() + 100)}};
const sess = { cookie: { expires: new Date(Date.now() + 100) } };
await set(sess);
await destroy();
assert(await db.get(`sessionstorage:${sid}`) == null);
});
it('destroy cancels the timeout', async function () {
const sess = {cookie: {expires: new Date(Date.now() + 100)}};
const sess = { cookie: { expires: new Date(Date.now() + 100) } };
await set(sess);
await destroy();
await db.set(`sessionstorage:${sid}`, sess);
await new Promise((resolve) => setTimeout(resolve, 110));
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
});
it('destroy session that does not exist', async function () {
await destroy();
});
});
describe('touch without refresh', function () {
it('touch before set is equivalent to set if session expires', async function () {
const sess = {cookie: {expires: new Date(Date.now() + 1000)}};
const sess = { cookie: { expires: new Date(Date.now() + 1000) } };
await touch(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
});
it('touch updates observed expiration but not database', async function () {
const start = Date.now();
const sess = {cookie: {expires: new Date(start + 200)}};
const sess = { cookie: { expires: new Date(start + 200) } };
await set(sess);
const sess2 = {cookie: {expires: new Date(start + 12000)}};
const sess2 = { cookie: { expires: new Date(start + 12000) } };
await touch(sess2);
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
});
});
describe('touch with refresh', function () {
beforeEach(async function () {
ss = new SessionStore(200);
});
it('touch before set is equivalent to set if session expires', async function () {
const sess = {cookie: {expires: new Date(Date.now() + 1000)}};
const sess = { cookie: { expires: new Date(Date.now() + 1000) } };
await touch(sess);
assert.equal(JSON.stringify(await get()), JSON.stringify(sess));
});
it('touch before eligible for refresh updates expiration but not DB', async function () {
const now = Date.now();
const sess = {foo: 'bar', cookie: {expires: new Date(now + 1000)}};
const sess = { foo: 'bar', cookie: { expires: new Date(now + 1000) } };
await set(sess);
const sess2 = {foo: 'bar', cookie: {expires: new Date(now + 1001)}};
const sess2 = { foo: 'bar', cookie: { expires: new Date(now + 1001) } };
await touch(sess2);
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
});
it('touch before eligible for refresh updates timeout', async function () {
const start = Date.now();
const sess = {foo: 'bar', cookie: {expires: new Date(start + 200)}};
const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } };
await set(sess);
await new Promise((resolve) => setTimeout(resolve, 100));
const sess2 = {foo: 'bar', cookie: {expires: new Date(start + 399)}};
const sess2 = { foo: 'bar', cookie: { expires: new Date(start + 399) } };
await touch(sess2);
await new Promise((resolve) => setTimeout(resolve, 110));
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess));
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
});
it('touch after eligible for refresh updates db', async function () {
const start = Date.now();
const sess = {foo: 'bar', cookie: {expires: new Date(start + 200)}};
const sess = { foo: 'bar', cookie: { expires: new Date(start + 200) } };
await set(sess);
await new Promise((resolve) => setTimeout(resolve, 100));
const sess2 = {foo: 'bar', cookie: {expires: new Date(start + 400)}};
const sess2 = { foo: 'bar', cookie: { expires: new Date(start + 400) } };
await touch(sess2);
await new Promise((resolve) => setTimeout(resolve, 110));
assert.equal(JSON.stringify(await db.get(`sessionstorage:${sid}`)), JSON.stringify(sess2));
assert.equal(JSON.stringify(await get()), JSON.stringify(sess2));
});
it('refresh=0 updates db every time', async function () {
ss = new SessionStore(0);
const sess = {foo: 'bar', cookie: {expires: new Date(Date.now() + 1000)}};
const sess = { foo: 'bar', cookie: { expires: new Date(Date.now() + 1000) } };
await set(sess);
await db.remove(`sessionstorage:${sid}`);
await touch(sess); // No change in expiration time.

View file

@ -1,39 +1,35 @@
import * as Stream from "../../../node/utils/Stream.js";
import assert$0 from "assert";
'use strict';
const Stream = require('../../../node/utils/Stream');
const assert = require('assert').strict;
const assert = assert$0.strict;
class DemoIterable {
constructor() {
this.value = 0;
this.errs = [];
this.rets = [];
}
completed() { return this.errs.length > 0 || this.rets.length > 0; }
next() {
if (this.completed()) return {value: undefined, done: true}; // Mimic standard generators.
return {value: this.value++, done: false};
if (this.completed())
return { value: undefined, done: true }; // Mimic standard generators.
return { value: this.value++, done: false };
}
throw(err) {
const alreadyCompleted = this.completed();
this.errs.push(err);
if (alreadyCompleted) throw err; // Mimic standard generator objects.
if (alreadyCompleted)
throw err; // Mimic standard generator objects.
throw err;
}
return(ret) {
const alreadyCompleted = this.completed();
this.rets.push(ret);
if (alreadyCompleted) return {value: ret, done: true}; // Mimic standard generator objects.
return {value: ret, done: true};
if (alreadyCompleted)
return { value: ret, done: true }; // Mimic standard generator objects.
return { value: ret, done: true };
}
[Symbol.iterator]() { return this; }
}
const assertUnhandledRejection = async (action, want) => {
// Temporarily remove unhandled Promise rejection listeners so that the unhandled rejections we
// expect to see don't trigger a test failure (or terminate node).
@ -53,48 +49,43 @@ const assertUnhandledRejection = async (action, want) => {
process.on(event, tempListener);
await action();
await seenErrPromise;
} finally {
}
finally {
// Restore the original listeners.
process.off(event, tempListener);
for (const listener of listenersBackup) process.on(event, listener);
for (const listener of listenersBackup)
process.on(event, listener);
}
await assert.rejects(Promise.reject(asyncErr), want);
};
describe(__filename, function () {
describe('basic behavior', function () {
it('takes a generator', async function () {
assert.deepEqual([...new Stream((function* () { yield 0; yield 1; yield 2; })())], [0, 1, 2]);
});
it('takes an array', async function () {
assert.deepEqual([...new Stream([0, 1, 2])], [0, 1, 2]);
});
it('takes an iterator', async function () {
assert.deepEqual([...new Stream([0, 1, 2][Symbol.iterator]())], [0, 1, 2]);
});
it('supports empty iterators', async function () {
assert.deepEqual([...new Stream([])], []);
});
it('is resumable', async function () {
const s = new Stream((function* () { yield 0; yield 1; yield 2; })());
let iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), {value: 0, done: false});
assert.deepEqual(iter.next(), { value: 0, done: false });
iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), {value: 1, done: false});
assert.deepEqual(iter.next(), { value: 1, done: false });
assert.deepEqual([...s], [2]);
});
it('supports return value', async function () {
const s = new Stream((function* () { yield 0; return 1; })());
const iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), {value: 0, done: false});
assert.deepEqual(iter.next(), {value: 1, done: true});
assert.deepEqual(iter.next(), { value: 0, done: false });
assert.deepEqual(iter.next(), { value: 1, done: true });
});
it('does not start until needed', async function () {
let lastYield = null;
new Stream((function* () { yield lastYield = 0; })());
@ -102,105 +93,95 @@ describe(__filename, function () {
// from the stream.
assert.equal(lastYield, null);
});
it('throw is propagated', async function () {
const underlying = new DemoIterable();
const s = new Stream(underlying);
const iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), {value: 0, done: false});
assert.deepEqual(iter.next(), { value: 0, done: false });
const err = new Error('injected');
assert.throws(() => iter.throw(err), err);
assert.equal(underlying.errs[0], err);
});
it('return is propagated', async function () {
const underlying = new DemoIterable();
const s = new Stream(underlying);
const iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), {value: 0, done: false});
assert.deepEqual(iter.return(42), {value: 42, done: true});
assert.deepEqual(iter.next(), { value: 0, done: false });
assert.deepEqual(iter.return(42), { value: 42, done: true });
assert.equal(underlying.rets[0], 42);
});
});
describe('range', function () {
it('basic', async function () {
assert.deepEqual([...Stream.range(0, 3)], [0, 1, 2]);
});
it('empty', async function () {
assert.deepEqual([...Stream.range(0, 0)], []);
});
it('positive start', async function () {
assert.deepEqual([...Stream.range(3, 5)], [3, 4]);
});
it('negative start', async function () {
assert.deepEqual([...Stream.range(-3, 0)], [-3, -2, -1]);
});
it('end before start', async function () {
assert.deepEqual([...Stream.range(3, 0)], []);
});
});
describe('batch', function () {
it('empty', async function () {
assert.deepEqual([...new Stream([]).batch(10)], []);
});
it('does not start until needed', async function () {
let lastYield = null;
new Stream((function* () { yield lastYield = 0; })()).batch(10);
assert.equal(lastYield, null);
});
it('fewer than batch size', async function () {
let lastYield = null;
const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i;
for (let i = 0; i < 5; i++)
yield lastYield = i;
})();
const s = new Stream(values).batch(10);
assert.equal(lastYield, null);
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
assert.equal(lastYield, 4);
assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4);
});
it('exactly batch size', async function () {
let lastYield = null;
const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i;
for (let i = 0; i < 5; i++)
yield lastYield = i;
})();
const s = new Stream(values).batch(5);
assert.equal(lastYield, null);
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
assert.equal(lastYield, 4);
assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4);
});
it('multiple batches, last batch is not full', async function () {
let lastYield = null;
const values = (function* () {
for (let i = 0; i < 10; i++) yield lastYield = i;
for (let i = 0; i < 10; i++)
yield lastYield = i;
})();
const s = new Stream(values).batch(3);
assert.equal(lastYield, null);
const iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), {value: 0, done: false});
assert.deepEqual(iter.next(), { value: 0, done: false });
assert.equal(lastYield, 2);
assert.deepEqual(iter.next(), {value: 1, done: false});
assert.deepEqual(iter.next(), {value: 2, done: false});
assert.deepEqual(iter.next(), { value: 1, done: false });
assert.deepEqual(iter.next(), { value: 2, done: false });
assert.equal(lastYield, 2);
assert.deepEqual(iter.next(), {value: 3, done: false});
assert.deepEqual(iter.next(), { value: 3, done: false });
assert.equal(lastYield, 5);
assert.deepEqual([...s], [4, 5, 6, 7, 8, 9]);
assert.equal(lastYield, 9);
});
it('batched Promise rejections are suppressed while iterating', async function () {
let lastYield = null;
const err = new Error('injected');
@ -220,7 +201,6 @@ describe(__filename, function () {
await assert.rejects(iter.next().value, err);
iter.return();
});
it('batched Promise rejections are unsuppressed when iteration completes', async function () {
let lastYield = null;
const err = new Error('injected');
@ -239,62 +219,59 @@ describe(__filename, function () {
await assertUnhandledRejection(() => iter.return(), err);
});
});
describe('buffer', function () {
it('empty', async function () {
assert.deepEqual([...new Stream([]).buffer(10)], []);
});
it('does not start until needed', async function () {
let lastYield = null;
new Stream((function* () { yield lastYield = 0; })()).buffer(10);
assert.equal(lastYield, null);
});
it('fewer than buffer size', async function () {
let lastYield = null;
const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i;
for (let i = 0; i < 5; i++)
yield lastYield = i;
})();
const s = new Stream(values).buffer(10);
assert.equal(lastYield, null);
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
assert.equal(lastYield, 4);
assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4);
});
it('exactly buffer size', async function () {
let lastYield = null;
const values = (function* () {
for (let i = 0; i < 5; i++) yield lastYield = i;
for (let i = 0; i < 5; i++)
yield lastYield = i;
})();
const s = new Stream(values).buffer(5);
assert.equal(lastYield, null);
assert.deepEqual(s[Symbol.iterator]().next(), {value: 0, done: false});
assert.deepEqual(s[Symbol.iterator]().next(), { value: 0, done: false });
assert.equal(lastYield, 4);
assert.deepEqual([...s], [1, 2, 3, 4]);
assert.equal(lastYield, 4);
});
it('more than buffer size', async function () {
let lastYield = null;
const values = (function* () {
for (let i = 0; i < 10; i++) yield lastYield = i;
for (let i = 0; i < 10; i++)
yield lastYield = i;
})();
const s = new Stream(values).buffer(3);
assert.equal(lastYield, null);
const iter = s[Symbol.iterator]();
assert.deepEqual(iter.next(), {value: 0, done: false});
assert.deepEqual(iter.next(), { value: 0, done: false });
assert.equal(lastYield, 3);
assert.deepEqual(iter.next(), {value: 1, done: false});
assert.deepEqual(iter.next(), { value: 1, done: false });
assert.equal(lastYield, 4);
assert.deepEqual(iter.next(), {value: 2, done: false});
assert.deepEqual(iter.next(), { value: 2, done: false });
assert.equal(lastYield, 5);
assert.deepEqual([...s], [3, 4, 5, 6, 7, 8, 9]);
assert.equal(lastYield, 9);
});
it('buffered Promise rejections are suppressed while iterating', async function () {
let lastYield = null;
const err = new Error('injected');
@ -314,7 +291,6 @@ describe(__filename, function () {
await assert.rejects(iter.next().value, err);
iter.return();
});
it('buffered Promise rejections are unsuppressed when iteration completes', async function () {
let lastYield = null;
const err = new Error('injected');
@ -333,25 +309,21 @@ describe(__filename, function () {
await assertUnhandledRejection(() => iter.return(), err);
});
});
describe('map', function () {
it('empty', async function () {
let called = false;
assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
assert.equal(called, false);
});
it('does not start until needed', async function () {
let called = false;
assert.deepEqual([...new Stream([]).map((v) => called = true)], []);
new Stream((function* () { yield 0; })()).map((v) => called = true);
assert.equal(called, false);
});
it('works', async function () {
const calls = [];
assert.deepEqual(
[...new Stream([0, 1, 2]).map((v) => { calls.push(v); return 2 * v; })], [0, 2, 4]);
assert.deepEqual([...new Stream([0, 1, 2]).map((v) => { calls.push(v); return 2 * v; })], [0, 2, 4]);
assert.deepEqual(calls, [0, 1, 2]);
});
});

View file

@ -1,53 +1,38 @@
import * as common from "../../common.js";
import { validate } from "openapi-schema-validation";
'use strict';
/**
* API specs
*
* Tests for generic overarching HTTP API related features not related to any
* specific part of the data model or domain. For example: tests for versioning
* and openapi definitions.
*/
const common = require('../../common');
const validateOpenAPI = require('openapi-schema-validation').validate;
const validateOpenAPI = { validate }.validate;
let agent;
const apiKey = common.apiKey;
let apiVersion = 1;
const makeid = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
const testPadId = makeid();
const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`;
describe(__filename, function () {
before(async function () { agent = await common.init(); });
it('can obtain API version', async function () {
await agent.get('/api/')
.expect(200)
.expect((res) => {
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API');
if (!res.body.currentVersion)
throw new Error('No version set in API');
return;
});
});
it('can obtain valid openapi definition document', async function () {
this.timeout(15000);
await agent.get('/api/openapi.json')
.expect(200)
.expect((res) => {
const {valid, errors} = validateOpenAPI(res.body, 3);
const { valid, errors } = validateOpenAPI(res.body, 3);
if (!valid) {
const prettyErrors = JSON.stringify(errors, null, 2);
throw new Error(`Document is not valid OpenAPI. ${errors.length} ` +

View file

@ -1,40 +1,33 @@
import assert$0 from "assert";
import * as common from "../../common.js";
import fs from "fs";
'use strict';
/*
* This file is copied & modified from <basedir>/src/tests/backend/specs/api/pad.js
*
* TODO: maybe unify those two files and merge in a single one.
*/
const assert = require('assert').strict;
const common = require('../../common');
const fs = require('fs');
const assert = assert$0.strict;
const fsp = fs.promises;
let agent;
const apiKey = common.apiKey;
let apiVersion = 1;
const testPadId = makeid();
const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`;
describe(__filename, function () {
before(async function () { agent = await common.init(); });
describe('Sanity checks', function () {
it('can connect', async function () {
await agent.get('/api/')
.expect(200)
.expect('Content-Type', /json/);
});
it('finds the version tag', async function () {
const res = await agent.get('/api/')
.expect(200);
apiVersion = res.body.currentVersion;
assert(apiVersion);
});
it('errors with invalid APIKey', async function () {
// This is broken because Etherpad doesn't handle HTTP codes properly see #2343
// If your APIKey is password you deserve to fail all tests anyway
@ -42,7 +35,6 @@ describe(__filename, function () {
.expect(401);
});
});
describe('Tests', function () {
it('creates a new Pad', async function () {
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
@ -50,7 +42,6 @@ describe(__filename, function () {
.expect('Content-Type', /json/);
assert.equal(res.body.code, 0);
});
it('Sets the HTML of a Pad attempting to weird utf8 encoded content', async function () {
const res = await agent.post(endPoint('setHTML'))
.send({
@ -61,7 +52,6 @@ describe(__filename, function () {
.expect('Content-Type', /json/);
assert.equal(res.body.code, 0);
});
it('get the HTML of Pad with emojis', async function () {
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
.expect(200)
@ -70,17 +60,14 @@ describe(__filename, function () {
});
});
});
/*
End of test
*/
function makeid() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 10; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}

View file

@ -1,35 +1,29 @@
import * as common from "../../common.js";
'use strict';
const common = require('../../common');
let agent;
const apiKey = common.apiKey;
let apiVersion = 1;
let authorID = '';
const padID = makeid();
const timestamp = Date.now();
const endPoint = (point) => `/api/${apiVersion}/${point}?apikey=${apiKey}`;
describe(__filename, function () {
before(async function () { agent = await common.init(); });
describe('API Versioning', function () {
it('errors if can not connect', function (done) {
agent.get('/api/')
.expect((res) => {
apiVersion = res.body.currentVersion;
if (!res.body.currentVersion) throw new Error('No version set in API');
if (!res.body.currentVersion)
throw new Error('No version set in API');
return;
})
.expect(200, done);
});
});
// BEGIN GROUP AND AUTHOR TESTS
// ///////////////////////////////////
// ///////////////////////////////////
/* Tests performed
-> createPad(padID)
-> createAuthor([name]) -- should return an authorID
@ -37,18 +31,17 @@ describe(__filename, function () {
-> getChatHead(padID)
-> getChatHistory(padID)
*/
describe('createPad', function () {
it('creates a new Pad', function (done) {
agent.get(`${endPoint('createPad')}&padID=${padID}`)
.expect((res) => {
if (res.body.code !== 0) throw new Error('Unable to create new Pad');
if (res.body.code !== 0)
throw new Error('Unable to create new Pad');
})
.expect('Content-Type', /json/)
.expect(200, done);
});
});
describe('createAuthor', function () {
it('Creates an author with a name set', function (done) {
agent.get(endPoint('createAuthor'))
@ -62,33 +55,31 @@ describe(__filename, function () {
.expect(200, done);
});
});
describe('appendChatMessage', function () {
it('Adds a chat message to the pad', function (done) {
agent.get(`${endPoint('appendChatMessage')}&padID=${padID}&text=blalblalbha` +
`&authorID=${authorID}&time=${timestamp}`)
.expect((res) => {
if (res.body.code !== 0) throw new Error('Unable to create chat message');
if (res.body.code !== 0)
throw new Error('Unable to create chat message');
})
.expect('Content-Type', /json/)
.expect(200, done);
});
});
describe('getChatHead', function () {
it('Gets the head of chat', function (done) {
agent.get(`${endPoint('getChatHead')}&padID=${padID}`)
.expect((res) => {
if (res.body.data.chatHead !== 0) throw new Error('Chat Head Length is wrong');
if (res.body.code !== 0) throw new Error('Unable to get chat head');
if (res.body.data.chatHead !== 0)
throw new Error('Chat Head Length is wrong');
if (res.body.code !== 0)
throw new Error('Unable to get chat head');
})
.expect('Content-Type', /json/)
.expect(200, done);
});
});
describe('getChatHistory', function () {
it('Gets Chat History of a Pad', function (done) {
agent.get(`${endPoint('getChatHistory')}&padID=${padID}`)
@ -96,18 +87,17 @@ describe(__filename, function () {
if (res.body.data.messages.length !== 1) {
throw new Error('Chat History Length is wrong');
}
if (res.body.code !== 0) throw new Error('Unable to get chat history');
if (res.body.code !== 0)
throw new Error('Unable to get chat history');
})
.expect('Content-Type', /json/)
.expect(200, done);
});
});
});
function makeid() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}

View file

@ -1,3 +1,5 @@
import assert$0 from "assert";
import * as common from "../../common.js";
'use strict';
/*
* ACHTUNG: there is a copied & modified version of this file in
@ -5,16 +7,11 @@
*
* TODO: unify those two files, and merge in a single one.
*/
const assert = require('assert').strict;
const common = require('../../common');
const assert = assert$0.strict;
let agent;
const apiKey = common.apiKey;
const apiVersion = 1;
const endPoint = (point, version) => `/api/${version || apiVersion}/${point}?apikey=${apiKey}`;
const testImports = {
'malformed': {
input: '<html><body><li>wtf</ul></body></html>',
@ -43,7 +40,6 @@ const testImports = {
wantHTML: '<!DOCTYPE HTML><html><body><ol start="1" class="number"><li>should be 1<ol start="2" class="number"><li>foo</ol><li>should be 2</ol><br></body></html>',
wantText: '\t1. should be 1\n\t\t1.1. foo\n\t2. should be 2\n\n',
},
/*
"prefixcorrectlinenumber when introduced none list item - currently not supported see #3450": {
input: '<html><body><ol><li>should be 1</li>test<li>should be 2</li></ol></body></html>',
@ -225,12 +221,9 @@ const testImports = {
wantText: 'Need more space s !\n\n',
},
};
describe(__filename, function () {
this.timeout(1000);
before(async function () { agent = await common.init(); });
Object.keys(testImports).forEach((testName) => {
describe(testName, function () {
const testPadId = makeid();
@ -240,14 +233,12 @@ describe(__filename, function () {
done();
});
}
it('createPad', async function () {
const res = await agent.get(`${endPoint('createPad')}&padID=${testPadId}`)
.expect(200)
.expect('Content-Type', /json/);
assert.equal(res.body.code, 0);
});
it('setHTML', async function () {
const res = await agent.get(`${endPoint('setHTML')}&padID=${testPadId}` +
`&html=${encodeURIComponent(test.input)}`)
@ -255,14 +246,12 @@ describe(__filename, function () {
.expect('Content-Type', /json/);
assert.equal(res.body.code, 0);
});
it('getHTML', async function () {
const res = await agent.get(`${endPoint('getHTML')}&padID=${testPadId}`)
.expect(200)
.expect('Content-Type', /json/);
assert.equal(res.body.data.html, test.wantHTML);
});
it('getText', async function () {
const res = await agent.get(`${endPoint('getText')}&padID=${testPadId}`)
.expect(200)
@ -272,11 +261,9 @@ describe(__filename, function () {
});
});
});
function makeid() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 5; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}

View file

@ -1,18 +1,10 @@
import * as common from "../common.js";
import assertLegacy from "../assert-legacy.js";
import queryString from "querystring";
import * as settings from "../../../node/utils/Settings.js";
'use strict';
/**
* caching_middleware is responsible for serving everything under path `/javascripts/`
* That includes packages as defined in `src/node/utils/tar.json` and probably also plugin code
*
*/
const common = require('../common');
const assert = require('../assert-legacy').strict;
const queryString = require('querystring');
const settings = require('../../../node/utils/Settings');
const assert = assertLegacy.strict;
let agent;
/**
* Hack! Returns true if the resource is not plaintext
* The file should start with the callback method, so we need the
@ -27,11 +19,9 @@ const isPlaintextResponse = (fileContent, resource) => {
const query = (new URL(resource, 'http://localhost')).search.slice(1);
// require.define
const jsonp = queryString.parse(query).callback;
// returns true if the first letters in fileContent equal the content of `jsonp`
return fileContent.substring(0, jsonp.length) === jsonp;
};
/**
* A hack to disable `superagent`'s auto unzip functionality
*
@ -40,7 +30,6 @@ const isPlaintextResponse = (fileContent, resource) => {
const disableAutoDeflate = (request) => {
request._shouldUnzip = () => false;
};
describe(__filename, function () {
const backups = {};
const fantasyEncoding = 'brainwaves'; // non-working encoding until https://github.com/visionmedia/superagent/pull/1560 is resolved
@ -50,7 +39,6 @@ describe(__filename, function () {
'/javascripts/lib/ep_etherpad-lite/static/js/pad.js?callback=require.define',
'/javascripts/lib/ep_etherpad-lite/static/js/timeslider.js?callback=require.define',
];
before(async function () {
agent = await common.init();
backups.settings = {};
@ -59,13 +47,11 @@ describe(__filename, function () {
after(async function () {
Object.assign(settings, backups.settings);
});
for (const minify of [false, true]) {
context(`when minify is ${minify}`, function () {
before(async function () {
settings.minify = minify;
});
describe('gets packages uncompressed without Accept-Encoding gzip', function () {
for (const resource of packages) {
it(resource, async function () {
@ -81,7 +67,6 @@ describe(__filename, function () {
});
}
});
describe('gets packages compressed with Accept-Encoding gzip', function () {
for (const resource of packages) {
it(resource, async function () {
@ -97,7 +82,6 @@ describe(__filename, function () {
});
}
});
it('does not cache content-encoding headers', async function () {
await agent.get(packages[0])
.set('Accept-Encoding', fantasyEncoding)

View file

@ -1,27 +1,28 @@
import ChatMessage from "../../../static/js/ChatMessage.js";
import { Pad } from "../../../node/db/Pad.js";
import assert$0 from "assert";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as pluginDefs from "../../../static/js/pluginfw/plugin_defs.js";
'use strict';
const ChatMessage = require('../../../static/js/ChatMessage');
const {Pad} = require('../../../node/db/Pad');
const assert = require('assert').strict;
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
const assert = assert$0.strict;
const logger = common.logger;
const checkHook = async (hookName, checkFn) => {
if (pluginDefs.hooks[hookName] == null) pluginDefs.hooks[hookName] = [];
if (pluginDefs.hooks[hookName] == null)
pluginDefs.hooks[hookName] = [];
await new Promise((resolve, reject) => {
pluginDefs.hooks[hookName].push({
hook_fn: async (hookName, context) => {
if (checkFn == null) return;
if (checkFn == null)
return;
logger.debug(`hook ${hookName} invoked`);
try {
// Make sure checkFn is called only once.
const _checkFn = checkFn;
checkFn = null;
await _checkFn(context);
} catch (err) {
}
catch (err) {
reject(err);
return;
}
@ -30,7 +31,6 @@ const checkHook = async (hookName, checkFn) => {
});
});
};
const sendMessage = (socket, data) => {
socket.send({
type: 'COLLABROOM',
@ -38,56 +38,51 @@ const sendMessage = (socket, data) => {
data,
});
};
const sendChat = (socket, message) => sendMessage(socket, {type: 'CHAT_MESSAGE', message});
const sendChat = (socket, message) => sendMessage(socket, { type: 'CHAT_MESSAGE', message });
describe(__filename, function () {
const padId = 'testChatPad';
const hooksBackup = {};
before(async function () {
for (const [name, defs] of Object.entries(pluginDefs.hooks)) {
if (defs == null) continue;
if (defs == null)
continue;
hooksBackup[name] = defs;
}
});
beforeEach(async function () {
for (const [name, defs] of Object.entries(hooksBackup)) pluginDefs.hooks[name] = [...defs];
for (const [name, defs] of Object.entries(hooksBackup))
pluginDefs.hooks[name] = [...defs];
for (const name of Object.keys(pluginDefs.hooks)) {
if (hooksBackup[name] == null) delete pluginDefs.hooks[name];
if (hooksBackup[name] == null)
delete pluginDefs.hooks[name];
}
if (await padManager.doesPadExist(padId)) {
const pad = await padManager.getPad(padId);
await pad.remove();
}
});
after(async function () {
Object.assign(pluginDefs.hooks, hooksBackup);
for (const name of Object.keys(pluginDefs.hooks)) {
if (hooksBackup[name] == null) delete pluginDefs.hooks[name];
if (hooksBackup[name] == null)
delete pluginDefs.hooks[name];
}
});
describe('chatNewMessage hook', function () {
let authorId;
let socket;
beforeEach(async function () {
socket = await common.connect();
const {data: clientVars} = await common.handshake(socket, padId);
const { data: clientVars } = await common.handshake(socket, padId);
authorId = clientVars.userId;
});
afterEach(async function () {
socket.close();
});
it('message', async function () {
const start = Date.now();
await Promise.all([
checkHook('chatNewMessage', ({message}) => {
checkHook('chatNewMessage', ({ message }) => {
assert(message != null);
assert(message instanceof ChatMessage);
assert.equal(message.authorId, authorId);
@ -95,65 +90,63 @@ describe(__filename, function () {
assert(message.time >= start);
assert(message.time <= Date.now());
}),
sendChat(socket, {text: this.test.title}),
sendChat(socket, { text: this.test.title }),
]);
});
it('pad', async function () {
await Promise.all([
checkHook('chatNewMessage', ({pad}) => {
checkHook('chatNewMessage', ({ pad }) => {
assert(pad != null);
assert(pad instanceof Pad);
assert.equal(pad.id, padId);
}),
sendChat(socket, {text: this.test.title}),
sendChat(socket, { text: this.test.title }),
]);
});
it('padId', async function () {
await Promise.all([
checkHook('chatNewMessage', (context) => {
assert.equal(context.padId, padId);
}),
sendChat(socket, {text: this.test.title}),
sendChat(socket, { text: this.test.title }),
]);
});
it('mutations propagate', async function () {
const listen = async (type) => await new Promise((resolve) => {
const handler = (msg) => {
if (msg.type !== 'COLLABROOM') return;
if (msg.data == null || msg.data.type !== type) return;
if (msg.type !== 'COLLABROOM')
return;
if (msg.data == null || msg.data.type !== type)
return;
resolve(msg.data);
socket.off('message', handler);
};
socket.on('message', handler);
});
const modifiedText = `${this.test.title} <added changes>`;
const customMetadata = {foo: this.test.title};
const customMetadata = { foo: this.test.title };
await Promise.all([
checkHook('chatNewMessage', ({message}) => {
checkHook('chatNewMessage', ({ message }) => {
message.text = modifiedText;
message.customMetadata = customMetadata;
}),
(async () => {
const {message} = await listen('CHAT_MESSAGE');
const { message } = await listen('CHAT_MESSAGE');
assert(message != null);
assert.equal(message.text, modifiedText);
assert.deepEqual(message.customMetadata, customMetadata);
})(),
sendChat(socket, {text: this.test.title}),
sendChat(socket, { text: this.test.title }),
]);
// Simulate fetch of historical chat messages when a pad is first loaded.
await Promise.all([
(async () => {
const {messages: [message]} = await listen('CHAT_MESSAGES');
const { messages: [message] } = await listen('CHAT_MESSAGES');
assert(message != null);
assert.equal(message.text, modifiedText);
assert.deepEqual(message.customMetadata, customMetadata);
})(),
sendMessage(socket, {type: 'GET_CHAT_MESSAGES', start: 0, end: 0}),
sendMessage(socket, { type: 'GET_CHAT_MESSAGES', start: 0, end: 0 }),
]);
});
});

View file

@ -1,21 +1,11 @@
import AttributePool from "../../../static/js/AttributePool.js";
import * as Changeset from "../../../static/js/Changeset.js";
import assert$0 from "assert";
import * as attributes from "../../../static/js/attributes.js";
import * as contentcollector from "../../../static/js/contentcollector.js";
import * as jsdom from "jsdom";
'use strict';
/*
* While importexport tests target the `setHTML` API endpoint, which is nearly identical to what
* happens when a user manually imports a document via the UI, the contentcollector tests here don't
* use rehype to process the document. Rehype removes spaces and newĺines were applicable, so the
* expected results here can differ from importexport.js.
*
* If you add tests here, please also add them to importexport.js
*/
const AttributePool = require('../../../static/js/AttributePool');
const Changeset = require('../../../static/js/Changeset');
const assert = require('assert').strict;
const attributes = require('../../../static/js/attributes');
const contentcollector = require('../../../static/js/contentcollector');
const jsdom = require('jsdom');
const assert = assert$0.strict;
// All test case `wantAlines` values must only refer to attributes in this list so that the
// attribute numbers do not change due to changes in pool insertion order.
const knownAttribs = [
@ -29,7 +19,6 @@ const knownAttribs = [
['start', '1'],
['start', '2'],
];
const testCases = [
{
description: 'Simple',
@ -330,21 +319,21 @@ pre
wantText: ['Need more space s !'],
},
];
describe(__filename, function () {
for (const tc of testCases) {
describe(tc.description, function () {
let apool;
let result;
before(async function () {
if (tc.disabled) return this.skip();
const {window: {document}} = new jsdom.JSDOM(tc.html);
if (tc.disabled)
return this.skip();
const { window: { document } } = new jsdom.JSDOM(tc.html);
apool = new AttributePool();
// To reduce test fragility, the attribute pool is seeded with `knownAttribs`, and all
// attributes in `tc.wantAlines` must be in `knownAttribs`. (This guarantees that attribute
// numbers do not change if the attribute processing code changes.)
for (const attrib of knownAttribs) apool.putAttrib(attrib);
for (const attrib of knownAttribs)
apool.putAttrib(attrib);
for (const aline of tc.wantAlines) {
for (const op of Changeset.deserializeOps(aline)) {
for (const n of attributes.decodeAttribString(op.attribs)) {
@ -356,15 +345,12 @@ describe(__filename, function () {
cc.collectContent(document.body);
result = cc.finish();
});
it('text matches', async function () {
assert.deepEqual(result.lines, tc.wantText);
});
it('alines match', async function () {
assert.deepEqual(result.lineAttribs, tc.wantAlines);
});
it('attributes are sorted in canonical order', async function () {
const gotAttribs = [];
const wantAttribs = [];

View file

@ -1,23 +1,18 @@
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as settings from "../../../node/utils/Settings.js";
'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')

View file

@ -1,13 +1,12 @@
import assert$0 from "assert";
import * as common from "../common.js";
import fs from "fs";
import path from "path";
import * as settings from "../../../node/utils/Settings.js";
import superagent from "superagent";
'use strict';
const assert = require('assert').strict;
const common = require('../common');
const fs = require('fs');
const assert = assert$0.strict;
const fsp = fs.promises;
const path = require('path');
const settings = require('../../../node/utils/Settings');
const superagent = require('superagent');
describe(__filename, function () {
let agent;
let backupSettings;
@ -15,20 +14,17 @@ describe(__filename, function () {
let wantCustomIcon;
let wantDefaultIcon;
let wantSkinIcon;
before(async function () {
agent = await common.init();
wantCustomIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-custom.png'));
wantDefaultIcon = await fsp.readFile(path.join(settings.root, 'src', 'static', 'favicon.ico'));
wantSkinIcon = await fsp.readFile(path.join(__dirname, 'favicon-test-skin.png'));
});
beforeEach(async function () {
backupSettings = {...settings};
backupSettings = { ...settings };
skinDir = await fsp.mkdtemp(path.join(settings.root, 'src', 'static', 'skins', 'test-'));
settings.skinName = path.basename(skinDir);
});
afterEach(async function () {
delete settings.favicon;
delete settings.skinName;
@ -37,53 +33,49 @@ describe(__filename, function () {
// TODO: The {recursive: true} option wasn't added to fsp.rmdir() until Node.js v12.10.0 so we
// can't rely on it until support for Node.js v10 is dropped.
await fsp.unlink(path.join(skinDir, 'favicon.ico'));
await fsp.rmdir(skinDir, {recursive: true});
} catch (err) { /* intentionally ignored */ }
await fsp.rmdir(skinDir, { recursive: true });
}
catch (err) { /* intentionally ignored */ }
});
it('uses custom favicon if set (relative pathname)', async function () {
settings.favicon =
path.relative(settings.root, path.join(__dirname, 'favicon-test-custom.png'));
assert(!path.isAbsolute(settings.favicon));
const {body: gotIcon} = await agent.get('/favicon.ico')
const { body: gotIcon } = await agent.get('/favicon.ico')
.accept('png').buffer(true).parse(superagent.parse.image)
.expect(200);
assert(gotIcon.equals(wantCustomIcon));
});
it('uses custom favicon if set (absolute pathname)', async function () {
settings.favicon = path.join(__dirname, 'favicon-test-custom.png');
assert(path.isAbsolute(settings.favicon));
const {body: gotIcon} = await agent.get('/favicon.ico')
const { body: gotIcon } = await agent.get('/favicon.ico')
.accept('png').buffer(true).parse(superagent.parse.image)
.expect(200);
assert(gotIcon.equals(wantCustomIcon));
});
it('falls back if custom favicon is missing', async function () {
// The previous default for settings.favicon was 'favicon.ico', so many users will continue to
// have that in their settings.json for a long time. There is unlikely to be a favicon at
// path.resolve(settings.root, 'favicon.ico'), so this test ensures that 'favicon.ico' won't be
// a problem for those users.
settings.favicon = 'favicon.ico';
const {body: gotIcon} = await agent.get('/favicon.ico')
const { body: gotIcon } = await agent.get('/favicon.ico')
.accept('png').buffer(true).parse(superagent.parse.image)
.expect(200);
assert(gotIcon.equals(wantDefaultIcon));
});
it('uses skin favicon if present', async function () {
await fsp.writeFile(path.join(skinDir, 'favicon.ico'), wantSkinIcon);
settings.favicon = null;
const {body: gotIcon} = await agent.get('/favicon.ico')
const { body: gotIcon } = await agent.get('/favicon.ico')
.accept('png').buffer(true).parse(superagent.parse.image)
.expect(200);
assert(gotIcon.equals(wantSkinIcon));
});
it('falls back to default favicon', async function () {
settings.favicon = null;
const {body: gotIcon} = await agent.get('/favicon.ico')
const { body: gotIcon } = await agent.get('/favicon.ico')
.accept('png').buffer(true).parse(superagent.parse.image)
.expect(200);
assert(gotIcon.equals(wantDefaultIcon));

View file

@ -1,49 +1,41 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as settings from "../../../node/utils/Settings.js";
import superagent from "superagent";
'use strict';
const assert = require('assert').strict;
const common = require('../common');
const settings = require('../../../node/utils/Settings');
const superagent = require('superagent');
const assert = assert$0.strict;
describe(__filename, function () {
let agent;
const backup = {};
const getHealth = () => agent.get('/health')
.accept('application/health+json')
.buffer(true)
.parse(superagent.parse['application/json'])
.expect(200)
.expect((res) => assert.equal(res.type, 'application/health+json'));
before(async function () {
agent = await common.init();
});
beforeEach(async function () {
backup.settings = {};
for (const setting of ['requireAuthentication', 'requireAuthorization']) {
backup.settings[setting] = settings[setting];
}
});
afterEach(async function () {
Object.assign(settings, backup.settings);
});
it('/health works', async function () {
const res = await getHealth();
assert.equal(res.body.status, 'pass');
assert.equal(res.body.releaseId, settings.getEpVersion());
});
it('auth is not required', async function () {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
const res = await getHealth();
assert.equal(res.body.status, 'pass');
});
// We actually want to test that no express-session state is created, but that is difficult to do
// without intrusive changes or unpleasant ueberdb digging. Instead, we assume that the lack of a
// cookie means that no express-session state was created (how would express-session look up the

View file

@ -1,37 +1,32 @@
import assertLegacy from "../assert-legacy.js";
import * as hooks from "../../../static/js/pluginfw/hooks.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import sinon from "sinon";
'use strict';
const assert = require('../assert-legacy').strict;
const hooks = require('../../../static/js/pluginfw/hooks');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const sinon = require('sinon');
const assert = assertLegacy.strict;
describe(__filename, function () {
const hookName = 'testHook';
const hookFnName = 'testPluginFileName:testHookFunctionName';
let testHooks; // Convenience shorthand for plugins.hooks[hookName].
let hook; // Convenience shorthand for plugins.hooks[hookName][0].
beforeEach(async function () {
// Make sure these are not already set so that we don't accidentally step on someone else's
// toes:
assert(plugins.hooks[hookName] == null);
assert(hooks.deprecationNotices[hookName] == null);
assert(hooks.exportedForTestingOnly.deprecationWarned[hookFnName] == null);
// Many of the tests only need a single registered hook function. Set that up here to reduce
// boilerplate.
hook = makeHook();
plugins.hooks[hookName] = [hook];
testHooks = plugins.hooks[hookName];
});
afterEach(async function () {
sinon.restore();
delete plugins.hooks[hookName];
delete hooks.deprecationNotices[hookName];
delete hooks.exportedForTestingOnly.deprecationWarned[hookFnName];
});
const makeHook = (ret) => ({
hook_name: hookName,
// Many tests will likely want to change this. Unfortunately, we can't use a convenience
@ -39,9 +34,8 @@ describe(__filename, function () {
// change behavior depending on the number of parameters.
hook_fn: (hn, ctx, cb) => cb(ret),
hook_fn_name: hookFnName,
part: {plugin: 'testPluginName'},
part: { plugin: 'testPluginName' },
});
// Hook functions that should work for both synchronous and asynchronous hooks.
const supportedSyncHookFunctions = [
{
@ -58,7 +52,7 @@ describe(__filename, function () {
},
{
name: 'return undefined, without callback parameter',
fn: (hn, ctx) => {},
fn: (hn, ctx) => { },
want: undefined,
syncOk: true,
},
@ -87,30 +81,25 @@ describe(__filename, function () {
syncOk: true,
},
];
describe('callHookFnSync', function () {
const callHookFnSync = hooks.exportedForTestingOnly.callHookFnSync; // Convenience shorthand.
describe('basic behavior', function () {
it('passes hook name', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
callHookFnSync(hook);
});
it('passes context', async function () {
for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); };
callHookFnSync(hook, val);
}
});
it('returns the value provided to the callback', async function () {
for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx, cb) => { cb(ctx); };
assert.equal(callHookFnSync(hook, val), val);
}
});
it('returns the value returned by the hook function', async function () {
for (const val of ['value', null, undefined]) {
// Must not have the cb parameter otherwise returning undefined will error.
@ -118,17 +107,14 @@ describe(__filename, function () {
assert.equal(callHookFnSync(hook, val), val);
}
});
it('does not catch exceptions', async function () {
hook.hook_fn = () => { throw new Error('test exception'); };
assert.throws(() => callHookFnSync(hook), {message: 'test exception'});
assert.throws(() => callHookFnSync(hook), { message: 'test exception' });
});
it('callback returns undefined', async function () {
hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); };
callHookFnSync(hook);
});
it('checks for deprecation', async function () {
sinon.stub(console, 'warn');
hooks.deprecationNotices[hookName] = 'test deprecation';
@ -138,7 +124,6 @@ describe(__filename, function () {
assert.match(console.warn.getCall(0).args[0], /test deprecation/);
});
});
describe('supported hook function styles', function () {
for (const tc of supportedSyncHookFunctions) {
it(tc.name, async function () {
@ -147,8 +132,9 @@ describe(__filename, function () {
hook.hook_fn = tc.fn;
const call = () => callHookFnSync(hook);
if (tc.wantErr) {
assert.throws(call, {message: tc.wantErr});
} else {
assert.throws(call, { message: tc.wantErr });
}
else {
assert.equal(call(), tc.want);
}
assert.equal(console.warn.callCount, 0);
@ -156,17 +142,15 @@ describe(__filename, function () {
});
}
});
describe('bad hook function behavior (other than double settle)', function () {
const promise1 = Promise.resolve('val1');
const promise2 = Promise.resolve('val2');
const testCases = [
{
name: 'never settles -> buggy hook detected',
// Note that returning undefined without calling the callback is permitted if the function
// has 2 or fewer parameters, so this test function must have 3 parameters.
fn: (hn, ctx, cb) => {},
fn: (hn, ctx, cb) => { },
wantVal: undefined,
wantError: /UNSETTLED FUNCTION BUG/,
},
@ -183,25 +167,23 @@ describe(__filename, function () {
wantError: /PROHIBITED PROMISE BUG/,
},
];
for (const tc of testCases) {
it(tc.name, async function () {
sinon.stub(console, 'error');
hook.hook_fn = tc.fn;
assert.equal(callHookFnSync(hook), tc.wantVal);
assert.equal(console.error.callCount, tc.wantError ? 1 : 0);
if (tc.wantError) assert.match(console.error.getCall(0).args[0], tc.wantError);
if (tc.wantError)
assert.match(console.error.getCall(0).args[0], tc.wantError);
});
}
});
// Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
// time, or call the callback and then return a value.)
describe('bad hook function behavior (double settle)', function () {
beforeEach(async function () {
sinon.stub(console, 'error');
});
// Each item in this array codifies a way to settle a synchronous hook function. Each of the
// test cases below combines two of these behaviors in a single hook function and confirms
// that callHookFnSync both (1) returns the result of the first settle attempt, and
@ -226,22 +208,21 @@ describe(__filename, function () {
async: true,
},
];
for (const step1 of behaviors) {
// There can't be a second step if the first step is to return or throw.
if (step1.name.startsWith('return ') || step1.name === 'throw') continue;
if (step1.name.startsWith('return ') || step1.name === 'throw')
continue;
for (const step2 of behaviors) {
// If step1 and step2 are both async then there would be three settle attempts (first an
// erroneous unsettled return, then async step 1, then async step 2). Handling triple
// settle would complicate the tests, and it is sufficient to test only double settles.
if (step1.async && step2.async) continue;
if (step1.async && step2.async)
continue;
it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () {
hook.hook_fn = (hn, ctx, cb) => {
step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
};
// Temporarily remove unhandled error listeners so that the errors we expect to see
// don't trigger a test failure (or terminate node).
const events = ['uncaughtException', 'unhandledRejection'];
@ -250,7 +231,6 @@ describe(__filename, function () {
listenerBackups[event] = process.rawListeners(event);
process.removeAllListeners(event);
}
// We should see an asynchronous error (either an unhandled Promise rejection or an
// uncaught exception) if and only if one of the two steps was asynchronous or there was
// a throw (in which case the double settle is deferred so that the caller sees the
@ -265,19 +245,23 @@ describe(__filename, function () {
asyncErr = err;
resolve();
};
if (!wantAsyncErr) resolve();
if (!wantAsyncErr)
resolve();
});
events.forEach((event) => process.on(event, tempListener));
const call = () => callHookFnSync(hook, {ret1: 'val1', ret2: 'val2'});
const call = () => callHookFnSync(hook, { ret1: 'val1', ret2: 'val2' });
if (step2.rejects) {
assert.throws(call, {message: 'val2'});
} else if (!step1.async && !step2.async) {
assert.throws(call, {message: /DOUBLE SETTLE BUG/});
} else {
assert.throws(call, { message: 'val2' });
}
else if (!step1.async && !step2.async) {
assert.throws(call, { message: /DOUBLE SETTLE BUG/ });
}
else {
assert.equal(call(), step1.async ? 'val2' : 'val1');
}
await seenErrPromise;
} finally {
}
finally {
// Restore the original listeners.
for (const event of events) {
process.off(event, tempListener);
@ -293,24 +277,23 @@ describe(__filename, function () {
assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
}
});
// This next test is the same as the above test, except the second settle attempt is for
// the same outcome. The two outcomes can't be the same if one step throws and the other
// doesn't, so skip those cases.
if (step1.rejects !== step2.rejects) continue;
if (step1.rejects !== step2.rejects)
continue;
it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () {
const err = new Error('val');
hook.hook_fn = (hn, ctx, cb) => {
step1.fn(cb, err, 'val');
return step2.fn(cb, err, 'val');
};
const errorLogged = new Promise((resolve) => console.error.callsFake(resolve));
const call = () => callHookFnSync(hook);
if (step2.rejects) {
assert.throws(call, {message: 'val'});
} else {
assert.throws(call, { message: 'val' });
}
else {
assert.equal(call(), 'val');
}
await errorLogged;
@ -321,7 +304,6 @@ describe(__filename, function () {
}
});
});
describe('hooks.callAll', function () {
describe('basic behavior', function () {
it('calls all in order', async function () {
@ -329,58 +311,48 @@ describe(__filename, function () {
testHooks.push(makeHook(1), makeHook(2), makeHook(3));
assert.deepEqual(hooks.callAll(hookName), [1, 2, 3]);
});
it('passes hook name', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
hooks.callAll(hookName);
});
it('undefined context -> {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callAll(hookName);
});
it('null context -> {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callAll(hookName, null);
});
it('context unmodified', async function () {
const wantContext = {};
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
hooks.callAll(hookName, wantContext);
});
});
describe('result processing', function () {
it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks.testHook;
assert.deepEqual(hooks.callAll(hookName), []);
});
it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0;
assert.deepEqual(hooks.callAll(hookName), []);
});
it('flattens one level', async function () {
testHooks.length = 0;
testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
assert.deepEqual(hooks.callAll(hookName), [1, 2, [3]]);
});
it('filters out undefined', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]));
assert.deepEqual(hooks.callAll(hookName), [2, [3]]);
});
it('preserves null', async function () {
testHooks.length = 0;
testHooks.push(makeHook(null), makeHook([2]), makeHook([[3]]));
assert.deepEqual(hooks.callAll(hookName), [null, 2, [3]]);
});
it('all undefined -> []', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook());
@ -388,39 +360,32 @@ describe(__filename, function () {
});
});
});
describe('hooks.callFirst', function () {
it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks.testHook;
assert.deepEqual(hooks.callFirst(hookName), []);
});
it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0;
assert.deepEqual(hooks.callFirst(hookName), []);
});
it('passes hook name => {}', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
hooks.callFirst(hookName);
});
it('undefined context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callFirst(hookName);
});
it('null context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
hooks.callFirst(hookName, null);
});
it('context unmodified', async function () {
const wantContext = {};
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
hooks.callFirst(hookName, wantContext);
});
it('predicate never satisfied -> calls all in order', async function () {
const gotCalls = [];
testHooks.length = 0;
@ -432,38 +397,32 @@ describe(__filename, function () {
assert.deepEqual(hooks.callFirst(hookName), []);
assert.deepEqual(gotCalls, [0, 1, 2]);
});
it('stops when predicate is satisfied', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
assert.deepEqual(hooks.callFirst(hookName), ['val1']);
});
it('skips values that do not satisfy predicate (undefined)', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1'));
assert.deepEqual(hooks.callFirst(hookName), ['val1']);
});
it('skips values that do not satisfy predicate (empty list)', async function () {
testHooks.length = 0;
testHooks.push(makeHook([]), makeHook('val1'));
assert.deepEqual(hooks.callFirst(hookName), ['val1']);
});
it('null satisifes the predicate', async function () {
testHooks.length = 0;
testHooks.push(makeHook(null), makeHook('val1'));
assert.deepEqual(hooks.callFirst(hookName), [null]);
});
it('non-empty arrays are returned unmodified', async function () {
const want = ['val1'];
testHooks.length = 0;
testHooks.push(makeHook(want), makeHook(['val2']));
assert.equal(hooks.callFirst(hookName), want); // Note: *NOT* deepEqual!
});
it('value can be passed via callback', async function () {
const want = {};
hook.hook_fn = (hn, ctx, cb) => { cb(want); };
@ -472,23 +431,19 @@ describe(__filename, function () {
assert.equal(got[0], want); // Note: *NOT* deepEqual!
});
});
describe('callHookFnAsync', function () {
const callHookFnAsync = hooks.exportedForTestingOnly.callHookFnAsync; // Convenience shorthand.
describe('basic behavior', function () {
it('passes hook name', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
await callHookFnAsync(hook);
});
it('passes context', async function () {
for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, val); };
await callHookFnAsync(hook, val);
}
});
it('returns the value provided to the callback', async function () {
for (const val of ['value', null, undefined]) {
hook.hook_fn = (hn, ctx, cb) => { cb(ctx); };
@ -496,7 +451,6 @@ describe(__filename, function () {
assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
}
});
it('returns the value returned by the hook function', async function () {
for (const val of ['value', null, undefined]) {
// Must not have the cb parameter otherwise returning undefined will never resolve.
@ -505,27 +459,22 @@ describe(__filename, function () {
assert.equal(await callHookFnAsync(hook, Promise.resolve(val)), val);
}
});
it('rejects if it throws an exception', async function () {
hook.hook_fn = () => { throw new Error('test exception'); };
await assert.rejects(callHookFnAsync(hook), {message: 'test exception'});
await assert.rejects(callHookFnAsync(hook), { message: 'test exception' });
});
it('rejects if rejected Promise passed to callback', async function () {
hook.hook_fn = (hn, ctx, cb) => cb(Promise.reject(new Error('test exception')));
await assert.rejects(callHookFnAsync(hook), {message: 'test exception'});
await assert.rejects(callHookFnAsync(hook), { message: 'test exception' });
});
it('rejects if rejected Promise returned', async function () {
hook.hook_fn = (hn, ctx, cb) => Promise.reject(new Error('test exception'));
await assert.rejects(callHookFnAsync(hook), {message: 'test exception'});
await assert.rejects(callHookFnAsync(hook), { message: 'test exception' });
});
it('callback returns undefined', async function () {
hook.hook_fn = (hn, ctx, cb) => { assert.equal(cb('foo'), undefined); };
await callHookFnAsync(hook);
});
it('checks for deprecation', async function () {
sinon.stub(console, 'warn');
hooks.deprecationNotices[hookName] = 'test deprecation';
@ -535,7 +484,6 @@ describe(__filename, function () {
assert.match(console.warn.getCall(0).args[0], /test deprecation/);
});
});
describe('supported hook function styles', function () {
const supportedHookFunctions = supportedSyncHookFunctions.concat([
{
@ -616,7 +564,6 @@ describe(__filename, function () {
wantErr: 'test rejection',
},
]);
for (const tc of supportedSyncHookFunctions.concat(supportedHookFunctions)) {
it(tc.name, async function () {
sinon.stub(console, 'warn');
@ -624,8 +571,9 @@ describe(__filename, function () {
hook.hook_fn = tc.fn;
const p = callHookFnAsync(hook);
if (tc.wantErr) {
await assert.rejects(p, {message: tc.wantErr});
} else {
await assert.rejects(p, { message: tc.wantErr });
}
else {
assert.equal(await p, tc.want);
}
assert.equal(console.warn.callCount, 0);
@ -633,14 +581,12 @@ describe(__filename, function () {
});
}
});
// Test various ways a hook might attempt to settle twice. (Examples: call the callback a second
// time, or call the callback and then return a value.)
describe('bad hook function behavior (double settle)', function () {
beforeEach(async function () {
sinon.stub(console, 'error');
});
// Each item in this array codifies a way to settle an asynchronous hook function. Each of the
// test cases below combines two of these behaviors in a single hook function and confirms
// that callHookFnAsync both (1) resolves to the result of the first settle attempt, and (2)
@ -760,23 +706,21 @@ describe(__filename, function () {
when: 3,
},
];
for (const step1 of behaviors) {
// There can't be a second step if the first step is to return or throw.
if (step1.name.startsWith('return ') || step1.name === 'throw') continue;
if (step1.name.startsWith('return ') || step1.name === 'throw')
continue;
for (const step2 of behaviors) {
it(`${step1.name} then ${step2.name} (diff. outcomes) -> log+throw`, async function () {
hook.hook_fn = (hn, ctx, cb) => {
step1.fn(cb, new Error(ctx.ret1), ctx.ret1);
return step2.fn(cb, new Error(ctx.ret2), ctx.ret2);
};
// Temporarily remove unhandled Promise rejection listeners so that the unhandled
// rejections we expect to see don't trigger a test failure (or terminate node).
const event = 'unhandledRejection';
const listenersBackup = process.rawListeners(event);
process.removeAllListeners(event);
let tempListener;
let asyncErr;
try {
@ -791,33 +735,32 @@ describe(__filename, function () {
const step1Wins = step1.when <= step2.when;
const winningStep = step1Wins ? step1 : step2;
const winningVal = step1Wins ? 'val1' : 'val2';
const p = callHookFnAsync(hook, {ret1: 'val1', ret2: 'val2'});
const p = callHookFnAsync(hook, { ret1: 'val1', ret2: 'val2' });
if (winningStep.rejects) {
await assert.rejects(p, {message: winningVal});
} else {
await assert.rejects(p, { message: winningVal });
}
else {
assert.equal(await p, winningVal);
}
await seenErrPromise;
} finally {
}
finally {
// Restore the original listeners.
process.off(event, tempListener);
for (const listener of listenersBackup) {
process.on(event, listener);
}
}
assert.equal(console.error.callCount, 1,
`Got errors:\n${
console.error.getCalls().map((call) => call.args[0]).join('\n')}`);
assert.equal(console.error.callCount, 1, `Got errors:\n${console.error.getCalls().map((call) => call.args[0]).join('\n')}`);
assert.match(console.error.getCall(0).args[0], /DOUBLE SETTLE BUG/);
assert(asyncErr instanceof Error);
assert.match(asyncErr.message, /DOUBLE SETTLE BUG/);
});
// This next test is the same as the above test, except the second settle attempt is for
// the same outcome. The two outcomes can't be the same if one step rejects and the other
// doesn't, so skip those cases.
if (step1.rejects !== step2.rejects) continue;
if (step1.rejects !== step2.rejects)
continue;
it(`${step1.name} then ${step2.name} (same outcome) -> only log`, async function () {
const err = new Error('val');
hook.hook_fn = (hn, ctx, cb) => {
@ -828,8 +771,9 @@ describe(__filename, function () {
const errorLogged = new Promise((resolve) => console.error.callsFake(resolve));
const p = callHookFnAsync(hook);
if (winningStep.rejects) {
await assert.rejects(p, {message: 'val'});
} else {
await assert.rejects(p, { message: 'val' });
}
else {
assert.equal(await p, 'val');
}
await errorLogged;
@ -840,7 +784,6 @@ describe(__filename, function () {
}
});
});
describe('hooks.aCallAll', function () {
describe('basic behavior', function () {
it('calls all asynchronously, returns values in order', async function () {
@ -861,10 +804,10 @@ describe(__filename, function () {
resolve(i);
};
});
return {hook_fn: () => {
return { hook_fn: () => {
hookStarted[i] = true;
return entry.promise;
}};
} };
};
testHooks.push(makeHook(), makeHook());
const p = hooks.aCallAll(hookName);
@ -876,35 +819,29 @@ describe(__filename, function () {
hookPromises[0].resolve();
assert.deepEqual(await p, [0, 1]);
});
it('passes hook name', async function () {
hook.hook_fn = async (hn) => { assert.equal(hn, hookName); };
await hooks.aCallAll(hookName);
});
it('undefined context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallAll(hookName);
});
it('null context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallAll(hookName, null);
});
it('context unmodified', async function () {
const wantContext = {};
hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); };
await hooks.aCallAll(hookName, wantContext);
});
});
describe('aCallAll callback', function () {
it('exception in callback rejects', async function () {
const p = hooks.aCallAll(hookName, {}, () => { throw new Error('test exception'); });
await assert.rejects(p, {message: 'test exception'});
await assert.rejects(p, { message: 'test exception' });
});
it('propagates error on exception', async function () {
hook.hook_fn = () => { throw new Error('test exception'); };
await hooks.aCallAll(hookName, {}, (err) => {
@ -912,54 +849,45 @@ describe(__filename, function () {
assert.equal(err.message, 'test exception');
});
});
it('propagages null error on success', async function () {
await hooks.aCallAll(hookName, {}, (err) => {
assert(err == null, `got non-null error: ${err}`);
});
});
it('propagages results on success', async function () {
hook.hook_fn = () => 'val';
await hooks.aCallAll(hookName, {}, (err, results) => {
assert.deepEqual(results, ['val']);
});
});
it('returns callback return value', async function () {
assert.equal(await hooks.aCallAll(hookName, {}, () => 'val'), 'val');
});
});
describe('result processing', function () {
it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks[hookName];
assert.deepEqual(await hooks.aCallAll(hookName), []);
});
it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0;
assert.deepEqual(await hooks.aCallAll(hookName), []);
});
it('flattens one level', async function () {
testHooks.length = 0;
testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
assert.deepEqual(await hooks.aCallAll(hookName), [1, 2, [3]]);
});
it('filters out undefined', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
assert.deepEqual(await hooks.aCallAll(hookName), [2, [3]]);
});
it('preserves null', async function () {
testHooks.length = 0;
testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
assert.deepEqual(await hooks.aCallAll(hookName), [null, 2, null]);
});
it('all undefined -> []', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook(Promise.resolve()));
@ -967,7 +895,6 @@ describe(__filename, function () {
});
});
});
describe('hooks.callAllSerial', function () {
describe('basic behavior', function () {
it('calls all asynchronously, serially, in order', async function () {
@ -991,58 +918,48 @@ describe(__filename, function () {
assert.deepEqual(await hooks.callAllSerial(hookName), [0, 1, 2]);
assert.deepEqual(gotCalls, [0, 1, 2]);
});
it('passes hook name', async function () {
hook.hook_fn = async (hn) => { assert.equal(hn, hookName); };
await hooks.callAllSerial(hookName);
});
it('undefined context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.callAllSerial(hookName);
});
it('null context -> {}', async function () {
hook.hook_fn = async (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.callAllSerial(hookName, null);
});
it('context unmodified', async function () {
const wantContext = {};
hook.hook_fn = async (hn, ctx) => { assert.equal(ctx, wantContext); };
await hooks.callAllSerial(hookName, wantContext);
});
});
describe('result processing', function () {
it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks[hookName];
assert.deepEqual(await hooks.callAllSerial(hookName), []);
});
it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0;
assert.deepEqual(await hooks.callAllSerial(hookName), []);
});
it('flattens one level', async function () {
testHooks.length = 0;
testHooks.push(makeHook(1), makeHook([2]), makeHook([[3]]));
assert.deepEqual(await hooks.callAllSerial(hookName), [1, 2, [3]]);
});
it('filters out undefined', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook([2]), makeHook([[3]]), makeHook(Promise.resolve()));
assert.deepEqual(await hooks.callAllSerial(hookName), [2, [3]]);
});
it('preserves null', async function () {
testHooks.length = 0;
testHooks.push(makeHook(null), makeHook([2]), makeHook(Promise.resolve(null)));
assert.deepEqual(await hooks.callAllSerial(hookName), [null, 2, null]);
});
it('all undefined -> []', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook(Promise.resolve()));
@ -1050,39 +967,32 @@ describe(__filename, function () {
});
});
});
describe('hooks.aCallFirst', function () {
it('no registered hooks (undefined) -> []', async function () {
delete plugins.hooks.testHook;
assert.deepEqual(await hooks.aCallFirst(hookName), []);
});
it('no registered hooks (empty list) -> []', async function () {
testHooks.length = 0;
assert.deepEqual(await hooks.aCallFirst(hookName), []);
});
it('passes hook name => {}', async function () {
hook.hook_fn = (hn) => { assert.equal(hn, hookName); };
await hooks.aCallFirst(hookName);
});
it('undefined context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallFirst(hookName);
});
it('null context => {}', async function () {
hook.hook_fn = (hn, ctx) => { assert.deepEqual(ctx, {}); };
await hooks.aCallFirst(hookName, null);
});
it('context unmodified', async function () {
const wantContext = {};
hook.hook_fn = (hn, ctx) => { assert.equal(ctx, wantContext); };
await hooks.aCallFirst(hookName, wantContext);
});
it('default predicate: predicate never satisfied -> calls all in order', async function () {
const gotCalls = [];
testHooks.length = 0;
@ -1094,7 +1004,6 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName), []);
assert.deepEqual(gotCalls, [0, 1, 2]);
});
it('calls hook functions serially', async function () {
const gotCalls = [];
testHooks.length = 0;
@ -1116,31 +1025,26 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName), []);
assert.deepEqual(gotCalls, [0, 1, 2]);
});
it('default predicate: stops when satisfied', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1'), makeHook('val2'));
assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
});
it('default predicate: skips values that do not satisfy (undefined)', async function () {
testHooks.length = 0;
testHooks.push(makeHook(), makeHook('val1'));
assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
});
it('default predicate: skips values that do not satisfy (empty list)', async function () {
testHooks.length = 0;
testHooks.push(makeHook([]), makeHook('val1'));
assert.deepEqual(await hooks.aCallFirst(hookName), ['val1']);
});
it('default predicate: null satisifes', async function () {
testHooks.length = 0;
testHooks.push(makeHook(null), makeHook('val1'));
assert.deepEqual(await hooks.aCallFirst(hookName), [null]);
});
it('custom predicate: called for each hook function', async function () {
testHooks.length = 0;
testHooks.push(makeHook(0), makeHook(1), makeHook(2));
@ -1148,7 +1052,6 @@ describe(__filename, function () {
await hooks.aCallFirst(hookName, null, null, (val) => { ++got; return false; });
assert.equal(got, 3);
});
it('custom predicate: boolean false/true continues/stops iteration', async function () {
testHooks.length = 0;
testHooks.push(makeHook(1), makeHook(2), makeHook(3));
@ -1160,7 +1063,6 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
assert.equal(nCall, 2);
});
it('custom predicate: non-boolean falsy/truthy continues/stops iteration', async function () {
testHooks.length = 0;
testHooks.push(makeHook(1), makeHook(2), makeHook(3));
@ -1172,32 +1074,27 @@ describe(__filename, function () {
assert.deepEqual(await hooks.aCallFirst(hookName, null, null, predicate), [2]);
assert.equal(nCall, 2);
});
it('custom predicate: array value passed unmodified to predicate', async function () {
const want = [0];
hook.hook_fn = () => want;
const predicate = (got) => { assert.equal(got, want); }; // Note: *NOT* deepEqual!
await hooks.aCallFirst(hookName, null, null, predicate);
});
it('custom predicate: normalized value passed to predicate (undefined)', async function () {
const predicate = (got) => { assert.deepEqual(got, []); };
await hooks.aCallFirst(hookName, null, null, predicate);
});
it('custom predicate: normalized value passed to predicate (null)', async function () {
hook.hook_fn = () => null;
const predicate = (got) => { assert.deepEqual(got, [null]); };
await hooks.aCallFirst(hookName, null, null, predicate);
});
it('non-empty arrays are returned unmodified', async function () {
const want = ['val1'];
testHooks.length = 0;
testHooks.push(makeHook(want), makeHook(['val2']));
assert.equal(await hooks.aCallFirst(hookName), want); // Note: *NOT* deepEqual!
});
it('value can be passed via callback', async function () {
const want = {};
hook.hook_fn = (hn, ctx, cb) => { cb(want); };

View file

@ -1,11 +1,10 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
'use strict';
const assert = require('assert').strict;
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
const assert = assert$0.strict;
describe(__filename, function () {
let agent;
let pad;
@ -15,13 +14,11 @@ describe(__filename, function () {
let socket;
let roSocket;
const backups = {};
before(async function () {
agent = await common.init();
});
beforeEach(async function () {
backups.hooks = {handleMessageSecurity: plugins.hooks.handleMessageSecurity};
backups.hooks = { handleMessageSecurity: plugins.hooks.handleMessageSecurity };
plugins.hooks.handleMessageSecurity = [];
padId = common.randomString();
assert(!await padManager.doesPadExist(padId));
@ -30,26 +27,26 @@ describe(__filename, function () {
assert.equal(pad.text(), '\n');
let res = await agent.get(`/p/${padId}`).expect(200);
socket = await common.connect(res);
const {type, data: clientVars} = await common.handshake(socket, padId);
const { type, data: clientVars } = await common.handshake(socket, padId);
assert.equal(type, 'CLIENT_VARS');
rev = clientVars.collab_client_vars.rev;
roPadId = await readOnlyManager.getReadOnlyId(padId);
res = await agent.get(`/p/${roPadId}`).expect(200);
roSocket = await common.connect(res);
await common.handshake(roSocket, roPadId);
});
afterEach(async function () {
Object.assign(plugins.hooks, backups.hooks);
if (socket != null) socket.close();
if (socket != null)
socket.close();
socket = null;
if (roSocket != null) roSocket.close();
if (roSocket != null)
roSocket.close();
roSocket = null;
if (pad != null) await pad.remove();
if (pad != null)
await pad.remove();
pad = null;
});
describe('CHANGESET_REQ', function () {
it('users are unable to read changesets from other pads', async function () {
const otherPadId = `${padId}other`;
@ -60,7 +57,7 @@ describe(__filename, function () {
const resP = common.waitForSocketEvent(roSocket, 'message');
await common.sendMessage(roSocket, {
component: 'pad',
padId: otherPadId, // The server should ignore this.
padId: otherPadId,
type: 'CHANGESET_REQ',
data: {
granularity: 1,
@ -73,24 +70,22 @@ describe(__filename, function () {
assert.equal(res.data.requestID, 'requestId');
// Should match padId's text, not otherPadId's text.
assert.match(res.data.forwardsChangesets[0], /^[^$]*\$dummy text\n/);
} finally {
}
finally {
await otherPad.remove();
}
});
});
describe('USER_CHANGES', function () {
const sendUserChanges =
async (socket, cs) => await common.sendUserChanges(socket, {baseRev: rev, changeset: cs});
const sendUserChanges = async (socket, cs) => await common.sendUserChanges(socket, { baseRev: rev, changeset: cs });
const assertAccepted = async (socket, wantRev) => {
await common.waitForAcceptCommit(socket, wantRev);
rev = wantRev;
};
const assertRejected = async (socket) => {
const msg = await common.waitForSocketEvent(socket, 'message');
assert.deepEqual(msg, {disconnect: 'badChangeset'});
assert.deepEqual(msg, { disconnect: 'badChangeset' });
};
it('changes are applied', async function () {
await Promise.all([
assertAccepted(socket, rev + 1),
@ -98,14 +93,12 @@ describe(__filename, function () {
]);
assert.equal(pad.text(), 'hello\n');
});
it('bad changeset is rejected', async function () {
await Promise.all([
assertRejected(socket),
sendUserChanges(socket, 'this is not a valid changeset'),
]);
});
it('retransmission is accepted, has no effect', async function () {
const cs = 'Z:1>5+5$hello';
await Promise.all([
@ -119,7 +112,6 @@ describe(__filename, function () {
]);
assert.equal(pad.text(), 'hello\n');
});
it('identity changeset is accepted, has no effect', async function () {
await Promise.all([
assertAccepted(socket, rev + 1),
@ -131,7 +123,6 @@ describe(__filename, function () {
]);
assert.equal(pad.text(), 'hello\n');
});
it('non-identity changeset with no net change is accepted, has no effect', async function () {
await Promise.all([
assertAccepted(socket, rev + 1),
@ -143,7 +134,6 @@ describe(__filename, function () {
]);
assert.equal(pad.text(), 'hello\n');
});
it('handleMessageSecurity can grant one-time write access', async function () {
const cs = 'Z:1>5+5$hello';
const errRegEx = /write attempt on read-only pad/;
@ -152,15 +142,13 @@ describe(__filename, function () {
// sendUserChanges() waits for message ack, so if the message was accepted then head should
// have already incremented by the time we get here.
assert.equal(pad.head, rev); // Not incremented.
// Now allow the change.
plugins.hooks.handleMessageSecurity.push({hook_fn: () => 'permitOnce'});
plugins.hooks.handleMessageSecurity.push({ hook_fn: () => 'permitOnce' });
await Promise.all([
assertAccepted(roSocket, rev + 1),
sendUserChanges(roSocket, cs),
]);
assert.equal(pad.text(), 'hello\n');
// The next change should be dropped.
plugins.hooks.handleMessageSecurity = [];
await assert.rejects(sendUserChanges(roSocket, 'Z:6>6=5+6$ world'), errRegEx);

View file

@ -1,34 +1,29 @@
import assert$0 from "assert";
import { padutils } from "../../../static/js/pad_utils.js";
'use strict';
const assert = require('assert').strict;
const {padutils} = require('../../../static/js/pad_utils');
const assert = assert$0.strict;
describe(__filename, function () {
describe('warnDeprecated', function () {
const {warnDeprecated} = padutils;
const { warnDeprecated } = padutils;
const backups = {};
before(async function () {
backups.logger = warnDeprecated.logger;
});
afterEach(async function () {
warnDeprecated.logger = backups.logger;
delete warnDeprecated._rl; // Reset internal rate limiter state.
});
it('includes the stack', async function () {
let got;
warnDeprecated.logger = {warn: (stack) => got = stack};
warnDeprecated.logger = { warn: (stack) => got = stack };
warnDeprecated();
assert(got.includes(__filename));
});
it('rate limited', async function () {
let got = 0;
warnDeprecated.logger = {warn: () => ++got};
warnDeprecated.logger = { warn: () => ++got };
warnDeprecated(); // Initialize internal rate limiter state.
const {period} = warnDeprecated._rl;
const { period } = warnDeprecated._rl;
got = 0;
const testCases = [[0, 1], [0, 1], [period - 1, 1], [period, 2]];
for (const [now, want] of testCases) { // In a loop so that the stack trace is the same.

View file

@ -1,21 +1,17 @@
import * as common from "../common.js";
import assertLegacy from "../assert-legacy.js";
'use strict';
const common = require('../common');
const assert = require('../assert-legacy').strict;
const assert = assertLegacy.strict;
let agent;
describe(__filename, function () {
before(async function () {
agent = await common.init();
});
it('supports pads with spaces, regression test for #4883', async function () {
await agent.get('/p/pads with spaces')
.expect(302)
.expect('location', 'pads_with_spaces');
});
it('supports pads with spaces and query, regression test for #4883', async function () {
await agent.get('/p/pads with spaces?showChat=true&noColors=false')
.expect(302)

View file

@ -1,6 +1,6 @@
const assert = require('assert').strict;
const promises = require('../../../node/utils/promises');
import assert$0 from "assert";
import * as promises from "../../../node/utils/promises.js";
const assert = assert$0.strict;
describe(__filename, function () {
describe('promises.timesLimit', function () {
let wantIndex = 0;
@ -18,37 +18,31 @@ describe(__filename, function () {
testPromises.push(p);
return p.promise;
};
const total = 11;
const concurrency = 7;
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
it('honors concurrency', async function () {
assert.equal(wantIndex, concurrency);
});
it('creates another when one completes', async function () {
const {promise, resolve} = testPromises.shift();
const { promise, resolve } = testPromises.shift();
resolve();
await promise;
assert.equal(wantIndex, concurrency + 1);
});
it('creates the expected total number of promises', async function () {
while (testPromises.length > 0) {
// Resolve them in random order to ensure that the resolution order doesn't matter.
const i = Math.floor(Math.random() * Math.floor(testPromises.length));
const {promise, resolve} = testPromises.splice(i, 1)[0];
const { promise, resolve } = testPromises.splice(i, 1)[0];
resolve();
await promise;
}
assert.equal(wantIndex, total);
});
it('resolves', async function () {
await timesLimitPromise;
});
it('does not create too many promises if total < concurrency', async function () {
wantIndex = 0;
assert.equal(testPromises.length, 0);
@ -56,28 +50,25 @@ describe(__filename, function () {
const concurrency = 11;
const timesLimitPromise = promises.timesLimit(total, concurrency, makePromise);
while (testPromises.length > 0) {
const {promise, resolve} = testPromises.pop();
const { promise, resolve } = testPromises.pop();
resolve();
await promise;
}
await timesLimitPromise;
assert.equal(wantIndex, total);
});
it('accepts total === 0, concurrency > 0', async function () {
wantIndex = 0;
assert.equal(testPromises.length, 0);
await promises.timesLimit(0, concurrency, makePromise);
assert.equal(wantIndex, 0);
});
it('accepts total === 0, concurrency === 0', async function () {
wantIndex = 0;
assert.equal(testPromises.length, 0);
await promises.timesLimit(0, 0, makePromise);
assert.equal(wantIndex, 0);
});
it('rejects total > 0, concurrency === 0', async function () {
await assert.rejects(promises.timesLimit(total, 0, makePromise), RangeError);
});

View file

@ -1,30 +1,25 @@
import * as AuthorManager from "../../../node/db/AuthorManager.js";
import assert$0 from "assert";
import * as common from "../common.js";
import * as db from "../../../node/db/DB.js";
'use strict';
const AuthorManager = require('../../../node/db/AuthorManager');
const assert = require('assert').strict;
const common = require('../common');
const db = require('../../../node/db/DB');
const assert = assert$0.strict;
describe(__filename, function () {
let setBackup;
before(async function () {
await common.init();
setBackup = db.set;
db.set = async (...args) => {
// delay db.set
await new Promise((resolve) => { setTimeout(() => resolve(), 500); });
return await setBackup.call(db, ...args);
};
});
after(async function () {
db.set = setBackup;
});
it('regression test for missing await in createAuthor (#5000)', async function () {
const {authorID} = await AuthorManager.createAuthor(); // Should block until db.set() finishes.
const { authorID } = await AuthorManager.createAuthor(); // Should block until db.set() finishes.
assert(await AuthorManager.doesAuthorExist(authorID));
});
});

View file

@ -1,9 +1,8 @@
import assert$0 from "assert";
import path from "path";
import sanitizePathname from "../../../node/utils/sanitizePathname.js";
'use strict';
const assert = require('assert').strict;
const path = require('path');
const sanitizePathname = require('../../../node/utils/sanitizePathname');
const assert = assert$0.strict;
describe(__filename, function () {
describe('absolute paths rejected', function () {
const testCases = [
@ -20,7 +19,7 @@ describe(__filename, function () {
];
for (const [platform, p] of testCases) {
it(`${platform} ${p}`, async function () {
assert.throws(() => sanitizePathname(p, path[platform]), {message: /absolute path/});
assert.throws(() => sanitizePathname(p, path[platform]), { message: /absolute path/ });
});
}
});
@ -40,11 +39,10 @@ describe(__filename, function () {
];
for (const [platform, p] of testCases) {
it(`${platform} ${p}`, async function () {
assert.throws(() => sanitizePathname(p, path[platform]), {message: /travers/});
assert.throws(() => sanitizePathname(p, path[platform]), { message: /travers/ });
});
}
});
describe('accepted paths', function () {
const testCases = [
['posix', '', '.'],
@ -89,7 +87,6 @@ describe(__filename, function () {
});
}
});
it('default path API', async function () {
assert.equal(sanitizePathname('foo'), 'foo');
});

View file

@ -1,30 +1,29 @@
import assert$0 from "assert";
import { exportedForTestingOnly } from "../../../node/utils/Settings.js";
import path from "path";
import process from "process";
'use strict';
const assert = require('assert').strict;
const {parseSettings} = require('../../../node/utils/Settings').exportedForTestingOnly;
const path = require('path');
const process = require('process');
const assert = assert$0.strict;
const { parseSettings } = { exportedForTestingOnly }.exportedForTestingOnly;
describe(__filename, function () {
describe('parseSettings', function () {
let settings;
const envVarSubstTestCases = [
{name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true},
{name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false},
{name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null},
{name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined},
{name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123},
{name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo'},
{name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: ''},
{ name: 'true', val: 'true', var: 'SET_VAR_TRUE', want: true },
{ name: 'false', val: 'false', var: 'SET_VAR_FALSE', want: false },
{ name: 'null', val: 'null', var: 'SET_VAR_NULL', want: null },
{ name: 'undefined', val: 'undefined', var: 'SET_VAR_UNDEFINED', want: undefined },
{ name: 'number', val: '123', var: 'SET_VAR_NUMBER', want: 123 },
{ name: 'string', val: 'foo', var: 'SET_VAR_STRING', want: 'foo' },
{ name: 'empty string', val: '', var: 'SET_VAR_EMPTY_STRING', want: '' },
];
before(async function () {
for (const tc of envVarSubstTestCases) process.env[tc.var] = tc.val;
for (const tc of envVarSubstTestCases)
process.env[tc.var] = tc.val;
delete process.env.UNSET_VAR;
settings = parseSettings(path.join(__dirname, 'settings.json'), true);
assert(settings != null);
});
describe('environment variable substitution', function () {
describe('set', function () {
for (const tc of envVarSubstTestCases) {
@ -32,25 +31,25 @@ describe(__filename, function () {
const obj = settings['environment variable substitution'].set;
if (tc.name === 'undefined') {
assert(!(tc.name in obj));
} else {
}
else {
assert.equal(obj[tc.name], tc.want);
}
});
}
});
describe('unset', function () {
it('no default', async function () {
const obj = settings['environment variable substitution'].unset;
assert.equal(obj['no default'], null);
});
for (const tc of envVarSubstTestCases) {
it(tc.name, async function () {
const obj = settings['environment variable substitution'].unset;
if (tc.name === 'undefined') {
assert(!(tc.name in obj));
} else {
}
else {
assert.equal(obj[tc.name], tc.want);
}
});

View file

@ -1,13 +1,12 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as padManager from "../../../node/db/PadManager.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as readOnlyManager from "../../../node/db/ReadOnlyManager.js";
import * as settings from "../../../node/utils/Settings.js";
import * as socketIoRouter from "../../../node/handler/SocketIORouter.js";
'use strict';
const assert = require('assert').strict;
const common = require('../common');
const padManager = require('../../../node/db/PadManager');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const readOnlyManager = require('../../../node/db/ReadOnlyManager');
const settings = require('../../../node/utils/Settings');
const socketIoRouter = require('../../../node/handler/SocketIORouter');
const assert = assert$0.strict;
describe(__filename, function () {
this.timeout(30000);
let agent;
@ -23,7 +22,6 @@ describe(__filename, function () {
}));
};
let socket;
before(async function () { agent = await common.init(); });
beforeEach(async function () {
backups.hooks = {};
@ -39,22 +37,22 @@ describe(__filename, function () {
settings.requireAuthentication = false;
settings.requireAuthorization = false;
settings.users = {
admin: {password: 'admin-password', is_admin: true},
user: {password: 'user-password'},
admin: { password: 'admin-password', is_admin: true },
user: { password: 'user-password' },
};
assert(socket == null);
authorize = () => true;
plugins.hooks.authorize = [{hook_fn: (hookName, {req}, cb) => cb([authorize(req)])}];
plugins.hooks.authorize = [{ hook_fn: (hookName, { req }, cb) => cb([authorize(req)]) }];
await cleanUpPads();
});
afterEach(async function () {
if (socket) socket.close();
if (socket)
socket.close();
socket = null;
await cleanUpPads();
Object.assign(plugins.hooks, backups.hooks);
Object.assign(settings, backups.settings);
});
describe('Normal accesses', function () {
it('!authn anonymous cookie /p/pad -> 200, ok', async function () {
const res = await agent.get('/p/pad').expect(200);
@ -80,13 +78,13 @@ describe(__filename, function () {
const clientVars = await common.handshake(socket, 'pad');
assert.equal(clientVars.type, 'CLIENT_VARS');
});
for (const authn of [false, true]) {
const desc = authn ? 'authn user' : '!authn anonymous';
it(`${desc} read-only /p/pad -> 200, ok`, async function () {
const get = (ep) => {
let res = agent.get(ep);
if (authn) res = res.auth('user', 'user-password');
if (authn)
res = res.auth('user', 'user-password');
return res.expect(200);
};
settings.requireAuthentication = authn;
@ -105,7 +103,6 @@ describe(__filename, function () {
assert.equal(clientVars.data.readonly, true);
});
}
it('authz user /p/pad -> 200, ok', async function () {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
@ -128,7 +125,6 @@ describe(__filename, function () {
assert.equal(clientVars.type, 'CLIENT_VARS');
});
});
describe('Abnormal access attempts', function () {
it('authn anonymous /p/pad -> 401, error', async function () {
settings.requireAuthentication = true;
@ -138,7 +134,6 @@ describe(__filename, function () {
const message = await common.handshake(socket, 'pad');
assert.equal(message.accessStatus, 'deny');
});
it('authn anonymous read-only /p/pad -> 401, error', async function () {
settings.requireAuthentication = true;
let res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
@ -154,7 +149,6 @@ describe(__filename, function () {
const message = await common.handshake(socket, readOnlyId);
assert.equal(message.accessStatus, 'deny');
});
it('authn !cookie -> error', async function () {
settings.requireAuthentication = true;
socket = await common.connect(null);
@ -174,13 +168,11 @@ describe(__filename, function () {
assert.equal(message.accessStatus, 'deny');
});
});
describe('Authorization levels via authorize hook', function () {
beforeEach(async function () {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
});
it("level='create' -> can create", async function () {
authorize = () => 'create';
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
@ -239,12 +231,10 @@ describe(__filename, function () {
assert.equal(clientVars.data.readonly, true);
});
});
describe('Authorization levels via user settings', function () {
beforeEach(async function () {
settings.requireAuthentication = true;
});
it('user.canCreate = true -> can create and modify', async function () {
settings.users.user.canCreate = true;
const res = await agent.get('/p/pad').auth('user', 'user-password').expect(200);
@ -293,13 +283,11 @@ describe(__filename, function () {
assert.equal(message.accessStatus, 'deny');
});
});
describe('Authorization level interaction between authorize hook and user settings', function () {
beforeEach(async function () {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
});
it('authorize hook does not elevate level from user settings', async function () {
settings.users.user.readOnly = true;
authorize = () => 'create';
@ -318,20 +306,17 @@ describe(__filename, function () {
assert.equal(message.accessStatus, 'deny');
});
});
describe('SocketIORouter.js', function () {
const Module = class {
setSocketIO(io) {}
handleConnect(socket) {}
handleDisconnect(socket) {}
handleMessage(socket, message) {}
setSocketIO(io) { }
handleConnect(socket) { }
handleDisconnect(socket) { }
handleMessage(socket, message) { }
};
afterEach(async function () {
socketIoRouter.deleteComponent(this.test.fullTitle());
socketIoRouter.deleteComponent(`${this.test.fullTitle()} #2`);
});
it('setSocketIO', async function () {
let ioServer;
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
@ -339,7 +324,6 @@ describe(__filename, function () {
}());
assert(ioServer != null);
});
it('handleConnect', async function () {
let serverSocket;
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
@ -348,7 +332,6 @@ describe(__filename, function () {
socket = await common.connect();
assert(serverSocket != null);
});
it('handleDisconnect', async function () {
let resolveConnected;
const connected = new Promise((resolve) => resolveConnected = resolve);
@ -362,7 +345,8 @@ describe(__filename, function () {
handleDisconnect(socket) {
assert(socket != null);
// There might be lingering disconnect events from sockets created by other tests.
if (this._socket == null || socket.id !== this._socket.id) return;
if (this._socket == null || socket.id !== this._socket.id)
return;
assert.equal(socket, this._socket);
resolveDisconnected();
}
@ -373,12 +357,11 @@ describe(__filename, function () {
socket = null;
await disconnected;
});
it('handleMessage (success)', async function () {
let serverSocket;
const want = {
component: this.test.fullTitle(),
foo: {bar: 'asdf'},
foo: { bar: 'asdf' },
};
let rx;
const got = new Promise((resolve) => { rx = resolve; });
@ -393,25 +376,21 @@ describe(__filename, function () {
socket.send(want);
assert.deepEqual(await got, want);
});
const tx = async (socket, message = {}) => await new Promise((resolve, reject) => {
const AckErr = class extends Error {
constructor(name, ...args) { super(...args); this.name = name; }
};
socket.send(message,
(errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val));
socket.send(message, (errj, val) => errj != null ? reject(new AckErr(errj.name, errj.message)) : resolve(val));
});
it('handleMessage with ack (success)', async function () {
const want = 'value';
socketIoRouter.addComponent(this.test.fullTitle(), new class extends Module {
handleMessage(socket, msg) { return want; }
}());
socket = await common.connect();
const got = await tx(socket, {component: this.test.fullTitle()});
const got = await tx(socket, { component: this.test.fullTitle() });
assert.equal(got, want);
});
it('handleMessage with ack (error)', async function () {
const InjectedError = class extends Error {
constructor() { super('injected test error'); this.name = 'InjectedError'; }
@ -420,7 +399,7 @@ describe(__filename, function () {
handleMessage(socket, msg) { throw new InjectedError(); }
}());
socket = await common.connect();
await assert.rejects(tx(socket, {component: this.test.fullTitle()}), new InjectedError());
await assert.rejects(tx(socket, { component: this.test.fullTitle() }), new InjectedError());
});
});
});

View file

@ -1,8 +1,6 @@
import * as common from "../common.js";
import * as settings from "../../../node/utils/Settings.js";
'use strict';
const common = require('../common');
const settings = require('../../../node/utils/Settings');
describe(__filename, function () {
this.timeout(30000);
let agent;
@ -19,7 +17,6 @@ describe(__filename, function () {
afterEach(async function () {
Object.assign(settings, backups.settings);
});
describe('/javascript', function () {
it('/javascript -> 200', async function () {
await agent.get('/javascript').expect(200);

View file

@ -1,10 +1,9 @@
import assert$0 from "assert";
import * as common from "../common.js";
import * as plugins from "../../../static/js/pluginfw/plugin_defs.js";
import * as settings from "../../../node/utils/Settings.js";
'use strict';
const assert = require('assert').strict;
const common = require('../common');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
const settings = require('../../../node/utils/Settings');
const assert = assert$0.strict;
describe(__filename, function () {
this.timeout(30000);
let agent;
@ -15,9 +14,8 @@ describe(__filename, function () {
hook_fn: hookFn,
hook_fn_name: `fake_plugin/${hookName}`,
hook_name: hookName,
part: {plugin: 'fake_plugin'},
part: { plugin: 'fake_plugin' },
});
before(async function () { agent = await common.init(); });
beforeEach(async function () {
backups.hooks = {};
@ -32,15 +30,14 @@ describe(__filename, function () {
settings.requireAuthentication = false;
settings.requireAuthorization = false;
settings.users = {
admin: {password: 'admin-password', is_admin: true},
user: {password: 'user-password'},
admin: { password: 'admin-password', is_admin: true },
user: { password: 'user-password' },
};
});
afterEach(async function () {
Object.assign(plugins.hooks, backups.hooks);
Object.assign(settings, backups.settings);
});
describe('webaccess: without plugins', function () {
it('!authn !authz anonymous / -> 200', async function () {
settings.requireAuthentication = false;
@ -102,7 +99,6 @@ describe(__filename, function () {
settings.requireAuthorization = true;
await agent.get('/admin/').auth('admin', 'admin-password').expect(200);
});
describe('login fails if password is nullish', function () {
for (const adminPassword of [undefined, null]) {
// https://tools.ietf.org/html/rfc7617 says that the username and password are sent as
@ -119,7 +115,6 @@ describe(__filename, function () {
}
});
});
describe('webaccess: preAuthorize, authenticate, and authorize hooks', function () {
let callOrder;
const Handler = class {
@ -128,7 +123,7 @@ describe(__filename, function () {
this.hookName = hookName;
this.innerHandle = () => [];
this.id = hookName + suffix;
this.checkContext = () => {};
this.checkContext = () => { };
}
handle(hookName, context, cb) {
assert.equal(hookName, this.hookName);
@ -144,7 +139,6 @@ describe(__filename, function () {
}
};
const handlers = {};
beforeEach(async function () {
callOrder = [];
for (const hookName of authHookNames) {
@ -158,13 +152,11 @@ describe(__filename, function () {
];
}
});
describe('preAuthorize', function () {
beforeEach(async function () {
settings.requireAuthentication = false;
settings.requireAuthorization = false;
});
it('defers if it returns []', async function () {
await agent.get('/').expect(200);
// Note: The preAuthorize hook always runs even if requireAuthorization is false.
@ -187,7 +179,7 @@ describe(__filename, function () {
it('bypasses authenticate and authorize hooks when next is called', async function () {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
handlers.preAuthorize[0].innerHandle = ({next}) => next();
handlers.preAuthorize[0].innerHandle = ({ next }) => next();
await agent.get('/').expect(200);
assert.deepEqual(callOrder, ['preAuthorize_0']);
});
@ -218,7 +210,7 @@ describe(__filename, function () {
it('runs preAuthzFailure hook when access is denied', async function () {
handlers.preAuthorize[0].innerHandle = () => [false];
let called = false;
plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, {req, res}, cb) => {
plugins.hooks.preAuthzFailure = [makeHook('preAuthzFailure', (hookName, { req, res }, cb) => {
assert.equal(hookName, 'preAuthzFailure');
assert(req != null);
assert(res != null);
@ -235,13 +227,11 @@ describe(__filename, function () {
await agent.get('/').expect(500);
});
});
describe('authenticate', function () {
beforeEach(async function () {
settings.requireAuthentication = true;
settings.requireAuthorization = false;
});
it('is not called if !requireAuthentication and not /admin/*', async function () {
settings.requireAuthentication = false;
await agent.get('/').expect(200);
@ -263,7 +253,7 @@ describe(__filename, function () {
'authenticate_1']);
});
it('does not defer if return [true], 200', async function () {
handlers.authenticate[0].innerHandle = ({req}) => { req.session.user = {}; return [true]; };
handlers.authenticate[0].innerHandle = ({ req }) => { req.session.user = {}; return [true]; };
await agent.get('/').expect(200);
// Note: authenticate_1 was not called because authenticate_0 handled it.
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
@ -282,7 +272,7 @@ describe(__filename, function () {
'authenticate_1']);
});
it('passes settings.users in context', async function () {
handlers.authenticate[0].checkContext = ({users}) => {
handlers.authenticate[0].checkContext = ({ users }) => {
assert.equal(users, settings.users);
};
await agent.get('/').expect(401);
@ -292,7 +282,7 @@ describe(__filename, function () {
'authenticate_1']);
});
it('passes user, password in context if provided', async function () {
handlers.authenticate[0].checkContext = ({username, password}) => {
handlers.authenticate[0].checkContext = ({ username, password }) => {
assert.equal(username, 'user');
assert.equal(password, 'user-password');
};
@ -303,7 +293,7 @@ describe(__filename, function () {
'authenticate_1']);
});
it('does not pass user, password in context if not provided', async function () {
handlers.authenticate[0].checkContext = ({username, password}) => {
handlers.authenticate[0].checkContext = ({ username, password }) => {
assert(username == null);
assert(password == null);
};
@ -324,13 +314,11 @@ describe(__filename, function () {
assert.deepEqual(callOrder, ['preAuthorize_0', 'preAuthorize_1', 'authenticate_0']);
});
});
describe('authorize', function () {
beforeEach(async function () {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
});
it('is not called if !requireAuthorization (non-/admin)', async function () {
settings.requireAuthorization = false;
await agent.get('/').auth('user', 'user-password').expect(200);
@ -377,7 +365,7 @@ describe(__filename, function () {
'authorize_0']);
});
it('passes req.path in context', async function () {
handlers.authorize[0].checkContext = ({resource}) => {
handlers.authorize[0].checkContext = ({ resource }) => {
assert.equal(resource, '/');
};
await agent.get('/').auth('user', 'user-password').expect(403);
@ -399,7 +387,6 @@ describe(__filename, function () {
});
});
});
describe('webaccess: authnFailure, authzFailure, authFailure hooks', function () {
const Handler = class {
constructor(hookName) {
@ -422,7 +409,6 @@ describe(__filename, function () {
}
};
const handlers = {};
beforeEach(async function () {
failHookNames.forEach((hookName) => {
const handler = new Handler(hookName);
@ -432,7 +418,6 @@ describe(__filename, function () {
settings.requireAuthentication = true;
settings.requireAuthorization = true;
});
// authn failure tests
it('authn fail, no hooks handle -> 401', async function () {
await agent.get('/').expect(401);
@ -461,7 +446,6 @@ describe(__filename, function () {
assert(handlers.authnFailure.called);
assert(!handlers.authFailure.called);
});
// authz failure tests
it('authz fail, no hooks handle -> 403', async function () {
await agent.get('/').auth('user', 'user-password').expect(403);