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 ## Changeset
```javascript ```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 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 ## Attribute Pool
```javascript ```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 Changesets do not include any attribute keyvalue pairs. Instead, they use

View file

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

8
pnpm-lock.yaml generated
View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ const settings = require('../utils/Settings');
const webaccess = require('../hooks/express/webaccess'); const webaccess = require('../hooks/express/webaccess');
const log4js = require('log4js'); const log4js = require('log4js');
const authLogger = log4js.getLogger('auth'); 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'}); const DENY = Object.freeze({accessStatus: 'deny'});

View file

@ -21,12 +21,12 @@
import {MapArrayType} from "../types/MapType"; import {MapArrayType} from "../types/MapType";
const AttributeMap = require('../../static/js/AttributeMap'); import AttributeMap from '../../static/js/AttributeMap';
const padManager = require('../db/PadManager'); const padManager = require('../db/PadManager');
const Changeset = require('../../static/js/Changeset'); const Changeset = require('../../static/js/Changeset');
const ChatMessage = require('../../static/js/ChatMessage'); const ChatMessage = require('../../static/js/ChatMessage');
const AttributePool = require('../../static/js/AttributePool'); import AttributePool from '../../static/js/AttributePool';
const AttributeManager = require('../../static/js/AttributeManager'); import AttributeManager from '../../static/js/AttributeManager';
const authorManager = require('../db/AuthorManager'); const authorManager = require('../db/AuthorManager');
const {padutils} = require('../../static/js/pad_utils'); const {padutils} = require('../../static/js/pad_utils');
const readOnlyManager = require('../db/ReadOnlyManager'); 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... * 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; const text = atext.text;
// collect char positions of line markers (e.g. bullets) in new atext // collect char positions of line markers (e.g. bullets) in new atext

View file

@ -19,7 +19,8 @@
* limitations under the License. * 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 Changeset = require('../../static/js/Changeset');
const { checkValidRev } = require('./checkValidRev'); const { checkValidRev } = require('./checkValidRev');
@ -51,7 +52,7 @@ type LineModel = {
[id:string]:string|number|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 = {}; const line: LineModel = {};
// identify list // identify list

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,13 @@
'use strict'; 'use strict';
const AttributeMap = require('./AttributeMap'); import AttributeMap from './AttributeMap'
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils'); const ChangesetUtils = require('./ChangesetUtils');
const attributes = require('./attributes'); 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'; const lineMarkerAttribute = 'lmkr';
@ -33,21 +36,20 @@ const lineAttributes = [lineMarkerAttribute, 'list'];
- a SkipList `lines` containing the text lines of the document. - a SkipList `lines` containing the text lines of the document.
*/ */
const AttributeManager = function (rep, applyChangesetCallback) { export class AttributeManager {
this.rep = rep; private readonly rep: RepModel
this.applyChangesetCallback = applyChangesetCallback; private readonly applyChangesetCallback: Function
this.author = ''; 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 constructor(rep: RepModel, applyChangesetCallback: Function) {
// it will be considered as a line marker this.rep = rep;
}; this.applyChangesetCallback = applyChangesetCallback;
this.author = '';
}
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES; applyChangeset(changeset: string) {
AttributeManager.lineAttributes = lineAttributes;
AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({
applyChangeset(changeset) {
if (!this.applyChangesetCallback) return changeset; if (!this.applyChangesetCallback) return changeset;
const cs = changeset.toString(); const cs = changeset.toString();
@ -56,15 +58,15 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
} }
return changeset; return changeset;
}, }
/* /*
Sets attributes on a range Sets attributes on a range
@param start [row, col] tuple pointing to the start of the 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 end [row, col] tuple pointing to the end of the range
@param attribs: an array of attributes @param attribs: an array of attributes
*/ */
setAttributesOnRange(start, end, attribs) { setAttributesOnRange(start: RangePos, end: RangePos, attribs: Attribute[]) {
if (start[0] < 0) throw new RangeError('selection start line number is negative'); if (start[0] < 0) throw new RangeError('selection start line number is negative');
if (start[1] < 0) throw new RangeError('selection start column number is negative'); if (start[1] < 0) throw new RangeError('selection start column number is negative');
if (end[0] < 0) throw new RangeError('selection end line number is negative'); if (end[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])) { if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) {
throw new RangeError('selection ends before it starts'); throw new RangeError('selection ends before it starts');
} }
// instead of applying the attributes to the whole range at once, we need to apply them // 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, // 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 // see https://github.com/ether/etherpad-lite/issues/2772
let allChangesets; let allChangesets;
for (let row = start[0]; row <= end[0]; row++) { for (let row = start[0]; row <= end[0]; row++) {
const [startCol, endCol] = this._findRowRange(row, start, end); const [startCol, endCol] = this.findRowRange(row, start, end);
const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs); const rowChangeset = this.setAttributesOnRangeByLine(row, startCol, endCol, attribs);
// compose changesets of all rows into a single changeset // compose changesets of all rows into a single changeset
// as the range might not be continuous // as the range might not be continuous
// due to the presence of line markers on the rows // due to the presence of line markers on the rows
if (allChangesets) { if (allChangesets) {
allChangesets = Changeset.compose( allChangesets = Changeset.compose(
allChangesets.toString(), rowChangeset.toString(), this.rep.apool); allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
} else { } else {
allChangesets = rowChangeset; allChangesets = rowChangeset;
} }
} }
return this.applyChangeset(allChangesets); 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 < start[0] || row > end[0]) throw new RangeError(`line ${row} not in selection`);
if (row >= this.rep.lines.length()) throw new RangeError(`selected line ${row} does not exist`); if (row >= 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). // Subtract 1 for the end-of-line '\n' (it is never selected).
const lineLength = 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; const markerWidth = this.lineHasMarker(row) ? 1 : 0;
if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`); 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'); if (startCol > endCol) throw new RangeError('selection ends before it starts');
return [startCol, endCol]; return [startCol, endCol];
}, }
/** /**
* Sets attributes on a range, by line * 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 endCol column where range ends (one past the last selected column)
* @param attribs an array of attributes * @param attribs an array of attributes
*/ */
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) { setAttributesOnRangeByLine(row: number, startCol: number, endCol: number, attribs: Attribute[]) {
const builder = Changeset.builder(this.rep.lines.totalWidth()); const builder = Changeset.builder(this.rep.lines.totalWidth);
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]); ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
ChangesetUtils.buildKeepRange( 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; return builder;
}, }
/* /*
Returns if the line already has a line marker Returns if the line already has a line marker
@param lineNum: the number of the line @param lineNum: the number of the line
*/ */
lineHasMarker(lineNum) { lineHasMarker(lineNum: number) {
return lineAttributes.find( return lineAttributes.find(
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined; (attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
}, }
/* /*
Gets a specified attribute on a line Gets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for @param lineNum: the number of the line to set the attribute for
@param attributeKey: the name of the attribute to get, e.g. list @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 // get `attributeName` attribute of first char of line
const aline = this.rep.alines[lineNum]; const aline = this.rep.alines[lineNum];
if (!aline) return ''; if (!aline) return '';
const [op] = Changeset.deserializeOps(aline); const [op] = Changeset.deserializeOps(aline);
if (op == null) return ''; if (op == null) return '';
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || ''; return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
}, }
/* /*
Gets all attributes on a line Gets all attributes on a line
@param lineNum: the number of the line to get the attribute for @param lineNum: the number of the line to get the attribute for
*/ */
getAttributesOnLine(lineNum) { getAttributesOnLine(lineNum: number) {
// get attributes of first char of line // get attributes of first char of line
const aline = this.rep.alines[lineNum]; const aline = this.rep.alines[lineNum];
if (!aline) return []; if (!aline) return [];
const [op] = Changeset.deserializeOps(aline); const [op] = Changeset.deserializeOps(aline);
if (op == null) return []; if (op == null) return [];
return [...attributes.attribsFromString(op.attribs, this.rep.apool)]; return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
}, }
/* /*
Gets a given attribute on a selection Gets a given attribute on a selection
@param attributeName @param attributeName
@param prevChar @param prevChar
returns true or false if an attribute is visible in range returns true or false if an attribute is visible in range
*/ */
getAttributeOnSelection(attributeName, prevChar) { getAttributeOnSelection(attributeName: string, prevChar?: string) {
const rep = this.rep; const rep = this.rep;
if (!(rep.selStart && rep.selEnd)) return; if (!(rep.selStart && rep.selEnd)) return;
// If we're looking for the caret attribute not the selection // 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 withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString();
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`); 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 range is collapsed -> no attribs in range
if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false; if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false;
if (selStart[0] !== selEnd[0]) { // -> More than one line selected if (selStart[0] !== selEnd[0]) { // -> More than one line selected
// from selStart to the end of the first line // from selStart to the end of the first line
let hasAttrib = rangeHasAttrib( 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 all lines in between
for (let n = selStart[0] + 1; n < selEnd[0]; n++) { for (let n = selStart[0] + 1; n < selEnd[0]; n++) {
@ -238,16 +243,17 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
return hasAttrib; return hasAttrib;
}; };
return rangeHasAttrib(rep.selStart, rep.selEnd); return rangeHasAttrib(rep.selStart, rep.selEnd);
}, }
/* /*
Gets all attributes at a position containing line number and column Gets all attributes at a position containing line number and column
@param lineNumber starting with zero @param lineNumber starting with zero
@param column starting with zero @param column starting with zero
returns a list of attributes in the format returns a list of attributes in the format
[ ["key","value"], ["key","value"], ... ] [ ["key","value"], ["key","value"], ... ]
*/ */
getAttributesOnPosition(lineNumber, column) { getAttributesOnPosition(lineNumber: number, column: number) {
// get all attributes of the line // get all attributes of the line
const aline = this.rep.alines[lineNumber]; 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 [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
} }
return []; return [];
}, }
/* /*
Gets all attributes at caret position Gets all attributes at caret position
@ -274,18 +280,18 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
*/ */
getAttributesOnCaret() { getAttributesOnCaret() {
return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]); return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]);
}, }
/* /*
Sets a specified attribute on a line Sets a specified attribute on a line
@param lineNum: the number of the line to set the attribute for @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 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) @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]; 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); const hasMarker = this.lineHasMarker(lineNum);
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0])); ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
@ -305,7 +311,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
} }
return this.applyChangeset(builder); return this.applyChangeset(builder);
}, }
/** /**
* Removes a specified attribute on a line * 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 attributeName the name of the attribute to remove, e.g. list
* @param attributeValue if given only attributes with equal value will be removed * @param attributeValue if given only attributes with equal value will be removed
*/ */
removeAttributeOnLine(lineNum, attributeName, attributeValue) { removeAttributeOnLine(lineNum: number, attributeName: string, attributeValue?: string) {
const builder = Changeset.builder(this.rep.lines.totalWidth()); const builder = Changeset.builder(this.rep.lines.totalWidth);
const hasMarker = this.lineHasMarker(lineNum); const hasMarker = this.lineHasMarker(lineNum);
let found = false; let found = false;
@ -336,34 +342,35 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]); ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1]) 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 we have marker and any of attributes don't need to have marker. we need delete it
if (hasMarker && !countAttribsWithMarker) { if (hasMarker && !countAttribsWithMarker) {
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]); ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
} else { } else {
ChangesetUtils.buildKeepRange( 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); return this.applyChangeset(builder);
}, }
/* /*
Toggles a line attribute for the specified line number 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 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 Otherwise it will be set to the given value
@param lineNum: the number of the line to toggle the attribute for @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 attributeKey: the name of the attribute to toggle, e.g. list
@param attributeValue: the value to pass to the attribute (e.g. indention level) @param attributeValue: the value to pass to the attribute (e.g. indention level)
*/ */
toggleAttributeOnLine(lineNum, attributeName, attributeValue) { toggleAttributeOnLine(lineNum: number, attributeName: string, attributeValue: string) {
return this.getAttributeOnLine(lineNum, attributeName) return this.getAttributeOnLine(lineNum, attributeName)
? this.removeAttributeOnLine(lineNum, attributeName) ? this.removeAttributeOnLine(lineNum, attributeName)
: this.setAttributeOnLine(lineNum, attributeName, attributeValue); : this.setAttributeOnLine(lineNum, attributeName, attributeValue);
}, }
hasAttributeOnSelectionOrCaretPosition(attributeName) {
hasAttributeOnSelectionOrCaretPosition(attributeName: string) {
const hasSelection = ( const hasSelection = (
(this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1]) (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); hasAttrib = this.getAttributeOnSelection(attributeName);
} else { } else {
const attributesOnCaretPosition = this.getAttributesOnCaret(); const attributesOnCaretPosition = this.getAttributesOnCaret();
const allAttribs = [].concat(...attributesOnCaretPosition); // flatten const allAttribs = [].concat(...attributesOnCaretPosition) as string[]; // flatten
hasAttrib = allAttribs.includes(attributeName); hasAttrib = allAttribs.includes(attributeName);
} }
return hasAttrib; return hasAttrib;
}, }
}); }
module.exports = AttributeManager;
export default AttributeManager

