Moved more files to typescript

This commit is contained in:
SamTv12345 2024-07-18 19:20:35 +02:00
parent b1139e1aff
commit d1ffd5d02f
75 changed files with 2079 additions and 1929 deletions

View file

@ -7,7 +7,7 @@ provides tools to create, read, and apply changesets.
## Changeset
```javascript
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const Changeset = require('src/static/js/Changeset');
```
A changeset describes the difference between two revisions of a document. When a
@ -24,7 +24,7 @@ A transmitted changeset looks like this:
## Attribute Pool
```javascript
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
const AttributePool = require('src/static/js/AttributePool');
```
Changesets do not include any attribute keyvalue pairs. Instead, they use

View file

@ -825,16 +825,16 @@ Context properties:
Example:
```javascript
const AttributeMap = require('ep_etherpad-lite/static/js/AttributeMap');
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const AttributeMap = require('src/static/js/AttributeMap');
const Changeset = require('src/static/js/Changeset');
exports.getLineHTMLForExport = async (hookName, context) => {
if (!context.attribLine) return;
const [op] = Changeset.deserializeOps(context.attribLine);
if (op == null) return;
const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading');
if (!heading) return;
context.lineContent = `<${heading}>${context.lineContent}</${heading}>`;
if (!context.attribLine) return;
const [op] = Changeset.deserializeOps(context.attribLine);
if (op == null) return;
const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading');
if (!heading) return;
context.lineContent = `<${heading}>${context.lineContent}</${heading}>`;
};
```

View file

@ -294,6 +294,9 @@ importers:
'@types/jquery':
specifier: ^3.5.30
version: 3.5.30
'@types/js-cookie':
specifier: ^3.0.6
version: 3.0.6
'@types/jsdom':
specifier: ^21.1.7
version: 21.1.7
@ -1498,6 +1501,9 @@ packages:
'@types/jquery@3.5.30':
resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==}
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
'@types/jsdom@21.1.7':
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
@ -5469,6 +5475,8 @@ snapshots:
dependencies:
'@types/sizzle': 2.3.8
'@types/js-cookie@3.0.6': {}
'@types/jsdom@21.1.7':
dependencies:
'@types/node': 20.14.11

View file

