mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Moved more files to typescript
This commit is contained in:
parent
b1139e1aff
commit
d1ffd5d02f
75 changed files with 2079 additions and 1929 deletions
|
@ -7,7 +7,7 @@ provides tools to create, read, and apply changesets.
|
|||
## Changeset
|
||||
|
||||
```javascript
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const Changeset = require('src/static/js/Changeset');
|
||||
```
|
||||
|
||||
A changeset describes the difference between two revisions of a document. When a
|
||||
|
@ -24,7 +24,7 @@ A transmitted changeset looks like this:
|
|||
## Attribute Pool
|
||||
|
||||
```javascript
|
||||
const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
|
||||
const AttributePool = require('src/static/js/AttributePool');
|
||||
```
|
||||
|
||||
Changesets do not include any attribute key–value pairs. Instead, they use
|
||||
|
|
|
@ -825,16 +825,16 @@ Context properties:
|
|||
Example:
|
||||
|
||||
```javascript
|
||||
const AttributeMap = require('ep_etherpad-lite/static/js/AttributeMap');
|
||||
const Changeset = require('ep_etherpad-lite/static/js/Changeset');
|
||||
const AttributeMap = require('src/static/js/AttributeMap');
|
||||
const Changeset = require('src/static/js/Changeset');
|
||||
|
||||
exports.getLineHTMLForExport = async (hookName, context) => {
|
||||
if (!context.attribLine) return;
|
||||
const [op] = Changeset.deserializeOps(context.attribLine);
|
||||
if (op == null) return;
|
||||
const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading');
|
||||
if (!heading) return;
|
||||
context.lineContent = `<${heading}>${context.lineContent}</${heading}>`;
|
||||
if (!context.attribLine) return;
|
||||
const [op] = Changeset.deserializeOps(context.attribLine);
|
||||
if (op == null) return;
|
||||
const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading');
|
||||
if (!heading) return;
|
||||
context.lineContent = `<${heading}>${context.lineContent}</${heading}>`;
|
||||
};
|
||||
```
|
||||
|
||||
|
|
|
@ -294,6 +294,9 @@ importers:
|
|||
'@types/jquery':
|
||||
specifier: ^3.5.30
|
||||
version: 3.5.30
|
||||
'@types/js-cookie':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
|
@ -1498,6 +1501,9 @@ packages:
|
|||
'@types/jquery@3.5.30':
|
||||
resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==}
|
||||
|
||||
'@types/js-cookie@3.0.6':
|
||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||
|
||||
'@types/jsdom@21.1.7':
|
||||
resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==}
|
||||
|
||||
|
@ -5469,6 +5475,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/sizzle': 2.3.8
|
||||
|
||||
'@types/js-cookie@3.0.6': {}
|
||||
|
||||
'@types/jsdom@21.1.7':
|
||||
dependencies:
|
||||
'@types/node': 20.14.11
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
const db = require('./DB');
|
||||
const CustomError = require('../utils/customError');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
import {padUtils, randomString} from '../../static/js/pad_utils'
|
||||
|
||||
exports.getColorPalette = () => [
|
||||
'#ffc7c7',
|
||||
|
@ -169,7 +169,7 @@ exports.getAuthorId = async (token: string, user: object) => {
|
|||
* @param {String} token The token
|
||||
*/
|
||||
exports.getAuthor4Token = async (token: string) => {
|
||||
warnDeprecated(
|
||||
padUtils.warnDeprecated(
|
||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
||||
return await getAuthor4Token(token);
|
||||
};
|
||||
|
|
|
@ -7,10 +7,10 @@ import {MapArrayType} from "../types/MapType";
|
|||
* The pad object, defined with joose
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const Stream = require('../utils/Stream');
|
||||
const assert = require('assert').strict;
|
||||
const db = require('./DB');
|
||||
|
@ -23,7 +23,7 @@ const CustomError = require('../utils/customError');
|
|||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const randomString = require('../utils/randomstring');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
import {padUtils} from "../../static/js/pad_utils";
|
||||
const promises = require('../utils/promises');
|
||||
|
||||
/**
|
||||
|
@ -40,7 +40,7 @@ exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
|
|||
class Pad {
|
||||
private db: Database;
|
||||
private atext: AText;
|
||||
private pool: APool;
|
||||
private pool: AttributePool;
|
||||
private head: number;
|
||||
private chatHead: number;
|
||||
private publicStatus: boolean;
|
||||
|
@ -126,11 +126,11 @@ class Pad {
|
|||
pad: this,
|
||||
authorId,
|
||||
get author() {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
return this.authorId;
|
||||
},
|
||||
set author(authorId) {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
padUtils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
this.authorId = authorId;
|
||||
},
|
||||
...this.head === 0 ? {} : {
|
||||
|
@ -437,11 +437,11 @@ class Pad {
|
|||
// let the plugins know the pad was copied
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
padUtils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -538,11 +538,11 @@ class Pad {
|
|||
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
padUtils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
padUtils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -603,7 +603,7 @@ class Pad {
|
|||
p.push(padManager.removePad(padID));
|
||||
p.push(hooks.aCallAll('padRemove', {
|
||||
get padID() {
|
||||
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
padUtils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
return this.pad.id;
|
||||
},
|
||||
pad: this,
|
||||
|
|
|
@ -30,7 +30,7 @@ const settings = require('../utils/Settings');
|
|||
const webaccess = require('../hooks/express/webaccess');
|
||||
const log4js = require('log4js');
|
||||
const authLogger = log4js.getLogger('auth');
|
||||
const {padutils} = require('../../static/js/pad_utils');
|
||||
import {padUtils as padutils} from '../../static/js/pad_utils';
|
||||
|
||||
const DENY = Object.freeze({accessStatus: 'deny'});
|
||||
|
||||
|
|
|
@ -21,12 +21,12 @@
|
|||
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
const padManager = require('../db/PadManager');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
const AttributeManager = require('../../static/js/AttributeManager');
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
import AttributeManager from '../../static/js/AttributeManager';
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
const {padutils} = require('../../static/js/pad_utils');
|
||||
const readOnlyManager = require('../db/ReadOnlyManager');
|
||||
|
@ -738,7 +738,7 @@ exports.updatePadClients = async (pad: PadType) => {
|
|||
/**
|
||||
* Copied from the Etherpad Source Code. Don't know what this method does excatly...
|
||||
*/
|
||||
const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
||||
const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||
const text = atext.text;
|
||||
|
||||
// collect char positions of line markers (e.g. bullets) in new atext
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import AttributePool from "../../static/js/AttributePool";
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const { checkValidRev } = require('./checkValidRev');
|
||||
|
||||
|
@ -51,7 +52,7 @@ type LineModel = {
|
|||
[id:string]:string|number|LineModel
|
||||
}
|
||||
|
||||
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
||||
exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) => {
|
||||
const line: LineModel = {};
|
||||
|
||||
// identify list
|
||||
|
|
|
@ -22,7 +22,7 @@ const Changeset = require('../../static/js/Changeset');
|
|||
const attributes = require('../../static/js/attributes');
|
||||
const padManager = require('../db/PadManager');
|
||||
const _ = require('underscore');
|
||||
const Security = require('../../static/js/security');
|
||||
const Security = require('security');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const eejs = require('../eejs');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
|
|
|
@ -18,7 +18,7 @@ import {APool} from "../types/PadType";
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const {Pad} = require('../db/Pad');
|
||||
const Stream = require('./Stream');
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
|
@ -61,7 +61,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
|||
try {
|
||||
const processRecord = async (key:string, value: null|{
|
||||
padIDs: string|Record<string, unknown>,
|
||||
pool: APool
|
||||
pool: AttributePool
|
||||
}) => {
|
||||
if (!value) return;
|
||||
const keyParts = key.split(':');
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import {PadAuthor, PadType} from "../types/PadType";
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const exportHtml = require('./ExportHtml');
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
"@types/formidable": "^3.4.5",
|
||||
"@types/http-errors": "^2.0.4",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mocha": "^10.0.7",
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
import AttributeMap from './AttributeMap'
|
||||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const attributes = require('./attributes');
|
||||
const underscore = require("underscore")
|
||||
import underscore from "underscore";
|
||||
import {RepModel} from "./types/RepModel";
|
||||
import {RangePos} from "./types/RangePos";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
const lineMarkerAttribute = 'lmkr';
|
||||
|
||||
|
@ -33,21 +36,20 @@ const lineAttributes = [lineMarkerAttribute, 'list'];
|
|||
- a SkipList `lines` containing the text lines of the document.
|
||||
*/
|
||||
|
||||
const AttributeManager = function (rep, applyChangesetCallback) {
|
||||
this.rep = rep;
|
||||
this.applyChangesetCallback = applyChangesetCallback;
|
||||
this.author = '';
|
||||
export class AttributeManager {
|
||||
private readonly rep: RepModel
|
||||
private readonly applyChangesetCallback: Function
|
||||
private readonly author: string
|
||||
public static DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES
|
||||
public static lineAttributes = lineAttributes
|
||||
|
||||
// If the first char in a line has one of the following attributes
|
||||
// it will be considered as a line marker
|
||||
};
|
||||
constructor(rep: RepModel, applyChangesetCallback: Function) {
|
||||
this.rep = rep;
|
||||
this.applyChangesetCallback = applyChangesetCallback;
|
||||
this.author = '';
|
||||
}
|
||||
|
||||
AttributeManager.DEFAULT_LINE_ATTRIBUTES = DEFAULT_LINE_ATTRIBUTES;
|
||||
AttributeManager.lineAttributes = lineAttributes;
|
||||
|
||||
AttributeManager.prototype = underscore.default(AttributeManager.prototype).extend({
|
||||
|
||||
applyChangeset(changeset) {
|
||||
applyChangeset(changeset: string) {
|
||||
if (!this.applyChangesetCallback) return changeset;
|
||||
|
||||
const cs = changeset.toString();
|
||||
|
@ -56,15 +58,15 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
}
|
||||
|
||||
return changeset;
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Sets attributes on a range
|
||||
@param start [row, col] tuple pointing to the start of the range
|
||||
@param end [row, col] tuple pointing to the end of the range
|
||||
@param attribs: an array of attributes
|
||||
*/
|
||||
setAttributesOnRange(start, end, attribs) {
|
||||
Sets attributes on a range
|
||||
@param start [row, col] tuple pointing to the start of the range
|
||||
@param end [row, col] tuple pointing to the end of the range
|
||||
@param attribs: an array of attributes
|
||||
*/
|
||||
setAttributesOnRange(start: RangePos, end: RangePos, attribs: Attribute[]) {
|
||||
if (start[0] < 0) throw new RangeError('selection start line number is negative');
|
||||
if (start[1] < 0) throw new RangeError('selection start column number is negative');
|
||||
if (end[0] < 0) throw new RangeError('selection end line number is negative');
|
||||
|
@ -72,36 +74,36 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
if (start[0] > end[0] || (start[0] === end[0] && start[1] > end[1])) {
|
||||
throw new RangeError('selection ends before it starts');
|
||||
}
|
||||
|
||||
// instead of applying the attributes to the whole range at once, we need to apply them
|
||||
// line by line, to be able to disregard the "*" used as line marker. For more details,
|
||||
// see https://github.com/ether/etherpad-lite/issues/2772
|
||||
let allChangesets;
|
||||
for (let row = start[0]; row <= end[0]; row++) {
|
||||
const [startCol, endCol] = this._findRowRange(row, start, end);
|
||||
const rowChangeset = this._setAttributesOnRangeByLine(row, startCol, endCol, attribs);
|
||||
const [startCol, endCol] = this.findRowRange(row, start, end);
|
||||
const rowChangeset = this.setAttributesOnRangeByLine(row, startCol, endCol, attribs);
|
||||
|
||||
// compose changesets of all rows into a single changeset
|
||||
// as the range might not be continuous
|
||||
// due to the presence of line markers on the rows
|
||||
if (allChangesets) {
|
||||
allChangesets = Changeset.compose(
|
||||
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||
} else {
|
||||
allChangesets = rowChangeset;
|
||||
}
|
||||
}
|
||||
|
||||
return this.applyChangeset(allChangesets);
|
||||
},
|
||||
}
|
||||
|
||||
_findRowRange(row, start, end) {
|
||||
|
||||
private findRowRange(row: number, start: RangePos, end: RangePos) {
|
||||
if (row < start[0] || row > end[0]) throw new RangeError(`line ${row} not in selection`);
|
||||
if (row >= this.rep.lines.length()) throw new RangeError(`selected line ${row} does not exist`);
|
||||
|
||||
// Subtract 1 for the end-of-line '\n' (it is never selected).
|
||||
const lineLength =
|
||||
this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1;
|
||||
this.rep.lines.offsetOfIndex(row + 1) - this.rep.lines.offsetOfIndex(row) - 1;
|
||||
const markerWidth = this.lineHasMarker(row) ? 1 : 0;
|
||||
if (lineLength - markerWidth < 0) throw new Error(`line ${row} has negative length`);
|
||||
|
||||
|
@ -115,7 +117,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
if (startCol > endCol) throw new RangeError('selection ends before it starts');
|
||||
|
||||
return [startCol, endCol];
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets attributes on a range, by line
|
||||
|
@ -124,57 +126,60 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
* @param endCol column where range ends (one past the last selected column)
|
||||
* @param attribs an array of attributes
|
||||
*/
|
||||
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
setAttributesOnRangeByLine(row: number, startCol: number, endCol: number, attribs: Attribute[]) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth);
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
||||
ChangesetUtils.buildKeepRange(
|
||||
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
||||
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
||||
return builder;
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Returns if the line already has a line marker
|
||||
@param lineNum: the number of the line
|
||||
*/
|
||||
lineHasMarker(lineNum) {
|
||||
Returns if the line already has a line marker
|
||||
@param lineNum: the number of the line
|
||||
*/
|
||||
lineHasMarker(lineNum: number) {
|
||||
return lineAttributes.find(
|
||||
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
|
||||
},
|
||||
(attribute) => this.getAttributeOnLine(lineNum, attribute) !== '') !== undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Gets a specified attribute on a line
|
||||
@param lineNum: the number of the line to set the attribute for
|
||||
@param attributeKey: the name of the attribute to get, e.g. list
|
||||
*/
|
||||
getAttributeOnLine(lineNum, attributeName) {
|
||||
getAttributeOnLine(lineNum: number, attributeName: string) {
|
||||
// get `attributeName` attribute of first char of line
|
||||
const aline = this.rep.alines[lineNum];
|
||||
if (!aline) return '';
|
||||
const [op] = Changeset.deserializeOps(aline);
|
||||
if (op == null) return '';
|
||||
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Gets all attributes on a line
|
||||
@param lineNum: the number of the line to get the attribute for
|
||||
*/
|
||||
getAttributesOnLine(lineNum) {
|
||||
Gets all attributes on a line
|
||||
@param lineNum: the number of the line to get the attribute for
|
||||
*/
|
||||
getAttributesOnLine(lineNum: number) {
|
||||
// get attributes of first char of line
|
||||
const aline = this.rep.alines[lineNum];
|
||||
if (!aline) return [];
|
||||
const [op] = Changeset.deserializeOps(aline);
|
||||
if (op == null) return [];
|
||||
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Gets a given attribute on a selection
|
||||
@param attributeName
|
||||
@param prevChar
|
||||
returns true or false if an attribute is visible in range
|
||||
*/
|
||||
getAttributeOnSelection(attributeName, prevChar) {
|
||||
Gets a given attribute on a selection
|
||||
@param attributeName
|
||||
@param prevChar
|
||||
returns true or false if an attribute is visible in range
|
||||
*/
|
||||
getAttributeOnSelection(attributeName: string, prevChar?: string) {
|
||||
const rep = this.rep;
|
||||
if (!(rep.selStart && rep.selEnd)) return;
|
||||
// If we're looking for the caret attribute not the selection
|
||||
|
@ -191,16 +196,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
|
||||
const withIt = new AttributeMap(rep.apool).set(attributeName, 'true').toString();
|
||||
const withItRegex = new RegExp(`${withIt.replace(/\*/g, '\\*')}(\\*|$)`);
|
||||
const hasIt = (attribs) => withItRegex.test(attribs);
|
||||
const hasIt = (attribs: string) => withItRegex.test(attribs);
|
||||
|
||||
const rangeHasAttrib = (selStart, selEnd) => {
|
||||
const rangeHasAttrib = (selStart: RangePos, selEnd: RangePos):boolean => {
|
||||
// if range is collapsed -> no attribs in range
|
||||
if (selStart[1] === selEnd[1] && selStart[0] === selEnd[0]) return false;
|
||||
|
||||
if (selStart[0] !== selEnd[0]) { // -> More than one line selected
|
||||
// from selStart to the end of the first line
|
||||
let hasAttrib = rangeHasAttrib(
|
||||
selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
|
||||
selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]);
|
||||
|
||||
// for all lines in between
|
||||
for (let n = selStart[0] + 1; n < selEnd[0]; n++) {
|
||||
|
@ -238,16 +243,17 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
return hasAttrib;
|
||||
};
|
||||
return rangeHasAttrib(rep.selStart, rep.selEnd);
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Gets all attributes at a position containing line number and column
|
||||
@param lineNumber starting with zero
|
||||
@param column starting with zero
|
||||
returns a list of attributes in the format
|
||||
[ ["key","value"], ["key","value"], ... ]
|
||||
*/
|
||||
getAttributesOnPosition(lineNumber, column) {
|
||||
Gets all attributes at a position containing line number and column
|
||||
@param lineNumber starting with zero
|
||||
@param column starting with zero
|
||||
returns a list of attributes in the format
|
||||
[ ["key","value"], ["key","value"], ... ]
|
||||
*/
|
||||
getAttributesOnPosition(lineNumber: number, column: number) {
|
||||
// get all attributes of the line
|
||||
const aline = this.rep.alines[lineNumber];
|
||||
|
||||
|
@ -264,7 +270,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Gets all attributes at caret position
|
||||
|
@ -274,18 +280,18 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
*/
|
||||
getAttributesOnCaret() {
|
||||
return this.getAttributesOnPosition(this.rep.selStart[0], this.rep.selStart[1]);
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Sets a specified attribute on a line
|
||||
@param lineNum: the number of the line to set the attribute for
|
||||
@param attributeKey: the name of the attribute to set, e.g. list
|
||||
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
|
||||
Sets a specified attribute on a line
|
||||
@param lineNum: the number of the line to set the attribute for
|
||||
@param attributeKey: the name of the attribute to set, e.g. list
|
||||
@param attributeValue: an optional parameter to pass to the attribute (e.g. indention level)
|
||||
|
||||
*/
|
||||
setAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||
*/
|
||||
setAttributeOnLine(lineNum: number, attributeName: string, attributeValue: string) {
|
||||
let loc = [0, 0];
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth);
|
||||
const hasMarker = this.lineHasMarker(lineNum);
|
||||
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||
|
@ -305,7 +311,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
}
|
||||
|
||||
return this.applyChangeset(builder);
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specified attribute on a line
|
||||
|
@ -313,8 +319,8 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
* @param attributeName the name of the attribute to remove, e.g. list
|
||||
* @param attributeValue if given only attributes with equal value will be removed
|
||||
*/
|
||||
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
removeAttributeOnLine(lineNum: number, attributeName: string, attributeValue?: string) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth);
|
||||
const hasMarker = this.lineHasMarker(lineNum);
|
||||
let found = false;
|
||||
|
||||
|
@ -336,34 +342,35 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||
|
||||
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||
|
||||
// if we have marker and any of attributes don't need to have marker. we need delete it
|
||||
if (hasMarker && !countAttribsWithMarker) {
|
||||
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||
} else {
|
||||
ChangesetUtils.buildKeepRange(
|
||||
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
|
||||
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
|
||||
}
|
||||
|
||||
return this.applyChangeset(builder);
|
||||
},
|
||||
}
|
||||
|
||||
/*
|
||||
Toggles a line attribute for the specified line number
|
||||
If a line attribute with the specified name exists with any value it will be removed
|
||||
Otherwise it will be set to the given value
|
||||
@param lineNum: the number of the line to toggle the attribute for
|
||||
@param attributeKey: the name of the attribute to toggle, e.g. list
|
||||
@param attributeValue: the value to pass to the attribute (e.g. indention level)
|
||||
*/
|
||||
toggleAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||
Toggles a line attribute for the specified line number
|
||||
If a line attribute with the specified name exists with any value it will be removed
|
||||
Otherwise it will be set to the given value
|
||||
@param lineNum: the number of the line to toggle the attribute for
|
||||
@param attributeKey: the name of the attribute to toggle, e.g. list
|
||||
@param attributeValue: the value to pass to the attribute (e.g. indention level)
|
||||
*/
|
||||
toggleAttributeOnLine(lineNum: number, attributeName: string, attributeValue: string) {
|
||||
return this.getAttributeOnLine(lineNum, attributeName)
|
||||
? this.removeAttributeOnLine(lineNum, attributeName)
|
||||
: this.setAttributeOnLine(lineNum, attributeName, attributeValue);
|
||||
},
|
||||
}
|
||||
|
||||
hasAttributeOnSelectionOrCaretPosition(attributeName) {
|
||||
|
||||
hasAttributeOnSelectionOrCaretPosition(attributeName: string) {
|
||||
const hasSelection = (
|
||||
(this.rep.selStart[0] !== this.rep.selEnd[0]) || (this.rep.selEnd[1] !== this.rep.selStart[1])
|
||||
);
|
||||
|
@ -372,11 +379,12 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
hasAttrib = this.getAttributeOnSelection(attributeName);
|
||||
} else {
|
||||
const attributesOnCaretPosition = this.getAttributesOnCaret();
|
||||
const allAttribs = [].concat(...attributesOnCaretPosition); // flatten
|
||||
const allAttribs = [].concat(...attributesOnCaretPosition) as string[]; // flatten
|
||||
hasAttrib = allAttribs.includes(attributeName);
|
||||
}
|
||||
return hasAttrib;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttributeManager;
|
||||
|
||||
export default AttributeManager
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import AttributePool from "./AttributePool";
|
||||
|
||||
const attributes = require('./attributes');
|
||||
|
||||
/**
|
||||
|
@ -21,6 +23,7 @@ const attributes = require('./attributes');
|
|||
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
|
||||
*/
|
||||
class AttributeMap extends Map {
|
||||
private readonly pool? : AttributePool|null
|
||||
/**
|
||||
* Converts an attribute string into an AttributeMap.
|
||||
*
|
||||
|
@ -28,14 +31,14 @@ class AttributeMap extends Map {
|
|||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @returns {AttributeMap}
|
||||
*/
|
||||
static fromString(str, pool) {
|
||||
public static fromString(str: string, pool: AttributePool): AttributeMap {
|
||||
return new AttributeMap(pool).updateFromString(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
*/
|
||||
constructor(pool) {
|
||||
constructor(pool?: AttributePool|null) {
|
||||
super();
|
||||
/** @public */
|
||||
this.pool = pool;
|
||||
|
@ -46,10 +49,10 @@ class AttributeMap extends Map {
|
|||
* @param {string} v - Attribute value.
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
set(k, v) {
|
||||
set(k: string, v: string):this {
|
||||
k = k == null ? '' : String(k);
|
||||
v = v == null ? '' : String(v);
|
||||
this.pool.putAttrib([k, v]);
|
||||
this.pool!.putAttrib([k, v]);
|
||||
return super.set(k, v);
|
||||
}
|
||||
|
||||
|
@ -63,7 +66,7 @@ class AttributeMap extends Map {
|
|||
* key is removed from this map (if present).
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
update(entries, emptyValueIsDelete = false) {
|
||||
update(entries: Iterable<[string, string]>, emptyValueIsDelete: boolean = false): AttributeMap {
|
||||
for (let [k, v] of entries) {
|
||||
k = k == null ? '' : String(k);
|
||||
v = v == null ? '' : String(v);
|
||||
|
@ -83,9 +86,9 @@ class AttributeMap extends Map {
|
|||
* key is removed from this map (if present).
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
updateFromString(str, emptyValueIsDelete = false) {
|
||||
updateFromString(str: string, emptyValueIsDelete: boolean = false): AttributeMap {
|
||||
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttributeMap;
|
||||
export default AttributeMap
|
|
@ -44,6 +44,8 @@
|
|||
* @property {number} nextNum - The attribute ID to assign to the next new attribute.
|
||||
*/
|
||||
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
/**
|
||||
* Represents an attribute pool, which is a collection of attributes (pairs of key and value
|
||||
* strings) along with their identifiers (non-negative integers).
|
||||
|
@ -55,6 +57,14 @@
|
|||
* in the pad.
|
||||
*/
|
||||
class AttributePool {
|
||||
numToAttrib: {
|
||||
[key: number]: [string, string]
|
||||
}
|
||||
private attribToNum: {
|
||||
[key: number]: [string, string]
|
||||
}
|
||||
private nextNum: number
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* Maps an attribute identifier to the attribute's `[key, value]` string pair.
|
||||
|
@ -96,7 +106,10 @@ class AttributePool {
|
|||
*/
|
||||
clone() {
|
||||
const c = new AttributePool();
|
||||
for (const [n, a] of Object.entries(this.numToAttrib)) c.numToAttrib[n] = [a[0], a[1]];
|
||||
for (const [n, a] of Object.entries(this.numToAttrib)){
|
||||
// @ts-ignore
|
||||
c.numToAttrib[n] = [a[0], a[1]];
|
||||
}
|
||||
Object.assign(c.attribToNum, this.attribToNum);
|
||||
c.nextNum = this.nextNum;
|
||||
return c;
|
||||
|
@ -111,15 +124,17 @@ class AttributePool {
|
|||
* membership in the pool without mutating the pool.
|
||||
* @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool.
|
||||
*/
|
||||
putAttrib(attrib, dontAddIfAbsent = false) {
|
||||
putAttrib(attrib: Attribute, dontAddIfAbsent = false) {
|
||||
const str = String(attrib);
|
||||
if (str in this.attribToNum) {
|
||||
// @ts-ignore
|
||||
return this.attribToNum[str];
|
||||
}
|
||||
if (dontAddIfAbsent) {
|
||||
return -1;
|
||||
}
|
||||
const num = this.nextNum++;
|
||||
// @ts-ignore
|
||||
this.attribToNum[str] = num;
|
||||
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
|
||||
return num;
|
||||
|
@ -130,7 +145,7 @@ class AttributePool {
|
|||
* @returns {Attribute} The attribute with the given identifier, or nullish if there is no such
|
||||
* attribute.
|
||||
*/
|
||||
getAttrib(num) {
|
||||
getAttrib(num: number): Attribute {
|
||||
const pair = this.numToAttrib[num];
|
||||
if (!pair) {
|
||||
return pair;
|
||||
|
@ -143,7 +158,7 @@ class AttributePool {
|
|||
* @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty
|
||||
* string.
|
||||
*/
|
||||
getAttribKey(num) {
|
||||
getAttribKey(num: number): string {
|
||||
const pair = this.numToAttrib[num];
|
||||
if (!pair) return '';
|
||||
return pair[0];
|
||||
|
@ -154,7 +169,7 @@ class AttributePool {
|
|||
* @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty
|
||||
* string.
|
||||
*/
|
||||
getAttribValue(num) {
|
||||
getAttribValue(num: number) {
|
||||
const pair = this.numToAttrib[num];
|
||||
if (!pair) return '';
|
||||
return pair[1];
|
||||
|
@ -166,8 +181,8 @@ class AttributePool {
|
|||
* @param {Function} func - Callback to call with two arguments: key and value. Its return value
|
||||
* is ignored.
|
||||
*/
|
||||
eachAttrib(func) {
|
||||
for (const n of Object.keys(this.numToAttrib)) {
|
||||
eachAttrib(func: (k: string, v: string)=>void) {
|
||||
for (const n in this.numToAttrib) {
|
||||
const pair = this.numToAttrib[n];
|
||||
func(pair[0], pair[1]);
|
||||
}
|
||||
|
@ -196,11 +211,12 @@ class AttributePool {
|
|||
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
|
||||
* state will lead to pool corruption.
|
||||
*/
|
||||
fromJsonable(obj) {
|
||||
fromJsonable(obj: this) {
|
||||
this.numToAttrib = obj.numToAttrib;
|
||||
this.nextNum = obj.nextNum;
|
||||
this.attribToNum = {};
|
||||
for (const n of Object.keys(this.numToAttrib)) {
|
||||
// @ts-ignore
|
||||
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
|
||||
}
|
||||
return this;
|
||||
|
@ -213,6 +229,7 @@ class AttributePool {
|
|||
if (!Number.isInteger(this.nextNum)) throw new Error('nextNum property is not an integer');
|
||||
if (this.nextNum < 0) throw new Error('nextNum property is negative');
|
||||
for (const prop of ['numToAttrib', 'attribToNum']) {
|
||||
// @ts-ignore
|
||||
const obj = this[prop];
|
||||
if (obj == null) throw new Error(`${prop} property is null`);
|
||||
if (typeof obj !== 'object') throw new TypeError(`${prop} property is not an object`);
|
||||
|
@ -231,9 +248,10 @@ class AttributePool {
|
|||
if (v == null) throw new TypeError(`attrib ${i} value is null`);
|
||||
if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`);
|
||||
const attrStr = String(attr);
|
||||
// @ts-ignore
|
||||
if (this.attribToNum[attrStr] !== i) throw new Error(`attribToNum for ${attrStr} !== ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttributePool;
|
||||
export default AttributePool
|
|
@ -22,10 +22,15 @@
|
|||
* https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
|
||||
*/
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
const AttributePool = require('./AttributePool');
|
||||
const attributes = require('./attributes');
|
||||
const {padutils} = require('./pad_utils');
|
||||
import AttributeMap from './AttributeMap'
|
||||
import AttributePool from "./AttributePool";
|
||||
import {} from './attributes';
|
||||
import {padUtils as padutils} from "./pad_utils";
|
||||
import Op from './Op'
|
||||
import {numToString, parseNum} from './ChangesetUtils'
|
||||
import {StringAssembler} from "./StringAssembler";
|
||||
import {OpIter} from "./OpIter";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
/**
|
||||
* A `[key, value]` pair of strings describing a text attribute.
|
||||
|
@ -47,8 +52,9 @@ const {padutils} = require('./pad_utils');
|
|||
*
|
||||
* @param {string} msg - Just some message
|
||||
*/
|
||||
const error = (msg) => {
|
||||
const error = (msg: string) => {
|
||||
const e = new Error(msg);
|
||||
// @ts-ignore
|
||||
e.easysync = true;
|
||||
throw e;
|
||||
};
|
||||
|
@ -61,96 +67,10 @@ const error = (msg) => {
|
|||
* @param {string} msg - error message to include in the exception
|
||||
* @type {(b: boolean, msg: string) => asserts b}
|
||||
*/
|
||||
const assert = (b, msg) => {
|
||||
export const assert: (b: boolean, msg: string) => asserts b = (b: boolean, msg: string): asserts b => {
|
||||
if (!b) error(`Failed assertion: ${msg}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a number from string base 36.
|
||||
*
|
||||
* @param {string} str - string of the number in base 36
|
||||
* @returns {number} number
|
||||
*/
|
||||
exports.parseNum = (str) => parseInt(str, 36);
|
||||
|
||||
/**
|
||||
* Writes a number in base 36 and puts it in a string.
|
||||
*
|
||||
* @param {number} num - number
|
||||
* @returns {string} string
|
||||
*/
|
||||
exports.numToString = (num) => num.toString(36).toLowerCase();
|
||||
|
||||
/**
|
||||
* An operation to apply to a shared document.
|
||||
*/
|
||||
class Op {
|
||||
/**
|
||||
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
|
||||
*/
|
||||
constructor(opcode = '') {
|
||||
/**
|
||||
* The operation's operator:
|
||||
* - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
|
||||
* document.
|
||||
* - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
|
||||
* document.
|
||||
* - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
|
||||
* the document. The inserted characters come from the changeset's character bank.
|
||||
* - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
|
||||
* operation.
|
||||
*
|
||||
* @type {(''|'='|'+'|'-')}
|
||||
* @public
|
||||
*/
|
||||
this.opcode = opcode;
|
||||
|
||||
/**
|
||||
* The number of characters to keep, insert, or delete.
|
||||
*
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
this.chars = 0;
|
||||
|
||||
/**
|
||||
* The number of characters among the `chars` characters that are newlines. If non-zero, the
|
||||
* last character must be a newline.
|
||||
*
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
this.lines = 0;
|
||||
|
||||
/**
|
||||
* Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
|
||||
* sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
|
||||
* '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
|
||||
* identifiers come from the document's attribute pool.
|
||||
*
|
||||
* For keep ('=') operations, the attributes are merged with the base text's existing
|
||||
* attributes:
|
||||
* - A keep op attribute with a non-empty value replaces an existing base text attribute that
|
||||
* has the same key.
|
||||
* - A keep op attribute with an empty value is interpreted as an instruction to remove an
|
||||
* existing base text attribute that has the same key, if one exists.
|
||||
*
|
||||
* This is the empty string for remove ('-') operations.
|
||||
*
|
||||
* @type {string}
|
||||
* @public
|
||||
*/
|
||||
this.attribs = '';
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (!this.opcode) throw new TypeError('null op');
|
||||
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
|
||||
const l = this.lines ? `|${exports.numToString(this.lines)}` : '';
|
||||
return this.attribs + l + this.opcode + exports.numToString(this.chars);
|
||||
}
|
||||
}
|
||||
exports.Op = Op;
|
||||
|
||||
/**
|
||||
* Describes changes to apply to a document. Does not include the attribute pool or the original
|
||||
|
@ -170,7 +90,7 @@ exports.Op = Op;
|
|||
* @param {string} cs - String representation of the Changeset
|
||||
* @returns {number} oldLen property
|
||||
*/
|
||||
exports.oldLen = (cs) => exports.unpack(cs).oldLen;
|
||||
export const oldLen = (cs: string) => unpack(cs).oldLen
|
||||
|
||||
/**
|
||||
* Returns the length of the text after changeset is applied.
|
||||
|
@ -178,7 +98,7 @@ exports.oldLen = (cs) => exports.unpack(cs).oldLen;
|
|||
* @param {string} cs - String representation of the Changeset
|
||||
* @returns {number} newLen property
|
||||
*/
|
||||
exports.newLen = (cs) => exports.unpack(cs).newLen;
|
||||
export const newLen = (cs: string) => unpack(cs).newLen
|
||||
|
||||
/**
|
||||
* Parses a string of serialized changeset operations.
|
||||
|
@ -187,63 +107,23 @@ exports.newLen = (cs) => exports.unpack(cs).newLen;
|
|||
* @yields {Op}
|
||||
* @returns {Generator<Op>}
|
||||
*/
|
||||
exports.deserializeOps = function* (ops) {
|
||||
export const deserializeOps = function* (ops: string) {
|
||||
// TODO: Migrate to String.prototype.matchAll() once there is enough browser support.
|
||||
const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g;
|
||||
let match;
|
||||
while ((match = regex.exec(ops)) != null) {
|
||||
if (match[5] === '$') return; // Start of the insert operation character bank.
|
||||
if (match[5] != null) error(`invalid operation: ${ops.slice(regex.lastIndex - 1)}`);
|
||||
const op = new Op(match[3]);
|
||||
op.lines = exports.parseNum(match[2] || '0');
|
||||
op.chars = exports.parseNum(match[4]);
|
||||
const opMatch = match[3] as ""|"=" | "+" | "-" | undefined
|
||||
const op = new Op(opMatch);
|
||||
op.lines = parseNum(match[2] || '0');
|
||||
op.chars = parseNum(match[4]);
|
||||
op.attribs = match[1];
|
||||
yield op;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterator over a changeset's operations.
|
||||
*
|
||||
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
|
||||
*
|
||||
* @deprecated Use `deserializeOps` instead.
|
||||
*/
|
||||
class OpIter {
|
||||
/**
|
||||
* @param {string} ops - String encoding the change operations to iterate over.
|
||||
*/
|
||||
constructor(ops) {
|
||||
this._gen = exports.deserializeOps(ops);
|
||||
this._next = this._gen.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether there are any remaining operations.
|
||||
*/
|
||||
hasNext() {
|
||||
return !this._next.done;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next operation object and advances the iterator.
|
||||
*
|
||||
* Note: This does NOT implement the ECMAScript iterator protocol.
|
||||
*
|
||||
* @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value.
|
||||
* @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are
|
||||
* no more operations.
|
||||
*/
|
||||
next(opOut = new Op()) {
|
||||
if (this.hasNext()) {
|
||||
copyOp(this._next.value, opOut);
|
||||
this._next = this._gen.next();
|
||||
} else {
|
||||
clearOp(opOut);
|
||||
}
|
||||
return opOut;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an iterator which decodes string changeset operations.
|
||||
|
@ -252,7 +132,7 @@ class OpIter {
|
|||
* @param {string} opsStr - String encoding of the change operations to perform.
|
||||
* @returns {OpIter} Operator iterator object.
|
||||
*/
|
||||
exports.opIterator = (opsStr) => {
|
||||
export const opIterator = (opsStr: string) => {
|
||||
padutils.warnDeprecated(
|
||||
'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead');
|
||||
return new OpIter(opsStr);
|
||||
|
@ -263,7 +143,7 @@ exports.opIterator = (opsStr) => {
|
|||
*
|
||||
* @param {Op} op - object to clear
|
||||
*/
|
||||
const clearOp = (op) => {
|
||||
export const clearOp = (op: Op) => {
|
||||
op.opcode = '';
|
||||
op.chars = 0;
|
||||
op.lines = 0;
|
||||
|
@ -277,7 +157,7 @@ const clearOp = (op) => {
|
|||
* @param {('+'|'-'|'='|'')} [optOpcode=''] - The operation's operator.
|
||||
* @returns {Op}
|
||||
*/
|
||||
exports.newOp = (optOpcode) => {
|
||||
export const newOp = (optOpcode:'+'|'-'|'='|'' ): Op => {
|
||||
padutils.warnDeprecated('Changeset.newOp() is deprecated; use the Changeset.Op class instead');
|
||||
return new Op(optOpcode);
|
||||
};
|
||||
|
@ -289,7 +169,7 @@ exports.newOp = (optOpcode) => {
|
|||
* @param {Op} [op2] - dest Op. If not given, a new Op is used.
|
||||
* @returns {Op} `op2`
|
||||
*/
|
||||
const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1);
|
||||
export const copyOp = (op1: Op, op2: Op = new Op()): Op => Object.assign(op2, op1);
|
||||
|
||||
/**
|
||||
* Serializes a sequence of Ops.
|
||||
|
@ -320,12 +200,12 @@ const copyOp = (op1, op2 = new Op()) => Object.assign(op2, op1);
|
|||
* (if necessary) and encode. If an attribute string, no checking is performed to ensure that
|
||||
* the attributes exist in the pool, are in the canonical order, and contain no duplicate keys.
|
||||
* If this is an iterable of attributes, `pool` must be non-null.
|
||||
* @param {?AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of
|
||||
* @param {?AttributePool.ts} pool - Attribute pool. Required if `attribs` is an iterable of
|
||||
* attributes, ignored if `attribs` is an attribute string.
|
||||
* @yields {Op} One or two ops (depending on the presense of newlines) that cover the given text.
|
||||
* @returns {Generator<Op>}
|
||||
*/
|
||||
const opsFromText = function* (opcode, text, attribs = '', pool = null) {
|
||||
export const opsFromText = function* (opcode: "" | "=" | "+" | "-" | undefined, text: string, attribs: string|Attribute[] = '', pool: AttributePool|null = null) {
|
||||
const op = new Op(opcode);
|
||||
op.attribs = typeof attribs === 'string'
|
||||
? attribs : new AttributeMap(pool).update(attribs || [], opcode === '+').toString();
|
||||
|
@ -336,7 +216,7 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
|
|||
yield op;
|
||||
} else {
|
||||
op.chars = lastNewlinePos + 1;
|
||||
op.lines = text.match(/\n/g).length;
|
||||
op.lines = text.match(/\n/g)!.length;
|
||||
yield op;
|
||||
const op2 = copyOp(op);
|
||||
op2.chars = text.length - (lastNewlinePos + 1);
|
||||
|
@ -345,23 +225,7 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an object that allows you to append operations (type Op) and also compresses them if
|
||||
* possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser
|
||||
* input, at the cost of speed. Specifically:
|
||||
* - merges consecutive operations that can be merged
|
||||
* - strips final "="
|
||||
* - ignores 0-length changes
|
||||
* - reorders consecutive + and - (which MergingOpAssembler doesn't do)
|
||||
*
|
||||
* @typedef {object} SmartOpAssembler
|
||||
* @property {Function} append -
|
||||
* @property {Function} appendOpWithText -
|
||||
* @property {Function} clear -
|
||||
* @property {Function} endDocument -
|
||||
* @property {Function} getLengthChange -
|
||||
* @property {Function} toString -
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Used to check if a Changeset is valid. This function does not check things that require access to
|
||||
|
@ -370,7 +234,7 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) {
|
|||
* @param {string} cs - Changeset to check
|
||||
* @returns {string} the checked Changeset
|
||||
*/
|
||||
exports.checkRep = (cs) => {
|
||||
export const checkRep = (cs: string) => {
|
||||
const unpacked = exports.unpack(cs);
|
||||
const oldLen = unpacked.oldLen;
|
||||
const newLen = unpacked.newLen;
|
||||
|
@ -418,254 +282,6 @@ exports.checkRep = (cs) => {
|
|||
return cs;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {SmartOpAssembler}
|
||||
*/
|
||||
exports.smartOpAssembler = () => {
|
||||
const minusAssem = exports.mergingOpAssembler();
|
||||
const plusAssem = exports.mergingOpAssembler();
|
||||
const keepAssem = exports.mergingOpAssembler();
|
||||
const assem = exports.stringAssembler();
|
||||
let lastOpcode = '';
|
||||
let lengthChange = 0;
|
||||
|
||||
const flushKeeps = () => {
|
||||
assem.append(keepAssem.toString());
|
||||
keepAssem.clear();
|
||||
};
|
||||
|
||||
const flushPlusMinus = () => {
|
||||
assem.append(minusAssem.toString());
|
||||
minusAssem.clear();
|
||||
assem.append(plusAssem.toString());
|
||||
plusAssem.clear();
|
||||
};
|
||||
|
||||
const append = (op) => {
|
||||
if (!op.opcode) return;
|
||||
if (!op.chars) return;
|
||||
|
||||
if (op.opcode === '-') {
|
||||
if (lastOpcode === '=') {
|
||||
flushKeeps();
|
||||
}
|
||||
minusAssem.append(op);
|
||||
lengthChange -= op.chars;
|
||||
} else if (op.opcode === '+') {
|
||||
if (lastOpcode === '=') {
|
||||
flushKeeps();
|
||||
}
|
||||
plusAssem.append(op);
|
||||
lengthChange += op.chars;
|
||||
} else if (op.opcode === '=') {
|
||||
if (lastOpcode !== '=') {
|
||||
flushPlusMinus();
|
||||
}
|
||||
keepAssem.append(op);
|
||||
}
|
||||
lastOpcode = op.opcode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates operations from the given text and attributes.
|
||||
*
|
||||
* @deprecated Use `opsFromText` instead.
|
||||
* @param {('-'|'+'|'=')} opcode - The operator to use.
|
||||
* @param {string} text - The text to remove/add/keep.
|
||||
* @param {(string|Iterable<Attribute>)} attribs - The attributes to apply to the operations.
|
||||
* @param {?AttributePool} pool - Attribute pool. Only required if `attribs` is an iterable of
|
||||
* attribute key, value pairs.
|
||||
*/
|
||||
const appendOpWithText = (opcode, text, attribs, pool) => {
|
||||
padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' +
|
||||
'use opsFromText() instead.');
|
||||
for (const op of opsFromText(opcode, text, attribs, pool)) append(op);
|
||||
};
|
||||
|
||||
const toString = () => {
|
||||
flushPlusMinus();
|
||||
flushKeeps();
|
||||
return assem.toString();
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
minusAssem.clear();
|
||||
plusAssem.clear();
|
||||
keepAssem.clear();
|
||||
assem.clear();
|
||||
lengthChange = 0;
|
||||
};
|
||||
|
||||
const endDocument = () => {
|
||||
keepAssem.endDocument();
|
||||
};
|
||||
|
||||
const getLengthChange = () => lengthChange;
|
||||
|
||||
return {
|
||||
append,
|
||||
toString,
|
||||
clear,
|
||||
endDocument,
|
||||
appendOpWithText,
|
||||
getLengthChange,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {MergingOpAssembler}
|
||||
*/
|
||||
exports.mergingOpAssembler = () => {
|
||||
const assem = exports.opAssembler();
|
||||
const bufOp = new Op();
|
||||
|
||||
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
|
||||
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
|
||||
// This variable stores the length of yyy and any other newline-less
|
||||
// ops immediately after it.
|
||||
let bufOpAdditionalCharsAfterNewline = 0;
|
||||
|
||||
/**
|
||||
* @param {boolean} [isEndDocument]
|
||||
*/
|
||||
const flush = (isEndDocument) => {
|
||||
if (!bufOp.opcode) return;
|
||||
if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) {
|
||||
// final merged keep, leave it implicit
|
||||
} else {
|
||||
assem.append(bufOp);
|
||||
if (bufOpAdditionalCharsAfterNewline) {
|
||||
bufOp.chars = bufOpAdditionalCharsAfterNewline;
|
||||
bufOp.lines = 0;
|
||||
assem.append(bufOp);
|
||||
bufOpAdditionalCharsAfterNewline = 0;
|
||||
}
|
||||
}
|
||||
bufOp.opcode = '';
|
||||
};
|
||||
|
||||
const append = (op) => {
|
||||
if (op.chars <= 0) return;
|
||||
if (bufOp.opcode === op.opcode && bufOp.attribs === op.attribs) {
|
||||
if (op.lines > 0) {
|
||||
// bufOp and additional chars are all mergeable into a multi-line op
|
||||
bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars;
|
||||
bufOp.lines += op.lines;
|
||||
bufOpAdditionalCharsAfterNewline = 0;
|
||||
} else if (bufOp.lines === 0) {
|
||||
// both bufOp and op are in-line
|
||||
bufOp.chars += op.chars;
|
||||
} else {
|
||||
// append in-line text to multi-line bufOp
|
||||
bufOpAdditionalCharsAfterNewline += op.chars;
|
||||
}
|
||||
} else {
|
||||
flush();
|
||||
copyOp(op, bufOp);
|
||||
}
|
||||
};
|
||||
|
||||
const endDocument = () => {
|
||||
flush(true);
|
||||
};
|
||||
|
||||
const toString = () => {
|
||||
flush();
|
||||
return assem.toString();
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
assem.clear();
|
||||
clearOp(bufOp);
|
||||
};
|
||||
return {
|
||||
append,
|
||||
toString,
|
||||
clear,
|
||||
endDocument,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {OpAssembler}
|
||||
*/
|
||||
exports.opAssembler = () => {
|
||||
let serialized = '';
|
||||
|
||||
/**
|
||||
* @param {Op} op - Operation to add. Ownership remains with the caller.
|
||||
*/
|
||||
const append = (op) => {
|
||||
assert(op instanceof Op, 'argument must be an instance of Op');
|
||||
serialized += op.toString();
|
||||
};
|
||||
|
||||
const toString = () => serialized;
|
||||
|
||||
const clear = () => {
|
||||
serialized = '';
|
||||
};
|
||||
return {
|
||||
append,
|
||||
toString,
|
||||
clear,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom made String Iterator
|
||||
*
|
||||
* @typedef {object} StringIterator
|
||||
* @property {Function} newlines -
|
||||
* @property {Function} peek -
|
||||
* @property {Function} remaining -
|
||||
* @property {Function} skip -
|
||||
* @property {Function} take -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} str - String to iterate over
|
||||
* @returns {StringIterator}
|
||||
*/
|
||||
exports.stringIterator = (str) => {
|
||||
let curIndex = 0;
|
||||
// newLines is the number of \n between curIndex and str.length
|
||||
let newLines = str.split('\n').length - 1;
|
||||
const getnewLines = () => newLines;
|
||||
|
||||
const assertRemaining = (n) => {
|
||||
assert(n <= remaining(), `!(${n} <= ${remaining()})`);
|
||||
};
|
||||
|
||||
const take = (n) => {
|
||||
assertRemaining(n);
|
||||
const s = str.substr(curIndex, n);
|
||||
newLines -= s.split('\n').length - 1;
|
||||
curIndex += n;
|
||||
return s;
|
||||
};
|
||||
|
||||
const peek = (n) => {
|
||||
assertRemaining(n);
|
||||
const s = str.substr(curIndex, n);
|
||||
return s;
|
||||
};
|
||||
|
||||
const skip = (n) => {
|
||||
assertRemaining(n);
|
||||
curIndex += n;
|
||||
};
|
||||
|
||||
const remaining = () => str.length - curIndex;
|
||||
return {
|
||||
take,
|
||||
skip,
|
||||
remaining,
|
||||
peek,
|
||||
newlines: getnewLines,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom made StringBuffer
|
||||
*
|
||||
|
@ -674,19 +290,6 @@ exports.stringIterator = (str) => {
|
|||
* @property {Function} toString -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {StringAssembler}
|
||||
*/
|
||||
exports.stringAssembler = () => ({
|
||||
_str: '',
|
||||
clear() { this._str = ''; },
|
||||
/**
|
||||
* @param {string} x -
|
||||
*/
|
||||
append(x) { this._str += String(x); },
|
||||
toString() { return this._str; },
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {object} StringArrayLike
|
||||
* @property {(i: number) => string} get - Returns the line at index `i`.
|
||||
|
@ -1067,9 +670,9 @@ exports.unpack = (cs) => {
|
|||
const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
|
||||
const headerMatch = headerRegex.exec(cs);
|
||||
if ((!headerMatch) || (!headerMatch[0])) error(`Not a changeset: ${cs}`);
|
||||
const oldLen = exports.parseNum(headerMatch[1]);
|
||||
const oldLen = parseNum(headerMatch[1]);
|
||||
const changeSign = (headerMatch[2] === '>') ? 1 : -1;
|
||||
const changeMag = exports.parseNum(headerMatch[3]);
|
||||
const changeMag = parseNum(headerMatch[3]);
|
||||
const newLen = oldLen + changeSign * changeMag;
|
||||
const opsStart = headerMatch[0].length;
|
||||
let opsEnd = cs.indexOf('$');
|
||||
|
@ -1112,7 +715,7 @@ exports.applyToText = (cs, str) => {
|
|||
assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`);
|
||||
const bankIter = exports.stringIterator(unpacked.charBank);
|
||||
const strIter = exports.stringIterator(str);
|
||||
const assem = exports.stringAssembler();
|
||||
const assem = new StringAssembler();
|
||||
for (const op of exports.deserializeOps(unpacked.ops)) {
|
||||
switch (op.opcode) {
|
||||
case '+':
|
||||
|
@ -1177,7 +780,7 @@ exports.mutateTextLines = (cs, lines) => {
|
|||
* @param {AttributeString} att1 - first attribute string
|
||||
* @param {AttributeString} att2 - second attribue string
|
||||
* @param {boolean} resultIsMutation -
|
||||
* @param {AttributePool} pool - attribute pool
|
||||
* @param {AttributePool.ts} pool - attribute pool
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.composeAttributes = (att1, att2, resultIsMutation, pool) => {
|
||||
|
@ -1211,7 +814,7 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => {
|
|||
* @param {Op} attOp - The op from the sequence that is being operated on, either an attribution
|
||||
* string or the earlier of two exportss being composed.
|
||||
* @param {Op} csOp -
|
||||
* @param {AttributePool} pool - Can be null if definitely not needed.
|
||||
* @param {AttributePool.ts} pool - Can be null if definitely not needed.
|
||||
* @returns {Op} The result of applying `csOp` to `attOp`.
|
||||
*/
|
||||
const slicerZipperFunc = (attOp, csOp, pool) => {
|
||||
|
@ -1272,7 +875,7 @@ const slicerZipperFunc = (attOp, csOp, pool) => {
|
|||
*
|
||||
* @param {string} cs - Changeset
|
||||
* @param {string} astr - the attribs string of a AText
|
||||
* @param {AttributePool} pool - the attibutes pool
|
||||
* @param {AttributePool.ts} pool - the attibutes pool
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.applyToAttribution = (cs, astr, pool) => {
|
||||
|
@ -1285,7 +888,7 @@ exports.applyToAttribution = (cs, astr, pool) => {
|
|||
*
|
||||
* @param {string} cs - The encoded changeset.
|
||||
* @param {Array<string>} lines - Attribute lines. Modified in place.
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @param {AttributePool.ts} pool - Attribute pool.
|
||||
*/
|
||||
exports.mutateAttributionLines = (cs, lines, pool) => {
|
||||
const unpacked = exports.unpack(cs);
|
||||
|
@ -1454,7 +1057,7 @@ exports.splitTextLines = (text) => text.match(/[^\n]*(?:\n|[^\n]$)/g);
|
|||
*
|
||||
* @param {string} cs1 - first Changeset
|
||||
* @param {string} cs2 - second Changeset
|
||||
* @param {AttributePool} pool - Attribs pool
|
||||
* @param {AttributePool.ts} pool - Attribs pool
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.compose = (cs1, cs2, pool) => {
|
||||
|
@ -1466,7 +1069,7 @@ exports.compose = (cs1, cs2, pool) => {
|
|||
const len3 = unpacked2.newLen;
|
||||
const bankIter1 = exports.stringIterator(unpacked1.charBank);
|
||||
const bankIter2 = exports.stringIterator(unpacked2.charBank);
|
||||
const bankAssem = exports.stringAssembler();
|
||||
const bankAssem = new StringAssembler();
|
||||
|
||||
const newOps = applyZip(unpacked1.ops, unpacked2.ops, (op1, op2) => {
|
||||
const op1code = op1.opcode;
|
||||
|
@ -1493,7 +1096,7 @@ exports.compose = (cs1, cs2, pool) => {
|
|||
* key,value that is already present in the pool.
|
||||
*
|
||||
* @param {Attribute} attribPair - `[key, value]` pair of strings.
|
||||
* @param {AttributePool} pool - Attribute pool
|
||||
* @param {AttributePool.ts} pool - Attribute pool
|
||||
* @returns {Function}
|
||||
*/
|
||||
exports.attributeTester = (attribPair, pool) => {
|
||||
|
@ -1523,7 +1126,7 @@ exports.identity = (N) => exports.pack(N, N, '', '');
|
|||
* @param {number} ndel - Number of characters to delete at `start`.
|
||||
* @param {string} ins - Text to insert at `start` (after deleting `ndel` characters).
|
||||
* @param {string} [attribs] - Optional attributes to apply to the inserted text.
|
||||
* @param {AttributePool} [pool] - Attribute pool.
|
||||
* @param {AttributePool.ts} [pool] - Attribute pool.
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.makeSplice = (orig, start, ndel, ins, attribs, pool) => {
|
||||
|
@ -1646,13 +1249,13 @@ exports.moveOpsToNewPool = (cs, oldPool, newPool) => {
|
|||
const fromDollar = cs.substring(dollarPos);
|
||||
// order of attribs stays the same
|
||||
return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
|
||||
const oldNum = exports.parseNum(a);
|
||||
const oldNum = parseNum(a);
|
||||
const pair = oldPool.getAttrib(oldNum);
|
||||
// The attribute might not be in the old pool if the user is viewing the current revision in the
|
||||
// timeslider and text is deleted. See: https://github.com/ether/etherpad-lite/issues/3932
|
||||
if (!pair) return '';
|
||||
const newNum = newPool.putAttrib(pair);
|
||||
return `*${exports.numToString(newNum)}`;
|
||||
return `*${numToString(newNum)}`;
|
||||
}) + fromDollar;
|
||||
};
|
||||
|
||||
|
@ -1688,7 +1291,7 @@ exports.eachAttribNumber = (cs, func) => {
|
|||
// WARNING: The following cannot be replaced with a call to `attributes.decodeAttribString()`
|
||||
// because that function only works on attribute strings, not serialized operations or changesets.
|
||||
upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
|
||||
func(exports.parseNum(a));
|
||||
func(parseNum(a));
|
||||
return '';
|
||||
});
|
||||
};
|
||||
|
@ -1719,11 +1322,11 @@ exports.mapAttribNumbers = (cs, func) => {
|
|||
const upToDollar = cs.substring(0, dollarPos);
|
||||
|
||||
const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => {
|
||||
const n = func(exports.parseNum(a));
|
||||
const n = func(parseNum(a));
|
||||
if (n === true) {
|
||||
return s;
|
||||
} else if ((typeof n) === 'number') {
|
||||
return `*${exports.numToString(n)}`;
|
||||
return `*${numToString(n)}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
@ -1759,7 +1362,7 @@ exports.makeAText = (text, attribs) => ({
|
|||
*
|
||||
* @param {string} cs - Changeset to apply
|
||||
* @param {AText} atext -
|
||||
* @param {AttributePool} pool - Attribute Pool to add to
|
||||
* @param {AttributePool.ts} pool - Attribute Pool to add to
|
||||
* @returns {AText}
|
||||
*/
|
||||
exports.applyToAText = (cs, atext, pool) => ({
|
||||
|
@ -1840,8 +1443,8 @@ exports.appendATextToAssembler = (atext, assem) => {
|
|||
* Creates a clone of a Changeset and it's APool.
|
||||
*
|
||||
* @param {string} cs -
|
||||
* @param {AttributePool} pool -
|
||||
* @returns {{translated: string, pool: AttributePool}}
|
||||
* @param {AttributePool.ts} pool -
|
||||
* @returns {{translated: string, pool: AttributePool.ts}}
|
||||
*/
|
||||
exports.prepareForWire = (cs, pool) => {
|
||||
const newPool = new AttributePool();
|
||||
|
@ -1880,7 +1483,7 @@ const attribsAttributeValue = (attribs, key, pool) => {
|
|||
* @deprecated Use an AttributeMap instead.
|
||||
* @param {Op} op - Op
|
||||
* @param {string} key - string to search for
|
||||
* @param {AttributePool} pool - attribute pool
|
||||
* @param {AttributePool.ts} pool - attribute pool
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.opAttributeValue = (op, key, pool) => {
|
||||
|
@ -1895,7 +1498,7 @@ exports.opAttributeValue = (op, key, pool) => {
|
|||
* @deprecated Use an AttributeMap instead.
|
||||
* @param {AttributeString} attribs - Attribute string
|
||||
* @param {string} key - string to search for
|
||||
* @param {AttributePool} pool - attribute pool
|
||||
* @param {AttributePool.ts} pool - attribute pool
|
||||
* @returns {string}
|
||||
*/
|
||||
exports.attribsAttributeValue = (attribs, key, pool) => {
|
||||
|
@ -1922,7 +1525,7 @@ exports.attribsAttributeValue = (attribs, key, pool) => {
|
|||
exports.builder = (oldLen) => {
|
||||
const assem = exports.smartOpAssembler();
|
||||
const o = new Op();
|
||||
const charBank = exports.stringAssembler();
|
||||
const charBank = new StringAssembler();
|
||||
|
||||
const self = {
|
||||
/**
|
||||
|
@ -1931,7 +1534,7 @@ exports.builder = (oldLen) => {
|
|||
* character must be a newline.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
|
@ -1949,7 +1552,7 @@ exports.builder = (oldLen) => {
|
|||
* @param {string} text - Text to keep.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
|
@ -1962,7 +1565,7 @@ exports.builder = (oldLen) => {
|
|||
* @param {string} text - Text to insert.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
|
@ -2006,7 +1609,7 @@ exports.builder = (oldLen) => {
|
|||
* (if necessary) and encode. If an attribute string, no checking is performed to ensure that
|
||||
* the attributes exist in the pool, are in the canonical order, and contain no duplicate keys.
|
||||
* If this is an iterable of attributes, `pool` must be non-null.
|
||||
* @param {AttributePool} pool - Attribute pool. Required if `attribs` is an iterable of attributes,
|
||||
* @param {AttributePool.ts} pool - Attribute pool. Required if `attribs` is an iterable of attributes,
|
||||
* ignored if `attribs` is an attribute string.
|
||||
* @returns {AttributeString}
|
||||
*/
|
||||
|
@ -2163,7 +1766,7 @@ exports.inverse = (cs, lines, alines, pool) => {
|
|||
|
||||
const nextText = (numChars) => {
|
||||
let len = 0;
|
||||
const assem = exports.stringAssembler();
|
||||
const assem = new StringAssembler();
|
||||
const firstString = linesGet(curLine).substring(curChar);
|
||||
len += firstString.length;
|
||||
assem.append(firstString);
|
||||
|
@ -2379,20 +1982,20 @@ const followAttributes = (att1, att2, pool) => {
|
|||
if (!att1) return att2;
|
||||
const atts = new Map();
|
||||
att2.replace(/\*([0-9a-z]+)/g, (_, a) => {
|
||||
const [key, val] = pool.getAttrib(exports.parseNum(a));
|
||||
const [key, val] = pool.getAttrib(parseNum(a));
|
||||
atts.set(key, val);
|
||||
return '';
|
||||
});
|
||||
att1.replace(/\*([0-9a-z]+)/g, (_, a) => {
|
||||
const [key, val] = pool.getAttrib(exports.parseNum(a));
|
||||
const [key, val] = pool.getAttrib(parseNum(a));
|
||||
if (atts.has(key) && val <= atts.get(key)) atts.delete(key);
|
||||
return '';
|
||||
});
|
||||
// we've only removed attributes, so they're already sorted
|
||||
const buf = exports.stringAssembler();
|
||||
const buf = new StringAssembler();
|
||||
for (const att of atts) {
|
||||
buf.append('*');
|
||||
buf.append(exports.numToString(pool.putAttrib(att)));
|
||||
buf.append(numToString(pool.putAttrib(att)));
|
||||
}
|
||||
return buf.toString();
|
||||
};
|
|
@ -5,6 +5,11 @@
|
|||
* based on a SkipList
|
||||
*/
|
||||
|
||||
import {RepModel} from "./types/RepModel";
|
||||
import {ChangeSetBuilder} from "./types/ChangeSetBuilder";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
import AttributePool from "./AttributePool";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -20,7 +25,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
exports.buildRemoveRange = (rep, builder, start, end) => {
|
||||
export const buildRemoveRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number,number], end: [number, number]) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
|
@ -32,7 +37,7 @@ exports.buildRemoveRange = (rep, builder, start, end) => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
|
||||
export const buildKeepRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number], end:[number, number], attribs?: Attribute[], pool?: AttributePool) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
|
@ -44,9 +49,25 @@ exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.buildKeepToStartOfRange = (rep, builder, start) => {
|
||||
export const buildKeepToStartOfRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number]) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
|
||||
builder.keep(startLineOffset, start[0]);
|
||||
builder.keep(start[1]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a number from string base 36.
|
||||
*
|
||||
* @param {string} str - string of the number in base 36
|
||||
* @returns {number} number
|
||||
*/
|
||||
export const parseNum = (str: string) => parseInt(str, 36);
|
||||
|
||||
/**
|
||||
* Writes a number in base 36 and puts it in a string.
|
||||
*
|
||||
* @param {number} num - number
|
||||
* @returns {string} string
|
||||
*/
|
||||
export const numToString = (num: number): string => num.toString(36).toLowerCase();
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const {padutils: {warnDeprecated}} = require('./pad_utils');
|
||||
import {padUtils} from './pad_utils'
|
||||
|
||||
/**
|
||||
* Represents a chat message stored in the database and transmitted among users. Plugins can extend
|
||||
|
@ -9,13 +9,24 @@ const {padutils: {warnDeprecated}} = require('./pad_utils');
|
|||
* Supports serialization to JSON.
|
||||
*/
|
||||
class ChatMessage {
|
||||
static fromObject(obj) {
|
||||
|
||||
private text: string|null
|
||||
private authorId: string|null
|
||||
private displayName: string|null
|
||||
private time: number|null
|
||||
static fromObject(obj: ChatMessage) {
|
||||
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept
|
||||
// the old names in case the db record was written by an older version of Etherpad.
|
||||
obj = Object.assign({}, obj); // Don't mutate the caller's object.
|
||||
if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId;
|
||||
if ('userId' in obj && !('authorId' in obj)) { // @ts-ignore
|
||||
obj.authorId = obj.userId;
|
||||
}
|
||||
// @ts-ignore
|
||||
delete obj.userId;
|
||||
if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName;
|
||||
if ('userName' in obj && !('displayName' in obj)) { // @ts-ignore
|
||||
obj.displayName = obj.userName;
|
||||
}
|
||||
// @ts-ignore
|
||||
delete obj.userName;
|
||||
return Object.assign(new ChatMessage(), obj);
|
||||
}
|
||||
|
@ -25,7 +36,7 @@ class ChatMessage {
|
|||
* @param {?string} [authorId] - Initial value of the `authorId` property.
|
||||
* @param {?number} [time] - Initial value of the `time` property.
|
||||
*/
|
||||
constructor(text = null, authorId = null, time = null) {
|
||||
constructor(text: string | null = null, authorId: string | null = null, time: number | null = null) {
|
||||
/**
|
||||
* The raw text of the user's chat message (before any rendering or processing).
|
||||
*
|
||||
|
@ -62,11 +73,11 @@ class ChatMessage {
|
|||
* @type {string}
|
||||
*/
|
||||
get userId() {
|
||||
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
return this.authorId;
|
||||
}
|
||||
set userId(val) {
|
||||
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
this.authorId = val;
|
||||
}
|
||||
|
||||
|
@ -77,11 +88,11 @@ class ChatMessage {
|
|||
* @type {string}
|
||||
*/
|
||||
get userName() {
|
||||
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
return this.displayName;
|
||||
}
|
||||
set userName(val) {
|
||||
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
this.displayName = val;
|
||||
}
|
||||
|
||||
|
@ -89,7 +100,9 @@ class ChatMessage {
|
|||
// doesn't support authorId and displayName.
|
||||
toJSON() {
|
||||
const {authorId, displayName, ...obj} = this;
|
||||
// @ts-ignore
|
||||
obj.userId = authorId;
|
||||
// @ts-ignore
|
||||
obj.userName = displayName;
|
||||
return obj;
|
||||
}
|
73
src/static/js/MergingOpAssembler.ts
Normal file
73
src/static/js/MergingOpAssembler.ts
Normal 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
73
src/static/js/Op.ts
Normal 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);
|
||||
}
|
||||
}
|
21
src/static/js/OpAssembler.ts
Normal file
21
src/static/js/OpAssembler.ts
Normal 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
45
src/static/js/OpIter.ts
Normal 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;
|
||||
}
|
||||
}
|
115
src/static/js/SmartOpAssembler.ts
Normal file
115
src/static/js/SmartOpAssembler.ts
Normal 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;
|
||||
}
|
18
src/static/js/StringAssembler.ts
Normal file
18
src/static/js/StringAssembler.ts
Normal 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
|
||||
}
|
||||
}
|
54
src/static/js/StringIterator.ts
Normal file
54
src/static/js/StringIterator.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -25,12 +25,12 @@
|
|||
// requires: undefined
|
||||
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||
|
||||
const pluginUtils = require('./pluginfw/shared');
|
||||
const ace2_inner = require('ep_etherpad-lite/static/js/ace2_inner')
|
||||
const debugLog = (...args) => {};
|
||||
const cl_plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins')
|
||||
const rJQuery = require('ep_etherpad-lite/static/js/rjquery')
|
||||
const {Cssmanager} = require("./cssmanager");
|
||||
// The inner and outer iframe's locations are about:blank, so relative URLs are relative to that.
|
||||
// Firefox and Chrome seem to do what the developer intends if given a relative URL, but Safari
|
||||
// errors out unless given an absolute URL for a JavaScript-created element.
|
||||
|
@ -298,16 +298,16 @@ const Ace2Editor = function () {
|
|||
innerWindow.Ace2Inner = ace2_inner;
|
||||
innerWindow.plugins = cl_plugins;
|
||||
|
||||
innerWindow.$ = innerWindow.jQuery = rJQuery.jQuery;
|
||||
innerWindow.$ = innerWindow.jQuery = window.$;
|
||||
|
||||
debugLog('Ace2Editor.init() waiting for plugins');
|
||||
/*await new Promise((resolve, reject) => innerWindow.plugins.ensure(
|
||||
(err) => err != null ? reject(err) : resolve()));*/
|
||||
debugLog('Ace2Editor.init() waiting for Ace2Inner.init()');
|
||||
await innerWindow.Ace2Inner.init(info, {
|
||||
inner: makeCSSManager(innerStyle.sheet),
|
||||
outer: makeCSSManager(outerStyle.sheet),
|
||||
parent: makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet),
|
||||
inner: new Cssmanager(innerStyle.sheet),
|
||||
outer: new Cssmanager(outerStyle.sheet),
|
||||
parent: new Cssmanager(document.querySelector('style[title="dynamicsyntax"]').sheet),
|
||||
});
|
||||
debugLog('Ace2Editor.init() Ace2Inner.init() returned');
|
||||
loaded = true;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import linestylefilter from "./linestylefilter";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
* Copyright 2020 John McLear - The Etherpad Foundation.
|
||||
|
@ -18,32 +20,31 @@
|
|||
*/
|
||||
let documentAttributeManager;
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
import AttributeMap from './AttributeMap'
|
||||
const browser = require('./vendors/browser');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import {padUtils as padutils} from './pad_utils'
|
||||
const Ace2Common = require('./ace2_common');
|
||||
const $ = require('./rjquery').$;
|
||||
|
||||
const isNodeText = Ace2Common.isNodeText;
|
||||
const getAssoc = Ace2Common.getAssoc;
|
||||
const setAssoc = Ace2Common.setAssoc;
|
||||
const noop = Ace2Common.noop;
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
import AttributePool from "./AttributePool";
|
||||
|
||||
import Scroll from './scroll'
|
||||
import AttributeManager from "./AttributeManager";
|
||||
import ChangesetTracker from './changesettracker'
|
||||
import SkipList from "./skiplist";
|
||||
import {undoModule, pool as undoModPool, setPool} from './undomodule'
|
||||
|
||||
function Ace2Inner(editorInfo, cssManagers) {
|
||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
||||
const domline = require('./domline').domline;
|
||||
const AttribPool = require('./AttributePool');
|
||||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||
const SkipList = require('./skiplist');
|
||||
const undoModule = require('./undomodule').undoModule;
|
||||
const AttributeManager = require('./AttributeManager');
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
const THE_TAB = ' '; // 4
|
||||
|
@ -126,12 +127,12 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
selFocusAtStart: false,
|
||||
alltext: '',
|
||||
alines: [],
|
||||
apool: new AttribPool(),
|
||||
apool: new AttributePool(),
|
||||
};
|
||||
|
||||
// lines, alltext, alines, and DOM are set up in init()
|
||||
if (undoModule.enabled) {
|
||||
undoModule.apool = rep.apool;
|
||||
setPool(rep.apool)
|
||||
}
|
||||
|
||||
let isEditable = true;
|
||||
|
@ -174,7 +175,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// CCCCCCCCCCCCCCCCCCCC\n
|
||||
// CCCC\n
|
||||
// end[0]: <CCC end[1] CCC>-------\n
|
||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
||||
const builder = Changeset.builder(rep.lines.totalWidth);
|
||||
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
|
||||
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
|
||||
builder.insert(newText, [
|
||||
|
@ -185,7 +186,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
performDocumentApplyChangeset(cs);
|
||||
};
|
||||
|
||||
const changesetTracker = makeChangesetTracker(scheduler, rep.apool, {
|
||||
const changesetTracker = new ChangesetTracker(scheduler, rep.apool, {
|
||||
withCallbacks: (operationName, f) => {
|
||||
inCallStackIfNecessary(operationName, () => {
|
||||
fastIncorp(1);
|
||||
|
@ -497,7 +498,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const importAText = (atext, apoolJsonObj, undoable) => {
|
||||
atext = Changeset.cloneAText(atext);
|
||||
if (apoolJsonObj) {
|
||||
const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
|
||||
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||
atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
|
||||
}
|
||||
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||
|
@ -523,7 +524,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
fastIncorp(8);
|
||||
|
||||
const oldLen = rep.lines.totalWidth();
|
||||
const oldLen = rep.lines.totalWidth;
|
||||
const numLines = rep.lines.length();
|
||||
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
|
||||
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
|
||||
|
@ -827,7 +828,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
const recolorLinesInRange = (startChar, endChar) => {
|
||||
if (endChar <= startChar) return;
|
||||
if (startChar < 0 || startChar >= rep.lines.totalWidth()) return;
|
||||
if (startChar < 0 || startChar >= rep.lines.totalWidth) return;
|
||||
let lineEntry = rep.lines.atOffset(startChar); // rounds down to line boundary
|
||||
let lineStart = rep.lines.offsetOfEntry(lineEntry);
|
||||
let lineIndex = rep.lines.indexOfEntry(lineEntry);
|
||||
|
@ -1271,7 +1272,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
||||
theIndent += THE_TAB;
|
||||
}
|
||||
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
|
||||
const cs = Changeset.builder(rep.lines.totalWidth).keep(
|
||||
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
|
||||
theIndent, [
|
||||
['author', thisAuthor],
|
||||
|
@ -2297,7 +2298,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
// 3-renumber every list item of the same level from the beginning, level 1
|
||||
// IMPORTANT: never skip a level because there imbrication may be arbitrary
|
||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
||||
const builder = Changeset.builder(rep.lines.totalWidth);
|
||||
let loc = [0, 0];
|
||||
const applyNumberList = (line, level) => {
|
||||
// init
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
* @typedef {string} AttributeString
|
||||
*/
|
||||
|
||||
import AttributePool from "./AttributePool";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
/**
|
||||
* Converts an attribute string into a sequence of attribute identifier numbers.
|
||||
*
|
||||
|
@ -28,7 +31,7 @@
|
|||
* appear in `str`.
|
||||
* @returns {Generator<number>}
|
||||
*/
|
||||
exports.decodeAttribString = function* (str) {
|
||||
export const decodeAttribString = function* (str: string): Generator<number> {
|
||||
const re = /\*([0-9a-z]+)|./gy;
|
||||
let match;
|
||||
while ((match = re.exec(str)) != null) {
|
||||
|
@ -38,7 +41,7 @@ exports.decodeAttribString = function* (str) {
|
|||
}
|
||||
};
|
||||
|
||||
const checkAttribNum = (n) => {
|
||||
const checkAttribNum = (n: number|object) => {
|
||||
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
|
||||
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
|
||||
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
|
||||
|
@ -50,7 +53,7 @@ const checkAttribNum = (n) => {
|
|||
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
|
||||
* @returns {AttributeString}
|
||||
*/
|
||||
exports.encodeAttribString = (attribNums) => {
|
||||
export const encodeAttribString = (attribNums: Iterable<number>): string => {
|
||||
let str = '';
|
||||
for (const n of attribNums) {
|
||||
checkAttribNum(n);
|
||||
|
@ -67,7 +70,7 @@ exports.encodeAttribString = (attribNums) => {
|
|||
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
|
||||
* @returns {Generator<Attribute>}
|
||||
*/
|
||||
exports.attribsFromNums = function* (attribNums, pool) {
|
||||
export const attribsFromNums = function* (attribNums: Iterable<number>, pool: AttributePool): Generator<Attribute> {
|
||||
for (const n of attribNums) {
|
||||
checkAttribNum(n);
|
||||
const attrib = pool.getAttrib(n);
|
||||
|
@ -87,7 +90,7 @@ exports.attribsFromNums = function* (attribNums, pool) {
|
|||
* @yields {number} The attribute number of each attribute in `attribs`, in order.
|
||||
* @returns {Generator<number>}
|
||||
*/
|
||||
exports.attribsToNums = function* (attribs, pool) {
|
||||
export const attribsToNums = function* (attribs: Iterable<Attribute>, pool: AttributePool) {
|
||||
for (const attrib of attribs) yield pool.putAttrib(attrib);
|
||||
};
|
||||
|
||||
|
@ -102,8 +105,8 @@ exports.attribsToNums = function* (attribs, pool) {
|
|||
* @yields {Attribute} The attributes identified in `str`, in order.
|
||||
* @returns {Generator<Attribute>}
|
||||
*/
|
||||
exports.attribsFromString = function* (str, pool) {
|
||||
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
|
||||
export const attribsFromString = function* (str: string, pool: AttributePool): Generator<Attribute> {
|
||||
yield* attribsFromNums(decodeAttribString(str), pool);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -116,8 +119,8 @@ exports.attribsFromString = function* (str, pool) {
|
|||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @returns {AttributeString}
|
||||
*/
|
||||
exports.attribsToString =
|
||||
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
|
||||
export const attribsToString =
|
||||
(attribs: Iterable<Attribute>, pool: AttributePool): string => encodeAttribString(attribsToNums(attribs, pool));
|
||||
|
||||
/**
|
||||
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
|
||||
|
@ -126,5 +129,4 @@ exports.attribsToString =
|
|||
* @param {Attribute[]} attribs - Attributes to sort in place.
|
||||
* @returns {Attribute[]} `attribs` (for chaining).
|
||||
*/
|
||||
exports.sort =
|
||||
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
|
||||
export const sort = (attribs: Attribute[]): Attribute[] => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
|
|
@ -26,7 +26,7 @@
|
|||
const msgBlock = document.createElement('blockquote');
|
||||
box.appendChild(msgBlock);
|
||||
msgBlock.style.fontWeight = 'bold';
|
||||
msgBlock.appendChild(document.createTextNode(msg));
|
||||
msgBlock.appendChild(document.createTextNode(msg as string));
|
||||
const loc = document.createElement('p');
|
||||
box.appendChild(loc);
|
||||
loc.appendChild(document.createTextNode(`in ${url}`));
|
||||
|
@ -39,7 +39,7 @@
|
|||
box.appendChild(stackBlock);
|
||||
const stack = document.createElement('pre');
|
||||
stackBlock.appendChild(stack);
|
||||
stack.appendChild(document.createTextNode(err.stack || err.toString()));
|
||||
stack.appendChild(document.createTextNode(err!.stack || err!.toString()));
|
||||
|
||||
if (typeof originalHandler === 'function') originalHandler(...args);
|
||||
};
|
|
@ -6,6 +6,8 @@
|
|||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
import {Cssmanager} from "./cssmanager";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -22,14 +24,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||
|
||||
const domline = require('./domline').domline;
|
||||
const AttribPool = require('./AttributePool');
|
||||
import AttributePool from "./AttributePool";
|
||||
const Changeset = require('./Changeset');
|
||||
const attributes = require('./attributes');
|
||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||
import linestylefilter from './linestylefilter'
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
const _ = require('./underscore');
|
||||
const _ = require('underscore');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import html10n from './vendors/html10n';
|
||||
|
@ -56,7 +58,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
||||
currentDivs: null,
|
||||
// to be filled in once the dom loads
|
||||
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
|
||||
apool: (new AttributePool()).fromJsonable(clientVars.collab_client_vars.apool),
|
||||
alines: Changeset.splitAttributionLines(
|
||||
clientVars.collab_client_vars.initialAttributedText.attribs,
|
||||
clientVars.collab_client_vars.initialAttributedText.text),
|
||||
|
@ -389,7 +391,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
setTimeout(() => this.loadFromQueue(), 10);
|
||||
},
|
||||
handleResponse: (data, start, granularity, callback) => {
|
||||
const pool = (new AttribPool()).fromJsonable(data.apool);
|
||||
const pool = (new AttributePool()).fromJsonable(data.apool);
|
||||
for (let i = 0; i < data.forwardsChangesets.length; i++) {
|
||||
const astart = start + i * granularity - 1; // rev -1 is a blank single line
|
||||
let aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision
|
||||
|
@ -409,13 +411,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
|
||||
if (obj.type === 'NEW_CHANGES') {
|
||||
const changeset = Changeset.moveOpsToNewPool(
|
||||
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
obj.changeset, (new AttributePool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
let changesetBack = Changeset.inverse(
|
||||
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
||||
|
||||
changesetBack = Changeset.moveOpsToNewPool(
|
||||
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
changesetBack, (new AttributePool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
||||
} else if (obj.type === 'NEW_AUTHORDATA') {
|
||||
|
@ -465,7 +467,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
|
||||
BroadcastSlider.onSlider(goToRevisionIfEnabled);
|
||||
|
||||
const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet);
|
||||
const dynamicCSS = new Cssmanager(document.querySelector('style[title="dynamicsyntax"]').sheet);
|
||||
const authorData = {};
|
||||
|
||||
const receiveAuthorData = (newAuthorData) => {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
// These parameters were global, now they are injected. A reference to the
|
||||
// Timeslider controller would probably be more appropriate.
|
||||
const _ = require('./underscore');
|
||||
const _ = require('underscore');
|
||||
const padmodals = require('./pad_modals').padmodals;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
import html10n from './vendors/html10n';
|
||||
|
|
|
@ -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;
|
216
src/static/js/changesettracker.ts
Normal file
216
src/static/js/changesettracker.ts
Normal 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
|
|
@ -16,8 +16,9 @@
|
|||
*/
|
||||
|
||||
const ChatMessage = require('./ChatMessage');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const padcookie = require('./pad_cookie').padcookie;
|
||||
|
||||
import {padUtils as padutils} from "./pad_utils";
|
||||
import padcookie from "./pad_cookie";
|
||||
const Tinycon = require('tinycon/tinycon');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const padeditor = require('./pad_editor').padeditor;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
const _MAX_LIST_LEVEL = 16;
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
import AttributeMap from './AttributeMap'
|
||||
const UNorm = require('unorm');
|
||||
const Changeset = require('./Changeset');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
|
|
@ -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}`,
|
||||
};
|
||||
};
|
72
src/static/js/cssmanager.ts
Normal file
72
src/static/js/cssmanager.ts
Normal 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}`
|
||||
}
|
|
@ -22,10 +22,11 @@
|
|||
// requires: plugins
|
||||
// requires: undefined
|
||||
|
||||
const Security = require('./security');
|
||||
const Security = require('security');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const _ = require('./underscore');
|
||||
const lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
|
||||
const _ = require('underscore');
|
||||
import {lineAttributeMarker} from "./linestylefilter";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {getRandomValues} from 'crypto'
|
||||
|
||||
const randomPadName = () => {
|
||||
// the number of distinct chars (64) is chosen to ensure that the selection will be uniform when
|
||||
// using the PRNG below
|
||||
|
@ -28,8 +30,7 @@ const randomPadName = () => {
|
|||
// make room for 8-bit integer values that span from 0 to 255.
|
||||
const randomarray = new Uint8Array(stringLength);
|
||||
// use browser's PRNG to generate a "unique" sequence
|
||||
const cryptoObj = window.crypto || window.msCrypto; // for IE 11
|
||||
cryptoObj.getRandomValues(randomarray);
|
||||
getRandomValues(randomarray);
|
||||
let randomstring = '';
|
||||
for (let i = 0; i < stringLength; i++) {
|
||||
// instead of writing "Math.floor(randomarray[i]/256*64)"
|
||||
|
@ -42,9 +43,9 @@ const randomPadName = () => {
|
|||
|
||||
$(() => {
|
||||
$('#go2Name').on('submit', () => {
|
||||
const padname = $('#padname').val();
|
||||
const padname = $('#padname').val() as string;
|
||||
if (padname.length > 0) {
|
||||
window.location = `p/${encodeURIComponent(padname.trim())}`;
|
||||
window.location.href = `p/${encodeURIComponent(padname.trim())}`;
|
||||
} else {
|
||||
alert('Please enter a name');
|
||||
}
|
||||
|
@ -52,10 +53,11 @@ $(() => {
|
|||
});
|
||||
|
||||
$('#button').on('click', () => {
|
||||
window.location = `p/${randomPadName()}`;
|
||||
window.location.href = `p/${randomPadName()}`;
|
||||
});
|
||||
|
||||
// start the custom js
|
||||
// @ts-ignore
|
||||
if (typeof window.customStart === 'function') window.customStart();
|
||||
});
|
||||
|
|
@ -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;
|
298
src/static/js/linestylefilter.ts
Normal file
298
src/static/js/linestylefilter.ts
Normal 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';
|
|
@ -37,17 +37,17 @@ const Cookies = require('./pad_utils').Cookies;
|
|||
const chat = require('./chat').chat;
|
||||
const getCollabClient = require('./collab_client').getCollabClient;
|
||||
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
|
||||
const padcookie = require('./pad_cookie').padcookie;
|
||||
import padcookie from "./pad_cookie";
|
||||
const padeditbar = require('./pad_editbar').padeditbar;
|
||||
const padeditor = require('./pad_editor').padeditor;
|
||||
const padimpexp = require('./pad_impexp').padimpexp;
|
||||
const padmodals = require('./pad_modals').padmodals;
|
||||
const padsavedrevs = require('./pad_savedrevs');
|
||||
const paduserlist = require('./pad_userlist').paduserlist;
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import {padUtils as padutils} from "./pad_utils";
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
const randomString = require('./pad_utils').randomString;
|
||||
const socketio = require('./socketio');
|
||||
import connect from './socketio'
|
||||
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
|
@ -222,7 +222,7 @@ const handshake = async () => {
|
|||
|
||||
// padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear
|
||||
// to the proxy/gateway/whatever that this is a pad connection and should be treated as such
|
||||
socket = pad.socket = socketio.connect(exports.baseURL, '/', {
|
||||
socket = pad.socket = connect(exports.baseURL, '/', {
|
||||
query: {padId},
|
||||
reconnectionAttempts: 5,
|
||||
reconnection: true,
|
||||
|
|
|
@ -16,9 +16,12 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
import {Cookies} from './pad_utils'
|
||||
import html10n from "./vendors/html10n";
|
||||
|
||||
class PadCookie {
|
||||
private readonly cookieName_: string
|
||||
|
||||
exports.padcookie = new class {
|
||||
constructor() {
|
||||
this.cookieName_ = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp';
|
||||
}
|
||||
|
@ -31,6 +34,7 @@ exports.padcookie = new class {
|
|||
this.writePrefs_(prefs);
|
||||
// Re-read the saved cookie to test if cookies are enabled.
|
||||
if (this.readPrefs_() == null) {
|
||||
// @ts-ignore
|
||||
$.gritter.add({
|
||||
title: 'Error',
|
||||
text: html10n.get('pad.noCookie'),
|
||||
|
@ -50,15 +54,15 @@ exports.padcookie = new class {
|
|||
}
|
||||
}
|
||||
|
||||
writePrefs_(prefs) {
|
||||
writePrefs_(prefs: object) {
|
||||
Cookies.set(this.cookieName_, JSON.stringify(prefs), {expires: 365 * 100});
|
||||
}
|
||||
|
||||
getPref(prefName) {
|
||||
getPref(prefName: string) {
|
||||
return this.readPrefs_()[prefName];
|
||||
}
|
||||
|
||||
setPref(prefName, value) {
|
||||
setPref(prefName: string, value: string) {
|
||||
const prefs = this.readPrefs_();
|
||||
prefs[prefName] = value;
|
||||
this.writePrefs_(prefs);
|
||||
|
@ -67,4 +71,6 @@ exports.padcookie = new class {
|
|||
clear() {
|
||||
this.writePrefs_({});
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
export default new PadCookie
|
|
@ -24,7 +24,8 @@
|
|||
|
||||
const browser = require('./vendors/browser');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import {padUtils as padutils} from "./pad_utils";
|
||||
|
||||
const padeditor = require('./pad_editor').padeditor;
|
||||
const padsavedrevs = require('./pad_savedrevs');
|
||||
const _ = require('underscore');
|
||||
|
|
|
@ -22,8 +22,9 @@
|
|||
*/
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
const padcookie = require('./pad_cookie').padcookie;
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
|
||||
import padcookie from "./pad_cookie";
|
||||
import {padUtils as padutils} from "./pad_utils";
|
||||
const Ace2Editor = require('./ace').Ace2Editor;
|
||||
import html10n from '../js/vendors/html10n'
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import {padUtils as padutils} from "./pad_utils";
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
import html10n from './vendors/html10n';
|
||||
let myUserInfo = {};
|
||||
|
|
|
@ -22,13 +22,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Security = require('./security');
|
||||
const Security = require('security');
|
||||
import jsCookie, {CookiesStatic} from 'js-cookie'
|
||||
|
||||
/**
|
||||
* Generates a random String with the given length. Is needed to generate the Author, Group,
|
||||
* readonly, session Ids
|
||||
*/
|
||||
const randomString = (len) => {
|
||||
export const randomString = (len?: number) => {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
let randomstring = '';
|
||||
len = len || 20;
|
||||
|
@ -85,13 +86,41 @@ const urlRegex = (() => {
|
|||
'tel',
|
||||
].join('|')}):`;
|
||||
return new RegExp(
|
||||
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
|
||||
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
|
||||
})();
|
||||
|
||||
// https://stackoverflow.com/a/68957976
|
||||
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
|
||||
|
||||
const padutils = {
|
||||
type PadEvent = {
|
||||
which: number
|
||||
}
|
||||
|
||||
type JQueryNode = JQuery<HTMLElement>
|
||||
|
||||
class PadUtils {
|
||||
public urlRegex: RegExp
|
||||
public wordCharRegex: RegExp
|
||||
public warnDeprecatedFlags: {
|
||||
disabledForTestingOnly: boolean,
|
||||
_rl?: {
|
||||
prevs: Map<string, number>,
|
||||
now: () => number,
|
||||
period: number
|
||||
}
|
||||
logger?: any
|
||||
}
|
||||
public globalExceptionHandler: null | any = null;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.warnDeprecatedFlags = {
|
||||
disabledForTestingOnly: false
|
||||
}
|
||||
this.wordCharRegex = wordCharRegex
|
||||
this.urlRegex = urlRegex
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
|
||||
* is using the deprecated function).
|
||||
|
@ -107,41 +136,41 @@ const padutils = {
|
|||
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
|
||||
* logger is set), with a stack trace appended if available.
|
||||
*/
|
||||
warnDeprecated: (...args) => {
|
||||
if (padutils.warnDeprecated.disabledForTestingOnly) return;
|
||||
warnDeprecated = (...args: any[]) => {
|
||||
if (this.warnDeprecatedFlags.disabledForTestingOnly) return;
|
||||
const err = new Error();
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, this.warnDeprecated);
|
||||
err.name = '';
|
||||
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
|
||||
if (typeof err.stack === 'string') {
|
||||
if (padutils.warnDeprecated._rl == null) {
|
||||
padutils.warnDeprecated._rl =
|
||||
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
|
||||
if (this.warnDeprecatedFlags._rl == null) {
|
||||
this.warnDeprecatedFlags._rl =
|
||||
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
|
||||
}
|
||||
const rl = padutils.warnDeprecated._rl;
|
||||
const rl = this.warnDeprecatedFlags._rl;
|
||||
const now = rl.now();
|
||||
const prev = rl.prevs.get(err.stack);
|
||||
if (prev != null && now - prev < rl.period) return;
|
||||
rl.prevs.set(err.stack, now);
|
||||
}
|
||||
if (err.stack) args.push(err.stack);
|
||||
(padutils.warnDeprecated.logger || console).warn(...args);
|
||||
},
|
||||
|
||||
escapeHtml: (x) => Security.escapeHTML(String(x)),
|
||||
uniqueId: () => {
|
||||
(this.warnDeprecatedFlags.logger || console).warn(...args);
|
||||
}
|
||||
escapeHtml = (x: string) => Security.escapeHTML(String(x))
|
||||
uniqueId = () => {
|
||||
const pad = require('./pad').pad; // Sidestep circular dependency
|
||||
// returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits
|
||||
const encodeNum =
|
||||
(n, width) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
|
||||
(n: number, width: number) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
|
||||
return [
|
||||
pad.getClientIp(),
|
||||
encodeNum(+new Date(), 7),
|
||||
encodeNum(Math.floor(Math.random() * 1e9), 4),
|
||||
].join('.');
|
||||
},
|
||||
}
|
||||
|
||||
// e.g. "Thu Jun 18 2009 13:09"
|
||||
simpleDateTime: (date) => {
|
||||
simpleDateTime = (date: string) => {
|
||||
const d = new Date(+date); // accept either number or date
|
||||
const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
|
||||
const month = ([
|
||||
|
@ -162,16 +191,14 @@ const padutils = {
|
|||
const year = d.getFullYear();
|
||||
const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`;
|
||||
return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`;
|
||||
},
|
||||
wordCharRegex,
|
||||
urlRegex,
|
||||
}
|
||||
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
|
||||
findURLs: (text) => {
|
||||
findURLs = (text: string) => {
|
||||
// Copy padutils.urlRegex so that the use of .exec() below (which mutates the RegExp object)
|
||||
// does not break other concurrent uses of padutils.urlRegex.
|
||||
const urlRegex = new RegExp(padutils.urlRegex, 'g');
|
||||
const urlRegex = new RegExp(this.urlRegex, 'g');
|
||||
urlRegex.lastIndex = 0;
|
||||
let urls = null;
|
||||
let urls: [number, string][] | null = null;
|
||||
let execResult;
|
||||
// TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped.
|
||||
while ((execResult = urlRegex.exec(text))) {
|
||||
|
@ -181,18 +208,19 @@ const padutils = {
|
|||
urls.push([startIndex, url]);
|
||||
}
|
||||
return urls;
|
||||
},
|
||||
escapeHtmlWithClickableLinks: (text, target) => {
|
||||
}
|
||||
escapeHtmlWithClickableLinks = (text: string, target: string) => {
|
||||
let idx = 0;
|
||||
const pieces = [];
|
||||
const urls = padutils.findURLs(text);
|
||||
const urls = this.findURLs(text);
|
||||
|
||||
const advanceTo = (i) => {
|
||||
if (i > idx) {
|
||||
pieces.push(Security.escapeHTML(text.substring(idx, i)));
|
||||
idx = i;
|
||||
const advanceTo = (i: number) => {
|
||||
if (i > idx) {
|
||||
pieces.push(Security.escapeHTML(text.substring(idx, i)));
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
};
|
||||
;
|
||||
if (urls) {
|
||||
for (let j = 0; j < urls.length; j++) {
|
||||
const startIndex = urls[j][0];
|
||||
|
@ -206,25 +234,25 @@ const padutils = {
|
|||
// https://mathiasbynens.github.io/rel-noopener/
|
||||
// https://github.com/ether/etherpad-lite/pull/3636
|
||||
pieces.push(
|
||||
'<a ',
|
||||
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
|
||||
'href="',
|
||||
Security.escapeHTMLAttribute(href),
|
||||
'" rel="noreferrer noopener">');
|
||||
'<a ',
|
||||
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
|
||||
'href="',
|
||||
Security.escapeHTMLAttribute(href),
|
||||
'" rel="noreferrer noopener">');
|
||||
advanceTo(startIndex + href.length);
|
||||
pieces.push('</a>');
|
||||
}
|
||||
}
|
||||
advanceTo(text.length);
|
||||
return pieces.join('');
|
||||
},
|
||||
bindEnterAndEscape: (node, onEnter, onEscape) => {
|
||||
}
|
||||
bindEnterAndEscape = (node: JQueryNode, onEnter: Function, onEscape: Function) => {
|
||||
// Use keypress instead of keyup in bindEnterAndEscape. Keyup event is fired on enter in IME
|
||||
// (Input Method Editor), But keypress is not. So, I changed to use keypress instead of keyup.
|
||||
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
|
||||
// 3.6.10, Chrome 6.0.472, Safari 5.0).
|
||||
if (onEnter) {
|
||||
node.on('keypress', (evt) => {
|
||||
node.on('keypress', (evt: { which: number; }) => {
|
||||
if (evt.which === 13) {
|
||||
onEnter(evt);
|
||||
}
|
||||
|
@ -238,13 +266,15 @@ const padutils = {
|
|||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
timediff: (d) => {
|
||||
}
|
||||
|
||||
timediff = (d: number) => {
|
||||
const pad = require('./pad').pad; // Sidestep circular dependency
|
||||
const format = (n, word) => {
|
||||
n = Math.round(n);
|
||||
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
|
||||
};
|
||||
const format = (n: number, word: string) => {
|
||||
n = Math.round(n);
|
||||
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
|
||||
}
|
||||
;
|
||||
d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000);
|
||||
if (d < 60) {
|
||||
return format(d, 'second');
|
||||
|
@ -259,78 +289,89 @@ const padutils = {
|
|||
}
|
||||
d /= 24;
|
||||
return format(d, 'day');
|
||||
},
|
||||
makeAnimationScheduler: (funcToAnimateOneStep, stepTime, stepsAtOnce) => {
|
||||
if (stepsAtOnce === undefined) {
|
||||
stepsAtOnce = 1;
|
||||
}
|
||||
makeAnimationScheduler =
|
||||
(funcToAnimateOneStep: any, stepTime: number, stepsAtOnce?: number) => {
|
||||
if (stepsAtOnce === undefined) {
|
||||
stepsAtOnce = 1;
|
||||
}
|
||||
|
||||
let animationTimer: any = null;
|
||||
|
||||
const scheduleAnimation = () => {
|
||||
if (!animationTimer) {
|
||||
animationTimer = window.setTimeout(() => {
|
||||
animationTimer = null;
|
||||
let n = stepsAtOnce;
|
||||
let moreToDo = true;
|
||||
while (moreToDo && n > 0) {
|
||||
moreToDo = funcToAnimateOneStep();
|
||||
n--;
|
||||
}
|
||||
if (moreToDo) {
|
||||
// more to do
|
||||
scheduleAnimation();
|
||||
}
|
||||
}, stepTime * stepsAtOnce);
|
||||
}
|
||||
};
|
||||
return {scheduleAnimation};
|
||||
}
|
||||
|
||||
let animationTimer = null;
|
||||
makeFieldLabeledWhenEmpty
|
||||
=
|
||||
(field: JQueryNode, labelText: string) => {
|
||||
field = $(field);
|
||||
|
||||
const scheduleAnimation = () => {
|
||||
if (!animationTimer) {
|
||||
animationTimer = window.setTimeout(() => {
|
||||
animationTimer = null;
|
||||
let n = stepsAtOnce;
|
||||
let moreToDo = true;
|
||||
while (moreToDo && n > 0) {
|
||||
moreToDo = funcToAnimateOneStep();
|
||||
n--;
|
||||
}
|
||||
if (moreToDo) {
|
||||
// more to do
|
||||
scheduleAnimation();
|
||||
}
|
||||
}, stepTime * stepsAtOnce);
|
||||
}
|
||||
};
|
||||
return {scheduleAnimation};
|
||||
},
|
||||
makeFieldLabeledWhenEmpty: (field, labelText) => {
|
||||
field = $(field);
|
||||
|
||||
const clear = () => {
|
||||
field.addClass('editempty');
|
||||
field.val(labelText);
|
||||
};
|
||||
field.focus(() => {
|
||||
if (field.hasClass('editempty')) {
|
||||
field.val('');
|
||||
}
|
||||
field.removeClass('editempty');
|
||||
});
|
||||
field.on('blur', () => {
|
||||
if (!field.val()) {
|
||||
clear();
|
||||
}
|
||||
});
|
||||
return {
|
||||
clear,
|
||||
};
|
||||
},
|
||||
getCheckbox: (node) => $(node).is(':checked'),
|
||||
setCheckbox: (node, value) => {
|
||||
if (value) {
|
||||
$(node).attr('checked', 'checked');
|
||||
} else {
|
||||
$(node).prop('checked', false);
|
||||
const clear = () => {
|
||||
field.addClass('editempty');
|
||||
field.val(labelText);
|
||||
}
|
||||
;
|
||||
field.focus(() => {
|
||||
if (field.hasClass('editempty')) {
|
||||
field.val('');
|
||||
}
|
||||
field.removeClass('editempty');
|
||||
});
|
||||
field.on('blur', () => {
|
||||
if (!field.val()) {
|
||||
clear();
|
||||
}
|
||||
});
|
||||
return {
|
||||
clear,
|
||||
};
|
||||
}
|
||||
},
|
||||
bindCheckboxChange: (node, func) => {
|
||||
$(node).on('change', func);
|
||||
},
|
||||
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => {
|
||||
if (c === '.') return '-';
|
||||
return `z${c.charCodeAt(0)}z`;
|
||||
}),
|
||||
decodeUserId: (encodedUserId) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
|
||||
if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') {
|
||||
return String.fromCharCode(Number(cc.slice(1, -1)));
|
||||
} else {
|
||||
return cc;
|
||||
getCheckbox = (node: JQueryNode) => $(node).is(':checked')
|
||||
setCheckbox =
|
||||
(node: JQueryNode, value: string) => {
|
||||
if (value) {
|
||||
$(node).attr('checked', 'checked');
|
||||
} else {
|
||||
$(node).prop('checked', false);
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
bindCheckboxChange =
|
||||
(node: JQueryNode, func: Function) => {
|
||||
// @ts-ignore
|
||||
$(node).on("change", func);
|
||||
}
|
||||
encodeUserId =
|
||||
(userId: string) => userId.replace(/[^a-y0-9]/g, (c) => {
|
||||
if (c === '.') return '-';
|
||||
return `z${c.charCodeAt(0)}z`;
|
||||
})
|
||||
decodeUserId =
|
||||
(encodedUserId: string) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
|
||||
if (cc === '-') {
|
||||
return '.';
|
||||
} else if (cc.charAt(0) === 'z') {
|
||||
return String.fromCharCode(Number(cc.slice(1, -1)));
|
||||
} else {
|
||||
return cc;
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Returns whether a string has the expected format to be used as a secret token identifying an
|
||||
* author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648
|
||||
|
@ -340,109 +381,109 @@ const padutils = {
|
|||
* conditional transformation of a token to a database key in a way that does not allow a
|
||||
* malicious user to impersonate another user).
|
||||
*/
|
||||
isValidAuthorToken: (t) => {
|
||||
isValidAuthorToken = (t: string | object) => {
|
||||
if (typeof t !== 'string' || !t.startsWith('t.')) return false;
|
||||
const v = t.slice(2);
|
||||
return v.length > 0 && base64url.test(v);
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
|
||||
* particular author.
|
||||
*/
|
||||
generateAuthorToken: () => `t.${randomString()}`,
|
||||
};
|
||||
|
||||
let globalExceptionHandler = null;
|
||||
padutils.setupGlobalExceptionHandler = () => {
|
||||
if (globalExceptionHandler == null) {
|
||||
globalExceptionHandler = (e) => {
|
||||
let type;
|
||||
let err;
|
||||
let msg, url, linenumber;
|
||||
if (e instanceof ErrorEvent) {
|
||||
type = 'Uncaught exception';
|
||||
err = e.error || {};
|
||||
({message: msg, filename: url, lineno: linenumber} = e);
|
||||
} else if (e instanceof PromiseRejectionEvent) {
|
||||
type = 'Unhandled Promise rejection';
|
||||
err = e.reason || {};
|
||||
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
|
||||
} else {
|
||||
throw new Error(`unknown event: ${e.toString()}`);
|
||||
}
|
||||
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
|
||||
msg = `${err.name}: ${msg}`;
|
||||
}
|
||||
const errorId = randomString(20);
|
||||
|
||||
let msgAlreadyVisible = false;
|
||||
$('.gritter-item .error-msg').each(function () {
|
||||
if ($(this).text() === msg) {
|
||||
msgAlreadyVisible = true;
|
||||
generateAuthorToken = () => `t.${randomString()}`
|
||||
setupGlobalExceptionHandler = () => {
|
||||
if (this.globalExceptionHandler == null) {
|
||||
this.globalExceptionHandler = (e: any) => {
|
||||
let type;
|
||||
let err;
|
||||
let msg, url, linenumber;
|
||||
if (e instanceof ErrorEvent) {
|
||||
type = 'Uncaught exception';
|
||||
err = e.error || {};
|
||||
({message: msg, filename: url, lineno: linenumber} = e);
|
||||
} else if (e instanceof PromiseRejectionEvent) {
|
||||
type = 'Unhandled Promise rejection';
|
||||
err = e.reason || {};
|
||||
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
|
||||
} else {
|
||||
throw new Error(`unknown event: ${e.toString()}`);
|
||||
}
|
||||
});
|
||||
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
|
||||
msg = `${err.name}: ${msg}`;
|
||||
}
|
||||
const errorId = randomString(20);
|
||||
|
||||
if (!msgAlreadyVisible) {
|
||||
const txt = document.createTextNode.bind(document); // Convenience shorthand.
|
||||
const errorMsg = [
|
||||
$('<p>')
|
||||
let msgAlreadyVisible = false;
|
||||
$('.gritter-item .error-msg').each(function () {
|
||||
if ($(this).text() === msg) {
|
||||
msgAlreadyVisible = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!msgAlreadyVisible) {
|
||||
const txt = document.createTextNode.bind(document); // Convenience shorthand.
|
||||
const errorMsg = [
|
||||
$('<p>')
|
||||
.append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')),
|
||||
$('<p>')
|
||||
$('<p>')
|
||||
.text('If the problem persists, please send this error message to your webmaster:'),
|
||||
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
|
||||
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
|
||||
.append($('<b>').addClass('error-msg').text(msg)).append($('<br>'))
|
||||
.append(txt(`at ${url} at line ${linenumber}`)).append($('<br>'))
|
||||
.append(txt(`ErrorId: ${errorId}`)).append($('<br>'))
|
||||
.append(txt(type)).append($('<br>'))
|
||||
.append(txt(`URL: ${window.location.href}`)).append($('<br>'))
|
||||
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
|
||||
];
|
||||
];
|
||||
|
||||
$.gritter.add({
|
||||
title: 'An error occurred',
|
||||
text: errorMsg,
|
||||
class_name: 'error',
|
||||
position: 'bottom',
|
||||
sticky: true,
|
||||
// @ts-ignore
|
||||
$.gritter.add({
|
||||
title: 'An error occurred',
|
||||
text: errorMsg,
|
||||
class_name: 'error',
|
||||
position: 'bottom',
|
||||
sticky: true,
|
||||
});
|
||||
}
|
||||
|
||||
// send javascript errors to the server
|
||||
$.post('../jserror', {
|
||||
errorInfo: JSON.stringify({
|
||||
errorId,
|
||||
type,
|
||||
msg,
|
||||
url: window.location.href,
|
||||
source: url,
|
||||
linenumber,
|
||||
userAgent: navigator.userAgent,
|
||||
stack: err.stack,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// send javascript errors to the server
|
||||
$.post('../jserror', {
|
||||
errorInfo: JSON.stringify({
|
||||
errorId,
|
||||
type,
|
||||
msg,
|
||||
url: window.location.href,
|
||||
source: url,
|
||||
linenumber,
|
||||
userAgent: navigator.userAgent,
|
||||
stack: err.stack,
|
||||
}),
|
||||
});
|
||||
};
|
||||
window.onerror = null; // Clear any pre-existing global error handler.
|
||||
window.addEventListener('error', globalExceptionHandler);
|
||||
window.addEventListener('unhandledrejection', globalExceptionHandler);
|
||||
};
|
||||
window.onerror = null; // Clear any pre-existing global error handler.
|
||||
window.addEventListener('error', this.globalExceptionHandler);
|
||||
window.addEventListener('unhandledrejection', this.globalExceptionHandler);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
padutils.binarySearch = require('./ace2_common').binarySearch;
|
||||
binarySearch = require('./ace2_common').binarySearch
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/42660748
|
||||
const inThirdPartyIframe = () => {
|
||||
try {
|
||||
return (!window.top.location.hostname);
|
||||
return (!window.top!.location.hostname);
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export let Cookies: CookiesStatic<string>
|
||||
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
||||
// window object.
|
||||
if (typeof window !== 'undefined') {
|
||||
exports.Cookies = require('js-cookie').withAttributes({
|
||||
Cookies = jsCookie.withAttributes({
|
||||
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
||||
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
||||
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
||||
|
@ -455,5 +496,5 @@ if (typeof window !== 'undefined') {
|
|||
secure: window.location.protocol === 'https:',
|
||||
});
|
||||
}
|
||||
exports.randomString = randomString;
|
||||
exports.padutils = padutils;
|
||||
|
||||
export const padUtils = new PadUtils()
|
|
@ -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;
|
|
@ -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');
|
|
@ -1,5 +1,3 @@
|
|||
'use strict';
|
||||
|
||||
// Specific hash to display the skin variants builder popup
|
||||
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
|
||||
$('#skin-variants').addClass('popup-show');
|
||||
|
@ -22,7 +20,7 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
|
|||
|
||||
domsToUpdate.forEach((el) => { el.removeClass('full-width-editor'); });
|
||||
|
||||
const newClasses = [];
|
||||
const newClasses:string[] = [];
|
||||
$('select.skin-variant-color').each(function () {
|
||||
newClasses.push(`${$(this).val()}-${$(this).data('container')}`);
|
||||
});
|
||||
|
@ -35,7 +33,8 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
|
|||
|
||||
// run on init
|
||||
const updateCheckboxFromSkinClasses = () => {
|
||||
$('html').attr('class').split(' ').forEach((classItem) => {
|
||||
const htmlTag = $('html')
|
||||
htmlTag.attr('class')!.split(' ').forEach((classItem) => {
|
||||
const container = classItem.substring(classItem.lastIndexOf('-') + 1, classItem.length);
|
||||
if (containers.indexOf(container) > -1) {
|
||||
const color = classItem.substring(0, classItem.lastIndexOf('-'));
|
||||
|
@ -43,7 +42,7 @@ if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') {
|
|||
}
|
||||
});
|
||||
|
||||
$('#skin-variant-full-width').prop('checked', $('html').hasClass('full-width-editor'));
|
||||
$('#skin-variant-full-width').prop('checked', htmlTag.hasClass('full-width-editor'));
|
||||
};
|
||||
|
||||
$('.skin-variant').on('change', () => {
|
|
@ -22,10 +22,24 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const _entryWidth = (e) => (e && e.width) || 0;
|
||||
const _entryWidth = (e: Entry) => (e && e.width) || 0;
|
||||
|
||||
type Entry = {
|
||||
key: string,
|
||||
value: string
|
||||
width: number
|
||||
}
|
||||
|
||||
class Node {
|
||||
constructor(entry, levels = 0, downSkips = 1, downSkipWidths = 0) {
|
||||
public key: string|null
|
||||
readonly entry: Entry|null
|
||||
levels: number
|
||||
upPtrs: Node[]
|
||||
downPtrs: Node[]
|
||||
downSkips: number[]
|
||||
readonly downSkipWidths: number[]
|
||||
|
||||
constructor(entry: Entry|null, levels = 0, downSkips: number|null = 1, downSkipWidths:number|null = 0) {
|
||||
this.key = entry != null ? entry.key : null;
|
||||
this.entry = entry;
|
||||
this.levels = levels;
|
||||
|
@ -37,9 +51,9 @@ class Node {
|
|||
|
||||
propagateWidthChange() {
|
||||
const oldWidth = this.downSkipWidths[0];
|
||||
const newWidth = _entryWidth(this.entry);
|
||||
const newWidth = _entryWidth(this.entry!);
|
||||
const widthChange = newWidth - oldWidth;
|
||||
let n = this;
|
||||
let n: Node = this;
|
||||
let lvl = 0;
|
||||
while (lvl < n.levels) {
|
||||
n.downSkipWidths[lvl] += widthChange;
|
||||
|
@ -57,17 +71,23 @@ class Node {
|
|||
// is still valid and points to the same index in the skiplist. Other operations with other points
|
||||
// invalidate this point.
|
||||
class Point {
|
||||
constructor(skipList, loc) {
|
||||
this._skipList = skipList;
|
||||
private skipList: SkipList
|
||||
private readonly loc: number
|
||||
private readonly idxs: number[]
|
||||
private readonly nodes: Node[]
|
||||
private widthSkips: number[]
|
||||
|
||||
constructor(skipList: SkipList, loc: number) {
|
||||
this.skipList = skipList;
|
||||
this.loc = loc;
|
||||
const numLevels = this._skipList._start.levels;
|
||||
const numLevels = this.skipList.start.levels;
|
||||
let lvl = numLevels - 1;
|
||||
let i = -1;
|
||||
let ws = 0;
|
||||
const nodes = new Array(numLevels);
|
||||
const idxs = new Array(numLevels);
|
||||
const widthSkips = new Array(numLevels);
|
||||
nodes[lvl] = this._skipList._start;
|
||||
const nodes: Node[] = new Array(numLevels);
|
||||
const idxs: number[] = new Array(numLevels);
|
||||
const widthSkips: number[] = new Array(numLevels);
|
||||
nodes[lvl] = this.skipList.start;
|
||||
idxs[lvl] = -1;
|
||||
widthSkips[lvl] = 0;
|
||||
while (lvl >= 0) {
|
||||
|
@ -94,9 +114,9 @@ class Point {
|
|||
return `Point(${this.loc})`;
|
||||
}
|
||||
|
||||
insert(entry) {
|
||||
insert(entry: Entry) {
|
||||
if (entry.key == null) throw new Error('entry.key must not be null');
|
||||
if (this._skipList.containsKey(entry.key)) {
|
||||
if (this.skipList.containsKey(entry.key)) {
|
||||
throw new Error(`an entry with key ${entry.key} already exists`);
|
||||
}
|
||||
|
||||
|
@ -115,14 +135,14 @@ class Point {
|
|||
if (lvl === pNodes.length) {
|
||||
// assume we have just passed the end of this.nodes, and reached one level greater
|
||||
// than the skiplist currently supports
|
||||
pNodes[lvl] = this._skipList._start;
|
||||
pNodes[lvl] = this.skipList.start;
|
||||
pIdxs[lvl] = -1;
|
||||
this._skipList._start.levels++;
|
||||
this._skipList._end.levels++;
|
||||
this._skipList._start.downPtrs[lvl] = this._skipList._end;
|
||||
this._skipList._end.upPtrs[lvl] = this._skipList._start;
|
||||
this._skipList._start.downSkips[lvl] = this._skipList._keyToNodeMap.size + 1;
|
||||
this._skipList._start.downSkipWidths[lvl] = this._skipList._totalWidth;
|
||||
this.skipList.start.levels++;
|
||||
this.skipList.end.levels++;
|
||||
this.skipList.start.downPtrs[lvl] = this.skipList.end;
|
||||
this.skipList.end.upPtrs[lvl] = this.skipList.start;
|
||||
this.skipList.start.downSkips[lvl] = this.skipList.keyToNodeMap.size + 1;
|
||||
this.skipList.start.downSkipWidths[lvl] = this.skipList.totalWidth;
|
||||
this.widthSkips[lvl] = 0;
|
||||
}
|
||||
const me = newNode;
|
||||
|
@ -146,13 +166,13 @@ class Point {
|
|||
up.downSkips[lvl]++;
|
||||
up.downSkipWidths[lvl] += newWidth;
|
||||
}
|
||||
this._skipList._keyToNodeMap.set(newNode.key, newNode);
|
||||
this._skipList._totalWidth += newWidth;
|
||||
this.skipList.keyToNodeMap.set(newNode.key as string, newNode);
|
||||
this.skipList.totalWidth += newWidth;
|
||||
}
|
||||
|
||||
delete() {
|
||||
const elem = this.nodes[0].downPtrs[0];
|
||||
const elemWidth = _entryWidth(elem.entry);
|
||||
const elemWidth = _entryWidth(elem.entry!);
|
||||
for (let i = 0; i < this.nodes.length; i++) {
|
||||
if (i < elem.levels) {
|
||||
const up = elem.upPtrs[i];
|
||||
|
@ -169,8 +189,8 @@ class Point {
|
|||
up.downSkipWidths[i] -= elemWidth;
|
||||
}
|
||||
}
|
||||
this._skipList._keyToNodeMap.delete(elem.key);
|
||||
this._skipList._totalWidth -= elemWidth;
|
||||
this.skipList.keyToNodeMap.delete(elem.key as string);
|
||||
this.skipList.totalWidth -= elemWidth;
|
||||
}
|
||||
|
||||
getNode() {
|
||||
|
@ -183,20 +203,26 @@ class Point {
|
|||
* property that is a string.
|
||||
*/
|
||||
class SkipList {
|
||||
start: Node
|
||||
end: Node
|
||||
totalWidth: number
|
||||
keyToNodeMap: Map<string, Node>
|
||||
|
||||
|
||||
constructor() {
|
||||
// if there are N elements in the skiplist, "start" is element -1 and "end" is element N
|
||||
this._start = new Node(null, 1);
|
||||
this._end = new Node(null, 1, null, null);
|
||||
this._totalWidth = 0;
|
||||
this._keyToNodeMap = new Map();
|
||||
this._start.downPtrs[0] = this._end;
|
||||
this._end.upPtrs[0] = this._start;
|
||||
this.start = new Node(null, 1);
|
||||
this.end = new Node(null, 1, null, null);
|
||||
this.totalWidth = 0;
|
||||
this.keyToNodeMap = new Map();
|
||||
this.start.downPtrs[0] = this.end;
|
||||
this.end.upPtrs[0] = this.start;
|
||||
}
|
||||
|
||||
_getNodeAtOffset(targetOffset) {
|
||||
_getNodeAtOffset(targetOffset: number) {
|
||||
let i = 0;
|
||||
let n = this._start;
|
||||
let lvl = this._start.levels - 1;
|
||||
let n = this.start;
|
||||
let lvl = this.start.levels - 1;
|
||||
while (lvl >= 0 && n.downPtrs[lvl]) {
|
||||
while (n.downPtrs[lvl] && (i + n.downSkipWidths[lvl] <= targetOffset)) {
|
||||
i += n.downSkipWidths[lvl];
|
||||
|
@ -204,17 +230,17 @@ class SkipList {
|
|||
}
|
||||
lvl--;
|
||||
}
|
||||
if (n === this._start) return (this._start.downPtrs[0] || null);
|
||||
if (n === this._end) {
|
||||
return targetOffset === this._totalWidth ? (this._end.upPtrs[0] || null) : null;
|
||||
if (n === this.start) return (this.start.downPtrs[0] || null);
|
||||
if (n === this.end) {
|
||||
return targetOffset === this.totalWidth ? (this.end.upPtrs[0] || null) : null;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
_getNodeIndex(node, byWidth) {
|
||||
_getNodeIndex(node: Node, byWidth?: boolean) {
|
||||
let dist = (byWidth ? 0 : -1);
|
||||
let n = node;
|
||||
while (n !== this._start) {
|
||||
while (n !== this.start) {
|
||||
const lvl = n.levels - 1;
|
||||
n = n.upPtrs[lvl];
|
||||
if (byWidth) dist += n.downSkipWidths[lvl];
|
||||
|
@ -226,14 +252,14 @@ class SkipList {
|
|||
// Returns index of first entry such that entryFunc(entry) is truthy,
|
||||
// or length() if no such entry. Assumes all falsy entries come before
|
||||
// all truthy entries.
|
||||
search(entryFunc) {
|
||||
let low = this._start;
|
||||
let lvl = this._start.levels - 1;
|
||||
search(entryFunc: Function) {
|
||||
let low = this.start;
|
||||
let lvl = this.start.levels - 1;
|
||||
let lowIndex = -1;
|
||||
|
||||
const f = (node) => {
|
||||
if (node === this._start) return false;
|
||||
else if (node === this._end) return true;
|
||||
const f = (node: Node) => {
|
||||
if (node === this.start) return false;
|
||||
else if (node === this.end) return true;
|
||||
else return entryFunc(node.entry);
|
||||
};
|
||||
|
||||
|
@ -249,20 +275,20 @@ class SkipList {
|
|||
return lowIndex + 1;
|
||||
}
|
||||
|
||||
length() { return this._keyToNodeMap.size; }
|
||||
length() { return this.keyToNodeMap.size; }
|
||||
|
||||
atIndex(i) {
|
||||
atIndex(i: number) {
|
||||
if (i < 0) console.warn(`atIndex(${i})`);
|
||||
if (i >= this._keyToNodeMap.size) console.warn(`atIndex(${i}>=${this._keyToNodeMap.size})`);
|
||||
if (i >= this.keyToNodeMap.size) console.warn(`atIndex(${i}>=${this.keyToNodeMap.size})`);
|
||||
return (new Point(this, i)).getNode().entry;
|
||||
}
|
||||
|
||||
// differs from Array.splice() in that new elements are in an array, not varargs
|
||||
splice(start, deleteCount, newEntryArray) {
|
||||
splice(start: number, deleteCount: number, newEntryArray: Entry[]) {
|
||||
if (start < 0) console.warn(`splice(${start}, ...)`);
|
||||
if (start + deleteCount > this._keyToNodeMap.size) {
|
||||
console.warn(`splice(${start}, ${deleteCount}, ...), N=${this._keyToNodeMap.size}`);
|
||||
console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this._keyToNodeMap.size);
|
||||
if (start + deleteCount > this.keyToNodeMap.size) {
|
||||
console.warn(`splice(${start}, ${deleteCount}, ...), N=${this.keyToNodeMap.size}`);
|
||||
console.warn('%s %s %s', typeof start, typeof deleteCount, typeof this.keyToNodeMap.size);
|
||||
console.trace();
|
||||
}
|
||||
|
||||
|
@ -275,56 +301,55 @@ class SkipList {
|
|||
}
|
||||
}
|
||||
|
||||
next(entry) { return this._keyToNodeMap.get(entry.key).downPtrs[0].entry || null; }
|
||||
prev(entry) { return this._keyToNodeMap.get(entry.key).upPtrs[0].entry || null; }
|
||||
push(entry) { this.splice(this._keyToNodeMap.size, 0, [entry]); }
|
||||
next(entry: Entry) { return this.keyToNodeMap.get(entry.key)!.downPtrs[0].entry || null; }
|
||||
prev(entry: Entry) { return this.keyToNodeMap.get(entry.key)!.upPtrs[0].entry || null; }
|
||||
push(entry: Entry) { this.splice(this.keyToNodeMap.size, 0, [entry]); }
|
||||
|
||||
slice(start, end) {
|
||||
slice(start: number, end: number) {
|
||||
// act like Array.slice()
|
||||
if (start === undefined) start = 0;
|
||||
else if (start < 0) start += this._keyToNodeMap.size;
|
||||
if (end === undefined) end = this._keyToNodeMap.size;
|
||||
else if (end < 0) end += this._keyToNodeMap.size;
|
||||
else if (start < 0) start += this.keyToNodeMap.size;
|
||||
if (end === undefined) end = this.keyToNodeMap.size;
|
||||
else if (end < 0) end += this.keyToNodeMap.size;
|
||||
|
||||
if (start < 0) start = 0;
|
||||
if (start > this._keyToNodeMap.size) start = this._keyToNodeMap.size;
|
||||
if (start > this.keyToNodeMap.size) start = this.keyToNodeMap.size;
|
||||
if (end < 0) end = 0;
|
||||
if (end > this._keyToNodeMap.size) end = this._keyToNodeMap.size;
|
||||
if (end > this.keyToNodeMap.size) end = this.keyToNodeMap.size;
|
||||
|
||||
if (end <= start) return [];
|
||||
let n = this.atIndex(start);
|
||||
const array = [n];
|
||||
for (let i = 1; i < (end - start); i++) {
|
||||
n = this.next(n);
|
||||
n = this.next(n!);
|
||||
array.push(n);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
atKey(key) { return this._keyToNodeMap.get(key).entry; }
|
||||
indexOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key)); }
|
||||
indexOfEntry(entry) { return this.indexOfKey(entry.key); }
|
||||
containsKey(key) { return this._keyToNodeMap.has(key); }
|
||||
atKey(key: string) { return this.keyToNodeMap.get(key)!.entry; }
|
||||
indexOfKey(key: string) { return this._getNodeIndex(this.keyToNodeMap.get(key)!); }
|
||||
indexOfEntry(entry: Entry) { return this.indexOfKey(entry.key); }
|
||||
containsKey(key: string) { return this.keyToNodeMap.has(key); }
|
||||
// gets the last entry starting at or before the offset
|
||||
atOffset(offset) { return this._getNodeAtOffset(offset).entry; }
|
||||
keyAtOffset(offset) { return this.atOffset(offset).key; }
|
||||
offsetOfKey(key) { return this._getNodeIndex(this._keyToNodeMap.get(key), true); }
|
||||
offsetOfEntry(entry) { return this.offsetOfKey(entry.key); }
|
||||
setEntryWidth(entry, width) {
|
||||
atOffset(offset: number) { return this._getNodeAtOffset(offset)!.entry; }
|
||||
keyAtOffset(offset: number) { return this.atOffset(offset)!.key; }
|
||||
offsetOfKey(key: string) { return this._getNodeIndex(this.keyToNodeMap.get(key)!, true); }
|
||||
offsetOfEntry(entry: Entry) { return this.offsetOfKey(entry.key); }
|
||||
setEntryWidth(entry: Entry, width: number) {
|
||||
entry.width = width;
|
||||
this._totalWidth += this._keyToNodeMap.get(entry.key).propagateWidthChange();
|
||||
this.totalWidth += this.keyToNodeMap.get(entry.key)!.propagateWidthChange();
|
||||
}
|
||||
totalWidth() { return this._totalWidth; }
|
||||
offsetOfIndex(i) {
|
||||
offsetOfIndex(i: number) {
|
||||
if (i < 0) return 0;
|
||||
if (i >= this._keyToNodeMap.size) return this._totalWidth;
|
||||
return this.offsetOfEntry(this.atIndex(i));
|
||||
if (i >= this.keyToNodeMap.size) return this.totalWidth;
|
||||
return this.offsetOfEntry(this.atIndex(i)!);
|
||||
}
|
||||
indexOfOffset(offset) {
|
||||
indexOfOffset(offset: number) {
|
||||
if (offset <= 0) return 0;
|
||||
if (offset >= this._totalWidth) return this._keyToNodeMap.size;
|
||||
return this.indexOfEntry(this.atOffset(offset));
|
||||
if (offset >= this.totalWidth) return this.keyToNodeMap.size;
|
||||
return this.indexOfEntry(this.atOffset(offset)!);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SkipList;
|
||||
export default SkipList
|
|
@ -1,4 +1,5 @@
|
|||
import io from 'socket.io-client';
|
||||
import {Socket} from "socket.io";
|
||||
|
||||
/**
|
||||
* Creates a socket.io connection.
|
||||
|
@ -9,14 +10,14 @@ import io from 'socket.io-client';
|
|||
* https://socket.io/docs/v2/client-api/#new-Manager-url-options
|
||||
* @return socket.io Socket object
|
||||
*/
|
||||
const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
|
||||
const connect = (etherpadBaseUrl: string, namespace = '/', options = {}): Socket => {
|
||||
// The API for socket.io's io() function is awkward. The documentation says that the first
|
||||
// argument is a URL, but it is not the URL of the socket.io endpoint. The URL's path part is used
|
||||
// as the name of the socket.io namespace to join, and the rest of the URL (including query
|
||||
// parameters, if present) is combined with the `path` option (which defaults to '/socket.io', but
|
||||
// is overridden here to allow users to host Etherpad at something like '/etherpad') to get the
|
||||
// URL of the socket.io endpoint.
|
||||
const baseUrl = new URL(etherpadBaseUrl, window.location);
|
||||
const baseUrl = new URL(etherpadBaseUrl, window.location.href);
|
||||
const socketioUrl = new URL('socket.io', baseUrl);
|
||||
const namespaceUrl = new URL(namespace, new URL('/', baseUrl));
|
||||
|
||||
|
@ -27,7 +28,7 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
|
|||
};
|
||||
socketOptions = Object.assign(options, socketOptions);
|
||||
|
||||
const socket = io(namespaceUrl.href, socketOptions);
|
||||
const socket = io(namespaceUrl.href, socketOptions) as unknown as Socket;
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.log('Error connecting to pad', error);
|
||||
|
@ -41,8 +42,8 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => {
|
|||
return socket;
|
||||
};
|
||||
|
||||
if (typeof exports === 'object') {
|
||||
exports.connect = connect;
|
||||
} else {
|
||||
window.socketio = {connect};
|
||||
}
|
||||
|
||||
export default connect
|
||||
|
||||
// @ts-ignore
|
||||
window.socketio = {connect};
|
|
@ -24,20 +24,28 @@
|
|||
|
||||
// These jQuery things should create local references, but for now `require()`
|
||||
// assigns to the global `$` and augments it with plugins.
|
||||
require('./vendors/jquery');
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
const randomString = require('./pad_utils').randomString;
|
||||
import {Cookies} from "./pad_utils";
|
||||
import {randomString, padUtils as padutils} from "./pad_utils";
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const socketio = require('./socketio');
|
||||
import connect from './socketio'
|
||||
import html10n from '../js/vendors/html10n'
|
||||
let token, padId, exportLinks, socket, changesetLoader, BroadcastSlider;
|
||||
import {Socket} from "socket.io";
|
||||
import {ClientVarMessage, SocketIOMessage} from "./types/SocketIOMessage";
|
||||
import {Func} from "mocha";
|
||||
|
||||
const init = () => {
|
||||
type ChangeSetLoader = {
|
||||
handleMessageFromServer(msg: ClientVarMessage): void
|
||||
}
|
||||
|
||||
|
||||
export let token: string, padId: string, exportLinks: JQuery<HTMLElement>, socket: Socket<any, any>, changesetLoader: ChangeSetLoader, BroadcastSlider: any;
|
||||
|
||||
export const init = () => {
|
||||
padutils.setupGlobalExceptionHandler();
|
||||
$(document).ready(() => {
|
||||
// start the custom js
|
||||
// @ts-ignore
|
||||
if (typeof customStart === 'function') customStart(); // eslint-disable-line no-undef
|
||||
|
||||
// get the padId out of the url
|
||||
|
@ -48,13 +56,13 @@ const init = () => {
|
|||
document.title = `${padId.replace(/_+/g, ' ')} | ${document.title}`;
|
||||
|
||||
// ensure we have a token
|
||||
token = Cookies.get('token');
|
||||
token = Cookies.get('token')!;
|
||||
if (token == null) {
|
||||
token = `t.${randomString()}`;
|
||||
Cookies.set('token', token, {expires: 60});
|
||||
}
|
||||
|
||||
socket = socketio.connect(exports.baseURL, '/', {query: {padId}});
|
||||
socket = connect(baseURL, '/', {query: {padId}});
|
||||
|
||||
// send the ready message once we're connected
|
||||
socket.on('connect', () => {
|
||||
|
@ -65,11 +73,11 @@ const init = () => {
|
|||
BroadcastSlider.showReconnectUI();
|
||||
// The socket.io client will automatically try to reconnect for all reasons other than "io
|
||||
// server disconnect".
|
||||
if (reason === 'io server disconnect') socket.connect();
|
||||
console.log("Disconnected")
|
||||
});
|
||||
|
||||
// route the incoming messages
|
||||
socket.on('message', (message) => {
|
||||
socket.on('message', (message: ClientVarMessage) => {
|
||||
if (message.type === 'CLIENT_VARS') {
|
||||
handleClientVars(message);
|
||||
} else if (message.accessStatus) {
|
||||
|
@ -85,16 +93,12 @@ const init = () => {
|
|||
$('button#forcereconnect').on('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
exports.socket = socket; // make the socket available
|
||||
exports.BroadcastSlider = BroadcastSlider; // Make the slider available
|
||||
|
||||
hooks.aCallAll('postTimesliderInit');
|
||||
});
|
||||
};
|
||||
|
||||
// sends a message over the socket
|
||||
const sendSocketMsg = (type, data) => {
|
||||
const sendSocketMsg = (type: string, data: Object) => {
|
||||
socket.emit("message", {
|
||||
component: 'pad', // FIXME: Remove this stupidity!
|
||||
type,
|
||||
|
@ -105,9 +109,9 @@ const sendSocketMsg = (type, data) => {
|
|||
});
|
||||
};
|
||||
|
||||
const fireWhenAllScriptsAreLoaded = [];
|
||||
const fireWhenAllScriptsAreLoaded: Function[] = [];
|
||||
|
||||
const handleClientVars = (message) => {
|
||||
const handleClientVars = (message: ClientVarMessage) => {
|
||||
// save the client Vars
|
||||
window.clientVars = message.data;
|
||||
|
||||
|
@ -140,13 +144,15 @@ const handleClientVars = (message) => {
|
|||
const baseURI = document.location.pathname;
|
||||
|
||||
// change export urls when the slider moves
|
||||
BroadcastSlider.onSlider((revno) => {
|
||||
BroadcastSlider.onSlider((revno: number) => {
|
||||
// exportLinks is a jQuery Array, so .each is allowed.
|
||||
exportLinks.each(function () {
|
||||
// Modified from regular expression to fix:
|
||||
// https://github.com/ether/etherpad-lite/issues/4071
|
||||
// Where a padId that was numeric would create the wrong export link
|
||||
// @ts-ignore
|
||||
if (this.href) {
|
||||
// @ts-ignore
|
||||
const type = this.href.split('export/')[1];
|
||||
let href = baseURI.split('timeslider')[0];
|
||||
href += `${revno}/export/${type}`;
|
||||
|
@ -159,7 +165,7 @@ const handleClientVars = (message) => {
|
|||
for (let i = 0; i < fireWhenAllScriptsAreLoaded.length; i++) {
|
||||
fireWhenAllScriptsAreLoaded[i]();
|
||||
}
|
||||
$('#ui-slider-handle').css('left', $('#ui-slider-bar').width() - 2);
|
||||
$('#ui-slider-handle').css('left', $('#ui-slider-bar').width()! - 2);
|
||||
|
||||
// Translate some strings where we only want to set the title not the actual values
|
||||
$('#playpause_button_icon').attr('title', html10n.get('timeslider.playPause'));
|
||||
|
@ -168,9 +174,13 @@ const handleClientVars = (message) => {
|
|||
|
||||
// font family change
|
||||
$('#viewfontmenu').on('change', function () {
|
||||
// @ts-ignore
|
||||
$('#innerdocbody').css('font-family', $(this).val() || '');
|
||||
});
|
||||
};
|
||||
|
||||
exports.baseURL = '';
|
||||
exports.init = init;
|
||||
export let baseURL = ''
|
||||
|
||||
export const setBaseURl = (url: string)=>{
|
||||
baseURL = url
|
||||
}
|
1
src/static/js/types/Attribute.ts
Normal file
1
src/static/js/types/Attribute.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type Attribute = [string, string]
|
7
src/static/js/types/ChangeSetBuilder.ts
Normal file
7
src/static/js/types/ChangeSetBuilder.ts
Normal 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
|
||||
}
|
1
src/static/js/types/RangePos.ts
Normal file
1
src/static/js/types/RangePos.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type RangePos = [number, number]
|
|
@ -1,13 +1,21 @@
|
|||
import AttributePool from "../AttributePool";
|
||||
import {RangePos} from "./RangePos";
|
||||
|
||||
export type RepModel = {
|
||||
lines: {
|
||||
atIndex: (num: number)=>RepNode,
|
||||
offsetOfIndex: (range: number)=>number,
|
||||
search: (filter: (e: RepNode)=>boolean)=>number,
|
||||
length: ()=>number
|
||||
length: ()=>number,
|
||||
totalWidth: ()=>number
|
||||
}
|
||||
selStart: RangePos,
|
||||
selEnd: RangePos,
|
||||
selFocusAtStart: boolean,
|
||||
apool: AttributePool,
|
||||
alines: {
|
||||
[key:string]: any
|
||||
}
|
||||
selStart: number[],
|
||||
selEnd: number[],
|
||||
selFocusAtStart: boolean
|
||||
}
|
||||
|
||||
export type Position = {
|
||||
|
@ -22,7 +30,8 @@ export type RepNode = {
|
|||
length: number,
|
||||
lastChild: RepNode,
|
||||
offsetHeight: number,
|
||||
offsetTop: number
|
||||
offsetTop: number,
|
||||
text: string
|
||||
}
|
||||
|
||||
export type WindowElementWithScrolling = HTMLIFrameElement & {
|
||||
|
|
13
src/static/js/types/SocketIOMessage.ts
Normal file
13
src/static/js/types/SocketIOMessage.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export type SocketIOMessage = {
|
||||
type: string
|
||||
accessStatus: string
|
||||
}
|
||||
|
||||
|
||||
export type ClientVarMessage = {
|
||||
data: {
|
||||
sessionRefreshInterval: number
|
||||
}
|
||||
type: string
|
||||
accessStatus: string
|
||||
}
|
6
src/static/js/types/Window.ts
Normal file
6
src/static/js/types/Window.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
declare global {
|
||||
interface Window {
|
||||
clientVars: any;
|
||||
$: any
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = require('underscore');
|
|
@ -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
275
src/static/js/undomodule.ts
Normal 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()
|
||||
|
|
@ -7,13 +7,13 @@
|
|||
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server
|
||||
// sends the CLIENT_VARS message.
|
||||
randomVersionString: <%-JSON.stringify(settings.randomVersionString)%>,
|
||||
};
|
||||
}
|
||||
|
||||
// Allow other frames to access this frame's modules.
|
||||
//window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie');
|
||||
|
||||
const basePath = new URL('..', window.location.href).pathname;
|
||||
window.$ = window.jQuery = require('../../src/static/js/rjquery').jQuery;
|
||||
window.$ = window.jQuery = require('../../src/static/js/vendors/jquery');
|
||||
window.browser = require('../../src/static/js/vendors/browser');
|
||||
const pad = require('../../src/static/js/pad');
|
||||
pad.baseURL = basePath;
|
||||
|
@ -25,8 +25,8 @@
|
|||
window.chat = require('../../src/static/js/chat').chat;
|
||||
window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar;
|
||||
window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp;
|
||||
require('../../src/static/js/skin_variants');
|
||||
require('../../src/static/js/basic_error_handler')
|
||||
await import('../../src/static/js/skin_variants')
|
||||
await import('../../src/static/js/basic_error_handler')
|
||||
|
||||
window.plugins.baseURL = basePath;
|
||||
await window.plugins.update(new Map([
|
||||
|
|
|
@ -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');
|
||||
})();
|
|
@ -1,4 +1,7 @@
|
|||
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt
|
||||
|
||||
import {setBaseURl} from "ep_etherpad-lite/static/js/timeslider";
|
||||
|
||||
window.clientVars = {
|
||||
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the
|
||||
// server sends the CLIENT_VARS message.
|
||||
|
@ -6,15 +9,14 @@ window.clientVars = {
|
|||
};
|
||||
let BroadcastSlider;
|
||||
|
||||
|
||||
import * as timeSlider from 'ep_etherpad-lite/static/js/timeslider'
|
||||
(function () {
|
||||
const timeSlider = require('ep_etherpad-lite/static/js/timeslider')
|
||||
const pathComponents = location.pathname.split('/');
|
||||
|
||||
// Strip 'p', the padname and 'timeslider' from the pathname and set as baseURL
|
||||
const baseURL = pathComponents.slice(0,pathComponents.length-3).join('/') + '/';
|
||||
require('ep_etherpad-lite/static/js/l10n')
|
||||
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; // Expose jQuery #HACK
|
||||
window.$ = window.jQuery = require('ep_etherpad-lite/static/js/vendors/jquery'); // Expose jQuery #HACK
|
||||
require('ep_etherpad-lite/static/js/vendors/gritter')
|
||||
|
||||
window.browser = require('ep_etherpad-lite/static/js/vendors/browser');
|
||||
|
@ -31,7 +33,7 @@ let BroadcastSlider;
|
|||
});
|
||||
const padeditbar = require('ep_etherpad-lite/static/js/pad_editbar').padeditbar;
|
||||
const padimpexp = require('ep_etherpad-lite/static/js/pad_impexp').padimpexp;
|
||||
timeSlider.baseURL = baseURL;
|
||||
setBaseURl(baseURL)
|
||||
timeSlider.init();
|
||||
padeditbar.init()
|
||||
})();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import {MapArrayType} from "../../node/types/MapType";
|
||||
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const apiHandler = require('../../node/handler/APIHandler');
|
||||
const assert = require('assert').strict;
|
||||
const io = require('socket.io-client');
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
|
||||
import {APool} from "../../../node/types/PadType";
|
||||
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
import AttributePool from '../../../static/js/AttributePool'
|
||||
import {Attribute} from "../../../static/js/types/Attribute";
|
||||
const Changeset = require('../../../static/js/Changeset');
|
||||
const assert = require('assert').strict;
|
||||
const attributes = require('../../../static/js/attributes');
|
||||
|
@ -20,7 +21,7 @@ const jsdom = require('jsdom');
|
|||
|
||||
// All test case `wantAlines` values must only refer to attributes in this list so that the
|
||||
// attribute numbers do not change due to changes in pool insertion order.
|
||||
const knownAttribs = [
|
||||
const knownAttribs: Attribute[] = [
|
||||
['insertorder', 'first'],
|
||||
['italic', 'true'],
|
||||
['list', 'bullet1'],
|
||||
|
@ -336,7 +337,7 @@ pre
|
|||
describe(__filename, function () {
|
||||
for (const tc of testCases) {
|
||||
describe(tc.description, function () {
|
||||
let apool: APool;
|
||||
let apool: AttributePool;
|
||||
let result: {
|
||||
lines: string[],
|
||||
lineAttribs: string[],
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributePool from "../../static/js/AttributePool";
|
||||
|
||||
const randInt = (maxValue) => Math.floor(Math.random() * maxValue);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const AttributeMap = require('../../../static/js/AttributeMap');
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
import AttributeMap from "../../../static/js/AttributeMap";
|
||||
import AttributePool from '../../../static/js/AttributePool';
|
||||
const attributes = require('../../../static/js/attributes');
|
||||
|
||||
describe('AttributeMap', function () {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
import AttributePool from '../../../static/js/AttributePool'
|
||||
const attributes = require('../../../static/js/attributes');
|
||||
|
||||
describe('attributes', function () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const Changeset = require('../../../static/js/Changeset');
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
import AttributePool from "../../../static/js/AttributePool";
|
||||
const {randomMultiline, randomTestChangeset} = require('../easysync-helper.js');
|
||||
|
||||
describe('easysync-compose', function () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const Changeset = require('../../../static/js/Changeset');
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
import AttributePool from "../../../static/js/AttributePool";
|
||||
const {randomMultiline, randomTestChangeset} = require('../easysync-helper.js');
|
||||
|
||||
describe('easysync-follow', function () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const Changeset = require('../../../static/js/Changeset');
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
import AttributePool from '../../../static/js/AttributePool'
|
||||
const {poolOrArray} = require('../easysync-helper.js');
|
||||
|
||||
describe('easysync-mutations', function () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const Changeset = require('../../../static/js/Changeset');
|
||||
const AttributePool = require('../../../static/js/AttributePool');
|
||||
import AttributePool from '../../../static/js/AttributePool'
|
||||
const {randomMultiline, poolOrArray} = require('../easysync-helper.js');
|
||||
const {padutils} = require('../../../static/js/pad_utils');
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const SkipList = require('ep_etherpad-lite/static/js/skiplist');
|
||||
import SkipList from "../../../static/js/skiplist";
|
||||
|
||||
describe('skiplist.js', function () {
|
||||
it('rejects null keys', async function () {
|
||||
|
|
Loading…
Reference in a new issue