View file

@ -1,5 +1,7 @@
'use strict'; 'use strict';
import AttributePool from "./AttributePool";
const attributes = require('./attributes'); 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. * Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
*/ */
class AttributeMap extends Map { class AttributeMap extends Map {
private readonly pool? : AttributePool|null
/** /**
* Converts an attribute string into an AttributeMap. * Converts an attribute string into an AttributeMap.
* *
@ -28,14 +31,14 @@ class AttributeMap extends Map {
* @param {AttributePool} pool - Attribute pool. * @param {AttributePool} pool - Attribute pool.
* @returns {AttributeMap} * @returns {AttributeMap}
*/ */
static fromString(str, pool) { public static fromString(str: string, pool: AttributePool): AttributeMap {
return new AttributeMap(pool).updateFromString(str); return new AttributeMap(pool).updateFromString(str);
} }
/** /**
* @param {AttributePool} pool - Attribute pool. * @param {AttributePool} pool - Attribute pool.
*/ */
constructor(pool) { constructor(pool?: AttributePool|null) {
super(); super();
/** @public */ /** @public */
this.pool = pool; this.pool = pool;
@ -46,10 +49,10 @@ class AttributeMap extends Map {
* @param {string} v - Attribute value. * @param {string} v - Attribute value.
* @returns {AttributeMap} `this` (for chaining). * @returns {AttributeMap} `this` (for chaining).
*/ */
set(k, v) { set(k: string, v: string):this {
k = k == null ? '' : String(k); k = k == null ? '' : String(k);
v = v == null ? '' : String(v); v = v == null ? '' : String(v);
this.pool.putAttrib([k, v]); this.pool!.putAttrib([k, v]);
return super.set(k, v); return super.set(k, v);
} }
@ -63,7 +66,7 @@ class AttributeMap extends Map {
* key is removed from this map (if present). * key is removed from this map (if present).
* @returns {AttributeMap} `this` (for chaining). * @returns {AttributeMap} `this` (for chaining).
*/ */
update(entries, emptyValueIsDelete = false) { update(entries: Iterable<[string, string]>, emptyValueIsDelete: boolean = false): AttributeMap {
for (let [k, v] of entries) { for (let [k, v] of entries) {
k = k == null ? '' : String(k); k = k == null ? '' : String(k);
v = v == null ? '' : String(v); v = v == null ? '' : String(v);
@ -83,9 +86,9 @@ class AttributeMap extends Map {
* key is removed from this map (if present). * key is removed from this map (if present).
* @returns {AttributeMap} `this` (for chaining). * @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); 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. * @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 * Represents an attribute pool, which is a collection of attributes (pairs of key and value
* strings) along with their identifiers (non-negative integers). * strings) along with their identifiers (non-negative integers).
@ -55,6 +57,14 @@
* in the pad. * in the pad.
*/ */
class AttributePool { class AttributePool {
numToAttrib: {
[key: number]: [string, string]
}
private attribToNum: {
[key: number]: [string, string]
}
private nextNum: number
constructor() { constructor() {
/** /**
* Maps an attribute identifier to the attribute's `[key, value]` string pair. * Maps an attribute identifier to the attribute's `[key, value]` string pair.
@ -96,7 +106,10 @@ class AttributePool {
*/ */
clone() { clone() {
const c = new AttributePool(); 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); Object.assign(c.attribToNum, this.attribToNum);
c.nextNum = this.nextNum; c.nextNum = this.nextNum;
return c; return c;
@ -111,15 +124,17 @@ class AttributePool {
* membership in the pool without mutating the pool. * membership in the pool without mutating the pool.
* @returns {number} The attribute's identifier, or -1 if the attribute is not in 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); const str = String(attrib);
if (str in this.attribToNum) { if (str in this.attribToNum) {
// @ts-ignore
return this.attribToNum[str]; return this.attribToNum[str];
} }
if (dontAddIfAbsent) { if (dontAddIfAbsent) {
return -1; return -1;
} }
const num = this.nextNum++; const num = this.nextNum++;
// @ts-ignore
this.attribToNum[str] = num; this.attribToNum[str] = num;
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
return num; return num;
@ -130,7 +145,7 @@ class AttributePool {
* @returns {Attribute} The attribute with the given identifier, or nullish if there is no such * @returns {Attribute} The attribute with the given identifier, or nullish if there is no such
* attribute. * attribute.
*/ */
getAttrib(num) { getAttrib(num: number): Attribute {
const pair = this.numToAttrib[num]; const pair = this.numToAttrib[num];
if (!pair) { if (!pair) {
return pair; return pair;
@ -143,7 +158,7 @@ class AttributePool {
* @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty * @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty
* string. * string.
*/ */
getAttribKey(num) { getAttribKey(num: number): string {
const pair = this.numToAttrib[num]; const pair = this.numToAttrib[num];
if (!pair) return ''; if (!pair) return '';
return pair[0]; return pair[0];
@ -154,7 +169,7 @@ class AttributePool {
* @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty * @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty
* string. * string.
*/ */
getAttribValue(num) { getAttribValue(num: number) {
const pair = this.numToAttrib[num]; const pair = this.numToAttrib[num];
if (!pair) return ''; if (!pair) return '';
return pair[1]; return pair[1];
@ -166,8 +181,8 @@ class AttributePool {
* @param {Function} func - Callback to call with two arguments: key and value. Its return value * @param {Function} func - Callback to call with two arguments: key and value. Its return value
* is ignored. * is ignored.
*/ */
eachAttrib(func) { eachAttrib(func: (k: string, v: string)=>void) {
for (const n of Object.keys(this.numToAttrib)) { for (const n in this.numToAttrib) {
const pair = this.numToAttrib[n]; const pair = this.numToAttrib[n];
func(pair[0], pair[1]); func(pair[0], pair[1]);
} }
@ -196,11 +211,12 @@ class AttributePool {
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared * `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
* state will lead to pool corruption. * state will lead to pool corruption.
*/ */
fromJsonable(obj) { fromJsonable(obj: this) {
this.numToAttrib = obj.numToAttrib; this.numToAttrib = obj.numToAttrib;
this.nextNum = obj.nextNum; this.nextNum = obj.nextNum;
this.attribToNum = {}; this.attribToNum = {};
for (const n of Object.keys(this.numToAttrib)) { for (const n of Object.keys(this.numToAttrib)) {
// @ts-ignore
this.attribToNum[String(this.numToAttrib[n])] = Number(n); this.attribToNum[String(this.numToAttrib[n])] = Number(n);
} }
return this; return this;
@ -213,6 +229,7 @@ class AttributePool {
if (!Number.isInteger(this.nextNum)) throw new Error('nextNum property is not an integer'); 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'); if (this.nextNum < 0) throw new Error('nextNum property is negative');
for (const prop of ['numToAttrib', 'attribToNum']) { for (const prop of ['numToAttrib', 'attribToNum']) {
// @ts-ignore
const obj = this[prop]; const obj = this[prop];
if (obj == null) throw new Error(`${prop} property is null`); if (obj == null) throw new Error(`${prop} property is null`);
if (typeof obj !== 'object') throw new TypeError(`${prop} property is not an object`); 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 (v == null) throw new TypeError(`attrib ${i} value is null`);
if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`); if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`);
const attrStr = String(attr); const attrStr = String(attr);
// @ts-ignore
if (this.attribToNum[attrStr] !== i) throw new Error(`attribToNum for ${attrStr} !== ${i}`); 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 * https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
*/ */
const AttributeMap = require('./AttributeMap'); import AttributeMap from './AttributeMap'
const AttributePool = require('./AttributePool'); import AttributePool from "./AttributePool";
const attributes = require('./attributes'); import {} from './attributes';
const {padutils} = require('./pad_utils'); 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. * 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 * @param {string} msg - Just some message
*/ */
const error = (msg) => { const error = (msg: string) => {
const e = new Error(msg); const e = new Error(msg);
// @ts-ignore
e.easysync = true; e.easysync = true;
throw e; throw e;
}; };
@ -61,96 +67,10 @@ const error = (msg) => {
* @param {string} msg - error message to include in the exception * @param {string} msg - error message to include in the exception
* @type {(b: boolean, msg: string) => asserts b} * @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}`); 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 * 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 * @param {string} cs - String representation of the Changeset
* @returns {number} oldLen property * @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. * 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 * @param {string} cs - String representation of the Changeset
* @returns {number} newLen property * @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. * Parses a string of serialized changeset operations.
@ -187,63 +107,23 @@ exports.newLen = (cs) => exports.unpack(cs).newLen;
* @yields {Op} * @yields {Op}
* @returns {Generator<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. // TODO: Migrate to String.prototype.matchAll() once there is enough browser support.
const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g;
let match; let match;
while ((match = regex.exec(ops)) != null) { while ((match = regex.exec(ops)) != null) {
if (match[5] === '$') return; // Start of the insert operation character bank. if (match[5] === '$') return; // Start of the insert operation character bank.
if (match[5] != null) error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`); if (match[5] != null) error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`);
const op = new Op(match[3]); const opMatch = match[3] as ""|"=" | "+" | "-" | undefined
op.lines = exports.parseNum(match[2] || '0'); const op = new Op(opMatch);
op.chars = exports.parseNum(match[4]); op.lines = parseNum(match[2] || '0');
op.chars = parseNum(match[4]);
op.attribs = match[1]; op.attribs = match[1];
yield op; 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. * 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. * @param {string} opsStr - String encoding of the change operations to perform.
* @returns {OpIter} Operator iterator object. * @returns {OpIter} Operator iterator object.
*/ */
exports.opIterator = (opsStr) => { export const opIterator = (opsStr: string) => {
padutils.warnDeprecated( padutils.warnDeprecated(
'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead'); 'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead');
return new OpIter(opsStr); return new OpIter(opsStr);
@ -263,7 +143,7 @@ exports.opIterator = (opsStr) => {
* *
* @param {Op} op - object to clear * @param {Op} op - object to clear
*/ */
const clearOp = (op) => { export const clearOp = (op: Op) => {
op.opcode = ''; op.opcode = '';
op.chars = 0; op.chars = 0;
op.lines = 0; op.lines = 0;
@ -277,7 +157,7 @@ const clearOp = (op) => {
* @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator. * @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator.
* @returns {Op} * @returns {Op}
*/ */
exports.newOp = (optOpcode) => { export const newOp = (optOpcode:'+'|'-'|'='|'' ): Op => {
padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead'); padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead');
return new Op(optOpcode); return new Op(optOpcode);
}; };
@ -289,7 +169,7 @@ exports.newOp = (optOpcode) => {
* @param {Op} [op2] - dest Op. If not given, a new Op is used. * @param {Op} [op2] - dest Op. If not given, a new Op is used.
* @returns {Op} `op2` * @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. * 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 * (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. * 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. * 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. * 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. * @yields {Op} One or two ops (depending on the presense of newlines) that cover the given text.
* @returns {Generator<Op>} * @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); const op = new Op(opcode);
op.attribs = typeof attribs === 'string' op.attribs = typeof attribs === 'string'
? attribs : new AttributeMap(pool).update(attribs || [], opcode === '+').toString(); ? attribs : new AttributeMap(pool).update(attribs || [], opcode === '+').toString();
@ -336,7 +216,7 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
yield op; yield op;
} else { } else {
op.chars = lastNewlinePos + 1; op.chars = lastNewlinePos + 1;
op.lines = text.match(/\n/g).length; op.lines = text.match(/\n/g)!.length;
yield op; yield op;
const op2 = copyOp(op); const op2 = copyOp(op);
op2.chars = text.length - (lastNewlinePos + 1); 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 * 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 * @param {string} cs - Changeset to check
* @returns {string} the checked Changeset * @returns {string} the checked Changeset
*/ */
exports.checkRep = (cs) => { export const checkRep = (cs: string) => {
const unpacked = exports.unpack(cs); const unpacked = exports.unpack(cs);
const oldLen = unpacked.oldLen; const oldLen = unpacked.oldLen;
const newLen = unpacked.newLen; const newLen = unpacked.newLen;
@ -418,254 +282,6 @@ exports.checkRep = (cs) => {
return 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 * A custom made StringBuffer
* *
@ -674,19 +290,6 @@ exports.stringIterator = (str) => {
* @property {Function} toString - * @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 * @typedef {object} StringArrayLike
* @property {(i: number) => string} get - Returns the line at index `i`. * @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 headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
const headerMatch = headerRegex.exec(cs); const headerMatch = headerRegex.exec(cs);
if ((!headerMatch) || (!headerMatch[0])) error(`Not a changeset: ${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 changeSign = (headerMatch[2] === '>') ? 1 : -1;
const changeMag = exports.parseNum(headerMatch[3]); const changeMag = parseNum(headerMatch[3]);
const newLen = oldLen + changeSign * changeMag; const newLen = oldLen + changeSign * changeMag;
const opsStart = headerMatch[0].length; const opsStart = headerMatch[0].length;
let opsEnd = cs.indexOf('$'); let opsEnd = cs.indexOf('$');
@ -1112,7 +715,7 @@ exports.applyToText = (cs, str) => {
assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`);
const bankIter = exports.stringIterator(unpacked.charBank); const bankIter = exports.stringIterator(unpacked.charBank);
const strIter = exports.stringIterator(str); const strIter = exports.stringIterator(str);
const assem = exports.stringAssembler(); const assem = new StringAssembler();
for (const op of exports.deserializeOps(unpacked.ops)) { for (const op of exports.deserializeOps(unpacked.ops)) {
switch (op.opcode) { switch (op.opcode) {
case '+': case '+':
@ -1177,7 +780,7 @@ exports.mutateTextLines = (cs, lines) => {
* @param {AttributeString} att1 - first attribute string * @param {AttributeString} att1 - first attribute string
* @param {AttributeString} att2 - second attribue string * @param {AttributeString} att2 - second attribue string
* @param {boolean} resultIsMutation - * @param {boolean} resultIsMutation -
* @param {AttributePool} pool - attribute pool * @param {AttributePool.ts} pool - attribute pool
* @returns {string} * @returns {string}
*/ */
exports.composeAttributes = (att1, att2, resultIsMutation, pool) => { 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 * @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. * string or the earlier of two exportss being composed.
* @param {Op} csOp - * @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`. * @returns {Op} The result of applying `csOp` to `attOp`.
*/ */
const slicerZipperFunc = (attOp, csOp, pool) => { const slicerZipperFunc = (attOp, csOp, pool) => {
@ -1272,7 +875,7 @@ const slicerZipperFunc = (attOp, csOp, pool) => {
* *
* @param {string} cs - Changeset * @param {string} cs - Changeset
* @param {string} astr - the attribs string of a AText * @param {string} astr - the attribs string of a AText
* @param {AttributePool} pool - the attibutes pool * @param {AttributePool.ts} pool - the attibutes pool
* @returns {string} * @returns {string}
*/ */
exports.applyToAttribution = (cs, astr, pool) => { exports.applyToAttribution = (cs, astr, pool) => {
@ -1285,7 +888,7 @@ exports.applyToAttribution = (cs, astr, pool) => {
* *
* @param {string} cs - The encoded changeset. * @param {string} cs - The encoded changeset.
* @param {Array<string>} lines - Attribute lines. Modified in place. * @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) => { exports.mutateAttributionLines = (cs, lines, pool) => {
const unpacked = exports.unpack(cs); 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} cs1 - first Changeset
* @param {string} cs2 - second Changeset * @param {string} cs2 - second Changeset
* @param {AttributePool} pool - Attribs pool * @param {AttributePool.ts} pool - Attribs pool
* @returns {string} * @returns {string}
*/ */
exports.compose = (cs1, cs2, pool) => { exports.compose = (cs1, cs2, pool) => {
@ -1466,7 +1069,7 @@ exports.compose = (cs1, cs2, pool) => {
const len3 = unpacked2.newLen; const len3 = unpacked2.newLen;
const bankIter1 = exports.stringIterator(unpacked1.charBank); const bankIter1 = exports.stringIterator(unpacked1.charBank);
const bankIter2 = exports.stringIterator(unpacked2.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 newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => {
const op1code = op1.opcode; const op1code = op1.opcode;
@ -1493,7 +1096,7 @@ exports.compose = (cs1, cs2, pool) => {
* key,value that is already present in the pool. * key,value that is already present in the pool.
* *
* @param {Attribute} attribPair - `[key, value]` pair of strings. * @param {Attribute} attribPair - `[key, value]` pair of strings.
* @param {AttributePool} pool - Attribute pool * @param {AttributePool.ts} pool - Attribute pool
* @returns {Function} * @returns {Function}
*/ */
exports.attributeTester = (attribPair, pool) => { 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 {number} ndel - Number of characters to delete at `start`.
* @param {string} ins - Text to insert at `start` (after deleting `ndel` characters). * @param {string} ins - Text to insert at `start` (after deleting `ndel` characters).
* @param {string} [attribs] - Optional attributes to apply to the inserted text. * @param {string} [attribs] - Optional attributes to apply to the inserted text.
* @param {AttributePool} [pool] - Attribute pool. * @param {AttributePool.ts} [pool] - Attribute pool.
* @returns {string} * @returns {string}
*/ */
exports.makeSplice = (orig, start, ndel, ins, attribs, pool) => { exports.makeSplice = (orig, start, ndel, ins, attribs, pool) => {
@ -1646,13 +1249,13 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => {
const fromDollar = cs.substring(dollarPos); const fromDollar = cs.substring(dollarPos);
// order of attribs stays the same // order of attribs stays the same
return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
const oldNum = exports.parseNum(a); const oldNum = parseNum(a);
const pair = oldPool.getAttrib(oldNum); const pair = oldPool.getAttrib(oldNum);
// The attribute might not be in the old pool if the user is viewing the current revision in the // 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 // timeslider and text is deleted. See: https://github.com/ether/etherpad-lite/issues/3932
if (!pair) return ''; if (!pair) return '';
const newNum = newPool.putAttrib(pair); const newNum = newPool.putAttrib(pair);
return `*${exports.numToString(newNum)}`; return `*${numToString(newNum)}`;
}) + fromDollar; }) + fromDollar;
}; };
@ -1688,7 +1291,7 @@ exports.eachAttribNumber = (cs, func) => {
// WARNING: The following cannot be replaced with a call to `attributes.decodeAttribString()` // 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. // because that function only works on attribute strings, not serialized operations or changesets.
upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => { upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
func(exports.parseNum(a)); func(parseNum(a));
return ''; return '';
}); });
}; };
@ -1719,11 +1322,11 @@ exports.mapAttribNumbers = (cs, func) => {
const upToDollar = cs.substring(0, dollarPos); const upToDollar = cs.substring(0, dollarPos);
const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => { const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => {
const n = func(exports.parseNum(a)); const n = func(parseNum(a));
if (n === true) { if (n === true) {
return s; return s;
} else if ((typeof n) === 'number') { } else if ((typeof n) === 'number') {
return `*${exports.numToString(n)}`; return `*${numToString(n)}`;
} else { } else {
return ''; return '';
} }
@ -1759,7 +1362,7 @@ exports.makeAText = (text, attribs) => ({
* *
* @param {string} cs - Changeset to apply * @param {string} cs - Changeset to apply
* @param {AText} atext - * @param {AText} atext -
* @param {AttributePool} pool - Attribute Pool to add to * @param {AttributePool.ts} pool - Attribute Pool to add to
* @returns {AText} * @returns {AText}
*/ */
exports.applyToAText = (cs, atext, pool) => ({ exports.applyToAText = (cs, atext, pool) => ({
@ -1840,8 +1443,8 @@ exports.appendATextToAssembler = (atext, assem) => {
* Creates a clone of a Changeset and it's APool. * Creates a clone of a Changeset and it's APool.
* *
* @param {string} cs - * @param {string} cs -
* @param {AttributePool} pool - * @param {AttributePool.ts} pool -
* @returns {{translated: string, pool: AttributePool}} * @returns {{translated: string, pool: AttributePool.ts}}
*/ */
exports.prepareForWire = (cs, pool) => { exports.prepareForWire = (cs, pool) => {
const newPool = new AttributePool(); const newPool = new AttributePool();
@ -1880,7 +1483,7 @@ const attribsAttributeValue = (attribs, key, pool) => {
* @deprecated Use an AttributeMap instead. * @deprecated Use an AttributeMap instead.
* @param {Op} op - Op * @param {Op} op - Op
* @param {string} key - string to search for * @param {string} key - string to search for
* @param {AttributePool} pool - attribute pool * @param {AttributePool.ts} pool - attribute pool
* @returns {string} * @returns {string}
*/ */
exports.opAttributeValue = (op, key, pool) => { exports.opAttributeValue = (op, key, pool) => {
@ -1895,7 +1498,7 @@ exports.opAttributeValue = (op, key, pool) => {
* @deprecated Use an AttributeMap instead. * @deprecated Use an AttributeMap instead.
* @param {AttributeString} attribs - Attribute string * @param {AttributeString} attribs - Attribute string
* @param {string} key - string to search for * @param {string} key - string to search for
* @param {AttributePool} pool - attribute pool * @param {AttributePool.ts} pool - attribute pool
* @returns {string} * @returns {string}
*/ */
exports.attribsAttributeValue = (attribs, key, pool) => { exports.attribsAttributeValue = (attribs, key, pool) => {
@ -1922,7 +1525,7 @@ exports.attribsAttributeValue = (attribs, key, pool) => {
exports.builder = (oldLen) => { exports.builder = (oldLen) => {
const assem = exports.smartOpAssembler(); const assem = exports.smartOpAssembler();
const o = new Op(); const o = new Op();
const charBank = exports.stringAssembler(); const charBank = new StringAssembler();
const self = { const self = {
/** /**
@ -1931,7 +1534,7 @@ exports.builder = (oldLen) => {
* character must be a newline. * character must be a newline.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' * @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case). * (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. * attribute key, value pairs.
* @returns {Builder} this * @returns {Builder} this
*/ */
@ -1949,7 +1552,7 @@ exports.builder = (oldLen) => {
* @param {string} text - Text to keep. * @param {string} text - Text to keep.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' * @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case). * (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. * attribute key, value pairs.
* @returns {Builder} this * @returns {Builder} this
*/ */
@ -1962,7 +1565,7 @@ exports.builder = (oldLen) => {
* @param {string} text - Text to insert. * @param {string} text - Text to insert.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...' * @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case). * (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. * attribute key, value pairs.
* @returns {Builder} this * @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 * (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. * 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. * 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. * ignored if `attribs` is an attribute string.
* @returns {AttributeString} * @returns {AttributeString}
*/ */
@ -2163,7 +1766,7 @@ exports.inverse = (cs, lines, alines, pool) => {
const nextText = (numChars) => { const nextText = (numChars) => {
let len = 0; let len = 0;
const assem = exports.stringAssembler(); const assem = new StringAssembler();
const firstString = linesGet(curLine).substring(curChar); const firstString = linesGet(curLine).substring(curChar);
len += firstString.length; len += firstString.length;
assem.append(firstString); assem.append(firstString);
@ -2379,20 +1982,20 @@ const followAttributes = (att1, att2, pool) => {
if (!att1) return att2; if (!att1) return att2;
const atts = new Map(); const atts = new Map();
att2.replace(/\*([0-9a-z]+)/g, (_, a) => { 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); atts.set(key, val);
return ''; return '';
}); });
att1.replace(/\*([0-9a-z]+)/g, (_, a) => { 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); if (atts.has(key) && val <= atts.get(key)) atts.delete(key);
return ''; return '';
}); });
// we've only removed attributes, so they're already sorted // we've only removed attributes, so they're already sorted
const buf = exports.stringAssembler(); const buf = new StringAssembler();
for (const att of atts) { for (const att of atts) {
buf.append('*'); buf.append('*');
buf.append(exports.numToString(pool.putAttrib(att))); buf.append(numToString(pool.putAttrib(att)));
} }
return buf.toString(); return buf.toString();
}; };

View file

@ -5,6 +5,11 @@
* based on a SkipList * 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. * Copyright 2009 Google Inc.
* *
@ -20,7 +25,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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 startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[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 startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[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]); const startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]); builder.keep(startLineOffset, start[0]);
builder.keep(start[1]); 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'; '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 * 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. * Supports serialization to JSON.
*/ */
class ChatMessage { 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 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. // 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. 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; 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; delete obj.userName;
return Object.assign(new ChatMessage(), obj); return Object.assign(new ChatMessage(), obj);
} }
@ -25,7 +36,7 @@ class ChatMessage {
* @param {?string} [authorId] - Initial value of the `authorId` property. * @param {?string} [authorId] - Initial value of the `authorId` property.
* @param {?number} [time] - Initial value of the `time` 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). * The raw text of the user's chat message (before any rendering or processing).
* *
@ -62,11 +73,11 @@ class ChatMessage {
* @type {string} * @type {string}
*/ */
get userId() { get userId() {
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead'); padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
return this.authorId; return this.authorId;
} }
set userId(val) { 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; this.authorId = val;
} }
@ -77,11 +88,11 @@ class ChatMessage {
* @type {string} * @type {string}
*/ */
get userName() { get userName() {
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead'); padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
return this.displayName; return this.displayName;
} }
set userName(val) { 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; this.displayName = val;
} }
@ -89,7 +100,9 @@ class ChatMessage {
// doesn't support authorId and displayName. // doesn't support authorId and displayName.
toJSON() { toJSON() {
const {authorId, displayName, ...obj} = this; const {authorId, displayName, ...obj} = this;
// @ts-ignore
obj.userId = authorId; obj.userId = authorId;
// @ts-ignore
obj.userName = displayName; obj.userName = displayName;
return obj; 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 // requires: undefined
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const makeCSSManager = require('./cssmanager').makeCSSManager;
const pluginUtils = require('./pluginfw/shared'); const pluginUtils = require('./pluginfw/shared');
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner') const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
const debugLog = (...args) => {}; const debugLog = (...args) => {};
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins') 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. // The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari // Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
// errors out unless given an absolute URL for a JavaScript-created element. // errors out unless given an absolute URL for a JavaScript-created element.
@ -298,16 +298,16 @@ const Ace2Editor = function () {
innerWindow.Ace2Inner = ace2_inner; innerWindow.Ace2Inner = ace2_inner;
innerWindow.plugins = cl_plugins; innerWindow.plugins = cl_plugins;
innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery; innerWindow.$ = innerWindow.jQuery = window.$;
debugLog('Ace2Editor.init() waiting for plugins'); debugLog('Ace2Editor.init() waiting for plugins');
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure( /*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
(err) => err != null ? reject(err) : resolve()));*/ (err) => err != null ? reject(err) : resolve()));*/
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
await innerWindow.Ace2Inner.init(info, { await innerWindow.Ace2Inner.init(info, {
inner: makeCSSManager(innerStyle.sheet), inner: new Cssmanager(innerStyle.sheet),
outer: makeCSSManager(outerStyle.sheet), outer: new Cssmanager(outerStyle.sheet),
parent: makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet), parent: new Cssmanager(document.querySelector('style[title="dynamicsyntax"]').sheet),
}); });
debugLog('Ace2Editor.init() Ace2Inner.init() returned'); debugLog('Ace2Editor.init() Ace2Inner.init() returned');
loaded = true; loaded = true;

View file

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

View file

@ -17,6 +17,9 @@
* @typedef {string} AttributeString * @typedef {string} AttributeString
*/ */
import AttributePool from "./AttributePool";
import {Attribute} from "./types/Attribute";
/** /**
* Converts an attribute string into a sequence of attribute identifier numbers. * Converts an attribute string into a sequence of attribute identifier numbers.
* *
@ -28,7 +31,7 @@
* appear in `str`. * appear in `str`.
* @returns {Generator<number>} * @returns {Generator<number>}
*/ */
exports.decodeAttribString = function* (str) { export const decodeAttribString = function* (str: string): Generator<number> {
const re = /\*([0-9a-z]+)|./gy; const re = /\*([0-9a-z]+)|./gy;
let match; let match;
while ((match = re.exec(str)) != null) { 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 (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
if (n < 0) throw new Error(`attribute number is negative: ${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}`); 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. * @param {Iterable<number>} attribNums - Sequence of attribute numbers.
* @returns {AttributeString} * @returns {AttributeString}
*/ */
exports.encodeAttribString = (attribNums) => { export const encodeAttribString = (attribNums: Iterable<number>): string => {
let str = ''; let str = '';
for (const n of attribNums) { for (const n of attribNums) {
checkAttribNum(n); checkAttribNum(n);
@ -67,7 +70,7 @@ exports.encodeAttribString = (attribNums) => {
* @yields {Attribute} The identified attributes, in the same order as `attribNums`. * @yields {Attribute} The identified attributes, in the same order as `attribNums`.
* @returns {Generator<Attribute>} * @returns {Generator<Attribute>}
*/ */
exports.attribsFromNums = function* (attribNums, pool) { export const attribsFromNums = function* (attribNums: Iterable<number>, pool: AttributePool): Generator<Attribute> {
for (const n of attribNums) { for (const n of attribNums) {
checkAttribNum(n); checkAttribNum(n);
const attrib = pool.getAttrib(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. * @yields {number} The attribute number of each attribute in `attribs`, in order.
* @returns {Generator<number>} * @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); 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. * @yields {Attribute} The attributes identified in `str`, in order.
* @returns {Generator<Attribute>} * @returns {Generator<Attribute>}
*/ */
exports.attribsFromString = function* (str, pool) { export const attribsFromString = function* (str: string, pool: AttributePool): Generator<Attribute> {
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool); yield* attribsFromNums(decodeAttribString(str), pool);
}; };
/** /**
@ -116,8 +119,8 @@ exports.attribsFromString = function* (str, pool) {
* @param {AttributePool} pool - Attribute pool. * @param {AttributePool} pool - Attribute pool.
* @returns {AttributeString} * @returns {AttributeString}
*/ */
exports.attribsToString = export const attribsToString =
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool)); (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 * 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. * @param {Attribute[]} attribs - Attributes to sort in place.
* @returns {Attribute[]} `attribs` (for chaining). * @returns {Attribute[]} `attribs` (for chaining).
*/ */
exports.sort = export const sort = (attribs: Attribute[]): Attribute[] => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));

View file

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

View file

@ -6,6 +6,8 @@
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/ */
import {Cssmanager} from "./cssmanager";
/** /**
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
@ -22,14 +24,14 @@
* limitations under the License. * limitations under the License.
*/ */
const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline; const domline = require('./domline').domline;
const AttribPool = require('./AttributePool'); import AttributePool from "./AttributePool";
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const attributes = require('./attributes'); const attributes = require('./attributes');
const linestylefilter = require('./linestylefilter').linestylefilter; import linestylefilter from './linestylefilter'
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;
const _ = require('./underscore'); const _ = require('underscore');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n'; import html10n from './vendors/html10n';
@ -56,7 +58,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text), Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
currentDivs: null, currentDivs: null,
// to be filled in once the dom loads // 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( alines: Changeset.splitAttributionLines(
clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.attribs,
clientVars.collab_client_vars.initialAttributedText.text), clientVars.collab_client_vars.initialAttributedText.text),
@ -389,7 +391,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
setTimeout(() => this.loadFromQueue(), 10); setTimeout(() => this.loadFromQueue(), 10);
}, },
handleResponse: (data, start, granularity, callback) => { 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++) { for (let i = 0; i < data.forwardsChangesets.length; i++) {
const astart = start + i * granularity - 1; // rev -1 is a blank single line 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 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') { if (obj.type === 'NEW_CHANGES') {
const changeset = Changeset.moveOpsToNewPool( 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( let changesetBack = Changeset.inverse(
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool); obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
changesetBack = Changeset.moveOpsToNewPool( 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); loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
} else if (obj.type === 'NEW_AUTHORDATA') { } else if (obj.type === 'NEW_AUTHORDATA') {
@ -465,7 +467,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
BroadcastSlider.onSlider(goToRevisionIfEnabled); 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 authorData = {};
const receiveAuthorData = (newAuthorData) => { const receiveAuthorData = (newAuthorData) => {

View file

@ -23,7 +23,7 @@
// These parameters were global, now they are injected. A reference to the // These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate. // Timeslider controller would probably be more appropriate.
const _ = require('./underscore'); const _ = require('underscore');
const padmodals = require('./pad_modals').padmodals; const padmodals = require('./pad_modals').padmodals;
const colorutils = require('./colorutils').colorutils; const colorutils = require('./colorutils').colorutils;
import html10n from './vendors/html10n'; 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 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 Tinycon = require('tinycon/tinycon');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const padeditor = require('./pad_editor').padeditor; const padeditor = require('./pad_editor').padeditor;

View file

@ -26,7 +26,7 @@
const _MAX_LIST_LEVEL = 16; const _MAX_LIST_LEVEL = 16;
const AttributeMap = require('./AttributeMap'); import AttributeMap from './AttributeMap'
const UNorm = require('unorm'); const UNorm = require('unorm');
const Changeset = require('./Changeset'); const Changeset = require('./Changeset');
const hooks = require('./pluginfw/hooks'); 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: plugins
// requires: undefined // requires: undefined
const Security = require('./security'); const Security = require('security');
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const _ = require('./underscore'); const _ = require('underscore');
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; import {lineAttributeMarker} from "./linestylefilter";
const noop = () => {}; const noop = () => {};

View file

@ -19,6 +19,8 @@
* limitations under the License. * limitations under the License.
*/ */
import {getRandomValues} from 'crypto'
const randomPadName = () => { const randomPadName = () => {
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when // the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
// using the PRNG below // using the PRNG below
@ -28,8 +30,7 @@ const randomPadName = () => {
// make room for 8-bit integer values that span from 0 to 255. // make room for 8-bit integer values that span from 0 to 255.
const randomarray = new Uint8Array(stringLength); const randomarray = new Uint8Array(stringLength);
// use browser's PRNG to generate a "unique" sequence // use browser's PRNG to generate a "unique" sequence
const cryptoObj = window.crypto || window.msCrypto; // for IE 11 getRandomValues(randomarray);
cryptoObj.getRandomValues(randomarray);
let randomstring = ''; let randomstring = '';
for (let i = 0; i < stringLength; i++) { for (let i = 0; i < stringLength; i++) {
// instead of writing "Math.floor(randomarray[i]/256*64)" // instead of writing "Math.floor(randomarray[i]/256*64)"
@ -42,9 +43,9 @@ const randomPadName = () => {
$(() => { $(() => {
$('#go2Name').on('submit', () => { $('#go2Name').on('submit', () => {
const padname = $('#padname').val(); const padname = $('#padname').val() as string;
if (padname.length > 0) { if (padname.length > 0) {
window.location = `p/${encodeURIComponent(padname.trim())}`; window.location.href = `p/${encodeURIComponent(padname.trim())}`;
} else { } else {
alert('Please enter a name'); alert('Please enter a name');
} }
@ -52,10 +53,11 @@ $(() => {
}); });
$('#button').on('click', () => { $('#button').on('click', () => {
window.location = `p/${randomPadName()}`; window.location.href = `p/${randomPadName()}`;
}); });
// start the custom js // start the custom js
// @ts-ignore
if (typeof window.customStart === 'function') window.customStart(); 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 chat = require('./chat').chat;
const getCollabClient = require('./collab_client').getCollabClient; const getCollabClient = require('./collab_client').getCollabClient;
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus; const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
const padcookie = require('./pad_cookie').padcookie; import padcookie from "./pad_cookie";
const padeditbar = require('./pad_editbar').padeditbar; const padeditbar = require('./pad_editbar').padeditbar;
const padeditor = require('./pad_editor').padeditor; const padeditor = require('./pad_editor').padeditor;
const padimpexp = require('./pad_impexp').padimpexp; const padimpexp = require('./pad_impexp').padimpexp;
const padmodals = require('./pad_modals').padmodals; const padmodals = require('./pad_modals').padmodals;
const padsavedrevs = require('./pad_savedrevs'); const padsavedrevs = require('./pad_savedrevs');
const paduserlist = require('./pad_userlist').paduserlist; 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 colorutils = require('./colorutils').colorutils;
const randomString = require('./pad_utils').randomString; const randomString = require('./pad_utils').randomString;
const socketio = require('./socketio'); import connect from './socketio'
const hooks = require('./pluginfw/hooks'); 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 // 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 // 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}, query: {padId},
reconnectionAttempts: 5, reconnectionAttempts: 5,
reconnection: true, reconnection: true,

View file

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

View file

@ -24,7 +24,8 @@
const browser = require('./vendors/browser'); const browser = require('./vendors/browser');
const hooks = require('./pluginfw/hooks'); 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 padeditor = require('./pad_editor').padeditor;
const padsavedrevs = require('./pad_savedrevs'); const padsavedrevs = require('./pad_savedrevs');
const _ = require('underscore'); const _ = require('underscore');

View file

@ -22,8 +22,9 @@
*/ */
const Cookies = require('./pad_utils').Cookies; 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; const Ace2Editor = require('./ace').Ace2Editor;
import html10n from '../js/vendors/html10n' import html10n from '../js/vendors/html10n'

View file

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

View file

@ -22,13 +22,14 @@
* limitations under the License. * 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, * Generates a random String with the given length. Is needed to generate the Author, Group,
* readonly, session Ids * readonly, session Ids
*/ */
const randomString = (len) => { export const randomString = (len?: number) => {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let randomstring = ''; let randomstring = '';
len = len || 20; len = len || 20;
@ -85,13 +86,41 @@ const urlRegex = (() => {
'tel', 'tel',
].join('|')}):`; ].join('|')}):`;
return new RegExp( return new RegExp(
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g'); `(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
})(); })();
// https://stackoverflow.com/a/68957976 // https://stackoverflow.com/a/68957976
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/; 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 * Prints a warning message followed by a stack trace (to make it easier to figure out what code
* is using the deprecated function). * is using the deprecated function).
@ -107,41 +136,41 @@ const padutils = {
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no * @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
* logger is set), with a stack trace appended if available. * logger is set), with a stack trace appended if available.
*/ */
warnDeprecated: (...args) => { warnDeprecated = (...args: any[]) => {
if (padutils.warnDeprecated.disabledForTestingOnly) return; if (this.warnDeprecatedFlags.disabledForTestingOnly) return;
const err = new Error(); const err = new Error();
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated); if (Error.captureStackTrace) Error.captureStackTrace(err, this.warnDeprecated);
err.name = ''; err.name = '';
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam. // Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
if (typeof err.stack === 'string') { if (typeof err.stack === 'string') {
if (padutils.warnDeprecated._rl == null) { if (this.warnDeprecatedFlags._rl == null) {
padutils.warnDeprecated._rl = this.warnDeprecatedFlags._rl =
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000}; {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 now = rl.now();
const prev = rl.prevs.get(err.stack); const prev = rl.prevs.get(err.stack);
if (prev != null && now - prev < rl.period) return; if (prev != null && now - prev < rl.period) return;
rl.prevs.set(err.stack, now); rl.prevs.set(err.stack, now);
} }
if (err.stack) args.push(err.stack); if (err.stack) args.push(err.stack);
(padutils.warnDeprecated.logger || console).warn(...args); (this.warnDeprecatedFlags.logger || console).warn(...args);
}, }
escapeHtml = (x: string) => Security.escapeHTML(String(x))
escapeHtml: (x) => Security.escapeHTML(String(x)), uniqueId = () => {
uniqueId: () => {
const pad = require('./pad').pad; // Sidestep circular dependency const pad = require('./pad').pad; // Sidestep circular dependency
// returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits // returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits
const encodeNum = 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 [ return [
pad.getClientIp(), pad.getClientIp(),
encodeNum(+new Date(), 7), encodeNum(+new Date(), 7),
encodeNum(Math.floor(Math.random() * 1e9), 4), encodeNum(Math.floor(Math.random() * 1e9), 4),
].join('.'); ].join('.');
}, }
// e.g. "Thu Jun 18 2009 13:09" // e.g. "Thu Jun 18 2009 13:09"
simpleDateTime: (date) => { simpleDateTime = (date: string) => {
const d = new Date(+date); // accept either number or date const d = new Date(+date); // accept either number or date
const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()]; const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
const month = ([ const month = ([
@ -162,16 +191,14 @@ const padutils = {
const year = d.getFullYear(); const year = d.getFullYear();
const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`; const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`;
return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`; return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`;
}, }
wordCharRegex,
urlRegex,
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...] // 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) // Copy padutils.urlRegex so that the use of .exec() below (which mutates the RegExp object)
// does not break other concurrent uses of padutils.urlRegex. // 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; urlRegex.lastIndex = 0;
let urls = null; let urls: [number, string][] | null = null;
let execResult; let execResult;
// TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped. // TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped.
while ((execResult = urlRegex.exec(text))) { while ((execResult = urlRegex.exec(text))) {
@ -181,18 +208,19 @@ const padutils = {
urls.push([startIndex, url]); urls.push([startIndex, url]);
} }
return urls; return urls;
}, }
escapeHtmlWithClickableLinks: (text, target) => { escapeHtmlWithClickableLinks = (text: string, target: string) => {
let idx = 0; let idx = 0;
const pieces = []; const pieces = [];
const urls = padutils.findURLs(text); const urls = this.findURLs(text);
const advanceTo = (i) => { const advanceTo = (i: number) => {
if (i > idx) { if (i > idx) {
pieces.push(Security.escapeHTML(text.substring(idx, i))); pieces.push(Security.escapeHTML(text.substring(idx, i)));
idx = i; idx = i;
}
} }
}; ;
if (urls) { if (urls) {
for (let j = 0; j < urls.length; j++) { for (let j = 0; j < urls.length; j++) {
const startIndex = urls[j][0]; const startIndex = urls[j][0];
@ -206,25 +234,25 @@ const padutils = {
// https://mathiasbynens.github.io/rel-noopener/ // https://mathiasbynens.github.io/rel-noopener/
// https://github.com/ether/etherpad-lite/pull/3636 // https://github.com/ether/etherpad-lite/pull/3636
pieces.push( pieces.push(
'<a ', '<a ',
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''), (target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
'href="', 'href="',
Security.escapeHTMLAttribute(href), Security.escapeHTMLAttribute(href),
'" rel="noreferrer noopener">'); '" rel="noreferrer noopener">');
advanceTo(startIndex + href.length); advanceTo(startIndex + href.length);
pieces.push('</a>'); pieces.push('</a>');
} }
} }
advanceTo(text.length); advanceTo(text.length);
return pieces.join(''); 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 // 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. // (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 // 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). // 3.6.10, Chrome 6.0.472, Safari 5.0).
if (onEnter) { if (onEnter) {
node.on('keypress', (evt) => { node.on('keypress', (evt: { which: number; }) => {
if (evt.which === 13) { if (evt.which === 13) {
onEnter(evt); onEnter(evt);
} }
@ -238,13 +266,15 @@ const padutils = {
} }
}); });
} }
}, }
timediff: (d) => {
timediff = (d: number) => {
const pad = require('./pad').pad; // Sidestep circular dependency const pad = require('./pad').pad; // Sidestep circular dependency
const format = (n, word) => { const format = (n: number, word: string) => {
n = Math.round(n); n = Math.round(n);
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`); return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
}; }
;
d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000); d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000);
if (d < 60) { if (d < 60) {
return format(d, 'second'); return format(d, 'second');
@ -259,78 +289,89 @@ const padutils = {
} }
d /= 24; d /= 24;
return format(d, 'day'); return format(d, 'day');
}, }
makeAnimationScheduler: (funcToAnimateOneStep, stepTime, stepsAtOnce) => { makeAnimationScheduler =
if (stepsAtOnce === undefined) { (funcToAnimateOneStep: any, stepTime: number, stepsAtOnce?: number) => {
stepsAtOnce = 1; 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 = () => { const clear = () => {
if (!animationTimer) { field.addClass('editempty');
animationTimer = window.setTimeout(() => { field.val(labelText);
animationTimer = null; }
let n = stepsAtOnce; ;
let moreToDo = true; field.focus(() => {
while (moreToDo && n > 0) { if (field.hasClass('editempty')) {
moreToDo = funcToAnimateOneStep(); field.val('');
n--; }
} field.removeClass('editempty');
if (moreToDo) { });
// more to do field.on('blur', () => {
scheduleAnimation(); if (!field.val()) {
} clear();
}, stepTime * stepsAtOnce); }
} });
}; return {
return {scheduleAnimation}; clear,
}, };
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);
} }
}, getCheckbox = (node: JQueryNode) => $(node).is(':checked')
bindCheckboxChange: (node, func) => { setCheckbox =
$(node).on('change', func); (node: JQueryNode, value: string) => {
}, if (value) {
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => { $(node).attr('checked', 'checked');
if (c === '.') return '-'; } else {
return `z${c.charCodeAt(0)}z`; $(node).prop('checked', false);
}), }
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;
} }
}), 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 * 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 * 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 * conditional transformation of a token to a database key in a way that does not allow a
* malicious user to impersonate another user). * malicious user to impersonate another user).
*/ */
isValidAuthorToken: (t) => { isValidAuthorToken = (t: string | object) => {
if (typeof t !== 'string' || !t.startsWith('t.')) return false; if (typeof t !== 'string' || !t.startsWith('t.')) return false;
const v = t.slice(2); const v = t.slice(2);
return v.length > 0 && base64url.test(v); return v.length > 0 && base64url.test(v);
}, }
/** /**
* Returns a string that can be used in the `token` cookie as a secret that authenticates a * Returns a string that can be used in the `token` cookie as a secret that authenticates a
* particular author. * particular author.
*/ */
generateAuthorToken: () => `t.${randomString()}`, generateAuthorToken = () => `t.${randomString()}`
}; setupGlobalExceptionHandler = () => {
if (this.globalExceptionHandler == null) {
let globalExceptionHandler = null; this.globalExceptionHandler = (e: any) => {
padutils.setupGlobalExceptionHandler = () => { let type;
if (globalExceptionHandler == null) { let err;
globalExceptionHandler = (e) => { let msg, url, linenumber;
let type; if (e instanceof ErrorEvent) {
let err; type = 'Uncaught exception';
let msg, url, linenumber; err = e.error || {};
if (e instanceof ErrorEvent) { ({message: msg, filename: url, lineno: linenumber} = e);
type = 'Uncaught exception'; } else if (e instanceof PromiseRejectionEvent) {
err = e.error || {}; type = 'Unhandled Promise rejection';
({message: msg, filename: url, lineno: linenumber} = e); err = e.reason || {};
} else if (e instanceof PromiseRejectionEvent) { ({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
type = 'Unhandled Promise rejection'; } else {
err = e.reason || {}; throw new Error(`unknown event: ${e.toString()}`);
({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;
} }
}); if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
msg = `${err.name}: ${msg}`;
}
const errorId = randomString(20);
if (!msgAlreadyVisible) { let msgAlreadyVisible = false;
const txt = document.createTextNode.bind(document); // Convenience shorthand. $('.gritter-item .error-msg').each(function () {
const errorMsg = [ if ($(this).text() === msg) {
$('<p>') 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')), .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:'), .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($('<b>').addClass('error-msg').text(msg)).append($('<br>'))
.append(txt(`at ${url} at line ${linenumber}`)).append($('<br>')) .append(txt(`at ${url} at line ${linenumber}`)).append($('<br>'))
.append(txt(`ErrorId: ${errorId}`)).append($('<br>')) .append(txt(`ErrorId: ${errorId}`)).append($('<br>'))
.append(txt(type)).append($('<br>')) .append(txt(type)).append($('<br>'))
.append(txt(`URL: ${window.location.href}`)).append($('<br>')) .append(txt(`URL: ${window.location.href}`)).append($('<br>'))
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')), .append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
]; ];
$.gritter.add({ // @ts-ignore
title: 'An error occurred', $.gritter.add({
text: errorMsg, title: 'An error occurred',
class_name: 'error', text: errorMsg,
position: 'bottom', class_name: 'error',
sticky: true, 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,
}),
}); });
} };
window.onerror = null; // Clear any pre-existing global error handler.
// send javascript errors to the server window.addEventListener('error', this.globalExceptionHandler);
$.post('../jserror', { window.addEventListener('unhandledrejection', this.globalExceptionHandler);
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);
} }
}; binarySearch = require('./ace2_common').binarySearch
}
padutils.binarySearch = require('./ace2_common').binarySearch;
// https://stackoverflow.com/a/42660748 // https://stackoverflow.com/a/42660748
const inThirdPartyIframe = () => { const inThirdPartyIframe = () => {
try { try {
return (!window.top.location.hostname); return (!window.top!.location.hostname);
} catch (e) { } catch (e) {
return true; 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 // This file is included from Node so that it can reuse randomString, but Node doesn't have a global
// window object. // window object.
if (typeof window !== 'undefined') { 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=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 // 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 // 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:', 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 // Specific hash to display the skin variants builder popup
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') { if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
$('#skin-variants').addClass('popup-show'); $('#skin-variants').addClass('popup-show');
@ -22,7 +20,7 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); }); domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); });
const newClasses = []; const newClasses:string[] = [];
$('select.skin-variant-color').each(function () { $('select.skin-variant-color').each(function () {
newClasses.push(`${$(this).val()}-${$(this).data('container')}`); newClasses.push(`${$(this).val()}-${$(this).data('container')}`);
}); });
@ -35,7 +33,8 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
// run on init // run on init
const updateCheckboxFromSkinClasses = () => { 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); const container = classItem.substring(classItem.lastIndexOf('-') + 1, classItem.length);
if (containers.indexOf(container) > -1) { if (containers.indexOf(container) > -1) {
const color = classItem.substring(0, classItem.lastIndexOf('-')); 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', () => { $('.skin-variant').on('change', () => {

View file

@ -22,10 +22,24 @@
* limitations under the License. * 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 { 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.key = entry != null ? entry.key : null;
this.entry = entry; this.entry = entry;
this.levels = levels; this.levels = levels;
@ -37,9 +51,9 @@ class Node {
propagateWidthChange() { propagateWidthChange() {
const oldWidth = this.downSkipWidths[0]; const oldWidth = this.downSkipWidths[0];
const newWidth = _entryWidth(this.entry); const newWidth = _entryWidth(this.entry!);
const widthChange = newWidth - oldWidth; const widthChange = newWidth - oldWidth;
let n = this; let n: Node = this;
let lvl = 0; let lvl = 0;
while (lvl < n.levels) { while (lvl < n.levels) {
n.downSkipWidths[lvl] += widthChange; 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 // is still valid and points to the same index in the skiplist. Other operations with other points
// invalidate this point. // invalidate this point.
class Point { class Point {
constructor(skipList, loc) { private skipList: SkipList
this._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; this.loc = loc;
const numLevels = this._skipList._start.levels; const numLevels = this.skipList.start.levels;
let lvl = numLevels - 1; let lvl = numLevels - 1;
let i = -1; let i = -1;
let ws = 0; let ws = 0;
const nodes = new Array(numLevels); const nodes: Node[] = new Array(numLevels);
const idxs = new Array(numLevels); const idxs: number[] = new Array(numLevels);
const widthSkips = new Array(numLevels); const widthSkips: number[] = new Array(numLevels);
nodes[lvl] = this._skipList._start; nodes[lvl] = this.skipList.start;
idxs[lvl] = -1; idxs[lvl] = -1;
widthSkips[lvl] = 0; widthSkips[lvl] = 0;
while (lvl >= 0) { while (lvl >= 0) {
@ -94,9 +114,9 @@ class Point {
return `Point(${this.loc})`; return `Point(${this.loc})`;
} }
insert(entry) { insert(entry: Entry) {
if (entry.key == null) throw new Error('entry.key must not be null'); if (entry.key == null) throw new Error('entry.key must not be null');
if (this._skipList.containsKey(entry.key)) { if (this.skipList.containsKey(entry.key)) {
throw new Error(`an entry with key ${entry.key} already exists`); throw new Error(`an entry with key ${entry.key} already exists`);
} }
@ -115,14 +135,14 @@ class Point {
if (lvl === pNodes.length) { if (lvl === pNodes.length) {
// assume we have just passed the end of this.nodes, and reached one level greater // assume we have just passed the end of this.nodes, and reached one level greater
// than the skiplist currently supports // than the skiplist currently supports
pNodes[lvl] = this._skipList._start; pNodes[lvl] = this.skipList.start;
pIdxs[lvl] = -1; pIdxs[lvl] = -1;
this._skipList._start.levels++; this.skipList.start.levels++;
this._skipList._end.levels++; this.skipList.end.levels++;
this._skipList._start.downPtrs[lvl] = this._skipList._end; this.skipList.start.downPtrs[lvl] = this.skipList.end;
this._skipList._end.upPtrs[lvl] = this._skipList._start; this.skipList.end.upPtrs[lvl] = this.skipList.start;
this._skipList._start.downSkips[lvl] = this._skipList._keyToNodeMap.size + 1; this.skipList.start.downSkips[lvl] = this.skipList.keyToNodeMap.size + 1;
this._skipList._start.downSkipWidths[lvl] = this._skipList._totalWidth; this.skipList.start.downSkipWidths[lvl] = this.skipList.totalWidth;
this.widthSkips[lvl] = 0; this.widthSkips[lvl] = 0;
} }
const me = newNode; const me = newNode;
@ -146,13 +166,13 @@ class Point {
up.downSkips[lvl]++; up.downSkips[lvl]++;
up.downSkipWidths[lvl] += newWidth; up.downSkipWidths[lvl] += newWidth;
} }
this._skipList._keyToNodeMap.set(newNode.key, newNode); this.skipList.keyToNodeMap.set(newNode.key as string, newNode);
this._skipList._totalWidth += newWidth; this.skipList.totalWidth += newWidth;
} }
delete() { delete() {
const elem = this.nodes[0].downPtrs[0]; 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++) { for (let i = 0; i < this.nodes.length; i++) {
if (i < elem.levels) { if (i < elem.levels) {
const up = elem.upPtrs[i]; const up = elem.upPtrs[i];
@ -169,8 +189,8 @@ class Point {
up.downSkipWidths[i] -= elemWidth; up.downSkipWidths[i] -= elemWidth;
} }
} }
this._skipList._keyToNodeMap.delete(elem.key); this.skipList.keyToNodeMap.delete(elem.key as string);
this._skipList._totalWidth -= elemWidth; this.skipList.totalWidth -= elemWidth;
} }
getNode() { getNode() {
@ -183,20 +203,26 @@ class Point {
* property that is a string. * property that is a string.
*/ */
class SkipList { class SkipList {
start: Node
end: Node
totalWidth: number
keyToNodeMap: Map<string, Node>
constructor() { constructor() {
// if there are N elements in the skiplist, "start" is element -1 and "end" is element N // if there are N elements in the skiplist, "start" is element -1 and "end" is element N
this._start = new Node(null, 1); this.start = new Node(null, 1);
this._end = new Node(null, 1, null, null); this.end = new Node(null, 1, null, null);
this._totalWidth = 0; this.totalWidth = 0;
this._keyToNodeMap = new Map(); this.keyToNodeMap = new Map();
this._start.downPtrs[0] = this._end; this.start.downPtrs[0] = this.end;
this._end.upPtrs[0] = this._start; this.end.upPtrs[0] = this.start;
} }
_getNodeAtOffset(targetOffset) { _getNodeAtOffset(targetOffset: number) {
let i = 0; let i = 0;
let n = this._start; let n = this.start;
let lvl = this._start.levels - 1; let lvl = this.start.levels - 1;
while (lvl >= 0 && n.downPtrs[lvl]) { while (lvl >= 0 && n.downPtrs[lvl]) {
while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) { while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) {
i += n.downSkipWidths[lvl]; i += n.downSkipWidths[lvl];
@ -204,17 +230,17 @@ class SkipList {
} }
lvl--; lvl--;
} }
if (n === this._start) return (this._start.downPtrs[0] || null); if (n === this.start) return (this.start.downPtrs[0] || null);
if (n === this._end) { if (n === this.end) {
return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null; return targetOffset === this.totalWidth ? (this.end.upPtrs[0] || null) : null;
} }
return n; return n;
} }
_getNodeIndex(node, byWidth) { _getNodeIndex(node: Node, byWidth?: boolean) {
let dist = (byWidth ? 0 : -1); let dist = (byWidth ? 0 : -1);
let n = node; let n = node;
while (n !== this._start) { while (n !== this.start) {
const lvl = n.levels - 1; const lvl = n.levels - 1;
n = n.upPtrs[lvl]; n = n.upPtrs[lvl];
if (byWidth) dist += n.downSkipWidths[lvl]; if (byWidth) dist += n.downSkipWidths[lvl];
@ -226,14 +252,14 @@ class SkipList {
// Returns index of first entry such that entryFunc(entry) is truthy, // Returns index of first entry such that entryFunc(entry) is truthy,
// or length() if no such entry. Assumes all falsy entries come before // or length() if no such entry. Assumes all falsy entries come before
// all truthy entries. // all truthy entries.
search(entryFunc) { search(entryFunc: Function) {
let low = this._start; let low = this.start;
let lvl = this._start.levels - 1; let lvl = this.start.levels - 1;
let lowIndex = -1; let lowIndex = -1;
const f = (node) => { const f = (node: Node) => {
if (node === this._start) return false; if (node === this.start) return false;
else if (node === this._end) return true; else if (node === this.end) return true;
else return entryFunc(node.entry); else return entryFunc(node.entry);
}; };
@ -249,20 +275,20 @@ class SkipList {
return lowIndex + 1; 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 < 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; return (new Point(this, i)).getNode().entry;
} }
// differs from Array.splice() in that new elements are in an array, not varargs // 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 < 0) console.warn(`splice(${start}, ...)`);
if (start + deleteCount > this._keyToNodeMap.size) { if (start + deleteCount > this.keyToNodeMap.size) {
console.warn(`splice(${start}, ${deleteCount}, ...), N=${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.warn('%s %s %s', typeof start, typeof deleteCount, typeof this.keyToNodeMap.size);
console.trace(); console.trace();
} }
@ -275,56 +301,55 @@ class SkipList {
} }
} }
next(entry) { return this._keyToNodeMap.get(entry.key).downPtrs[0].entry || null; } next(entry: Entry) { return this.keyToNodeMap.get(entry.key)!.downPtrs[0].entry || null; }
prev(entry) { return this._keyToNodeMap.get(entry.key).upPtrs[0].entry || null; } prev(entry: Entry) { return this.keyToNodeMap.get(entry.key)!.upPtrs[0].entry || null; }
push(entry) { this.splice(this._keyToNodeMap.size, 0, [entry]); } push(entry: Entry) { this.splice(this.keyToNodeMap.size, 0, [entry]); }
slice(start, end) { slice(start: number, end: number) {
// act like Array.slice() // act like Array.slice()
if (start === undefined) start = 0; if (start === undefined) start = 0;
else if (start < 0) start += this._keyToNodeMap.size; else if (start < 0) start += this.keyToNodeMap.size;
if (end === undefined) end = this._keyToNodeMap.size; if (end === undefined) end = this.keyToNodeMap.size;
else if (end < 0) end += this._keyToNodeMap.size; else if (end < 0) end += this.keyToNodeMap.size;
if (start < 0) start = 0; 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 < 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 []; if (end <= start) return [];
let n = this.atIndex(start); let n = this.atIndex(start);
const array = [n]; const array = [n];
for (let i = 1; i < (end - start); i++) { for (let i = 1; i < (end - start); i++) {
n = this.next(n); n = this.next(n!);
array.push(n); array.push(n);
} }
return array; return array;
} }
atKey(key) { return this._keyToNodeMap.get(key).entry; } atKey(key: string) { return this.keyToNodeMap.get(key)!.entry; }
indexOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key)); } indexOfKey(key: string) { return this._getNodeIndex(this.keyToNodeMap.get(key)!); }
indexOfEntry(entry) { return this.indexOfKey(entry.key); } indexOfEntry(entry: Entry) { return this.indexOfKey(entry.key); }
containsKey(key) { return this._keyToNodeMap.has(key); } containsKey(key: string) { return this.keyToNodeMap.has(key); }
// gets the last entry starting at or before the offset // gets the last entry starting at or before the offset
atOffset(offset) { return this._getNodeAtOffset(offset).entry; } atOffset(offset: number) { return this._getNodeAtOffset(offset)!.entry; }
keyAtOffset(offset) { return this.atOffset(offset).key; } keyAtOffset(offset: number) { return this.atOffset(offset)!.key; }
offsetOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key), true); } offsetOfKey(key: string) { return this._getNodeIndex(this.keyToNodeMap.get(key)!, true); }
offsetOfEntry(entry) { return this.offsetOfKey(entry.key); } offsetOfEntry(entry: Entry) { return this.offsetOfKey(entry.key); }
setEntryWidth(entry, width) { setEntryWidth(entry: Entry, width: number) {
entry.width = width; 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: number) {
offsetOfIndex(i) {
if (i < 0) return 0; if (i < 0) return 0;
if (i >= this._keyToNodeMap.size) return this._totalWidth; if (i >= this.keyToNodeMap.size) return this.totalWidth;
return this.offsetOfEntry(this.atIndex(i)); return this.offsetOfEntry(this.atIndex(i)!);
} }
indexOfOffset(offset) { indexOfOffset(offset: number) {
if (offset <= 0) return 0; if (offset <= 0) return 0;
if (offset >= this._totalWidth) return this._keyToNodeMap.size; if (offset >= this.totalWidth) return this.keyToNodeMap.size;
return this.indexOfEntry(this.atOffset(offset)); 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 io from 'socket.io-client';
import {Socket} from "socket.io";
/** /**
* Creates a socket.io connection. * 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 * https://socket.io/docs/v2/client-api/#new-Manager-url-options
* @return socket.io Socket object * @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 // 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 // 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 // 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 // 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 // is overridden here to allow users to host Etherpad at something like '/etherpad') to get the
// URL of the socket.io endpoint. // 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 socketioUrl = new URL('socket.io', baseUrl);
const namespaceUrl = new URL(namespace, new URL('/', baseUrl)); const namespaceUrl = new URL(namespace, new URL('/', baseUrl));
@ -27,7 +28,7 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
}; };
socketOptions = Object.assign(options, socketOptions); 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) => { socket.on('connect_error', (error) => {
console.log('Error connecting to pad', error); console.log('Error connecting to pad', error);
@ -41,8 +42,8 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
return socket; return socket;
}; };
if (typeof exports === 'object') {
exports.connect = connect; export default connect
} else {
window.socketio = {connect}; // @ts-ignore
} window.socketio = {connect};

View file

@ -24,20 +24,28 @@
// These jQuery things should create local references, but for now `require()` // These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins. // assigns to the global `$` and augments it with plugins.
require('./vendors/jquery');
const Cookies = require('./pad_utils').Cookies; import {Cookies} from "./pad_utils";
const randomString = require('./pad_utils').randomString; import {randomString, padUtils as padutils} from "./pad_utils";
const hooks = require('./pluginfw/hooks'); const hooks = require('./pluginfw/hooks');
const padutils = require('./pad_utils').padutils; import connect from './socketio'
const socketio = require('./socketio');
import html10n from '../js/vendors/html10n' 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(); padutils.setupGlobalExceptionHandler();
$(document).ready(() => { $(document).ready(() => {
// start the custom js // start the custom js
// @ts-ignore
if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef
// get the padId out of the url // get the padId out of the url
@ -48,13 +56,13 @@ const init = () => {
document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`; document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`;
// ensure we have a token // ensure we have a token
token = Cookies.get('token'); token = Cookies.get('token')!;
if (token == null) { if (token == null) {
token = `t.${randomString()}`; token = `t.${randomString()}`;
Cookies.set('token', token, {expires: 60}); 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 // send the ready message once we're connected
socket.on('connect', () => { socket.on('connect', () => {
@ -65,11 +73,11 @@ const init = () => {
BroadcastSlider.showReconnectUI(); BroadcastSlider.showReconnectUI();
// The socket.io client will automatically try to reconnect for all reasons other than "io // The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect". // server disconnect".
if (reason === 'io server disconnect') socket.connect(); console.log("Disconnected")
}); });
// route the incoming messages // route the incoming messages
socket.on('message', (message) => { socket.on('message', (message: ClientVarMessage) => {
if (message.type === 'CLIENT_VARS') { if (message.type === 'CLIENT_VARS') {
handleClientVars(message); handleClientVars(message);
} else if (message.accessStatus) { } else if (message.accessStatus) {
@ -85,16 +93,12 @@ const init = () => {
$('button#forcereconnect').on('click', () => { $('button#forcereconnect').on('click', () => {
window.location.reload(); window.location.reload();
}); });
exports.socket = socket; // make the socket available
exports.BroadcastSlider = BroadcastSlider; // Make the slider available
hooks.aCallAll('postTimesliderInit'); hooks.aCallAll('postTimesliderInit');
}); });
}; };
// sends a message over the socket // sends a message over the socket
const sendSocketMsg = (type, data) => { const sendSocketMsg = (type: string, data: Object) => {
socket.emit("message", { socket.emit("message", {
component: 'pad', // FIXME: Remove this stupidity! component: 'pad', // FIXME: Remove this stupidity!
type, 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 // save the client Vars
window.clientVars = message.data; window.clientVars = message.data;
@ -140,13 +144,15 @@ const handleClientVars = (message) => {
const baseURI = document.location.pathname; const baseURI = document.location.pathname;
// change export urls when the slider moves // change export urls when the slider moves
BroadcastSlider.onSlider((revno) => { BroadcastSlider.onSlider((revno: number) => {
// exportLinks is a jQuery Array, so .each is allowed. // exportLinks is a jQuery Array, so .each is allowed.
exportLinks.each(function () { exportLinks.each(function () {
// Modified from regular expression to fix: // Modified from regular expression to fix:
// https://github.com/ether/etherpad-lite/issues/4071 // https://github.com/ether/etherpad-lite/issues/4071
// Where a padId that was numeric would create the wrong export link // Where a padId that was numeric would create the wrong export link
// @ts-ignore
if (this.href) { if (this.href) {
// @ts-ignore
const type = this.href.split('export/')[1]; const type = this.href.split('export/')[1];
let href = baseURI.split('timeslider')[0]; let href = baseURI.split('timeslider')[0];
href += `${revno}/export/${type}`; href += `${revno}/export/${type}`;
@ -159,7 +165,7 @@ const handleClientVars = (message) => {
for (let i = 0; i < fireWhenAllScriptsAreLoaded.length; i++) { for (let i = 0; i < fireWhenAllScriptsAreLoaded.length; i++) {
fireWhenAllScriptsAreLoaded[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 // Translate some strings where we only want to set the title not the actual values
$('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause')); $('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause'));
@ -168,9 +174,13 @@ const handleClientVars = (message) => {
// font family change // font family change
$('#viewfontmenu').on('change', function () { $('#viewfontmenu').on('change', function () {
// @ts-ignore
$('#innerdocbody').css('font-family', $(this).val() || ''); $('#innerdocbody').css('font-family', $(this).val() || '');
}); });
}; };
exports.baseURL = ''; export let baseURL = ''
exports.init = init;
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 = { export type RepModel = {
lines: { lines: {
atIndex: (num: number)=>RepNode, atIndex: (num: number)=>RepNode,
offsetOfIndex: (range: number)=>number, offsetOfIndex: (range: number)=>number,
search: (filter: (e: RepNode)=>boolean)=>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 = { export type Position = {
@ -22,7 +30,8 @@ export type RepNode = {
length: number, length: number,
lastChild: RepNode, lastChild: RepNode,
offsetHeight: number, offsetHeight: number,
offsetTop: number offsetTop: number,
text: string
} }
export type WindowElementWithScrolling = HTMLIFrameElement & { 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 // This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server
// sends the CLIENT_VARS message. // sends the CLIENT_VARS message.
randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>, randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>,
}; }
// Allow other frames to access this frame's modules. // Allow other frames to access this frame's modules.
//window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie'); //window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie');
const basePath = new URL('..', window.location.href).pathname; 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'); window.browser = require('../../src/static/js/vendors/browser');
const pad = require('../../src/static/js/pad'); const pad = require('../../src/static/js/pad');
pad.baseURL = basePath; pad.baseURL = basePath;
@ -25,8 +25,8 @@
window.chat = require('../../src/static/js/chat').chat; window.chat = require('../../src/static/js/chat').chat;
window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar; window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar;
window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp; window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp;
require('../../src/static/js/skin_variants'); await import('../../src/static/js/skin_variants')
require('../../src/static/js/basic_error_handler') await import('../../src/static/js/basic_error_handler')
window.plugins.baseURL = basePath; window.plugins.baseURL = basePath;
await window.plugins.update(new Map([ 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 // @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt
import {setBaseURl} from "ep_etherpad-lite/static/js/timeslider";
window.clientVars = { window.clientVars = {
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the // This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the
// server sends the CLIENT_VARS message. // server sends the CLIENT_VARS message.
@ -6,15 +9,14 @@ window.clientVars = {
}; };
let BroadcastSlider; let BroadcastSlider;
import * as timeSlider from 'ep_etherpad-lite/static/js/timeslider'
(function () { (function () {
const timeSlider = require('ep_etherpad-lite/static/js/timeslider')
const pathComponents = location.pathname.split('/'); const pathComponents = location.pathname.split('/');
// Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL // Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL
const baseURL = pathComponents.slice(0,pathComponents.length-3).join('/') + '/'; const baseURL = pathComponents.slice(0,pathComponents.length-3).join('/') + '/';
require('ep_etherpad-lite/static/js/l10n') 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') require('ep_etherpad-lite/static/js/vendors/gritter')
window.browser = require('ep_etherpad-lite/static/js/vendors/browser'); 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 padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp; const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
timeSlider.baseURL = baseURL; setBaseURl(baseURL)
timeSlider.init(); timeSlider.init();
padeditbar.init() padeditbar.init()
})(); })();

View file

@ -2,7 +2,7 @@
import {MapArrayType} from "../../node/types/MapType"; 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 apiHandler = require('../../node/handler/APIHandler');
const assert = require('assert').strict; const assert = require('assert').strict;
const io = require('socket.io-client'); const io = require('socket.io-client');

View file

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

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const Changeset = require('../../static/js/Changeset'); 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); const randInt = (maxValue) => Math.floor(Math.random() * maxValue);

View file

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

View file

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

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const Changeset = require('../../../static/js/Changeset'); 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'); const {randomMultiline, randomTestChangeset} = require('../easysync-helper.js');
describe('easysync-compose', function () { describe('easysync-compose', function () {

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const Changeset = require('../../../static/js/Changeset'); 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'); const {randomMultiline, randomTestChangeset} = require('../easysync-helper.js');
describe('easysync-follow', function () { describe('easysync-follow', function () {

View file

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

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const Changeset = require('../../../static/js/Changeset'); 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 {randomMultiline, poolOrArray} = require('../easysync-helper.js');
const {padutils} = require('../../../static/js/pad_utils'); const {padutils} = require('../../../static/js/pad_utils');

View file

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