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