@ -22,7 +22,7 @@
const db = require('./DB');
const CustomError = require('../utils/customError');
const hooks = require('../../static/js/pluginfw/hooks.js');
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
import {padUtils, randomString} from '../../static/js/pad_utils'
exports.getColorPalette = () => [
'#ffc7c7',
@ -169,7 +169,7 @@ exports.getAuthorId = async (token: string, user: object) => {
* @param {String} token The token
*/
exports.getAuthor4Token = async (token: string) => {
warnDeprecated(
padUtils.warnDeprecated(
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
return await getAuthor4Token(token);
};

View file

@ -7,10 +7,10 @@ import {MapArrayType} from "../types/MapType";
* The pad object, defined with joose
*/
const AttributeMap = require('../../static/js/AttributeMap');
import AttributeMap from '../../static/js/AttributeMap';
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
import AttributePool from '../../static/js/AttributePool';
const Stream = require('../utils/Stream');
const assert = require('assert').strict;
const db = require('./DB');
@ -23,7 +23,7 @@ const CustomError = require('../utils/customError');
const readOnlyManager = require('./ReadOnlyManager');
const randomString = require('../utils/randomstring');
const hooks = require('../../static/js/pluginfw/hooks');
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
import {padUtils} from "../../static/js/pad_utils";
const promises = require('../utils/promises');
/**
@ -40,7 +40,7 @@ exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
class Pad {
private db: Database;
private atext: AText;
private pool: APool;
private pool: AttributePool;
private head: number;
private chatHead: number;
private publicStatus: boolean;
@ -126,11 +126,11 @@ class Pad {
pad: this,
authorId,
get author() {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
return this.authorId;
},
set author(authorId) {
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
this.authorId = authorId;
},
...this.head === 0 ? {} : {
@ -437,11 +437,11 @@ class Pad {
// let the plugins know the pad was copied
await hooks.aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
padUtils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -538,11 +538,11 @@ class Pad {
await hooks.aCallAll('padCopy', {
get originalPad() {
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
return this.srcPad;
},
get destinationID() {
warnDeprecated(
padUtils.warnDeprecated(
'padCopy destinationID context property is deprecated; use dstPad.id instead');
return this.dstPad.id;
},
@ -603,7 +603,7 @@ class Pad {
p.push(padManager.removePad(padID));
p.push(hooks.aCallAll('padRemove', {
get padID() {
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
padUtils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
return this.pad.id;
},
pad: this,

View file

@ -30,7 +30,7 @@ const settings = require('../utils/Settings');
const webaccess = require('../hooks/express/webaccess');
const log4js = require('log4js');
const authLogger = log4js.getLogger('auth');
const {padutils} = require('../../static/js/pad_utils');
import {padUtils as padutils} from '../../static/js/pad_utils';
const DENY = Object.freeze({accessStatus: 'deny'});

View file

@ -21,12 +21,12 @@
import {MapArrayType} from "../types/MapType";
const AttributeMap = require('../../static/js/AttributeMap');
import AttributeMap from '../../static/js/AttributeMap';
const padManager = require('../db/PadManager');
const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool');
const AttributeManager = require('../../static/js/AttributeManager');
import AttributePool from '../../static/js/AttributePool';
import AttributeManager from '../../static/js/AttributeManager';
const authorManager = require('../db/AuthorManager');
const {padutils} = require('../../static/js/pad_utils');
const readOnlyManager = require('../db/ReadOnlyManager');
@ -738,7 +738,7 @@ exports.updatePadClients = async (pad: PadType) => {
/**
* Copied from the Etherpad Source Code. Don't know what this method does excatly...
*/
const _correctMarkersInPad = (atext: AText, apool: APool) => {
const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
const text = atext.text;
// collect char positions of line markers (e.g. bullets) in new atext

View file

@ -19,7 +19,8 @@
* limitations under the License.
*/
const AttributeMap = require('../../static/js/AttributeMap');
import AttributeMap from '../../static/js/AttributeMap';
import AttributePool from "../../static/js/AttributePool";
const Changeset = require('../../static/js/Changeset');
const { checkValidRev } = require('./checkValidRev');
@ -51,7 +52,7 @@ type LineModel = {
[id:string]:string|number|LineModel
}
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) => {
const line: LineModel = {};
// identify list

View file

@ -22,7 +22,7 @@ const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager');
const _ = require('underscore');
const Security = require('../../static/js/security');
const Security = require('security');
const hooks = require('../../static/js/pluginfw/hooks');
const eejs = require('../eejs');
const _analyzeLine = require('./ExportHelper')._analyzeLine;

View file

@ -18,7 +18,7 @@ import {APool} from "../types/PadType";
* limitations under the License.
*/
const AttributePool = require('../../static/js/AttributePool');
import AttributePool from '../../static/js/AttributePool';
const {Pad} = require('../db/Pad');
const Stream = require('./Stream');
const authorManager = require('../db/AuthorManager');
@ -61,7 +61,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
try {
const processRecord = async (key:string, value: null|{
padIDs: string|Record<string, unknown>,
pool: APool
pool: AttributePool
}) => {
if (!value) return;
const keyParts = key.split(':');

View file

@ -3,7 +3,7 @@
import {PadAuthor, PadType} from "../types/PadType";
import {MapArrayType} from "../types/MapType";
const AttributeMap = require('../../static/js/AttributeMap');
import AttributeMap from '../../static/js/AttributeMap';
const Changeset = require('../../static/js/Changeset');
const attributes = require('../../static/js/attributes');
const exportHtml = require('./ExportHtml');

View file

@ -87,6 +87,7 @@
"@types/formidable": "^3.4.5",
"@types/http-errors": "^2.0.4",
"@types/jquery": "^3.5.30",
"@types/js-cookie": "^3.0.6",
"@types/jsdom": "^21.1.7",
"@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.7",

View file

@ -1,10 +1,13 @@
'use strict';
const AttributeMap = require('./AttributeMap');
import AttributeMap from './AttributeMap'
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const attributes = require('./attributes');
const underscore = require("underscore")
import underscore from "underscore";
import {RepModel} from "./types/RepModel";
import {RangePos} from "./types/RangePos";
import {Attribute} from "./types/Attribute";
const lineMarkerAttribute = 'lmkr';
@ -33,21 +36,20 @@ const lineAttributes = [lineMarkerAttribute, 'list'];
- a SkipList `lines` containing the text lines of the document.
*/
const AttributeManager = function (rep, applyChangesetCallback) {
this.rep = rep;
this.applyChangesetCallback = applyChangesetCallback;
this.author = '';
export class AttributeManager {
private readonly rep: RepModel
private readonly applyChangesetCallback: Function
private readonly author: string
public static DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES
public static lineAttributes = lineAttributes
// If the first char in a line has one of the following attributes
// it will be considered as a line marker
};
constructor(rep: RepModel, applyChangesetCallback: Function) {
this.rep = rep;
this.applyChangesetCallback = applyChangesetCallback;
this.author = '';
}
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
AttributeManager.lineAttributes = lineAttributes;
AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({
applyChangeset(changeset) {
applyChangeset(changeset: string) {
if (!this.applyChangesetCallback) return changeset;
const cs = changeset.toString();
@ -56,15 +58,15 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
}
return changeset;
},
}
/*
Sets attributes on a range
@param start [row, col] tuple pointing to the start of the range
@param end [row, col] tuple pointing to the end of the range
@param attribs: an array of attributes
*/
setAttributesOnRange(start, end, attribs) {
Sets attributes on a range
@param start [row, col] tuple pointing to the start of the range
@param end [row, col] tuple pointing to the end of the range
@param attribs: an array of attributes
*/
setAttributesOnRange(start: RangePos, end: RangePos, attribs: Attribute[]) {
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');
@ -72,36 +74,36 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
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
let allChangesets;
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);
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);
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
} else {
allChangesets = rowChangeset;
}
}
return this.applyChangeset(allChangesets);
},
}
_findRowRange(row, start, end) {
private findRowRange(row: number, start: RangePos, end: RangePos) {
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;
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`);
@ -115,7 +117,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
if (startCol > endCol) throw new RangeError('selection ends before it starts');
return [startCol, endCol];
},
}
/**
* Sets attributes on a range, by line
@ -124,57 +126,60 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
* @param endCol column where range ends (one past the last selected column)
* @param attribs an array of attributes
*/
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
const builder = Changeset.builder(this.rep.lines.totalWidth());
setAttributesOnRangeByLine(row: number, startCol: number, endCol: number, attribs: Attribute[]) {
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);
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) {
Returns if the line already has a line marker
@param lineNum: the number of the line
*/
lineHasMarker(lineNum: number) {
return lineAttributes.find(
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
},
(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
@param attributeKey: the name of the attribute to get, e.g. list
*/
getAttributeOnLine(lineNum, attributeName) {
getAttributeOnLine(lineNum: number, attributeName: string) {
// get `attributeName` attribute of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return '';
const [op] = Changeset.deserializeOps(aline);
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
*/
getAttributesOnLine(lineNum) {
Gets all attributes on a line
@param lineNum: the number of the line to get the attribute for
*/
getAttributesOnLine(lineNum: number) {
// get attributes of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return [];
const [op] = Changeset.deserializeOps(aline);
if (op == null) return [];
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
},
}
/*
Gets a given attribute on a selection
@param attributeName
@param prevChar
returns true or false if an attribute is visible in range
*/
getAttributeOnSelection(attributeName, prevChar) {
Gets a given attribute on a selection
@param attributeName
@param prevChar
returns true or false if an attribute is visible in range
*/
getAttributeOnSelection(attributeName: string, prevChar?: string) {
const rep = this.rep;
if (!(rep.selStart && rep.selEnd)) return;
// If we're looking for the caret attribute not the selection
@ -191,16 +196,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString();
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
const hasIt = (attribs) => withItRegex.test(attribs);
const hasIt = (attribs: string) => withItRegex.test(attribs);
const rangeHasAttrib = (selStart, selEnd) => {
const rangeHasAttrib = (selStart: RangePos, selEnd: RangePos):boolean => {
// if range is collapsed -> no attribs in range
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]);
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++) {
@ -238,16 +243,17 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
return hasAttrib;
};
return rangeHasAttrib(rep.selStart, rep.selEnd);
},
}
/*
Gets all attributes at a position containing line number and column
@param lineNumber starting with zero
@param column starting with zero
returns a list of attributes in the format
[ ["key","value"], ["key","value"], ... ]
*/
getAttributesOnPosition(lineNumber, column) {
Gets all attributes at a position containing line number and column
@param lineNumber starting with zero
@param column starting with zero
returns a list of attributes in the format
[ ["key","value"], ["key","value"], ... ]
*/
getAttributesOnPosition(lineNumber: number, column: number) {
// get all attributes of the line
const aline = this.rep.alines[lineNumber];
@ -264,7 +270,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
}
return [];
},
}
/*
Gets all attributes at caret position
@ -274,18 +280,18 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
*/
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
@param attributeKey: the name of the attribute to set, e.g. list
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for
@param attributeKey: the name of the attribute to set, e.g. list
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
*/
setAttributeOnLine(lineNum, attributeName, attributeValue) {
*/
setAttributeOnLine(lineNum: number, attributeName: string, attributeValue: string) {
let loc = [0, 0];
const builder = Changeset.builder(this.rep.lines.totalWidth());
const builder = Changeset.builder(this.rep.lines.totalWidth);
const hasMarker = this.lineHasMarker(lineNum);
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
@ -305,7 +311,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
}
return this.applyChangeset(builder);
},
}
/**
* Removes a specified attribute on a line
@ -313,8 +319,8 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
* @param attributeName the name of the attribute to remove, e.g. list
* @param attributeValue if given only attributes with equal value will be removed
*/
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
const builder = Changeset.builder(this.rep.lines.totalWidth());
removeAttributeOnLine(lineNum: number, attributeName: string, attributeValue?: string) {
const builder = Changeset.builder(this.rep.lines.totalWidth);
const hasMarker = this.lineHasMarker(lineNum);
let found = false;
@ -336,34 +342,35 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
.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);
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
Otherwise it will be set to the given value
@param lineNum: the number of the line to toggle the attribute for
@param attributeKey: the name of the attribute to toggle, e.g. list
@param attributeValue: the value to pass to the attribute (e.g. indention level)
*/
toggleAttributeOnLine(lineNum, attributeName, attributeValue) {
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
Otherwise it will be set to the given value
@param lineNum: the number of the line to toggle the attribute for
@param attributeKey: the name of the attribute to toggle, e.g. list
@param attributeValue: the value to pass to the attribute (e.g. indention level)
*/
toggleAttributeOnLine(lineNum: number, attributeName: string, attributeValue: string) {
return this.getAttributeOnLine(lineNum, attributeName)
? this.removeAttributeOnLine(lineNum, attributeName)
: this.setAttributeOnLine(lineNum, attributeName, attributeValue);
},
}
hasAttributeOnSelectionOrCaretPosition(attributeName) {
hasAttributeOnSelectionOrCaretPosition(attributeName: string) {
const hasSelection = (
(this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])
);
@ -372,11 +379,12 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
hasAttrib = this.getAttributeOnSelection(attributeName);
} else {
const attributesOnCaretPosition = this.getAttributesOnCaret();
const allAttribs = [].concat(...attributesOnCaretPosition); // flatten
const allAttribs = [].concat(...attributesOnCaretPosition) as string[]; // flatten
hasAttrib = allAttribs.includes(attributeName);
}
return hasAttrib;
},
});
}
}
module.exports = AttributeManager;
export default AttributeManager

View file

@ -1,5 +1,7 @@
'use strict';
import AttributePool from "./AttributePool";
const attributes = require('./attributes');
/**
@ -21,6 +23,7 @@ const attributes = require('./attributes');
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
*/
class AttributeMap extends Map {
private readonly pool? : AttributePool|null
/**
* Converts an attribute string into an AttributeMap.
*
@ -28,14 +31,14 @@ class AttributeMap extends Map {
* @param {AttributePool} pool - Attribute pool.
* @returns {AttributeMap}
*/
static fromString(str, pool) {
public static fromString(str: string, pool: AttributePool): AttributeMap {
return new AttributeMap(pool).updateFromString(str);
}
/**
* @param {AttributePool} pool - Attribute pool.
*/
constructor(pool) {
constructor(pool?: AttributePool|null) {
super();
/** @public */
this.pool = pool;
@ -46,10 +49,10 @@ class AttributeMap extends Map {
* @param {string} v - Attribute value.
* @returns {AttributeMap} `this` (for chaining).
*/
set(k, v) {
set(k: string, v: string):this {
k = k == null ? '' : String(k);
v = v == null ? '' : String(v);
this.pool.putAttrib([k, v]);
this.pool!.putAttrib([k, v]);
return super.set(k, v);
}
@ -63,7 +66,7 @@ class AttributeMap extends Map {
* key is removed from this map (if present).
* @returns {AttributeMap} `this` (for chaining).
*/
update(entries, emptyValueIsDelete = false) {
update(entries: Iterable<[string, string]>, emptyValueIsDelete: boolean = false): AttributeMap {
for (let [k, v] of entries) {
k = k == null ? '' : String(k);
v = v == null ? '' : String(v);
@ -83,9 +86,9 @@ class AttributeMap extends Map {
* key is removed from this map (if present).
* @returns {AttributeMap} `this` (for chaining).
*/
updateFromString(str, emptyValueIsDelete = false) {
updateFromString(str: string, emptyValueIsDelete: boolean = false): AttributeMap {
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
}
}
module.exports = AttributeMap;
export default AttributeMap

View file

@ -44,6 +44,8 @@
* @property {number} nextNum - The attribute ID to assign to the next new attribute.
*/
import {Attribute} from "./types/Attribute";
/**
* Represents an attribute pool, which is a collection of attributes (pairs of key and value
* strings) along with their identifiers (non-negative integers).
@ -55,6 +57,14 @@
* in the pad.
*/
class AttributePool {
numToAttrib: {
[key: number]: [string, string]
}
private attribToNum: {
[key: number]: [string, string]
}
private nextNum: number
constructor() {
/**
* Maps an attribute identifier to the attribute's `[key, value]` string pair.
@ -96,7 +106,10 @@ class AttributePool {
*/
clone() {
const c = new AttributePool();
for (const [n, a] of Object.entries(this.numToAttrib)) c.numToAttrib[n] = [a[0], a[1]];
for (const [n, a] of Object.entries(this.numToAttrib)){
// @ts-ignore
c.numToAttrib[n] = [a[0], a[1]];
}
Object.assign(c.attribToNum, this.attribToNum);
c.nextNum = this.nextNum;
return c;
@ -111,15 +124,17 @@ class AttributePool {
* membership in the pool without mutating the pool.
* @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool.
*/
putAttrib(attrib, dontAddIfAbsent = false) {
putAttrib(attrib: Attribute, dontAddIfAbsent = false) {
const str = String(attrib);
if (str in this.attribToNum) {
// @ts-ignore
return this.attribToNum[str];
}
if (dontAddIfAbsent) {
return -1;
}
const num = this.nextNum++;
// @ts-ignore
this.attribToNum[str] = num;
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
return num;
@ -130,7 +145,7 @@ class AttributePool {
* @returns {Attribute} The attribute with the given identifier, or nullish if there is no such
* attribute.
*/
getAttrib(num) {
getAttrib(num: number): Attribute {
const pair = this.numToAttrib[num];
if (!pair) {
return pair;
@ -143,7 +158,7 @@ class AttributePool {
* @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty
* string.
*/
getAttribKey(num) {
getAttribKey(num: number): string {
const pair = this.numToAttrib[num];
if (!pair) return '';
return pair[0];
@ -154,7 +169,7 @@ class AttributePool {
* @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty
* string.
*/
getAttribValue(num) {
getAttribValue(num: number) {
const pair = this.numToAttrib[num];
if (!pair) return '';
return pair[1];
@ -166,8 +181,8 @@ class AttributePool {
* @param {Function} func - Callback to call with two arguments: key and value. Its return value
* is ignored.
*/
eachAttrib(func) {
for (const n of Object.keys(this.numToAttrib)) {
eachAttrib(func: (k: string, v: string)=>void) {
for (const n in this.numToAttrib) {
const pair = this.numToAttrib[n];
func(pair[0], pair[1]);
}
@ -196,11 +211,12 @@ class AttributePool {
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
* state will lead to pool corruption.
*/
fromJsonable(obj) {
fromJsonable(obj: this) {
this.numToAttrib = obj.numToAttrib;
this.nextNum = obj.nextNum;
this.attribToNum = {};
for (const n of Object.keys(this.numToAttrib)) {
// @ts-ignore
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
}
return this;
@ -213,6 +229,7 @@ class AttributePool {
if (!Number.isInteger(this.nextNum)) throw new Error('nextNum property is not an integer');
if (this.nextNum < 0) throw new Error('nextNum property is negative');
for (const prop of ['numToAttrib', 'attribToNum']) {
// @ts-ignore
const obj = this[prop];
if (obj == null) throw new Error(`${prop} property is null`);
if (typeof obj !== 'object') throw new TypeError(`${prop} property is not an object`);
@ -231,9 +248,10 @@ class AttributePool {
if (v == null) throw new TypeError(`attrib ${i} value is null`);
if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`);
const attrStr = String(attr);
// @ts-ignore
if (this.attribToNum[attrStr] !== i) throw new Error(`attribToNum for ${attrStr} !== ${i}`);
}
}
}
module.exports = AttributePool;
export default AttributePool

View file

@ -22,10 +22,15 @@
* https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
*/
const AttributeMap = require('./AttributeMap');
const AttributePool = require('./AttributePool');
const attributes = require('./attributes');
const {padutils} = require('./pad_utils');
import AttributeMap from './AttributeMap'
import AttributePool from "./AttributePool";
import {} from './attributes';
import {padUtils as padutils} from "./pad_utils";
import Op from './Op'
import {numToString, parseNum} from './ChangesetUtils'
import {StringAssembler} from "./StringAssembler";
import {OpIter} from "./OpIter";
import {Attribute} from "./types/Attribute";
/**
* A `[key, value]` pair of strings describing a text attribute.
@ -47,8 +52,9 @@ const {padutils} = require('./pad_utils');
*
* @param {string} msg - Just some message
*/
const error = (msg) => {
const error = (msg: string) => {
const e = new Error(msg);
// @ts-ignore
e.easysync = true;
throw e;
};
@ -61,96 +67,10 @@ const error = (msg) => {
* @param {string} msg - error message to include in the exception
* @type {(b: boolean, msg: string) => asserts b}
*/
const assert = (b, msg) => {
export const assert: (b: boolean, msg: string) => asserts b = (b: boolean, msg: string): asserts b => {
if (!b) error(`Failed assertion: ${msg}`);
};
/**
* Parses a number from string base 36.
*
* @param {string} str - string of the number in base 36
* @returns {number} number
*/
exports.parseNum = (str) => parseInt(str, 36);
/**
* Writes a number in base 36 and puts it in a string.
*
* @param {number} num - number
* @returns {string} string
*/
exports.numToString = (num) => num.toString(36).toLowerCase();
/**
* An operation to apply to a shared document.
*/
class Op {
/**
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
*/
constructor(opcode = '') {
/**
* The operation's operator:
* - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
* the document. The inserted characters come from the changeset's character bank.
* - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
* operation.
*
* @type {(''|'='|'+'|'-')}
* @public
*/
this.opcode = opcode;
/**
* The number of characters to keep, insert, or delete.
*
* @type {number}
* @public
*/
this.chars = 0;
/**
* The number of characters among the `chars` characters that are newlines. If non-zero, the
* last character must be a newline.
*
* @type {number}
* @public
*/
this.lines = 0;
/**
* Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
* sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
* '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
* identifiers come from the document's attribute pool.
*
* For keep ('=') operations, the attributes are merged with the base text's existing
* attributes:
* - A keep op attribute with a non-empty value replaces an existing base text attribute that
* has the same key.
* - A keep op attribute with an empty value is interpreted as an instruction to remove an
* existing base text attribute that has the same key, if one exists.
*
* This is the empty string for remove ('-') operations.
*
* @type {string}
* @public
*/
this.attribs = '';
}
toString() {
if (!this.opcode) throw new TypeError('null op');
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
const l = this.lines ? `|${exports.numToString(this.lines)}` : '';
return this.attribs + l + this.opcode + exports.numToString(this.chars);
}
}
exports.Op = Op;
/**
* Describes changes to apply to a document. Does not include the attribute pool or the original
@ -170,7 +90,7 @@ exports.Op = Op;
* @param {string} cs - String representation of the Changeset
* @returns {number} oldLen property
*/
exports.oldLen = (cs) => exports.unpack(cs).oldLen;
export const oldLen = (cs: string) => unpack(cs).oldLen
/**
* Returns the length of the text after changeset is applied.
@ -178,7 +98,7 @@ exports.oldLen = (cs) => exports.unpack(cs).oldLen;
* @param {string} cs - String representation of the Changeset
* @returns {number} newLen property
*/
exports.newLen = (cs) => exports.unpack(cs).newLen;
export const newLen = (cs: string) => unpack(cs).newLen
/**
* Parses a string of serialized changeset operations.
@ -187,63 +107,23 @@ exports.newLen = (cs) => exports.unpack(cs).newLen;
* @yields {Op}
* @returns {Generator<Op>}
*/
exports.deserializeOps = function* (ops) {
export const deserializeOps = function* (ops: string) {
// TODO: Migrate to String.prototype.matchAll() once there is enough browser support.
const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g;
let match;
while ((match = regex.exec(ops)) != null) {
if (match[5] === '$') return; // Start of the insert operation character bank.
if (match[5] != null) error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`);
const op = new Op(match[3]);
op.lines = exports.parseNum(match[2] || '0');
op.chars = exports.parseNum(match[4]);
const opMatch = match[3] as ""|"=" | "+" | "-" | undefined
const op = new Op(opMatch);
op.lines = parseNum(match[2] || '0');
op.chars = parseNum(match[4]);
op.attribs = match[1];
yield op;
}
};
/**
* Iterator over a changeset's operations.
*
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
*
* @deprecated Use `deserializeOps` instead.
*/
class OpIter {
/**
* @param {string} ops - String encoding the change operations to iterate over.
*/
constructor(ops) {
this._gen = exports.deserializeOps(ops);
this._next = this._gen.next();
}
/**
* @returns {boolean} Whether there are any remaining operations.
*/
hasNext() {
return !this._next.done;
}
/**
* Returns the next operation object and advances the iterator.
*
* Note: This does NOT implement the ECMAScript iterator protocol.
*
* @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value.
* @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are
* no more operations.
*/
next(opOut = new Op()) {
if (this.hasNext()) {
copyOp(this._next.value, opOut);
this._next = this._gen.next();
} else {
clearOp(opOut);
}
return opOut;
}
}
/**
* Creates an iterator which decodes string changeset operations.
@ -252,7 +132,7 @@ class OpIter {
* @param {string} opsStr - String encoding of the change operations to perform.
* @returns {OpIter} Operator iterator object.
*/
exports.opIterator = (opsStr) => {
export const opIterator = (opsStr: string) => {
padutils.warnDeprecated(
'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead');
return new OpIter(opsStr);
@ -263,7 +143,7 @@ exports.opIterator = (opsStr) => {
*
* @param {Op} op - object to clear
*/
const clearOp = (op) => {
export const clearOp = (op: Op) => {
op.opcode = '';
op.chars = 0;
op.lines = 0;
@ -277,7 +157,7 @@ const clearOp = (op) => {
* @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator.
* @returns {Op}
*/
exports.newOp = (optOpcode) => {
export const newOp = (optOpcode:'+'|'-'|'='|'' ): Op => {
padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead');
return new Op(optOpcode);
};
@ -289,7 +169,7 @@ exports.newOp = (optOpcode) => {
* @param {Op} [op2] - dest Op. If not given, a new Op is used.
* @returns {Op} `op2`
*/
const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1);
export const copyOp = (op1: Op, op2: Op = new Op()): Op => Object.assign(op2, op1);
/**
* Serializes a sequence of Ops.
@ -320,12 +200,12 @@ const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1);
* (if necessary) and encode. If an attribute string, no checking is performed to ensure that
* the attributes exist in the pool, are in the canonical order, and contain no duplicate keys.
* If this is an iterable of attributes, `pool` must be non-null.
* @param {?AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of
* @param {?AttributePool.ts} pool - Attribute pool. Required if `attribs` is an iterable of
* attributes, ignored if `attribs` is an attribute string.
* @yields {Op} One or two ops (depending on the presense of newlines) that cover the given text.
* @returns {Generator<Op>}
*/
const opsFromText = function* (opcode, text, attribs = '', pool = null) {
export const opsFromText = function* (opcode: "" | "=" | "+" | "-" | undefined, text: string, attribs: string|Attribute[] = '', pool: AttributePool|null = null) {
const op = new Op(opcode);
op.attribs = typeof attribs === 'string'
? attribs : new AttributeMap(pool).update(attribs || [], opcode === '+').toString();
@ -336,7 +216,7 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
yield op;
} else {
op.chars = lastNewlinePos + 1;
op.lines = text.match(/\n/g).length;
op.lines = text.match(/\n/g)!.length;
yield op;
const op2 = copyOp(op);
op2.chars = text.length - (lastNewlinePos + 1);
@ -345,23 +225,7 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
}
};
/**
* Creates an object that allows you to append operations (type Op) and also compresses them if
* possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser
* input, at the cost of speed. Specifically:
* - merges consecutive operations that can be merged
* - strips final "="
* - ignores 0-length changes
* - reorders consecutive + and - (which MergingOpAssembler doesn't do)
*
* @typedef {object} SmartOpAssembler
* @property {Function} append -
* @property {Function} appendOpWithText -
* @property {Function} clear -
* @property {Function} endDocument -
* @property {Function} getLengthChange -
* @property {Function} toString -
*/
/**
* Used to check if a Changeset is valid. This function does not check things that require access to
@ -370,7 +234,7 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
* @param {string} cs - Changeset to check
* @returns {string} the checked Changeset
*/
exports.checkRep = (cs) => {
export const checkRep = (cs: string) => {
const unpacked = exports.unpack(cs);
const oldLen = unpacked.oldLen;
const newLen = unpacked.newLen;
@ -418,254 +282,6 @@ exports.checkRep = (cs) => {
return cs;
};
/**
* @returns {SmartOpAssembler}
*/
exports.smartOpAssembler = () => {
const minusAssem = exports.mergingOpAssembler();
const plusAssem = exports.mergingOpAssembler();
const keepAssem = exports.mergingOpAssembler();
const assem = exports.stringAssembler();
let lastOpcode = '';
let lengthChange = 0;
const flushKeeps = () => {
assem.append(keepAssem.toString());
keepAssem.clear();
};
const flushPlusMinus = () => {
assem.append(minusAssem.toString());
minusAssem.clear();
assem.append(plusAssem.toString());
plusAssem.clear();
};
const append = (op) => {
if (!op.opcode) return;
if (!op.chars) return;
if (op.opcode === '-') {
if (lastOpcode === '=') {
flushKeeps();
}
minusAssem.append(op);
lengthChange -= op.chars;
} else if (op.opcode === '+') {
if (lastOpcode === '=') {
flushKeeps();
}
plusAssem.append(op);
lengthChange += op.chars;
} else if (op.opcode === '=') {
if (lastOpcode !== '=') {
flushPlusMinus();
}
keepAssem.append(op);
}
lastOpcode = op.opcode;
};
/**
* Generates operations from the given text and attributes.
*
* @deprecated Use `opsFromText` instead.
* @param {('-'|'+'|'=')} opcode - The operator to use.
* @param {string} text - The text to remove/add/keep.
* @param {(string|Iterable<Attribute>)} attribs - The attributes to apply to the operations.
* @param {?AttributePool} pool - Attribute pool. Only required if `attribs` is an iterable of
* attribute key, value pairs.
*/
const appendOpWithText = (opcode, text, attribs, pool) => {
padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' +
'use opsFromText() instead.');
for (const op of opsFromText(opcode, text, attribs, pool)) append(op);
};
const toString = () => {
flushPlusMinus();
flushKeeps();
return assem.toString();
};
const clear = () => {
minusAssem.clear();
plusAssem.clear();
keepAssem.clear();
assem.clear();
lengthChange = 0;
};
const endDocument = () => {
keepAssem.endDocument();
};
const getLengthChange = () => lengthChange;
return {
append,
toString,
clear,
endDocument,
appendOpWithText,
getLengthChange,
};
};
/**
* @returns {MergingOpAssembler}
*/
exports.mergingOpAssembler = () => {
const assem = exports.opAssembler();
const bufOp = new Op();
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
// This variable stores the length of yyy and any other newline-less
// ops immediately after it.
let bufOpAdditionalCharsAfterNewline = 0;
/**
* @param {boolean} [isEndDocument]
*/
const flush = (isEndDocument) => {
if (!bufOp.opcode) return;
if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) {
// final merged keep, leave it implicit
} else {
assem.append(bufOp);
if (bufOpAdditionalCharsAfterNewline) {
bufOp.chars = bufOpAdditionalCharsAfterNewline;
bufOp.lines = 0;
assem.append(bufOp);
bufOpAdditionalCharsAfterNewline = 0;
}
}
bufOp.opcode = '';
};
const append = (op) => {
if (op.chars <= 0) return;
if (bufOp.opcode === op.opcode && bufOp.attribs === op.attribs) {
if (op.lines > 0) {
// bufOp and additional chars are all mergeable into a multi-line op
bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars;
bufOp.lines += op.lines;
bufOpAdditionalCharsAfterNewline = 0;
} else if (bufOp.lines === 0) {
// both bufOp and op are in-line
bufOp.chars += op.chars;
} else {
// append in-line text to multi-line bufOp
bufOpAdditionalCharsAfterNewline += op.chars;
}
} else {
flush();
copyOp(op, bufOp);
}
};
const endDocument = () => {
flush(true);
};
const toString = () => {
flush();
return assem.toString();
};
const clear = () => {
assem.clear();
clearOp(bufOp);
};
return {
append,
toString,
clear,
endDocument,
};
};
/**
* @returns {OpAssembler}
*/
exports.opAssembler = () => {
let serialized = '';
/**
* @param {Op} op - Operation to add. Ownership remains with the caller.
*/
const append = (op) => {
assert(op instanceof Op, 'argument must be an instance of Op');
serialized += op.toString();
};
const toString = () => serialized;
const clear = () => {
serialized = '';
};
return {
append,
toString,
clear,
};
};
/**
* A custom made String Iterator
*
* @typedef {object} StringIterator
* @property {Function} newlines -
* @property {Function} peek -
* @property {Function} remaining -
* @property {Function} skip -
* @property {Function} take -
*/
/**
* @param {string} str - String to iterate over
* @returns {StringIterator}
*/
exports.stringIterator = (str) => {
let curIndex = 0;
// newLines is the number of \n between curIndex and str.length
let newLines = str.split('\n').length - 1;
const getnewLines = () => newLines;
const assertRemaining = (n) => {
assert(n <= remaining(), `!(${n} <= ${remaining()})`);
};
const take = (n) => {
assertRemaining(n);
const s = str.substr(curIndex, n);
newLines -= s.split('\n').length - 1;
curIndex += n;
return s;
};
const peek = (n) => {
assertRemaining(n);
const s = str.substr(curIndex, n);
return s;
};
const skip = (n) => {
assertRemaining(n);
curIndex += n;
};
const remaining = () => str.length - curIndex;
return {
take,
skip,
remaining,
peek,
newlines: getnewLines,
};
};
/**
* A custom made StringBuffer
*
@ -674,19 +290,6 @@ exports.stringIterator = (str) => {
* @property {Function} toString -
*/
/**
* @returns {StringAssembler}
*/
exports.stringAssembler = () => ({
_str: '',
clear() { this._str = ''; },
/**
* @param {string} x -
*/
append(x) { this._str += String(x); },
toString() { return this._str; },
});
/**
* @typedef {object} StringArrayLike
* @property {(i: number) => string} get - Returns the line at index `i`.
@ -1067,9 +670,9 @@ exports.unpack = (cs) => {
const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
const headerMatch = headerRegex.exec(cs);
if ((!headerMatch) || (!headerMatch[0])) error(`Not a changeset: ${cs}`);
const oldLen = exports.parseNum(headerMatch[1]);
const oldLen = parseNum(headerMatch[1]);
const changeSign = (headerMatch[2] === '>') ? 1 : -1;
const changeMag = exports.parseNum(headerMatch[3]);
const changeMag = parseNum(headerMatch[3]);
const newLen = oldLen + changeSign * changeMag;
const opsStart = headerMatch[0].length;
let opsEnd = cs.indexOf('$');
@ -1112,7 +715,7 @@ exports.applyToText = (cs, str) => {
assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`);
const bankIter = exports.stringIterator(unpacked.charBank);
const strIter = exports.stringIterator(str);
const assem = exports.stringAssembler();
const assem = new StringAssembler();
for (const op of exports.deserializeOps(unpacked.ops)) {
switch (op.opcode) {
case '+':
@ -1177,7 +780,7 @@ exports.mutateTextLines = (cs, lines) => {
* @param {AttributeString} att1 - first attribute string
* @param {AttributeString} att2 - second attribue string
* @param {boolean} resultIsMutation -
* @param {AttributePool} pool - attribute pool
* @param {AttributePool.ts} pool - attribute pool
* @returns {string}
*/
exports.composeAttributes = (att1, att2, resultIsMutation, pool) => {
@ -1211,7 +814,7 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => {
* @param {Op} attOp - The op from the sequence that is being operated on, either an attribution
* string or the earlier of two exportss being composed.
* @param {Op} csOp -
* @param {AttributePool} pool - Can be null if definitely not needed.
* @param {AttributePool.ts} pool - Can be null if definitely not needed.
* @returns {Op} The result of applying `csOp` to `attOp`.
*/
const slicerZipperFunc = (attOp, csOp, pool) => {
@ -1272,7 +875,7 @@ const slicerZipperFunc = (attOp, csOp, pool) => {
*
* @param {string} cs - Changeset
* @param {string} astr - the attribs string of a AText
* @param {AttributePool} pool - the attibutes pool
* @param {AttributePool.ts} pool - the attibutes pool
* @returns {string}
*/
exports.applyToAttribution = (cs, astr, pool) => {
@ -1285,7 +888,7 @@ exports.applyToAttribution = (cs, astr, pool) => {
*
* @param {string} cs - The encoded changeset.
* @param {Array<string>} lines - Attribute lines. Modified in place.
* @param {AttributePool} pool - Attribute pool.
* @param {AttributePool.ts} pool - Attribute pool.
*/
exports.mutateAttributionLines = (cs, lines, pool) => {
const unpacked = exports.unpack(cs);
@ -1454,7 +1057,7 @@ exports.splitTextLines = (text) => text.match(/[^\n]*(?:\n|[^\n]$)/g);
*
* @param {string} cs1 - first Changeset
* @param {string} cs2 - second Changeset
* @param {AttributePool} pool - Attribs pool
* @param {AttributePool.ts} pool - Attribs pool
* @returns {string}
*/
exports.compose = (cs1, cs2, pool) => {
@ -1466,7 +1069,7 @@ exports.compose = (cs1, cs2, pool) => {
const len3 = unpacked2.newLen;
const bankIter1 = exports.stringIterator(unpacked1.charBank);
const bankIter2 = exports.stringIterator(unpacked2.charBank);
const bankAssem = exports.stringAssembler();
const bankAssem = new StringAssembler();
const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => {
const op1code = op1.opcode;
@ -1493,7 +1096,7 @@ exports.compose = (cs1, cs2, pool) => {
* key,value that is already present in the pool.
*
* @param {Attribute} attribPair - `[key, value]` pair of strings.
* @param {AttributePool} pool - Attribute pool
* @param {AttributePool.ts} pool - Attribute pool
* @returns {Function}
*/
exports.attributeTester = (attribPair, pool) => {
@ -1523,7 +1126,7 @@ exports.identity = (N) => exports.pack(N, N, '', '');
* @param {number} ndel - Number of characters to delete at `start`.
* @param {string} ins - Text to insert at `start` (after deleting `ndel` characters).
* @param {string} [attribs] - Optional attributes to apply to the inserted text.
* @param {AttributePool} [pool] - Attribute pool.
* @param {AttributePool.ts} [pool] - Attribute pool.
* @returns {string}
*/
exports.makeSplice = (orig, start, ndel, ins, attribs, pool) => {
@ -1646,13 +1249,13 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => {
const fromDollar = cs.substring(dollarPos);
// order of attribs stays the same
return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
const oldNum = exports.parseNum(a);
const oldNum = parseNum(a);
const pair = oldPool.getAttrib(oldNum);
// The attribute might not be in the old pool if the user is viewing the current revision in the
// timeslider and text is deleted. See: https://github.com/ether/etherpad-lite/issues/3932
if (!pair) return '';
const newNum = newPool.putAttrib(pair);
return `*${exports.numToString(newNum)}`;
return `*${numToString(newNum)}`;
}) + fromDollar;
};
@ -1688,7 +1291,7 @@ exports.eachAttribNumber = (cs, func) => {
// WARNING: The following cannot be replaced with a call to `attributes.decodeAttribString()`
// because that function only works on attribute strings, not serialized operations or changesets.
upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
func(exports.parseNum(a));
func(parseNum(a));
return '';
});
};
@ -1719,11 +1322,11 @@ exports.mapAttribNumbers = (cs, func) => {
const upToDollar = cs.substring(0, dollarPos);
const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => {
const n = func(exports.parseNum(a));
const n = func(parseNum(a));
if (n === true) {
return s;
} else if ((typeof n) === 'number') {
return `*${exports.numToString(n)}`;
return `*${numToString(n)}`;
} else {
return '';
}
@ -1759,7 +1362,7 @@ exports.makeAText = (text, attribs) => ({
*
* @param {string} cs - Changeset to apply
* @param {AText} atext -
* @param {AttributePool} pool - Attribute Pool to add to
* @param {AttributePool.ts} pool - Attribute Pool to add to
* @returns {AText}
*/
exports.applyToAText = (cs, atext, pool) => ({
@ -1840,8 +1443,8 @@ exports.appendATextToAssembler = (atext, assem) => {
* Creates a clone of a Changeset and it's APool.
*
* @param {string} cs -
* @param {AttributePool} pool -
* @returns {{translated: string, pool: AttributePool}}
* @param {AttributePool.ts} pool -
* @returns {{translated: string, pool: AttributePool.ts}}
*/
exports.prepareForWire = (cs, pool) => {
const newPool = new AttributePool();
@ -1880,7 +1483,7 @@ const attribsAttributeValue = (attribs, key, pool) => {
* @deprecated Use an AttributeMap instead.
* @param {Op} op - Op
* @param {string} key - string to search for
* @param {AttributePool} pool - attribute pool
* @param {AttributePool.ts} pool - attribute pool
* @returns {string}
*/
exports.opAttributeValue = (op, key, pool) => {
@ -1895,7 +1498,7 @@ exports.opAttributeValue = (op, key, pool) => {
* @deprecated Use an AttributeMap instead.
* @param {AttributeString} attribs - Attribute string
* @param {string} key - string to search for
* @param {AttributePool} pool - attribute pool
* @param {AttributePool.ts} pool - attribute pool
* @returns {string}
*/
exports.attribsAttributeValue = (attribs, key, pool) => {
@ -1922,7 +1525,7 @@ exports.attribsAttributeValue = (attribs, key, pool) => {
exports.builder = (oldLen) => {
const assem = exports.smartOpAssembler();
const o = new Op();
const charBank = exports.stringAssembler();
const charBank = new StringAssembler();
const self = {
/**
@ -1931,7 +1534,7 @@ exports.builder = (oldLen) => {
* character must be a newline.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
@ -1949,7 +1552,7 @@ exports.builder = (oldLen) => {
* @param {string} text - Text to keep.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
@ -1962,7 +1565,7 @@ exports.builder = (oldLen) => {
* @param {string} text - Text to insert.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
@ -2006,7 +1609,7 @@ exports.builder = (oldLen) => {
* (if necessary) and encode. If an attribute string, no checking is performed to ensure that
* the attributes exist in the pool, are in the canonical order, and contain no duplicate keys.
* If this is an iterable of attributes, `pool` must be non-null.
* @param {AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of attributes,
* @param {AttributePool.ts} pool - Attribute pool. Required if `attribs` is an iterable of attributes,
* ignored if `attribs` is an attribute string.
* @returns {AttributeString}
*/
@ -2163,7 +1766,7 @@ exports.inverse = (cs, lines, alines, pool) => {
const nextText = (numChars) => {
let len = 0;
const assem = exports.stringAssembler();
const assem = new StringAssembler();
const firstString = linesGet(curLine).substring(curChar);
len += firstString.length;
assem.append(firstString);
@ -2379,20 +1982,20 @@ const followAttributes = (att1, att2, pool) => {
if (!att1) return att2;
const atts = new Map();
att2.replace(/\*([0-9a-z]+)/g, (_, a) => {
const [key, val] = pool.getAttrib(exports.parseNum(a));
const [key, val] = pool.getAttrib(parseNum(a));
atts.set(key, val);
return '';
});
att1.replace(/\*([0-9a-z]+)/g, (_, a) => {
const [key, val] = pool.getAttrib(exports.parseNum(a));
const [key, val] = pool.getAttrib(parseNum(a));
if (atts.has(key) && val <= atts.get(key)) atts.delete(key);
return '';
});
// we've only removed attributes, so they're already sorted
const buf = exports.stringAssembler();
const buf = new StringAssembler();
for (const att of atts) {
buf.append('*');
buf.append(exports.numToString(pool.putAttrib(att)));
buf.append(numToString(pool.putAttrib(att)));
}
return buf.toString();
};

View file

@ -5,6 +5,11 @@
* based on a SkipList
*/
import {RepModel} from "./types/RepModel";
import {ChangeSetBuilder} from "./types/ChangeSetBuilder";
import {Attribute} from "./types/Attribute";
import AttributePool from "./AttributePool";
/**
* Copyright 2009 Google Inc.
*
@ -20,7 +25,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.buildRemoveRange = (rep, builder, start, end) => {
export const buildRemoveRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number,number], end: [number, number]) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -32,7 +37,7 @@ exports.buildRemoveRange = (rep, builder, start, end) => {
}
};
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
export const buildKeepRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number], end:[number, number], attribs?: Attribute[], pool?: AttributePool) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -44,9 +49,25 @@ exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
}
};
exports.buildKeepToStartOfRange = (rep, builder, start) => {
export const buildKeepToStartOfRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number]) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]);
builder.keep(start[1]);
};
/**
* Parses a number from string base 36.
*
* @param {string} str - string of the number in base 36
* @returns {number} number
*/
export const parseNum = (str: string) => parseInt(str, 36);
/**
* Writes a number in base 36 and puts it in a string.
*
* @param {number} num - number
* @returns {string} string
*/
export const numToString = (num: number): string => num.toString(36).toLowerCase();

View file

@ -1,6 +1,6 @@
'use strict';
const {padutils: {warnDeprecated}} = require('./pad_utils');
import {padUtils} from './pad_utils'
/**
* Represents a chat message stored in the database and transmitted among users. Plugins can extend
@ -9,13 +9,24 @@ const {padutils: {warnDeprecated}} = require('./pad_utils');
* Supports serialization to JSON.
*/
class ChatMessage {
static fromObject(obj) {
private text: string|null
private authorId: string|null
private displayName: string|null
private time: number|null
static fromObject(obj: 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)) { // @ts-ignore
obj.authorId = obj.userId;
}
// @ts-ignore
delete obj.userId;
if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName;
if ('userName' in obj && !('displayName' in obj)) { // @ts-ignore
obj.displayName = obj.userName;
}
// @ts-ignore
delete obj.userName;
return Object.assign(new ChatMessage(), obj);
}
@ -25,7 +36,7 @@ class ChatMessage {
* @param {?string} [authorId] - Initial value of the `authorId` property.
* @param {?number} [time] - Initial value of the `time` property.
*/
constructor(text = null, authorId = null, time = null) {
constructor(text: string | null = null, authorId: string | null = null, time: number | null = null) {
/**
* The raw text of the user's chat message (before any rendering or processing).
*
@ -62,11 +73,11 @@ class ChatMessage {
* @type {string}
*/
get userId() {
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
return this.authorId;
}
set userId(val) {
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
this.authorId = val;
}
@ -77,11 +88,11 @@ class ChatMessage {
* @type {string}
*/
get userName() {
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
return this.displayName;
}
set userName(val) {
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
this.displayName = val;
}
@ -89,7 +100,9 @@ class ChatMessage {
// doesn't support authorId and displayName.
toJSON() {
const {authorId, displayName, ...obj} = this;
// @ts-ignore
obj.userId = authorId;
// @ts-ignore
obj.userName = displayName;
return obj;
}

View file

@ -0,0 +1,73 @@
import {OpAssembler} from "./OpAssembler";
import Op from "./Op";
import {clearOp, copyOp} from "./Changeset";
export class MergingOpAssembler {
private assem: OpAssembler;
private readonly bufOp: Op;
private bufOpAdditionalCharsAfterNewline: number;
constructor() {
this.assem = new OpAssembler()
this.bufOp = new Op()
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
// This variable stores the length of yyy and any other newline-less
// ops immediately after it.
this.bufOpAdditionalCharsAfterNewline = 0;
}
/**
* @param {boolean} [isEndDocument]
*/
flush = (isEndDocument?: boolean) => {
if (!this.bufOp.opcode) return;
if (isEndDocument && this.bufOp.opcode === '=' && !this.bufOp.attribs) {
// final merged keep, leave it implicit
} else {
this.assem.append(this.bufOp);
if (this.bufOpAdditionalCharsAfterNewline) {
this.bufOp.chars = this.bufOpAdditionalCharsAfterNewline;
this.bufOp.lines = 0;
this.assem.append(this.bufOp);
this.bufOpAdditionalCharsAfterNewline = 0;
}
}
this.bufOp.opcode = '';
}
append = (op: Op) => {
if (op.chars <= 0) return;
if (this.bufOp.opcode === op.opcode && this.bufOp.attribs === op.attribs) {
if (op.lines > 0) {
// bufOp and additional chars are all mergeable into a multi-line op
this.bufOp.chars += this.bufOpAdditionalCharsAfterNewline + op.chars;
this.bufOp.lines += op.lines;
this.bufOpAdditionalCharsAfterNewline = 0;
} else if (this.bufOp.lines === 0) {
// both bufOp and op are in-line
this.bufOp.chars += op.chars;
} else {
// append in-line text to multi-line bufOp
this.bufOpAdditionalCharsAfterNewline += op.chars;
}
} else {
this.flush();
copyOp(op, this.bufOp);
}
}
endDocument = () => {
this.flush(true);
};
toString = () => {
this.flush();
return this.assem.toString();
};
clear = () => {
this.assem.clear();
clearOp(this.bufOp);
};
}

73
src/static/js/Op.ts Normal file
View file

@ -0,0 +1,73 @@
/**
* An operation to apply to a shared document.
*/
export default class Op {
opcode: ''|'='|'+'|'-'
chars: number
lines: number
attribs: string
/**
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
*/
constructor(opcode:''|'='|'+'|'-' = '') {
/**
* The operation's operator:
* - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
* the document. The inserted characters come from the changeset's character bank.
* - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
* operation.
*
* @type {(''|'='|'+'|'-')}
* @public
*/
this.opcode = opcode;
/**
* The number of characters to keep, insert, or delete.
*
* @type {number}
* @public
*/
this.chars = 0;
/**
* The number of characters among the `chars` characters that are newlines. If non-zero, the
* last character must be a newline.
*
* @type {number}
* @public
*/
this.lines = 0;
/**
* Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
* sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
* '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
* identifiers come from the document's attribute pool.
*
* For keep ('=') operations, the attributes are merged with the base text's existing
* attributes:
* - A keep op attribute with a non-empty value replaces an existing base text attribute that
* has the same key.
* - A keep op attribute with an empty value is interpreted as an instruction to remove an
* existing base text attribute that has the same key, if one exists.
*
* This is the empty string for remove ('-') operations.
*
* @type {string}
* @public
*/
this.attribs = '';
}
toString() {
if (!this.opcode) throw new TypeError('null op');
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
const l = this.lines ? `|${exports.numToString(this.lines)}` : '';
return this.attribs + l + this.opcode + exports.numToString(this.chars);
}
}

View file

@ -0,0 +1,21 @@
import Op from "./Op";
import {assert} from './Changeset'
/**
* @returns {OpAssembler}
*/
export class OpAssembler {
private serialized: string;
constructor() {
this.serialized = ''
}
append = (op: Op) => {
assert(op instanceof Op, 'argument must be an instance of Op');
this.serialized += op.toString();
}
toString = () => this.serialized
clear = () => {
this.serialized = '';
}
}

45
src/static/js/OpIter.ts Normal file
View file

@ -0,0 +1,45 @@
import Op from "./Op";
/**
* Iterator over a changeset's operations.
*
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
*
* @deprecated Use `deserializeOps` instead.
*/
export class OpIter {
private gen
/**
* @param {string} ops - String encoding the change operations to iterate over.
*/
constructor(ops: string) {
this.gen = exports.deserializeOps(ops);
this.next = this.gen.next();
}
/**
* @returns {boolean} Whether there are any remaining operations.
*/
hasNext() {
return !this.next.done;
}
/**
* Returns the next operation object and advances the iterator.
*
* Note: This does NOT implement the ECMAScript iterator protocol.
*
* @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value.
* @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are
* no more operations.
*/
next(opOut = new Op()) {
if (this.hasNext()) {
copyOp(this._next.value, opOut);
this._next = this._gen.next();
} else {
clearOp(opOut);
}
return opOut;
}
}

View file

@ -0,0 +1,115 @@
import {MergingOpAssembler} from "./MergingOpAssembler";
import {StringAssembler} from "./StringAssembler";
import {padUtils as padutils} from "./pad_utils";
import Op from "./Op";
import { Attribute } from "./types/Attribute";
import AttributePool from "./AttributePool";
import {opsFromText} from "./Changeset";
/**
* Creates an object that allows you to append operations (type Op) and also compresses them if
* possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser
* input, at the cost of speed. Specifically:
* - merges consecutive operations that can be merged
* - strips final "="
* - ignores 0-length changes
* - reorders consecutive + and - (which MergingOpAssembler doesn't do)
*
* @typedef {object} SmartOpAssembler
* @property {Function} append -
* @property {Function} appendOpWithText -
* @property {Function} clear -
* @property {Function} endDocument -
* @property {Function} getLengthChange -
* @property {Function} toString -
*/
export class SmartOpAssembler {
private minusAssem: MergingOpAssembler;
private plusAssem: MergingOpAssembler;
private keepAssem: MergingOpAssembler;
private lastOpcode: string;
private lengthChange: number;
private assem: StringAssembler;
constructor() {
this.minusAssem = new MergingOpAssembler()
this.plusAssem = new MergingOpAssembler()
this.keepAssem = new MergingOpAssembler()
this.assem = new StringAssembler()
this.lastOpcode = ''
this.lengthChange = 0
}
flushKeeps = () => {
this.assem.append(this.keepAssem.toString());
this.keepAssem.clear();
};
flushPlusMinus = () => {
this.assem.append(this.minusAssem.toString());
this.minusAssem.clear();
this.assem.append(this.plusAssem.toString());
this.plusAssem.clear();
};
append = (op: Op) => {
if (!op.opcode) return;
if (!op.chars) return;
if (op.opcode === '-') {
if (this.lastOpcode === '=') {
this.flushKeeps();
}
this.minusAssem.append(op);
this.lengthChange -= op.chars;
} else if (op.opcode === '+') {
if (this.lastOpcode === '=') {
this.flushKeeps();
}
this.plusAssem.append(op);
this.lengthChange += op.chars;
} else if (op.opcode === '=') {
if (this.lastOpcode !== '=') {
this.flushPlusMinus();
}
this.keepAssem.append(op);
}
this.lastOpcode = op.opcode;
};
/**
* Generates operations from the given text and attributes.
*
* @deprecated Use `opsFromText` instead.
* @param {('-'|'+'|'=')} opcode - The operator to use.
* @param {string} text - The text to remove/add/keep.
* @param {(string|Iterable<Attribute>)} attribs - The attributes to apply to the operations.
* @param {?AttributePool.ts} pool - Attribute pool. Only required if `attribs` is an iterable of
* attribute key, value pairs.
*/
appendOpWithText = (opcode: '-'|'+'|'=', text: string, attribs: Attribute[], pool?: AttributePool) => {
padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' +
'use opsFromText() instead.');
for (const op of opsFromText(opcode, text, attribs, pool)) this.append(op);
};
toString = () => {
this.flushPlusMinus();
this.flushKeeps();
return this.assem.toString();
};
clear = () => {
this.minusAssem.clear();
this.plusAssem.clear();
this.keepAssem.clear();
this.assem.clear();
this.lengthChange = 0;
};
endDocument = () => {
this.keepAssem.endDocument();
};
getLengthChange = () => this.lengthChange;
}

View file

@ -0,0 +1,18 @@
/**
* @returns {StringAssembler}
*/
export class StringAssembler {
private str = ''
clear = ()=> {
this.str = '';
}
/**
* @param {string} x -
*/
append(x: string) {
this.str += String(x);
}
toString() {
return this.str
}
}

View file

@ -0,0 +1,54 @@
import {assert} from "./Changeset";
/**
* A custom made String Iterator
*
* @typedef {object} StringIterator
* @property {Function} newlines -
* @property {Function} peek -
* @property {Function} remaining -
* @property {Function} skip -
* @property {Function} take -
*/
/**
* @param {string} str - String to iterate over
* @returns {StringIterator}
*/
export class StringIterator {
private curIndex: number;
private newLines: number;
private str: String
constructor(str: string) {
this.curIndex = 0;
this.str = str
this.newLines = str.split('\n').length - 1;
}
remaining = () => this.str.length - this.curIndex;
getnewLines = () => this.newLines;
assertRemaining = (n: number) => {
assert(n <= this.remaining(), `!(${n} <= ${this.remaining()})`);
}
take = (n: number) => {
this.assertRemaining(n);
const s = this.str.substring(this.curIndex, n);
this.newLines -= s.split('\n').length - 1;
this.curIndex += n;
return s;
}
peek = (n: number) => {
this.assertRemaining(n);
return this.str.substring(this.curIndex, n);
}
skip = (n: number) => {
this.assertRemaining(n);
this.curIndex += n;
}
}

View file

@ -25,12 +25,12 @@
// requires: undefined
const hooks = require('./pluginfw/hooks');
const makeCSSManager = require('./cssmanager').makeCSSManager;
const pluginUtils = require('./pluginfw/shared');
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
const debugLog = (...args) => {};
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins')
const rJQuery = require('ep_etherpad-lite/static/js/rjquery')
const {Cssmanager} = require("./cssmanager");
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
// errors out unless given an absolute URL for a JavaScript-created element.
@ -298,16 +298,16 @@ const Ace2Editor = function () {
innerWindow.Ace2Inner = ace2_inner;
innerWindow.plugins = cl_plugins;
innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery;
innerWindow.$ = innerWindow.jQuery = window.$;
debugLog('Ace2Editor.init() waiting for plugins');
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
(err) => err != null ? reject(err) : resolve()));*/
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
await innerWindow.Ace2Inner.init(info, {
inner: makeCSSManager(innerStyle.sheet),
outer: makeCSSManager(outerStyle.sheet),
parent: makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet),
inner: new Cssmanager(innerStyle.sheet),
outer: new Cssmanager(outerStyle.sheet),
parent: new Cssmanager(document.querySelector('style[title="dynamicsyntax"]').sheet),
});
debugLog('Ace2Editor.init() Ace2Inner.init() returned');
loaded = true;

View file

@ -1,5 +1,7 @@
'use strict';
import linestylefilter from "./linestylefilter";
/**
* Copyright 2009 Google Inc.
* Copyright 2020 John McLear - The Etherpad Foundation.
@ -18,32 +20,31 @@
*/
let documentAttributeManager;
const AttributeMap = require('./AttributeMap');
import AttributeMap from './AttributeMap'
const browser = require('./vendors/browser');
const padutils = require('./pad_utils').padutils;
import {padUtils as padutils} from './pad_utils'
const Ace2Common = require('./ace2_common');
const $ = require('./rjquery').$;
const isNodeText = Ace2Common.isNodeText;
const getAssoc = Ace2Common.getAssoc;
const setAssoc = Ace2Common.setAssoc;
const noop = Ace2Common.noop;
const hooks = require('./pluginfw/hooks');
import AttributePool from "./AttributePool";
import Scroll from './scroll'
import AttributeManager from "./AttributeManager";
import ChangesetTracker from './changesettracker'
import SkipList from "./skiplist";
import {undoModule, pool as undoModPool, setPool} from './undomodule'
function Ace2Inner(editorInfo, cssManagers) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
const makeContentCollector = require('./contentcollector').makeContentCollector;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const linestylefilter = require('./linestylefilter').linestylefilter;
const SkipList = require('./skiplist');
const undoModule = require('./undomodule').undoModule;
const AttributeManager = require('./AttributeManager');
const DEBUG = false;
const THE_TAB = ' '; // 4
@ -126,12 +127,12 @@ function Ace2Inner(editorInfo, cssManagers) {
selFocusAtStart: false,
alltext: '',
alines: [],
apool: new AttribPool(),
apool: new AttributePool(),
};
// lines, alltext, alines, and DOM are set up in init()
if (undoModule.enabled) {
undoModule.apool = rep.apool;
setPool(rep.apool)
}
let isEditable = true;
@ -174,7 +175,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// CCCCCCCCCCCCCCCCCCCC\n
// CCCC\n
// end[0]: <CCC end[1] CCC>-------\n
const builder = Changeset.builder(rep.lines.totalWidth());
const builder = Changeset.builder(rep.lines.totalWidth);
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
builder.insert(newText, [
@ -185,7 +186,7 @@ function Ace2Inner(editorInfo, cssManagers) {
performDocumentApplyChangeset(cs);
};
const changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
const changesetTracker = new ChangesetTracker(scheduler, rep.apool, {
withCallbacks: (operationName, f) => {
inCallStackIfNecessary(operationName, () => {
fastIncorp(1);
@ -497,7 +498,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const importAText = (atext, apoolJsonObj, undoable) => {
atext = Changeset.cloneAText(atext);
if (apoolJsonObj) {
const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
}
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
@ -523,7 +524,7 @@ function Ace2Inner(editorInfo, cssManagers) {
fastIncorp(8);
const oldLen = rep.lines.totalWidth();
const oldLen = rep.lines.totalWidth;
const numLines = rep.lines.length();
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
@ -827,7 +828,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const recolorLinesInRange = (startChar, endChar) => {
if (endChar <= startChar) return;
if (startChar < 0 || startChar >= rep.lines.totalWidth()) return;
if (startChar < 0 || startChar >= rep.lines.totalWidth) return;
let lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary
let lineStart = rep.lines.offsetOfEntry(lineEntry);
let lineIndex = rep.lines.indexOfEntry(lineEntry);
@ -1271,7 +1272,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
theIndent += THE_TAB;
}
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
const cs = Changeset.builder(rep.lines.totalWidth).keep(
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
theIndent, [
['author', thisAuthor],
@ -2297,7 +2298,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// 3-renumber every list item of the same level from the beginning, level 1
// IMPORTANT: never skip a level because there imbrication may be arbitrary
const builder = Changeset.builder(rep.lines.totalWidth());
const builder = Changeset.builder(rep.lines.totalWidth);
let loc = [0, 0];
const applyNumberList = (line, level) => {
// init

View file

@ -17,6 +17,9 @@
* @typedef {string} AttributeString
*/
import AttributePool from "./AttributePool";
import {Attribute} from "./types/Attribute";
/**
* Converts an attribute string into a sequence of attribute identifier numbers.
*
@ -28,7 +31,7 @@
* appear in `str`.
* @returns {Generator<number>}
*/
exports.decodeAttribString = function* (str) {
export const decodeAttribString = function* (str: string): Generator<number> {
const re = /\*([0-9a-z]+)|./gy;
let match;
while ((match = re.exec(str)) != null) {
@ -38,7 +41,7 @@ exports.decodeAttribString = function* (str) {
}
};
const checkAttribNum = (n) => {
const checkAttribNum = (n: number|object) => {
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}`);
@ -50,7 +53,7 @@ const checkAttribNum = (n) => {
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
* @returns {AttributeString}
*/
exports.encodeAttribString = (attribNums) => {
export const encodeAttribString = (attribNums: Iterable<number>): string => {
let str = '';
for (const n of attribNums) {
checkAttribNum(n);
@ -67,7 +70,7 @@ exports.encodeAttribString = (attribNums) => {
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
* @returns {Generator<Attribute>}
*/
exports.attribsFromNums = function* (attribNums, pool) {
export const attribsFromNums = function* (attribNums: Iterable<number>, pool: AttributePool): Generator<Attribute> {
for (const n of attribNums) {
checkAttribNum(n);
const attrib = pool.getAttrib(n);
@ -87,7 +90,7 @@ exports.attribsFromNums = function* (attribNums, pool) {
* @yields {number} The attribute number of each attribute in `attribs`, in order.
* @returns {Generator<number>}
*/
exports.attribsToNums = function* (attribs, pool) {
export const attribsToNums = function* (attribs: Iterable<Attribute>, pool: AttributePool) {
for (const attrib of attribs) yield pool.putAttrib(attrib);
};
@ -102,8 +105,8 @@ exports.attribsToNums = function* (attribs, pool) {
* @yields {Attribute} The attributes identified in `str`, in order.
* @returns {Generator<Attribute>}
*/
exports.attribsFromString = function* (str, pool) {
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
export const attribsFromString = function* (str: string, pool: AttributePool): Generator<Attribute> {
yield* attribsFromNums(decodeAttribString(str), pool);
};
/**
@ -116,8 +119,8 @@ exports.attribsFromString = function* (str, pool) {
* @param {AttributePool} pool - Attribute pool.
* @returns {AttributeString}
*/
exports.attribsToString =
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
export const attribsToString =
(attribs: Iterable<Attribute>, pool: AttributePool): string => encodeAttribString(attribsToNums(attribs, pool));
/**
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
@ -126,5 +129,4 @@ exports.attribsToString =
* @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 sort = (attribs: Attribute[]): Attribute[] => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));

View file

@ -26,7 +26,7 @@
const msgBlock = document.createElement('blockquote');
box.appendChild(msgBlock);
msgBlock.style.fontWeight = 'bold';
msgBlock.appendChild(document.createTextNode(msg));
msgBlock.appendChild(document.createTextNode(msg as string));
const loc = document.createElement('p');
box.appendChild(loc);
loc.appendChild(document.createTextNode(`in ${url}`));
@ -39,7 +39,7 @@
box.appendChild(stackBlock);
const stack = document.createElement('pre');
stackBlock.appendChild(stack);
stack.appendChild(document.createTextNode(err.stack || err.toString()));
stack.appendChild(document.createTextNode(err!.stack || err!.toString()));
if (typeof originalHandler === 'function') originalHandler(...args);
};

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
import {Cssmanager} from "./cssmanager";
/**
* Copyright 2009 Google Inc.
*
@ -22,14 +24,14 @@
* limitations under the License.
*/
const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline;
const AttribPool = require('./AttributePool');
import AttributePool from "./AttributePool";
const Changeset = require('./Changeset');
const attributes = require('./attributes');
const linestylefilter = require('./linestylefilter').linestylefilter;
import linestylefilter from './linestylefilter'
const colorutils = require('./colorutils').colorutils;
const _ = require('./underscore');
const _ = require('underscore');
const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
@ -56,7 +58,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
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),
apool: (new AttributePool()).fromJsonable(clientVars.collab_client_vars.apool),
alines: Changeset.splitAttributionLines(
clientVars.collab_client_vars.initialAttributedText.attribs,
clientVars.collab_client_vars.initialAttributedText.text),
@ -389,7 +391,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
setTimeout(() => this.loadFromQueue(), 10);
},
handleResponse: (data, start, granularity, callback) => {
const pool = (new AttribPool()).fromJsonable(data.apool);
const pool = (new AttributePool()).fromJsonable(data.apool);
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
@ -409,13 +411,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
if (obj.type === 'NEW_CHANGES') {
const changeset = Changeset.moveOpsToNewPool(
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
obj.changeset, (new AttributePool()).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);
changesetBack, (new AttributePool()).fromJsonable(obj.apool), padContents.apool);
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
} else if (obj.type === 'NEW_AUTHORDATA') {
@ -465,7 +467,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
BroadcastSlider.onSlider(goToRevisionIfEnabled);
const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet);
const dynamicCSS = new Cssmanager(document.querySelector('style[title="dynamicsyntax"]').sheet);
const authorData = {};
const receiveAuthorData = (newAuthorData) => {

View file

@ -23,7 +23,7 @@
// These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate.
const _ = require('./underscore');
const _ = require('underscore');
const padmodals = require('./pad_modals').padmodals;
const colorutils = require('./colorutils').colorutils;
import html10n from './vendors/html10n';

View file

@ -1,203 +0,0 @@
'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');
// changes applied to baseText that have been submitted
let submittedChangeset = null;
// changes applied to submittedChangeset since it was prepared
let userChangeset = Changeset.identity(1);
// is the changesetTracker enabled
let tracking = false;
// stack state flag so that when we change the rep we don't
// handle the notification recursively. When setting, always
// 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
// and if there isn't a timeout already scheduled.
if (changeCallback && changeCallbackTimeout == null) {
changeCallbackTimeout = scheduler.setTimeout(() => {
try {
changeCallback();
} catch (pseudoError) {
// as empty as my soul
} finally {
changeCallbackTimeout = null;
}
}, 0);
}
};
let self;
return self = {
isTracking: () => tracking,
setBaseText: (text) => {
self.setBaseAttributedText(Changeset.makeAText(text), null);
},
setBaseAttributedText: (atext, apoolJsonObj) => {
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
tracking = true;
baseAText = Changeset.cloneAText(atext);
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
}
submittedChangeset = null;
userChangeset = Changeset.identity(atext.text.length);
applyingNonUserChanges = true;
try {
callbacks.setDocumentAttributedText(atext);
} finally {
applyingNonUserChanges = false;
}
});
},
composeUserChangeset: (c) => {
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;
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);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
applyingNonUserChanges = true;
try {
callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret);
} finally {
applyingNonUserChanges = false;
}
});
},
prepareUserChangeset: () => {
// If there are user changes to submit, 'changeset' will be the
// changeset, else it will be null.
let toSubmit;
if (submittedChangeset) {
// submission must have been canceled, prepare new changeset
// that includes old submittedChangeset
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
} 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);
const oldAuthorId = attribs.get('author');
if (oldAuthorId != null && oldAuthorId !== authorId) {
attribs.set('author', authorId);
op.attribs = attribs.toString();
}
}
assem.append(op);
}
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;
}
let cs = null;
if (toSubmit) {
submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
cs = toSubmit;
}
let wireApool = null;
if (cs) {
const forWire = Changeset.prepareForWire(cs, apool);
wireApool = forWire.pool.toJsonable();
cs = forWire.translated;
}
const data = {
changeset: cs,
apool: wireApool,
};
return data;
},
applyPreparedChangesetToBase: () => {
if (!submittedChangeset) {
// violation of protocol; use prepareUserChangeset first
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
}
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
submittedChangeset = null;
},
setUserChangeNotificationCallback: (callback) => {
changeCallback = callback;
},
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
};
};
exports.makeChangesetTracker = makeChangesetTracker;

View file

@ -0,0 +1,216 @@
'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.
*/
import AttributeMap from './AttributeMap'
import AttributePool from "./AttributePool";
import {AText} from "../../node/types/PadType";
import {Attribute} from "./types/Attribute";
const Changeset = require('./Changeset');
class Changesettracker {
private scheduler: WindowProxy
private readonly apool: AttributePool
private baseAText: {
attribs: Attribute[]
}
private submittedChangeset: null
private userChangeset: any
private tracking: boolean
private applyingNonUserChanges: boolean
private aceCallbacksProvider: any
private changeCallback: (() => void) | null = null
private changeCallbackTimeout: number | null = null
constructor(scheduler: WindowProxy, apool: AttributePool, aceCallbacksProvider: any) {
this.scheduler = scheduler
this.apool = apool
this.aceCallbacksProvider = aceCallbacksProvider
// latest official text from server
this.baseAText = Changeset.makeAText('\n');
// changes applied to baseText that have been submitted
this.submittedChangeset = null
// changes applied to submittedChangeset since it was prepared
this.userChangeset = Changeset.identity(1)
// is the changesetTracker enabled
this.tracking = false
this.applyingNonUserChanges = false
}
setChangeCallbackTimeout = () => {
// can call this multiple times per call-stack, because
// we only schedule a call to changeCallback if it exists
// and if there isn't a timeout already scheduled.
if (this.changeCallback && this.changeCallbackTimeout == null) {
this.changeCallbackTimeout = this.scheduler.setTimeout(() => {
try {
this.changeCallback!();
} catch (pseudoError) {
// as empty as my soul
} finally {
this.changeCallbackTimeout = null;
}
}, 0);
}
}
isTracking = () => this.tracking
setBaseText = (text: string) => {
this.setBaseAttributedText(Changeset.makeAText(text), null);
}
setBaseAttributedText = (atext: AText, apoolJsonObj?: AttributePool | null) => {
this.aceCallbacksProvider.withCallbacks('setBaseText', (callbacks: { setDocumentAttributedText: (arg0: AText) => void; }) => {
this.tracking = true;
this.baseAText = Changeset.cloneAText(atext);
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
this.baseAText.attribs = Changeset.moveOpsToNewPool(this.baseAText.attribs, wireApool, this.apool);
}
this.submittedChangeset = null;
this.userChangeset = Changeset.identity(atext.text.length);
this.applyingNonUserChanges = true;
try {
callbacks.setDocumentAttributedText(atext);
} finally {
this.applyingNonUserChanges = false;
}
});
}
composeUserChangeset = (c: number) => {
if (!this.tracking) return;
if (this.applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return;
this.userChangeset = Changeset.compose(this.userChangeset, c, this.apool);
this.setChangeCallbackTimeout();
}
applyChangesToBase = (c: number, optAuthor: string, apoolJsonObj: AttributePool) => {
if (!this.tracking) return;
this.aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks: { applyChangesetToDocument: (arg0: any, arg1: boolean) => void; }) => {
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
c = Changeset.moveOpsToNewPool(c, wireApool, this.apool);
}
this.baseAText = Changeset.applyToAText(c, this.baseAText, this.apool);
let c2 = c;
if (this.submittedChangeset) {
const oldSubmittedChangeset = this.submittedChangeset;
this.submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, this.apool);
c2 = Changeset.follow(oldSubmittedChangeset, c, true, this.apool);
}
const preferInsertingAfterUserChanges = true;
const oldUserChangeset = this.userChangeset;
this.userChangeset = Changeset.follow(
c2, oldUserChangeset, preferInsertingAfterUserChanges, this.apool);
const postChange = Changeset.follow(
oldUserChangeset, c2, !preferInsertingAfterUserChanges, this.apool);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
this.applyingNonUserChanges = true;
try {
callbacks.applyChangesetToDocument(postChange, preferInsertionAfterCaret);
} finally {
this.applyingNonUserChanges = false;
}
});
}
prepareUserChangeset = () => {
// If there are user changes to submit, 'changeset' will be the
// changeset, else it will be null.
let toSubmit;
if (this.submittedChangeset) {
// submission must have been canceled, prepare new changeset
// that includes old submittedChangeset
toSubmit = Changeset.compose(this.submittedChangeset, this.userChangeset, this.apool);
} else {
// Get my authorID
// @ts-ignore
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(this.userChangeset);
const assem = Changeset.mergingOpAssembler();
for (const op of Changeset.deserializeOps(cs.ops)) {
if (op.opcode === '+') {
const attribs = AttributeMap.fromString(op.attribs, this.apool);
const oldAuthorId = attribs.get('author');
if (oldAuthorId != null && oldAuthorId !== authorId) {
attribs.set('author', authorId);
op.attribs = attribs.toString();
}
}
assem.append(op);
}
assem.endDocument();
this.userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
Changeset.checkRep(this.userChangeset);
if (Changeset.isIdentity(this.userChangeset)) toSubmit = null;
else toSubmit = this.userChangeset;
}
let cs = null;
if (toSubmit) {
this.submittedChangeset = toSubmit;
this.userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
cs = toSubmit;
}
let wireApool = null;
if (cs) {
const forWire = Changeset.prepareForWire(cs, this.apool);
wireApool = forWire.pool.toJsonable();
cs = forWire.translated;
}
const data = {
changeset: cs,
apool: wireApool,
};
return data;
}
applyPreparedChangesetToBase = () => {
if (!this.submittedChangeset) {
// violation of protocol; use prepareUserChangeset first
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
}
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
this.baseAText = Changeset.applyToAText(this.submittedChangeset, this.baseAText, this.apool);
this.submittedChangeset = null;
}
setUserChangeNotificationCallback = (callback: (() => void) | null) => {
this.changeCallback = callback;
}
hasUncommittedChanges = () => !!(this.submittedChangeset || (!Changeset.isIdentity(this.userChangeset)))
}
export default Changesettracker

View file

@ -16,8 +16,9 @@
*/
const ChatMessage = require('./ChatMessage');
const padutils = require('./pad_utils').padutils;
const padcookie = require('./pad_cookie').padcookie;
import {padUtils as padutils} from "./pad_utils";
import padcookie from "./pad_cookie";
const Tinycon = require('tinycon/tinycon');
const hooks = require('./pluginfw/hooks');
const padeditor = require('./pad_editor').padeditor;

View file

@ -26,7 +26,7 @@
const _MAX_LIST_LEVEL = 16;
const AttributeMap = require('./AttributeMap');
import AttributeMap from './AttributeMap'
const UNorm = require('unorm');
const Changeset = require('./Changeset');
const hooks = require('./pluginfw/hooks');

View file

@ -1,72 +0,0 @@
'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) => {
const browserRules = () => (browserSheet.cssRules || browserSheet.rules);
const browserDeleteRule = (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);
};
const selectorList = [];
const indexOfSelector = (selector) => {
for (let i = 0; i < selectorList.length; i++) {
if (selectorList[i] === selector) {
return i;
}
}
return -1;
};
const selectorStyle = (selector) => {
let i = indexOfSelector(selector);
if (i < 0) {
// add selector
browserInsertRule(0, selector);
selectorList.splice(0, 0, selector);
i = 0;
}
return browserRules().item(i).style;
};
const removeSelectorStyle = (selector) => {
const i = indexOfSelector(selector);
if (i >= 0) {
browserDeleteRule(i);
selectorList.splice(i, 1);
}
};
return {
selectorStyle,
removeSelectorStyle,
info: () => `${selectorList.length}:${browserRules().length}`,
};
};

View file

@ -0,0 +1,72 @@
'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.
*/
export class Cssmanager {
private browserSheet: CSSStyleSheet
private selectorList:string[] = [];
constructor(browserSheet: CSSStyleSheet) {
this.browserSheet = browserSheet
}
browserRules = () => (this.browserSheet.cssRules || this.browserSheet.rules);
browserDeleteRule = (i: number) => {
if (this.browserSheet.deleteRule) this.browserSheet.deleteRule(i);
else this.browserSheet.removeRule(i);
}
browserInsertRule = (i: number, selector: string) => {
if (this.browserSheet.insertRule) this.browserSheet.insertRule(`${selector} {}`, i);
else { // @ts-ignore
this.browserSheet.addRule(selector, null, i);
}
}
indexOfSelector = (selector: string) => {
for (let i = 0; i < this.selectorList.length; i++) {
if (this.selectorList[i] === selector) {
return i;
}
}
return -1;
}
selectorStyle = (selector: string) => {
let i = this.indexOfSelector(selector);
if (i < 0) {
// add selector
this.browserInsertRule(0, selector);
this.selectorList.splice(0, 0, selector);
i = 0;
}
// @ts-ignore
return this.browserRules().item(i)!.style;
}
removeSelectorStyle = (selector: string) => {
const i = this.indexOfSelector(selector);
if (i >= 0) {
this.browserDeleteRule(i);
this.selectorList.splice(i, 1);
}
}
info= () => `${this.selectorList.length}:${this.browserRules().length}`
}

View file

@ -22,10 +22,11 @@
// requires: plugins
// requires: undefined
const Security = require('./security');
const Security = require('security');
const hooks = require('./pluginfw/hooks');
const _ = require('./underscore');
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
const _ = require('underscore');
import {lineAttributeMarker} from "./linestylefilter";
const noop = () => {};

View file

@ -19,6 +19,8 @@
* limitations under the License.
*/
import {getRandomValues} from 'crypto'
const randomPadName = () => {
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
// using the PRNG below
@ -28,8 +30,7 @@ const randomPadName = () => {
// make room for 8-bit integer values that span from 0 to 255.
const randomarray = new Uint8Array(stringLength);
// use browser's PRNG to generate a "unique" sequence
const cryptoObj = window.crypto || window.msCrypto; // for IE 11
cryptoObj.getRandomValues(randomarray);
getRandomValues(randomarray);
let randomstring = '';
for (let i = 0; i < stringLength; i++) {
// instead of writing "Math.floor(randomarray[i]/256*64)"
@ -42,9 +43,9 @@ const randomPadName = () => {
$(() => {
$('#go2Name').on('submit', () => {
const padname = $('#padname').val();
const padname = $('#padname').val() as string;
if (padname.length > 0) {
window.location = `p/${encodeURIComponent(padname.trim())}`;
window.location.href = `p/${encodeURIComponent(padname.trim())}`;
} else {
alert('Please enter a name');
}
@ -52,10 +53,11 @@ $(() => {
});
$('#button').on('click', () => {
window.location = `p/${randomPadName()}`;
window.location.href = `p/${randomPadName()}`;
});
// start the custom js
// @ts-ignore
if (typeof window.customStart === 'function') window.customStart();
});

View file

@ -1,291 +0,0 @@
'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;
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 '-';
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) => {
// Plugin Hook to add more Attrib Classes
for (const attribClasses of hooks.callAll('aceAttribClasses', linestylefilter.ATTRIB_CLASSES)) {
Object.assign(linestylefilter.ATTRIB_CLASSES, attribClasses);
}
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 (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) {
isLineAttribMarker = true;
}
if (key === 'author') {
classes += ` ${linestylefilter.getAuthorClassName(value)}`;
} else if (key === 'list') {
classes += ` list:${value}`;
} 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]) {
classes += ` ${linestylefilter.ATTRIB_CLASSES[key]}`;
} else {
const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value});
classes += ` ${results.join(' ')}`;
}
}
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();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
};
goNextOp();
const nextClasses = () => {
if (curIndex < lineEnd) {
extraClasses = nextOpClasses;
leftInAuthor = nextOp.chars;
goNextOp();
while (nextOp.opcode && nextOpClasses === extraClasses) {
leftInAuthor += nextOp.chars;
goNextOp();
}
}
};
nextClasses();
return (txt, cls) => {
const disableAuthColorForThisLine = hooks.callAll('disableAuthorColorsForThisLine', {
linestylefilter,
text: txt,
class: cls,
});
const disableAuthors = (disableAuthColorForThisLine == null ||
disableAuthColorForThisLine.length === 0) ? false : disableAuthColorForThisLine[0];
while (txt.length > 0) {
if (leftInAuthor <= 0 || disableAuthors) {
// prevent infinite loop if something funny's going on
return nextAfterAuthorColors(txt, cls);
}
let spanSize = txt.length;
if (spanSize > leftInAuthor) {
spanSize = leftInAuthor;
}
const curTxt = txt.substring(0, spanSize);
txt = txt.substring(spanSize);
nextAfterAuthorColors(curTxt, (cls && `${cls} `) + extraClasses);
curIndex += spanSize;
leftInAuthor -= spanSize;
if (leftInAuthor === 0) {
nextClasses();
}
}
};
})();
return authorColorFunc;
};
linestylefilter.getAtSignSplitterFilter = (lineText, textAndClassFunc) => {
const at = /@/g;
at.lastIndex = 0;
let splitPoints = null;
let execResult;
while ((execResult = at.exec(lineText))) {
if (!splitPoints) {
splitPoints = [];
}
splitPoints.push(execResult.index);
}
if (!splitPoints) return textAndClassFunc;
return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints);
};
linestylefilter.getRegexpFilter = (regExp, tag) => (lineText, textAndClassFunc) => {
regExp.lastIndex = 0;
let regExpMatchs = null;
let splitPoints = null;
let execResult;
while ((execResult = regExp.exec(lineText))) {
if (!regExpMatchs) {
regExpMatchs = [];
splitPoints = [];
}
const startIndex = execResult.index;
const regExpMatch = execResult[0];
regExpMatchs.push([startIndex, regExpMatch]);
splitPoints.push(startIndex, startIndex + regExpMatch.length);
}
if (!regExpMatchs) return textAndClassFunc;
const regExpMatchForIndex = (idx) => {
for (let k = 0; k < regExpMatchs.length; k++) {
const u = regExpMatchs[k];
if (idx >= u[0] && idx < u[0] + u[1].length) {
return u[1];
}
}
return false;
};
const handleRegExpMatchsAfterSplit = (() => {
let curIndex = 0;
return (txt, cls) => {
const txtlen = txt.length;
let newCls = cls;
const regExpMatch = regExpMatchForIndex(curIndex);
if (regExpMatch) {
newCls += ` ${tag}:${regExpMatch}`;
}
textAndClassFunc(txt, newCls);
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 {
const splitPoints = splitPointsOpt;
const pointLocInSpan = splitPoints[nextPointIndex] - idx;
const txtlen = txt.length;
if (pointLocInSpan >= txtlen) {
func(txt, cls);
idx += txt.length;
if (pointLocInSpan === txtlen) {
nextPointIndex++;
}
} else {
if (pointLocInSpan > 0) {
func(txt.substring(0, pointLocInSpan), cls);
idx += pointLocInSpan;
}
nextPointIndex++;
// recurse
spanHandler(txt.substring(pointLocInSpan), cls);
}
}
};
return spanHandler;
};
linestylefilter.getFilterStack = (lineText, textAndClassFunc, abrowser) => {
let func = linestylefilter.getURLFilter(lineText, textAndClassFunc);
const hookFilters = hooks.callAll('aceGetFilterStack', {
linestylefilter,
browser: 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
let text = textLine;
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;

View file

@ -0,0 +1,298 @@
'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");
import AttributePool from "./AttributePool";
/**
* 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 = {};
import AttributeManager from "./AttributeManager";
import {padUtils as padutils} from "./pad_utils";
import {Attribute} from "./types/Attribute";
import Op from "./Op";
type DomLineObject = {
appendSpan(tokenText: string, tokenClass:string):void
}
class Linestylefilter {
ATTRIB_CLASSES: {
[key: string]: string
} = {
bold: 'tag:b',
italic: 'tag:i',
underline: 'tag:u',
strikethrough: 'tag:s',
}
getAuthorClassName = (author: string) => `author-${author.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-';
return `z${c.charCodeAt(0)}z`;
})}`
// lineLength is without newline; aline includes newline,
// but may be falsy if lineLength == 0
getLineStyleFilter = (lineLength: number, aline: string, textAndClassFunc: Function, apool: AttributePool) => {
// Plugin Hook to add more Attrib Classes
for (const attribClasses of hooks.callAll('aceAttribClasses', this.ATTRIB_CLASSES)) {
Object.assign(this.ATTRIB_CLASSES, attribClasses);
}
if (lineLength === 0) return textAndClassFunc;
const nextAfterAuthorColors = textAndClassFunc;
const authorColorFunc = (() => {
const lineEnd = lineLength;
let curIndex = 0;
let extraClasses: string;
let leftInAuthor: number;
const attribsToClasses = (attribs: string) => {
let classes = '';
let isLineAttribMarker = false;
for (const [key, value] of attributes.attribsFromString(attribs, apool)) {
if (!key || !value) continue;
if (!isLineAttribMarker && AttributeManager.lineAttributes.indexOf(key) >= 0) {
isLineAttribMarker = true;
}
if (key === 'author') {
classes += ` ${this.getAuthorClassName(value)}`;
} else if (key === 'list') {
classes += ` list:${value}`;
} else if (key === 'start') {
// Needed to introduce the correct Ordered list item start number on import
classes += ` start:${value}`;
} else if (this.ATTRIB_CLASSES[key]) {
classes += ` ${this.ATTRIB_CLASSES[key]}`;
} else {
const results = hooks.callAll('aceAttribsToClasses', {linestylefilter, key, value});
classes += ` ${results.join(' ')}`;
}
}
if (isLineAttribMarker) classes += ` ${lineAttributeMarker}`;
return classes.substring(1);
};
const attrOps = Changeset.deserializeOps(aline);
let attrOpsNext = attrOps.next();
let nextOp: Op, nextOpClasses: string;
const goNextOp = () => {
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
};
goNextOp();
const nextClasses = () => {
if (curIndex < lineEnd) {
extraClasses = nextOpClasses;
leftInAuthor = nextOp.chars;
goNextOp();
while (nextOp.opcode && nextOpClasses === extraClasses) {
leftInAuthor += nextOp.chars;
goNextOp();
}
}
};
nextClasses();
return (txt: string, cls: string) => {
const disableAuthColorForThisLine = hooks.callAll('disableAuthorColorsForThisLine', {
linestylefilter,
text: txt,
class: cls,
});
const disableAuthors = (disableAuthColorForThisLine == null ||
disableAuthColorForThisLine.length === 0) ? false : disableAuthColorForThisLine[0];
while (txt.length > 0) {
if (leftInAuthor <= 0 || disableAuthors) {
// prevent infinite loop if something funny's going on
return nextAfterAuthorColors(txt, cls);
}
let spanSize = txt.length;
if (spanSize > leftInAuthor) {
spanSize = leftInAuthor;
}
const curTxt = txt.substring(0, spanSize);
txt = txt.substring(spanSize);
nextAfterAuthorColors(curTxt, (cls && `${cls} `) + extraClasses);
curIndex += spanSize;
leftInAuthor -= spanSize;
if (leftInAuthor === 0) {
nextClasses();
}
}
};
})();
return authorColorFunc;
}
getAtSignSplitterFilter = (lineText: string, textAndClassFunc: Function) => {
const at = /@/g;
at.lastIndex = 0;
let splitPoints = null;
let execResult;
while ((execResult = at.exec(lineText))) {
if (!splitPoints) {
splitPoints = [];
}
splitPoints.push(execResult.index);
}
if (!splitPoints) return textAndClassFunc;
return this.textAndClassFuncSplitter(textAndClassFunc, splitPoints);
}
getRegexpFilter = (regExp: RegExp, tag: string) => (lineText: string, textAndClassFunc: Function) => {
regExp.lastIndex = 0;
let regExpMatchs = null;
let splitPoints: number[]|null = null;
let execResult;
while ((execResult = regExp.exec(lineText))) {
if (!regExpMatchs) {
regExpMatchs = [];
splitPoints = [];
}
const startIndex = execResult.index;
const regExpMatch = execResult[0];
regExpMatchs.push([startIndex, regExpMatch]);
splitPoints!.push(startIndex, startIndex + regExpMatch.length);
}
if (!regExpMatchs) return textAndClassFunc;
const regExpMatchForIndex = (idx: number) => {
for (let k = 0; k < regExpMatchs.length; k++) {
const u = regExpMatchs[k] as number[];
// @ts-ignore
if (idx >= u[0] && idx < u[0] + u[1].length) {
return u[1];
}
}
return false;
}
const handleRegExpMatchsAfterSplit = (() => {
let curIndex = 0;
return (txt: string, cls: string) => {
const txtlen = txt.length;
let newCls = cls;
const regExpMatch = regExpMatchForIndex(curIndex);
if (regExpMatch) {
newCls += ` ${tag}:${regExpMatch}`;
}
textAndClassFunc(txt, newCls);
curIndex += txtlen;
};
})();
return this.textAndClassFuncSplitter(handleRegExpMatchsAfterSplit, splitPoints!);
}
getURLFilter = this.getRegexpFilter(padutils.urlRegex, 'url')
textAndClassFuncSplitter = (func: Function, splitPointsOpt: number[]) => {
let nextPointIndex = 0;
let idx = 0;
// don't split at 0
while (splitPointsOpt &&
nextPointIndex < splitPointsOpt.length &&
splitPointsOpt[nextPointIndex] === 0) {
nextPointIndex++;
}
const spanHandler = (txt: string, cls: string) => {
if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) {
func(txt, cls);
idx += txt.length;
} else {
const splitPoints = splitPointsOpt;
const pointLocInSpan = splitPoints[nextPointIndex] - idx;
const txtlen = txt.length;
if (pointLocInSpan >= txtlen) {
func(txt, cls);
idx += txt.length;
if (pointLocInSpan === txtlen) {
nextPointIndex++;
}
} else {
if (pointLocInSpan > 0) {
func(txt.substring(0, pointLocInSpan), cls);
idx += pointLocInSpan;
}
nextPointIndex++;
// recurse
spanHandler(txt.substring(pointLocInSpan), cls);
}
}
};
return spanHandler;
}
getFilterStack = (lineText: string, textAndClassFunc: Function, abrowser?:(tokenText: string, tokenClass: string)=>void) => {
let func = this.getURLFilter(lineText, textAndClassFunc);
const hookFilters = hooks.callAll('aceGetFilterStack', {
linestylefilter,
browser: abrowser,
});
hookFilters.map((hookFilter: (arg0: string, arg1: Function) => Function) => {
func = hookFilter(lineText, func);
});
return func;
}
// domLineObj is like that returned by domline.createDomLine
populateDomLine = (textLine: string, aline: string, apool: AttributePool, domLineObj: DomLineObject) => {
// remove final newline from text if any
let text = textLine;
if (text.slice(-1) === '\n') {
text = text.substring(0, text.length - 1);
}
const textAndClassFunc = (tokenText: string, tokenClass: string) => {
domLineObj.appendSpan(tokenText, tokenClass);
};
let func = this.getFilterStack(text, textAndClassFunc);
func = this.getLineStyleFilter(text.length, aline, func, apool);
func(text, '');
};
}
export default new Linestylefilter()
export const lineAttributeMarker = 'lineAttribMarker';

View file

@ -37,17 +37,17 @@ 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;
import padcookie from "./pad_cookie";
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;
import {padUtils as padutils} from "./pad_utils";
const colorutils = require('./colorutils').colorutils;
const randomString = require('./pad_utils').randomString;
const socketio = require('./socketio');
import connect from './socketio'
const hooks = require('./pluginfw/hooks');
@ -222,7 +222,7 @@ const handshake = async () => {
// 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, '/', {
socket = pad.socket = connect(exports.baseURL, '/', {
query: {padId},
reconnectionAttempts: 5,
reconnection: true,

View file

@ -16,9 +16,12 @@
* limitations under the License.
*/
const Cookies = require('./pad_utils').Cookies;
import {Cookies} from './pad_utils'
import html10n from "./vendors/html10n";
class PadCookie {
private readonly cookieName_: string
exports.padcookie = new class {
constructor() {
this.cookieName_ = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp';
}
@ -31,6 +34,7 @@ exports.padcookie = new class {
this.writePrefs_(prefs);
// Re-read the saved cookie to test if cookies are enabled.
if (this.readPrefs_() == null) {
// @ts-ignore
$.gritter.add({
title: 'Error',
text: html10n.get('pad.noCookie'),
@ -50,15 +54,15 @@ exports.padcookie = new class {
}
}
writePrefs_(prefs) {
writePrefs_(prefs: object) {
Cookies.set(this.cookieName_, JSON.stringify(prefs), {expires: 365 * 100});
}
getPref(prefName) {
getPref(prefName: string) {
return this.readPrefs_()[prefName];
}
setPref(prefName, value) {
setPref(prefName: string, value: string) {
const prefs = this.readPrefs_();
prefs[prefName] = value;
this.writePrefs_(prefs);
@ -67,4 +71,6 @@ exports.padcookie = new class {
clear() {
this.writePrefs_({});
}
}();
}
export default new PadCookie

View file

@ -24,7 +24,8 @@
const browser = require('./vendors/browser');
const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils;
import {padUtils as padutils} from "./pad_utils";
const padeditor = require('./pad_editor').padeditor;
const padsavedrevs = require('./pad_savedrevs');
const _ = require('underscore');

View file

@ -22,8 +22,9 @@
*/
const Cookies = require('./pad_utils').Cookies;
const padcookie = require('./pad_cookie').padcookie;
const padutils = require('./pad_utils').padutils;
import padcookie from "./pad_cookie";
import {padUtils as padutils} from "./pad_utils";
const Ace2Editor = require('./ace').Ace2Editor;
import html10n from '../js/vendors/html10n'

View file

@ -16,7 +16,7 @@
* limitations under the License.
*/
const padutils = require('./pad_utils').padutils;
import {padUtils as padutils} from "./pad_utils";
const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
let myUserInfo = {};

View file

@ -22,13 +22,14 @@
* limitations under the License.
*/
const Security = require('./security');
const Security = require('security');
import jsCookie, {CookiesStatic} from 'js-cookie'
/**
* Generates a random String with the given length. Is needed to generate the Author, Group,
* readonly, session Ids
*/
const randomString = (len) => {
export const randomString = (len?: number) => {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let randomstring = '';
len = len || 20;
@ -85,13 +86,41 @@ const urlRegex = (() => {
'tel',
].join('|')}):`;
return new RegExp(
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
})();
// https://stackoverflow.com/a/68957976
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
const padutils = {
type PadEvent = {
which: number
}
type JQueryNode = JQuery<HTMLElement>
class PadUtils {
public urlRegex: RegExp
public wordCharRegex: RegExp
public warnDeprecatedFlags: {
disabledForTestingOnly: boolean,
_rl?: {
prevs: Map<string, number>,
now: () => number,
period: number
}
logger?: any
}
public globalExceptionHandler: null | any = null;
constructor() {
this.warnDeprecatedFlags = {
disabledForTestingOnly: false
}
this.wordCharRegex = wordCharRegex
this.urlRegex = urlRegex
}
/**
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
* is using the deprecated function).
@ -107,41 +136,41 @@ const padutils = {
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
* logger is set), with a stack trace appended if available.
*/
warnDeprecated: (...args) => {
if (padutils.warnDeprecated.disabledForTestingOnly) return;
warnDeprecated = (...args: any[]) => {
if (this.warnDeprecatedFlags.disabledForTestingOnly) return;
const err = new Error();
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
if (Error.captureStackTrace) Error.captureStackTrace(err, this.warnDeprecated);
err.name = '';
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
if (typeof err.stack === 'string') {
if (padutils.warnDeprecated._rl == null) {
padutils.warnDeprecated._rl =
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
if (this.warnDeprecatedFlags._rl == null) {
this.warnDeprecatedFlags._rl =
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
}
const rl = padutils.warnDeprecated._rl;
const rl = this.warnDeprecatedFlags._rl;
const now = rl.now();
const prev = rl.prevs.get(err.stack);
if (prev != null && now - prev < rl.period) return;
rl.prevs.set(err.stack, now);
}
if (err.stack) args.push(err.stack);
(padutils.warnDeprecated.logger || console).warn(...args);
},
escapeHtml: (x) => Security.escapeHTML(String(x)),
uniqueId: () => {
(this.warnDeprecatedFlags.logger || console).warn(...args);
}
escapeHtml = (x: string) => Security.escapeHTML(String(x))
uniqueId = () => {
const pad = require('./pad').pad; // Sidestep circular dependency
// returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits
const encodeNum =
(n, width) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
(n: number, width: number) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
return [
pad.getClientIp(),
encodeNum(+new Date(), 7),
encodeNum(Math.floor(Math.random() * 1e9), 4),
].join('.');
},
}
// e.g. "Thu Jun 18 2009 13:09"
simpleDateTime: (date) => {
simpleDateTime = (date: string) => {
const d = new Date(+date); // accept either number or date
const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
const month = ([
@ -162,16 +191,14 @@ const padutils = {
const year = d.getFullYear();
const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`;
return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`;
},
wordCharRegex,
urlRegex,
}
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
findURLs: (text) => {
findURLs = (text: string) => {
// Copy padutils.urlRegex so that the use of .exec() below (which mutates the RegExp object)
// does not break other concurrent uses of padutils.urlRegex.
const urlRegex = new RegExp(padutils.urlRegex, 'g');
const urlRegex = new RegExp(this.urlRegex, 'g');
urlRegex.lastIndex = 0;
let urls = null;
let urls: [number, string][] | null = null;
let execResult;
// TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped.
while ((execResult = urlRegex.exec(text))) {
@ -181,18 +208,19 @@ const padutils = {
urls.push([startIndex, url]);
}
return urls;
},
escapeHtmlWithClickableLinks: (text, target) => {
}
escapeHtmlWithClickableLinks = (text: string, target: string) => {
let idx = 0;
const pieces = [];
const urls = padutils.findURLs(text);
const urls = this.findURLs(text);
const advanceTo = (i) => {
if (i > idx) {
pieces.push(Security.escapeHTML(text.substring(idx, i)));
idx = i;
const advanceTo = (i: number) => {
if (i > idx) {
pieces.push(Security.escapeHTML(text.substring(idx, i)));
idx = i;
}
}
};
;
if (urls) {
for (let j = 0; j < urls.length; j++) {
const startIndex = urls[j][0];
@ -206,25 +234,25 @@ const padutils = {
// https://mathiasbynens.github.io/rel-noopener/
// https://github.com/ether/etherpad-lite/pull/3636
pieces.push(
'<a ',
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
'href="',
Security.escapeHTMLAttribute(href),
'" rel="noreferrer noopener">');
'<a ',
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
'href="',
Security.escapeHTMLAttribute(href),
'" rel="noreferrer noopener">');
advanceTo(startIndex + href.length);
pieces.push('</a>');
}
}
advanceTo(text.length);
return pieces.join('');
},
bindEnterAndEscape: (node, onEnter, onEscape) => {
}
bindEnterAndEscape = (node: JQueryNode, onEnter: Function, onEscape: Function) => {
// Use keypress instead of keyup in bindEnterAndEscape. Keyup event is fired on enter in IME
// (Input Method Editor), But keypress is not. So, I changed to use keypress instead of keyup.
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
// 3.6.10, Chrome 6.0.472, Safari 5.0).
if (onEnter) {
node.on('keypress', (evt) => {
node.on('keypress', (evt: { which: number; }) => {
if (evt.which === 13) {
onEnter(evt);
}
@ -238,13 +266,15 @@ const padutils = {
}
});
}
},
timediff: (d) => {
}
timediff = (d: number) => {
const pad = require('./pad').pad; // Sidestep circular dependency
const format = (n, word) => {
n = Math.round(n);
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
};
const format = (n: number, word: string) => {
n = Math.round(n);
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
}
;
d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000);
if (d < 60) {
return format(d, 'second');
@ -259,78 +289,89 @@ const padutils = {
}
d /= 24;
return format(d, 'day');
},
makeAnimationScheduler: (funcToAnimateOneStep, stepTime, stepsAtOnce) => {
if (stepsAtOnce === undefined) {
stepsAtOnce = 1;
}
makeAnimationScheduler =
(funcToAnimateOneStep: any, stepTime: number, stepsAtOnce?: number) => {
if (stepsAtOnce === undefined) {
stepsAtOnce = 1;
}
let animationTimer: any = null;
const scheduleAnimation = () => {
if (!animationTimer) {
animationTimer = window.setTimeout(() => {
animationTimer = null;
let n = stepsAtOnce;
let moreToDo = true;
while (moreToDo && n > 0) {
moreToDo = funcToAnimateOneStep();
n--;
}
if (moreToDo) {
// more to do
scheduleAnimation();
}
}, stepTime * stepsAtOnce);
}
};
return {scheduleAnimation};
}
let animationTimer = null;
makeFieldLabeledWhenEmpty
=
(field: JQueryNode, labelText: string) => {
field = $(field);
const scheduleAnimation = () => {
if (!animationTimer) {
animationTimer = window.setTimeout(() => {
animationTimer = null;
let n = stepsAtOnce;
let moreToDo = true;
while (moreToDo && n > 0) {
moreToDo = funcToAnimateOneStep();
n--;
}
if (moreToDo) {
// more to do
scheduleAnimation();
}
}, stepTime * stepsAtOnce);
}
};
return {scheduleAnimation};
},
makeFieldLabeledWhenEmpty: (field, labelText) => {
field = $(field);
const clear = () => {
field.addClass('editempty');
field.val(labelText);
};
field.focus(() => {
if (field.hasClass('editempty')) {
field.val('');
}
field.removeClass('editempty');
});
field.on('blur', () => {
if (!field.val()) {
clear();
}
});
return {
clear,
};
},
getCheckbox: (node) => $(node).is(':checked'),
setCheckbox: (node, value) => {
if (value) {
$(node).attr('checked', 'checked');
} else {
$(node).prop('checked', false);
const clear = () => {
field.addClass('editempty');
field.val(labelText);
}
;
field.focus(() => {
if (field.hasClass('editempty')) {
field.val('');
}
field.removeClass('editempty');
});
field.on('blur', () => {
if (!field.val()) {
clear();
}
});
return {
clear,
};
}
},
bindCheckboxChange: (node, func) => {
$(node).on('change', func);
},
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-';
return `z${c.charCodeAt(0)}z`;
}),
decodeUserId: (encodedUserId) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') {
return String.fromCharCode(Number(cc.slice(1, -1)));
} else {
return cc;
getCheckbox = (node: JQueryNode) => $(node).is(':checked')
setCheckbox =
(node: JQueryNode, value: string) => {
if (value) {
$(node).attr('checked', 'checked');
} else {
$(node).prop('checked', false);
}
}
}),
bindCheckboxChange =
(node: JQueryNode, func: Function) => {
// @ts-ignore
$(node).on("change", func);
}
encodeUserId =
(userId: string) => userId.replace(/[^a-y0-9]/g, (c) => {
if (c === '.') return '-';
return `z${c.charCodeAt(0)}z`;
})
decodeUserId =
(encodedUserId: string) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
if (cc === '-') {
return '.';
} else if (cc.charAt(0) === 'z') {
return String.fromCharCode(Number(cc.slice(1, -1)));
} else {
return cc;
}
})
/**
* Returns whether a string has the expected format to be used as a secret token identifying an
* author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648
@ -340,109 +381,109 @@ const padutils = {
* conditional transformation of a token to a database key in a way that does not allow a
* malicious user to impersonate another user).
*/
isValidAuthorToken: (t) => {
isValidAuthorToken = (t: string | object) => {
if (typeof t !== 'string' || !t.startsWith('t.')) return false;
const v = t.slice(2);
return v.length > 0 && base64url.test(v);
},
}
/**
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
* particular author.
*/
generateAuthorToken: () => `t.${randomString()}`,
};
let globalExceptionHandler = null;
padutils.setupGlobalExceptionHandler = () => {
if (globalExceptionHandler == null) {
globalExceptionHandler = (e) => {
let type;
let err;
let msg, url, linenumber;
if (e instanceof ErrorEvent) {
type = 'Uncaught exception';
err = e.error || {};
({message: msg, filename: url, lineno: linenumber} = e);
} else if (e instanceof PromiseRejectionEvent) {
type = 'Unhandled Promise rejection';
err = e.reason || {};
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
} else {
throw new Error(`unknown event: ${e.toString()}`);
}
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
msg = `${err.name}: ${msg}`;
}
const errorId = randomString(20);
let msgAlreadyVisible = false;
$('.gritter-item .error-msg').each(function () {
if ($(this).text() === msg) {
msgAlreadyVisible = true;
generateAuthorToken = () => `t.${randomString()}`
setupGlobalExceptionHandler = () => {
if (this.globalExceptionHandler == null) {
this.globalExceptionHandler = (e: any) => {
let type;
let err;
let msg, url, linenumber;
if (e instanceof ErrorEvent) {
type = 'Uncaught exception';
err = e.error || {};
({message: msg, filename: url, lineno: linenumber} = e);
} else if (e instanceof PromiseRejectionEvent) {
type = 'Unhandled Promise rejection';
err = e.reason || {};
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
} else {
throw new Error(`unknown event: ${e.toString()}`);
}
});
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
msg = `${err.name}: ${msg}`;
}
const errorId = randomString(20);
if (!msgAlreadyVisible) {
const txt = document.createTextNode.bind(document); // Convenience shorthand.
const errorMsg = [
$('<p>')
let msgAlreadyVisible = false;
$('.gritter-item .error-msg').each(function () {
if ($(this).text() === msg) {
msgAlreadyVisible = true;
}
});
if (!msgAlreadyVisible) {
const txt = document.createTextNode.bind(document); // Convenience shorthand.
const errorMsg = [
$('<p>')
.append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')),
$('<p>')
$('<p>')
.text('If the problem persists, please send this error message to your webmaster:'),
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
.append($('<b>').addClass('error-msg').text(msg)).append($('<br>'))
.append(txt(`at ${url} at line ${linenumber}`)).append($('<br>'))
.append(txt(`ErrorId: ${errorId}`)).append($('<br>'))
.append(txt(type)).append($('<br>'))
.append(txt(`URL: ${window.location.href}`)).append($('<br>'))
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
];
];
$.gritter.add({
title: 'An error occurred',
text: errorMsg,
class_name: 'error',
position: 'bottom',
sticky: true,
// @ts-ignore
$.gritter.add({
title: 'An error occurred',
text: errorMsg,
class_name: 'error',
position: 'bottom',
sticky: true,
});
}
// send javascript errors to the server
$.post('../jserror', {
errorInfo: JSON.stringify({
errorId,
type,
msg,
url: window.location.href,
source: url,
linenumber,
userAgent: navigator.userAgent,
stack: err.stack,
}),
});
}
// send javascript errors to the server
$.post('../jserror', {
errorInfo: JSON.stringify({
errorId,
type,
msg,
url: window.location.href,
source: url,
linenumber,
userAgent: navigator.userAgent,
stack: err.stack,
}),
});
};
window.onerror = null; // Clear any pre-existing global error handler.
window.addEventListener('error', globalExceptionHandler);
window.addEventListener('unhandledrejection', globalExceptionHandler);
};
window.onerror = null; // Clear any pre-existing global error handler.
window.addEventListener('error', this.globalExceptionHandler);
window.addEventListener('unhandledrejection', this.globalExceptionHandler);
}
}
};
padutils.binarySearch = require('./ace2_common').binarySearch;
binarySearch = require('./ace2_common').binarySearch
}
// https://stackoverflow.com/a/42660748
const inThirdPartyIframe = () => {
try {
return (!window.top.location.hostname);
return (!window.top!.location.hostname);
} catch (e) {
return true;
}
};
export let Cookies: CookiesStatic<string>
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
// window object.
if (typeof window !== 'undefined') {
exports.Cookies = require('js-cookie').withAttributes({
Cookies = jsCookie.withAttributes({
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
// because the cookies are third-party (not same-site). Many browsers/users block third-party
@ -455,5 +496,5 @@ if (typeof window !== 'undefined') {
secure: window.location.protocol === 'https:',
});
}
exports.randomString = randomString;
exports.padutils = padutils;
export const padUtils = new PadUtils()

View file

@ -1,5 +0,0 @@
'use strict';
// Provides a require'able version of jQuery without leaking $ and jQuery;
window.$ = require('./vendors/jquery');
const jq = window.$.noConflict(true);
exports.jQuery = exports.$ = jq;

View file

@ -1,19 +0,0 @@
'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');

View file

@ -1,5 +1,3 @@
'use strict';
// Specific hash to display the skin variants builder popup
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
$('#skin-variants').addClass('popup-show');
@ -22,7 +20,7 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); });
const newClasses = [];
const newClasses:string[] = [];
$('select.skin-variant-color').each(function () {
newClasses.push(`${$(this).val()}-${$(this).data('container')}`);
});
@ -35,7 +33,8 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
// run on init
const updateCheckboxFromSkinClasses = () => {
$('html').attr('class').split(' ').forEach((classItem) => {
const htmlTag = $('html')
htmlTag.attr('class')!.split(' ').forEach((classItem) => {
const container = classItem.substring(classItem.lastIndexOf('-') + 1, classItem.length);
if (containers.indexOf(container) > -1) {
const color = classItem.substring(0, classItem.lastIndexOf('-'));
@ -43,7 +42,7 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
}
});
$('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor'));
$('#skin-variant-full-width').prop('checked', htmlTag.hasClass('full-width-editor'));
};
$('.skin-variant').on('change', () => {

View file

@ -22,10 +22,24 @@
* limitations under the License.
*/
const _entryWidth = (e) => (e && e.width) || 0;
const _entryWidth = (e: Entry) => (e && e.width) || 0;
type Entry = {
key: string,
value: string
width: number
}
class Node {
constructor(entry, levels = 0, downSkips = 1, downSkipWidths = 0) {
public key: string|null
readonly entry: Entry|null
levels: number
upPtrs: Node[]
downPtrs: Node[]
downSkips: number[]
readonly downSkipWidths: number[]
constructor(entry: Entry|null, levels = 0, downSkips: number|null = 1, downSkipWidths:number|null = 0) {
this.key = entry != null ? entry.key : null;
this.entry = entry;
this.levels = levels;
@ -37,9 +51,9 @@ class Node {
propagateWidthChange() {
const oldWidth = this.downSkipWidths[0];
const newWidth = _entryWidth(this.entry);
const newWidth = _entryWidth(this.entry!);
const widthChange = newWidth - oldWidth;
let n = this;
let n: Node = this;
let lvl = 0;
while (lvl < n.levels) {
n.downSkipWidths[lvl] += widthChange;
@ -57,17 +71,23 @@ class Node {
// is still valid and points to the same index in the skiplist. Other operations with other points
// invalidate this point.
class Point {
constructor(skipList, loc) {
this._skipList = skipList;
private skipList: SkipList
private readonly loc: number
private readonly idxs: number[]
private readonly nodes: Node[]
private widthSkips: number[]
constructor(skipList: SkipList, loc: number) {
this.skipList = skipList;
this.loc = loc;
const numLevels = this._skipList._start.levels;
const numLevels = this.skipList.start.levels;
let lvl = numLevels - 1;
let i = -1;
let ws = 0;
const nodes = new Array(numLevels);
const idxs = new Array(numLevels);
const widthSkips = new Array(numLevels);
nodes[lvl] = this._skipList._start;
const nodes: Node[] = new Array(numLevels);
const idxs: number[] = new Array(numLevels);
const widthSkips: number[] = new Array(numLevels);
nodes[lvl] = this.skipList.start;
idxs[lvl] = -1;
widthSkips[lvl] = 0;
while (lvl >= 0) {
@ -94,9 +114,9 @@ class Point {
return `Point(${this.loc})`;
}
insert(entry) {
insert(entry: Entry) {
if (entry.key == null) throw new Error('entry.key must not be null');
if (this._skipList.containsKey(entry.key)) {
if (this.skipList.containsKey(entry.key)) {
throw new Error(`an entry with key ${entry.key} already exists`);
}
@ -115,14 +135,14 @@ class Point {
if (lvl === pNodes.length) {
// assume we have just passed the end of this.nodes, and reached one level greater
// than the skiplist currently supports
pNodes[lvl] = this._skipList._start;
pNodes[lvl] = this.skipList.start;
pIdxs[lvl] = -1;
this._skipList._start.levels++;
this._skipList._end.levels++;
this._skipList._start.downPtrs[lvl] = this._skipList._end;
this._skipList._end.upPtrs[lvl] = this._skipList._start;
this._skipList._start.downSkips[lvl] = this._skipList._keyToNodeMap.size + 1;
this._skipList._start.downSkipWidths[lvl] = this._skipList._totalWidth;
this.skipList.start.levels++;
this.skipList.end.levels++;
this.skipList.start.downPtrs[lvl] = this.skipList.end;
this.skipList.end.upPtrs[lvl] = this.skipList.start;
this.skipList.start.downSkips[lvl] = this.skipList.keyToNodeMap.size + 1;
this.skipList.start.downSkipWidths[lvl] = this.skipList.totalWidth;
this.widthSkips[lvl] = 0;
}
const me = newNode;
@ -146,13 +166,13 @@ class Point {
up.downSkips[lvl]++;
up.downSkipWidths[lvl] += newWidth;
}
this._skipList._keyToNodeMap.set(newNode.key, newNode);
this._skipList._totalWidth += newWidth;
this.skipList.keyToNodeMap.set(newNode.key as string, newNode);
this.skipList.totalWidth += newWidth;
}
delete() {
const elem = this.nodes[0].downPtrs[0];
const elemWidth = _entryWidth(elem.entry);
const elemWidth = _entryWidth(elem.entry!);
for (let i = 0; i < this.nodes.length; i++) {
if (i < elem.levels) {
const up = elem.upPtrs[i];
@ -169,8 +189,8 @@ class Point {
up.downSkipWidths[i] -= elemWidth;
}
}
this._skipList._keyToNodeMap.delete(elem.key);
this._skipList._totalWidth -= elemWidth;
this.skipList.keyToNodeMap.delete(elem.key as string);
this.skipList.totalWidth -= elemWidth;
}
getNode() {
@ -183,20 +203,26 @@ class Point {
* property that is a string.
*/
class SkipList {
start: Node
end: Node
totalWidth: number
keyToNodeMap: Map<string, Node>
constructor() {
// if there are N elements in the skiplist, "start" is element -1 and "end" is element N
this._start = new Node(null, 1);
this._end = new Node(null, 1, null, null);
this._totalWidth = 0;
this._keyToNodeMap = new Map();
this._start.downPtrs[0] = this._end;
this._end.upPtrs[0] = this._start;
this.start = new Node(null, 1);
this.end = new Node(null, 1, null, null);
this.totalWidth = 0;
this.keyToNodeMap = new Map();
this.start.downPtrs[0] = this.end;
this.end.upPtrs[0] = this.start;
}
_getNodeAtOffset(targetOffset) {
_getNodeAtOffset(targetOffset: number) {
let i = 0;
let n = this._start;
let lvl = this._start.levels - 1;
let n = this.start;
let lvl = this.start.levels - 1;
while (lvl >= 0 && n.downPtrs[lvl]) {
while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) {
i += n.downSkipWidths[lvl];
@ -204,17 +230,17 @@ class SkipList {
}
lvl--;
}
if (n === this._start) return (this._start.downPtrs[0] || null);
if (n === this._end) {
return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : 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) {
_getNodeIndex(node: Node, byWidth?: boolean) {
let dist = (byWidth ? 0 : -1);
let n = node;
while (n !== this._start) {
while (n !== this.start) {
const lvl = n.levels - 1;
n = n.upPtrs[lvl];
if (byWidth) dist += n.downSkipWidths[lvl];
@ -226,14 +252,14 @@ class SkipList {
// 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.
search(entryFunc) {
let low = this._start;
let lvl = this._start.levels - 1;
search(entryFunc: Function) {
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;
const f = (node: Node) => {
if (node === this.start) return false;
else if (node === this.end) return true;
else return entryFunc(node.entry);
};
@ -249,20 +275,20 @@ class SkipList {
return lowIndex + 1;
}
length() { return this._keyToNodeMap.size; }
length() { return this.keyToNodeMap.size; }
atIndex(i) {
atIndex(i: number) {
if (i < 0) console.warn(`atIndex(${i})`);
if (i >= this._keyToNodeMap.size) console.warn(`atIndex(${i}>=${this._keyToNodeMap.size})`);
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) {
splice(start: number, deleteCount: number, newEntryArray: Entry[]) {
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);
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();
}
@ -275,56 +301,55 @@ class SkipList {
}
}
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]); }
next(entry: Entry) { return this.keyToNodeMap.get(entry.key)!.downPtrs[0].entry || null; }
prev(entry: Entry) { return this.keyToNodeMap.get(entry.key)!.upPtrs[0].entry || null; }
push(entry: Entry) { this.splice(this.keyToNodeMap.size, 0, [entry]); }
slice(start, end) {
slice(start: number, end: number) {
// 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;
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 (start > this.keyToNodeMap.size) start = this.keyToNodeMap.size;
if (end < 0) end = 0;
if (end > this._keyToNodeMap.size) end = this._keyToNodeMap.size;
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++) {
n = this.next(n);
n = this.next(n!);
array.push(n);
}
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); }
containsKey(key) { return this._keyToNodeMap.has(key); }
atKey(key: string) { return this.keyToNodeMap.get(key)!.entry; }
indexOfKey(key: string) { return this._getNodeIndex(this.keyToNodeMap.get(key)!); }
indexOfEntry(entry: Entry) { return this.indexOfKey(entry.key); }
containsKey(key: string) { return this.keyToNodeMap.has(key); }
// gets the last entry starting at or before the offset
atOffset(offset) { return this._getNodeAtOffset(offset).entry; }
keyAtOffset(offset) { return this.atOffset(offset).key; }
offsetOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key), true); }
offsetOfEntry(entry) { return this.offsetOfKey(entry.key); }
setEntryWidth(entry, width) {
atOffset(offset: number) { return this._getNodeAtOffset(offset)!.entry; }
keyAtOffset(offset: number) { return this.atOffset(offset)!.key; }
offsetOfKey(key: string) { return this._getNodeIndex(this.keyToNodeMap.get(key)!, true); }
offsetOfEntry(entry: Entry) { return this.offsetOfKey(entry.key); }
setEntryWidth(entry: Entry, width: number) {
entry.width = width;
this._totalWidth += this._keyToNodeMap.get(entry.key).propagateWidthChange();
this.totalWidth += this.keyToNodeMap.get(entry.key)!.propagateWidthChange();
}
totalWidth() { return this._totalWidth; }
offsetOfIndex(i) {
offsetOfIndex(i: number) {
if (i < 0) return 0;
if (i >= this._keyToNodeMap.size) return this._totalWidth;
return this.offsetOfEntry(this.atIndex(i));
if (i >= this.keyToNodeMap.size) return this.totalWidth;
return this.offsetOfEntry(this.atIndex(i)!);
}
indexOfOffset(offset) {
indexOfOffset(offset: number) {
if (offset <= 0) return 0;
if (offset >= this._totalWidth) return this._keyToNodeMap.size;
return this.indexOfEntry(this.atOffset(offset));
if (offset >= this.totalWidth) return this.keyToNodeMap.size;
return this.indexOfEntry(this.atOffset(offset)!);
}
}
module.exports = SkipList;
export default SkipList

View file

@ -1,4 +1,5 @@
import io from 'socket.io-client';
import {Socket} from "socket.io";
/**
* Creates a socket.io connection.
@ -9,14 +10,14 @@ import io from 'socket.io-client';
* https://socket.io/docs/v2/client-api/#new-Manager-url-options
* @return socket.io Socket object
*/
const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
const connect = (etherpadBaseUrl: string, namespace = '/', options = {}): Socket => {
// The API for socket.io's io() function is awkward. The documentation says that the first
// argument is a URL, but it is not the URL of the socket.io endpoint. The URL's path part is used
// as the name of the socket.io namespace to join, and the rest of the URL (including query
// parameters, if present) is combined with the `path` option (which defaults to '/socket.io', but
// is overridden here to allow users to host Etherpad at something like '/etherpad') to get the
// URL of the socket.io endpoint.
const baseUrl = new URL(etherpadBaseUrl, window.location);
const baseUrl = new URL(etherpadBaseUrl, window.location.href);
const socketioUrl = new URL('socket.io', baseUrl);
const namespaceUrl = new URL(namespace, new URL('/', baseUrl));
@ -27,7 +28,7 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
};
socketOptions = Object.assign(options, socketOptions);
const socket = io(namespaceUrl.href, socketOptions);
const socket = io(namespaceUrl.href, socketOptions) as unknown as Socket;
socket.on('connect_error', (error) => {
console.log('Error connecting to pad', error);
@ -41,8 +42,8 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
return socket;
};
if (typeof exports === 'object') {
exports.connect = connect;
} else {
window.socketio = {connect};
}
export default connect
// @ts-ignore
window.socketio = {connect};

View file

@ -24,20 +24,28 @@
// 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;
import {Cookies} from "./pad_utils";
import {randomString, padUtils as padutils} from "./pad_utils";
const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils;
const socketio = require('./socketio');
import connect from './socketio'
import html10n from '../js/vendors/html10n'
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
import {Socket} from "socket.io";
import {ClientVarMessage, SocketIOMessage} from "./types/SocketIOMessage";
import {Func} from "mocha";
const init = () => {
type ChangeSetLoader = {
handleMessageFromServer(msg: ClientVarMessage): void
}
export let token: string, padId: string, exportLinks: JQuery<HTMLElement>, socket: Socket<any, any>, changesetLoader: ChangeSetLoader, BroadcastSlider: any;
export const init = () => {
padutils.setupGlobalExceptionHandler();
$(document).ready(() => {
// start the custom js
// @ts-ignore
if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef
// get the padId out of the url
@ -48,13 +56,13 @@ const init = () => {
document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`;
// ensure we have a token
token = Cookies.get('token');
token = Cookies.get('token')!;
if (token == null) {
token = `t.${randomString()}`;
Cookies.set('token', token, {expires: 60});
}
socket = socketio.connect(exports.baseURL, '/', {query: {padId}});
socket = connect(baseURL, '/', {query: {padId}});
// send the ready message once we're connected
socket.on('connect', () => {
@ -65,11 +73,11 @@ const init = () => {
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();
console.log("Disconnected")
});
// route the incoming messages
socket.on('message', (message) => {
socket.on('message', (message: ClientVarMessage) => {
if (message.type === 'CLIENT_VARS') {
handleClientVars(message);
} else if (message.accessStatus) {
@ -85,16 +93,12 @@ const init = () => {
$('button#forcereconnect').on('click', () => {
window.location.reload();
});
exports.socket = socket; // make the socket available
exports.BroadcastSlider = BroadcastSlider; // Make the slider available
hooks.aCallAll('postTimesliderInit');
});
};
// sends a message over the socket
const sendSocketMsg = (type, data) => {
const sendSocketMsg = (type: string, data: Object) => {
socket.emit("message", {
component: 'pad', // FIXME: Remove this stupidity!
type,
@ -105,9 +109,9 @@ const sendSocketMsg = (type, data) => {
});
};
const fireWhenAllScriptsAreLoaded = [];
const fireWhenAllScriptsAreLoaded: Function[] = [];
const handleClientVars = (message) => {
const handleClientVars = (message: ClientVarMessage) => {
// save the client Vars
window.clientVars = message.data;
@ -140,13 +144,15 @@ const handleClientVars = (message) => {
const baseURI = document.location.pathname;
// change export urls when the slider moves
BroadcastSlider.onSlider((revno) => {
BroadcastSlider.onSlider((revno: number) => {
// exportLinks is a jQuery Array, so .each is allowed.
exportLinks.each(function () {
// Modified from regular expression to fix:
// https://github.com/ether/etherpad-lite/issues/4071
// Where a padId that was numeric would create the wrong export link
// @ts-ignore
if (this.href) {
// @ts-ignore
const type = this.href.split('export/')[1];
let href = baseURI.split('timeslider')[0];
href += `${revno}/export/${type}`;
@ -159,7 +165,7 @@ const handleClientVars = (message) => {
for (let i = 0; i < fireWhenAllScriptsAreLoaded.length; i++) {
fireWhenAllScriptsAreLoaded[i]();
}
$('#ui-slider-handle').css('left', $('#ui-slider-bar').width() - 2);
$('#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'));
@ -168,9 +174,13 @@ const handleClientVars = (message) => {
// font family change
$('#viewfontmenu').on('change', function () {
// @ts-ignore
$('#innerdocbody').css('font-family', $(this).val() || '');
});
};
exports.baseURL = '';
exports.init = init;
export let baseURL = ''
export const setBaseURl = (url: string)=>{
baseURL = url
}

View file

@ -0,0 +1 @@
export type Attribute = [string, string]

View file

@ -0,0 +1,7 @@
import {Attribute} from "./Attribute";
import AttributePool from "../AttributePool";
export type ChangeSetBuilder = {
remove: (start: number, end?: number)=>void,
keep: (start: number, end?: number, attribs?: Attribute[], pool?: AttributePool)=>void
}

View file

@ -0,0 +1 @@
export type RangePos = [number, number]

View file

@ -1,13 +1,21 @@
import AttributePool from "../AttributePool";
import {RangePos} from "./RangePos";
export type RepModel = {
lines: {
atIndex: (num: number)=>RepNode,
offsetOfIndex: (range: number)=>number,
search: (filter: (e: RepNode)=>boolean)=>number,
length: ()=>number
length: ()=>number,
totalWidth: ()=>number
}
selStart: RangePos,
selEnd: RangePos,
selFocusAtStart: boolean,
apool: AttributePool,
alines: {
[key:string]: any
}
selStart: number[],
selEnd: number[],
selFocusAtStart: boolean
}
export type Position = {
@ -22,7 +30,8 @@ export type RepNode = {
length: number,
lastChild: RepNode,
offsetHeight: number,
offsetTop: number
offsetTop: number,
text: string
}
export type WindowElementWithScrolling = HTMLIFrameElement & {

View file

@ -0,0 +1,13 @@
export type SocketIOMessage = {
type: string
accessStatus: string
}
export type ClientVarMessage = {
data: {
sessionRefreshInterval: number
}
type: string
accessStatus: string
}

View file

@ -0,0 +1,6 @@
declare global {
interface Window {
clientVars: any;
$: any
}
}

View file

@ -1,3 +0,0 @@
'use strict';
module.exports = require('underscore');

View file

@ -1,285 +0,0 @@
'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 = [];
// two types of stackElements:
// 1) { elementType: UNDOABLE_EVENT, eventType: "anything", [backset: <changeset>,]
// [selStart: <char number>, selEnd: <char number>, selFocusAtStart: <boolean>] }
// 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(
{
elementType: UNDOABLE_EVENT,
eventType: 'bottom',
});
numUndoableEvents = 1;
};
clearStack();
const pushEvent = (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(
{
elementType: EXTERNAL_CHANGE,
changeset: cs,
});
}
};
const _exposeEvent = (nthFromTop) => {
// precond: 0 <= nthFromTop < numUndoableEvents
const targetIndex = stackElements.length - 1 - nthFromTop;
let idx = stackElements.length - 1;
while (idx > targetIndex || stackElements[idx].elementType === EXTERNAL_CHANGE) {
if (stackElements[idx].elementType === EXTERNAL_CHANGE) {
const ex = stackElements[idx];
const un = stackElements[idx - 1];
if (un.backset) {
const excs = ex.changeset;
const unbs = un.backset;
un.backset = Changeset.follow(excs, un.backset, false, getAPool());
ex.changeset = Changeset.follow(unbs, ex.changeset, true, getAPool());
if ((typeof un.selStart) === 'number') {
const newSel = Changeset.characterRangeFollow(excs, un.selStart, un.selEnd);
un.selStart = newSel[0];
un.selEnd = newSel[1];
if (un.selStart === un.selEnd) {
un.selFocusAtStart = false;
}
}
}
stackElements[idx - 1] = ex;
stackElements[idx] = un;
if (idx >= 2 && stackElements[idx - 2].elementType === EXTERNAL_CHANGE) {
ex.changeset =
Changeset.compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
stackElements.splice(idx - 2, 1);
idx--;
}
} 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,
pushEvent,
pushExternalChange,
clearStack,
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;
while (i >= 0 && i < str.length) {
i = str.indexOf(c, i);
if (i >= 0) {
count++;
i++;
}
}
return count;
};
const _opcodeOccurrences = (cs, opcode) => _charOccurrences(Changeset.unpack(cs).ops, opcode);
const _mergeChangesets = (cs1, cs2) => {
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.
// A little weird in that it won't merge "make bold" with "insert char"
// but will merge "make bold and insert char" with "insert char",
// though that isn't expected to come up.
const plusCount1 = _opcodeOccurrences(cs1, '+');
const plusCount2 = _opcodeOccurrences(cs2, '+');
const minusCount1 = _opcodeOccurrences(cs1, '-');
const minusCount2 = _opcodeOccurrences(cs2, '-');
if (plusCount1 === 1 && plusCount2 === 1 && minusCount1 === 0 && minusCount2 === 0) {
const merge = Changeset.compose(cs1, cs2, getAPool());
const plusCount3 = _opcodeOccurrences(merge, '+');
const minusCount3 = _opcodeOccurrences(merge, '-');
if (plusCount3 === 1 && minusCount3 === 0) {
return merge;
}
} 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, '-');
if (plusCount3 === 0 && minusCount3 === 1) {
return merge;
}
}
return null;
};
const reportEvent = (event) => {
const topEvent = stack.getNthFromTop(0);
const applySelectionToTop = () => {
if ((typeof event.selStart) === 'number') {
topEvent.selStart = event.selStart;
topEvent.selEnd = event.selEnd;
topEvent.selFocusAtStart = event.selFocusAtStart;
}
};
if ((!event.backset) || Changeset.isIdentity(event.backset)) {
applySelectionToTop();
} else {
let merged = false;
if (topEvent.eventType === event.eventType) {
const merge = _mergeChangesets(event.backset, topEvent.backset);
if (merge) {
topEvent.backset = merge;
applySelectionToTop();
merged = true;
}
}
if (!merged) {
/*
* Push the event on the undo stack only if it exists, and if it's
* not a "clearauthorship". This disallows undoing the removal of the
* authorship colors, but is a necessary stopgap measure against
* https://github.com/ether/etherpad-lite/issues/2802
*/
if (event && (event.eventType !== 'clearauthorship')) {
stack.pushEvent(event);
}
}
undoPtr = 0;
}
};
const reportExternalChange = (changeset) => {
if (changeset && !Changeset.isIdentity(changeset)) {
stack.pushExternalChange(changeset);
}
};
const _getSelectionInfo = (event) => {
if ((typeof event.selStart) !== 'number') {
return null;
} else {
return {
selStart: event.selStart,
selEnd: event.selEnd,
selFocusAtStart: event.selFocusAtStart,
};
}
};
// 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);
const selectionEvent = stack.getNthFromTop(undoPtr + 1);
const undoEvent = eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.pushEvent(undoEvent);
undoPtr += 2;
} else { eventFunc(); }
};
const performRedo = (eventFunc) => {
if (undoPtr >= 2) {
const backsetEvent = stack.getNthFromTop(0);
const selectionEvent = stack.getNthFromTop(1);
eventFunc(backsetEvent.backset, _getSelectionInfo(selectionEvent));
stack.popEvent();
undoPtr -= 2;
} else { eventFunc(); }
};
const getAPool = () => undoModule.apool;
return {
clearHistory,
reportEvent,
reportExternalChange,
performUndo,
performRedo,
enabled: true,
apool: null,
}; // apool is filled in by caller
})();
exports.undoModule = undoModule;

275
src/static/js/undomodule.ts Normal file
View file

@ -0,0 +1,275 @@
'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
*/
import {RepModel} from "./types/RepModel";
/**
* 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');
import {extend} from 'underscore'
import AttributePool from "./AttributePool";
export let pool: AttributePool|null = null
export const setPool = (poolAssigned: AttributePool)=> {
pool = poolAssigned
}
class Stack {
private numUndoableEvents = 0
private UNDOABLE_EVENT = 'undoableEvent';
private EXTERNAL_CHANGE = 'externalChange';
private stackElements: any[] = []
constructor() {
// two types of stackElements:
// 1) { elementType: UNDOABLE_EVENT, eventType: "anything", [backset: <changeset>,]
// [selStart: <char number>, selEnd: <char number>, selFocusAtStart: <boolean>] }
// 2) { elementType: EXTERNAL_CHANGE, changeset: <changeset> }
// invariant: no two consecutive EXTERNAL_CHANGEs
this.clearStack();
}
clearStack = () => {
this.stackElements.length = 0;
this.stackElements.push(
{
elementType: this.UNDOABLE_EVENT,
eventType: 'bottom',
});
this.numUndoableEvents = 1;
};
pushEvent = (event: string) => {
const e = extend(
{}, event);
e.elementType = this.UNDOABLE_EVENT;
this.stackElements.push(e);
this.numUndoableEvents++;
}
pushExternalChange = (cs: string) => {
const idx = this.stackElements.length - 1;
if (this.stackElements[idx].elementType === this.EXTERNAL_CHANGE) {
this.stackElements[idx].changeset =
Changeset.compose(this.stackElements[idx].changeset, cs, pool);
} else {
this.stackElements.push(
{
elementType: this.EXTERNAL_CHANGE,
changeset: cs,
});
}
}
private exposeEvent = (nthFromTop: number) => {
// precond: 0 <= nthFromTop < numUndoableEvents
const targetIndex = this.stackElements.length - 1 - nthFromTop;
let idx = this.stackElements.length - 1;
while (idx > targetIndex || this.stackElements[idx].elementType === this.EXTERNAL_CHANGE) {
if (this.stackElements[idx].elementType === this.EXTERNAL_CHANGE) {
const ex = this.stackElements[idx];
const un = this.stackElements[idx - 1];
if (un.backset) {
const excs = ex.changeset;
const unbs = un.backset;
un.backset = Changeset.follow(excs, un.backset, false, pool);
ex.changeset = Changeset.follow(unbs, ex.changeset, true, pool);
if ((typeof un.selStart) === 'number') {
const newSel = Changeset.characterRangeFollow(excs, un.selStart, un.selEnd);
un.selStart = newSel[0];
un.selEnd = newSel[1];
if (un.selStart === un.selEnd) {
un.selFocusAtStart = false;
}
}
}
this.stackElements[idx - 1] = ex;
this.stackElements[idx] = un;
if (idx >= 2 && this.stackElements[idx - 2].elementType === this.EXTERNAL_CHANGE) {
ex.changeset =
Changeset.compose(this.stackElements[idx - 2].changeset, ex.changeset, pool);
this.stackElements.splice(idx - 2, 1);
idx--;
}
} else {
idx--;
}
}
}
getNthFromTop = (n: number) => {
// precond: 0 <= n < numEvents()
this.exposeEvent(n);
return this.stackElements[this.stackElements.length - 1 - n];
}
numEvents = () => this.numUndoableEvents;
popEvent = () => {
// precond: numEvents() > 0
this.exposeEvent(0);
this.numUndoableEvents--;
return this.stackElements.pop();
}
}
class UndoModule {
// invariant: stack always has at least one undoable event
private undoPtr = 0
private stack: Stack
public enabled: boolean
private readonly apool: AttributePool|null
constructor() {
this.stack = new Stack()
this.enabled = true
this.apool = null
}
clearHistory = () => {
this.stack.clearStack();
this.undoPtr = 0;
}
private charOccurrences = (str: string, c: string) => {
let i = 0;
let count = 0;
while (i >= 0 && i < str.length) {
i = str.indexOf(c, i);
if (i >= 0) {
count++;
i++;
}
}
return count;
}
private opcodeOccurrences = (cs: string, opcode: string) => this.charOccurrences(Changeset.unpack(cs).ops, opcode)
private mergeChangesets = (cs1: string, cs2:string) => {
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.
// A little weird in that it won't merge "make bold" with "insert char"
// but will merge "make bold and insert char" with "insert char",
// though that isn't expected to come up.
const plusCount1 = this.opcodeOccurrences(cs1, '+');
const plusCount2 = this.opcodeOccurrences(cs2, '+');
const minusCount1 = this.opcodeOccurrences(cs1, '-');
const minusCount2 = this.opcodeOccurrences(cs2, '-');
if (plusCount1 === 1 && plusCount2 === 1 && minusCount1 === 0 && minusCount2 === 0) {
const merge = Changeset.compose(cs1, cs2, this.getAPool());
const plusCount3 = this.opcodeOccurrences(merge, '+');
const minusCount3 = this.opcodeOccurrences(merge, '-');
if (plusCount3 === 1 && minusCount3 === 0) {
return merge;
}
} else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) {
const merge = Changeset.compose(cs1, cs2, this.getAPool());
const plusCount3 = this.opcodeOccurrences(merge, '+');
const minusCount3 = this.opcodeOccurrences(merge, '-');
if (plusCount3 === 0 && minusCount3 === 1) {
return merge;
}
}
return null;
}
reportEvent = (event: any) => {
const topEvent = this.stack.getNthFromTop(0);
const applySelectionToTop = () => {
if ((typeof event.selStart) === 'number') {
topEvent.selStart = event.selStart;
topEvent.selEnd = event.selEnd;
topEvent.selFocusAtStart = event.selFocusAtStart;
}
};
if ((!event.backset) || Changeset.isIdentity(event.backset)) {
applySelectionToTop();
} else {
let merged = false;
if (topEvent.eventType === event.eventType) {
const merge = this.mergeChangesets(event.backset, topEvent.backset);
if (merge) {
topEvent.backset = merge;
applySelectionToTop();
merged = true;
}
}
if (!merged) {
/*
* Push the event on the undo stack only if it exists, and if it's
* not a "clearauthorship". This disallows undoing the removal of the
* authorship colors, but is a necessary stopgap measure against
* https://github.com/ether/etherpad-lite/issues/2802
*/
if (event && (event.eventType !== 'clearauthorship')) {
this.stack.pushEvent(event);
}
}
this.undoPtr = 0;
}
}
reportExternalChange = (changeset: string) => {
if (changeset && !Changeset.isIdentity(changeset)) {
this.stack.pushExternalChange(changeset);
}
}
getSelectionInfo = (event: any) => {
if ((typeof event.selStart) !== 'number') {
return null;
} else {
return {
selStart: event.selStart,
selEnd: event.selEnd,
selFocusAtStart: event.selFocusAtStart,
};
}
}
// 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.
performUndo = (eventFunc: Function) => {
if (this.undoPtr < this.stack.numEvents() - 1) {
const backsetEvent = this.stack.getNthFromTop(this.undoPtr);
const selectionEvent = this.stack.getNthFromTop(this.undoPtr + 1);
const undoEvent = eventFunc(backsetEvent.backset, this.getSelectionInfo(selectionEvent));
this.stack.pushEvent(undoEvent);
this.undoPtr += 2;
} else { eventFunc(); }
}
performRedo = (eventFunc: Function) => {
if (this.undoPtr >= 2) {
const backsetEvent = this.stack.getNthFromTop(0);
const selectionEvent = this.stack.getNthFromTop(1);
eventFunc(backsetEvent.backset, this.getSelectionInfo(selectionEvent));
this.stack.popEvent();
this.undoPtr -= 2;
} else { eventFunc(); }
}
getAPool = () => this.apool;
}
export const undoModule = new UndoModule()

View file

@ -7,13 +7,13 @@
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server
// sends the CLIENT_VARS message.
randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>,
};
}
// Allow other frames to access this frame's modules.
//window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie');
const basePath = new URL('..', window.location.href).pathname;
window.$ = window.jQuery = require('../../src/static/js/rjquery').jQuery;
window.$ = window.jQuery = require('../../src/static/js/vendors/jquery');
window.browser = require('../../src/static/js/vendors/browser');
const pad = require('../../src/static/js/pad');
pad.baseURL = basePath;
@ -25,8 +25,8 @@
window.chat = require('../../src/static/js/chat').chat;
window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar;
window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp;
require('../../src/static/js/skin_variants');
require('../../src/static/js/basic_error_handler')
await import('../../src/static/js/skin_variants')
await import('../../src/static/js/basic_error_handler')
window.plugins.baseURL = basePath;
await window.plugins.update(new Map([

View file

@ -1,41 +0,0 @@
window.$ = window.jQuery = await import('../../src/static/js/rjquery').jQuery;
await import('../../src/static/js/l10n')
window.clientVars = {
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server
// sends the CLIENT_VARS message.
randomVersionString: "7a7bdbad",
};
(async () => {
// Allow other frames to access this frame's modules.
//window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie');
const basePath = new URL('..', window.location.href).pathname;
window.browser = require('../../src/static/js/vendors/browser');
const pad = require('../../src/static/js/pad');
pad.baseURL = basePath;
window.plugins = require('../../src/static/js/pluginfw/client_plugins');
const hooks = require('../../src/static/js/pluginfw/hooks');
// TODO: These globals shouldn't exist.
window.pad = pad.pad;
window.chat = require('../../src/static/js/chat').chat;
window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar;
window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp;
require('../../src/static/js/skin_variants');
require('../../src/static/js/basic_error_handler')
window.plugins.baseURL = basePath;
await window.plugins.update(new Map([
]));
// Mechanism for tests to register hook functions (install fake plugins).
window._postPluginUpdateForTestingDone = false;
if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting();
window._postPluginUpdateForTestingDone = true;
window.pluginDefs = require('../../src/static/js/pluginfw/plugin_defs');
pad.init();
await new Promise((resolve) => $(resolve));
await hooks.aCallAll('documentReady');
})();

View file

@ -1,4 +1,7 @@
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt
import {setBaseURl} from "ep_etherpad-lite/static/js/timeslider";
window.clientVars = {
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the
// server sends the CLIENT_VARS message.
@ -6,15 +9,14 @@ window.clientVars = {
};
let BroadcastSlider;
import * as timeSlider from 'ep_etherpad-lite/static/js/timeslider'
(function () {
const timeSlider = require('ep_etherpad-lite/static/js/timeslider')
const pathComponents = location.pathname.split('/');
// Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL
const baseURL = pathComponents.slice(0,pathComponents.length-3).join('/') + '/';
require('ep_etherpad-lite/static/js/l10n')
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; // Expose jQuery #HACK
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/vendors/jquery'); // Expose jQuery #HACK
require('ep_etherpad-lite/static/js/vendors/gritter')
window.browser = require('ep_etherpad-lite/static/js/vendors/browser');
@ -31,7 +33,7 @@ let BroadcastSlider;
});
const padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
timeSlider.baseURL = baseURL;
setBaseURl(baseURL)
timeSlider.init();
padeditbar.init()
})();

View file

@ -2,7 +2,7 @@
import {MapArrayType} from "../../node/types/MapType";
const AttributePool = require('../../static/js/AttributePool');
import AttributePool from '../../static/js/AttributePool';
const apiHandler = require('../../node/handler/APIHandler');
const assert = require('assert').strict;
const io = require('socket.io-client');

View file

@ -11,7 +11,8 @@
import {APool} from "../../../node/types/PadType";
const AttributePool = require('../../../static/js/AttributePool');
import AttributePool from '../../../static/js/AttributePool'
import {Attribute} from "../../../static/js/types/Attribute";
const Changeset = require('../../../static/js/Changeset');
const assert = require('assert').strict;
const attributes = require('../../../static/js/attributes');
@ -20,7 +21,7 @@ const jsdom = require('jsdom');
// 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 = [
const knownAttribs: Attribute[] = [
['insertorder', 'first'],
['italic', 'true'],
['list', 'bullet1'],
@ -336,7 +337,7 @@ pre
describe(__filename, function () {
for (const tc of testCases) {
describe(tc.description, function () {
let apool: APool;
let apool: AttributePool;
let result: {
lines: string[],
lineAttribs: string[],

View file

@ -1,7 +1,7 @@
'use strict';
const Changeset = require('../../static/js/Changeset');
const AttributePool = require('../../static/js/AttributePool');
import AttributePool from "../../static/js/AttributePool";
const randInt = (maxValue) => Math.floor(Math.random() * maxValue);

View file

@ -1,7 +1,7 @@
'use strict';
const AttributeMap = require('../../../static/js/AttributeMap');
const AttributePool = require('../../../static/js/AttributePool');
import AttributeMap from "../../../static/js/AttributeMap";
import AttributePool from '../../../static/js/AttributePool';
const attributes = require('../../../static/js/attributes');
describe('AttributeMap', function () {

View file

@ -1,6 +1,6 @@
'use strict';
const AttributePool = require('../../../static/js/AttributePool');
import AttributePool from '../../../static/js/AttributePool'
const attributes = require('../../../static/js/attributes');
describe('attributes', function () {

View file

@ -1,7 +1,7 @@
'use strict';
const Changeset = require('../../../static/js/Changeset');
const AttributePool = require('../../../static/js/AttributePool');
import AttributePool from "../../../static/js/AttributePool";
const {randomMultiline, randomTestChangeset} = require('../easysync-helper.js');
describe('easysync-compose', function () {

View file

@ -1,7 +1,7 @@
'use strict';
const Changeset = require('../../../static/js/Changeset');
const AttributePool = require('../../../static/js/AttributePool');
import AttributePool from "../../../static/js/AttributePool";
const {randomMultiline, randomTestChangeset} = require('../easysync-helper.js');
describe('easysync-follow', function () {

View file

@ -1,7 +1,7 @@
'use strict';
const Changeset = require('../../../static/js/Changeset');
const AttributePool = require('../../../static/js/AttributePool');
import AttributePool from '../../../static/js/AttributePool'
const {poolOrArray} = require('../easysync-helper.js');
describe('easysync-mutations', function () {

View file

@ -1,7 +1,7 @@
'use strict';
const Changeset = require('../../../static/js/Changeset');
const AttributePool = require('../../../static/js/AttributePool');
import AttributePool from '../../../static/js/AttributePool'
const {randomMultiline, poolOrArray} = require('../easysync-helper.js');
const {padutils} = require('../../../static/js/pad_utils');

View file

@ -1,6 +1,6 @@
'use strict';
const SkipList = require('ep_etherpad-lite/static/js/skiplist');
import SkipList from "../../../static/js/skiplist";
describe('skiplist.js', function () {
it('rejects null keys', async function () {