diff --git a/src/node/db/API.ts b/src/node/db/API.ts index 6f585ccee..5f0733908 100644 --- a/src/node/db/API.ts +++ b/src/node/db/API.ts @@ -19,7 +19,7 @@ * limitations under the License. */ -import Changeset from '../../static/js/Changeset'; +import {builder, deserializeOps} from '../../static/js/Changeset'; import ChatMessage from '../../static/js/ChatMessage'; import {CustomError} from '../utils/customError'; import {doesPadExist, getPad, isValidPadId, listAllPads} from './PadManager'; @@ -518,7 +518,7 @@ export const restoreRevision = async (padID, rev, authorId = '') => { let textIndex = 0; const newTextStart = 0; const newTextEnd = atext.text.length; - for (const op of Changeset.deserializeOps(attribs)) { + for (const op of deserializeOps(attribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); @@ -528,19 +528,19 @@ export const restoreRevision = async (padID, rev, authorId = '') => { }; // create a new changeset with a helper builder object - const builder = Changeset.builder(oldText.length); + const builder2 = builder(oldText.length); // assemble each line into the builder eachAttribRun(atext.attribs, (start, end, attribs) => { - builder.insert(atext.text.substring(start, end), attribs); + builder2.insert(atext.text.substring(start, end), attribs); }); const lastNewlinePos = oldText.lastIndexOf('\n'); if (lastNewlinePos < 0) { - builder.remove(oldText.length - 1, 0); + builder2.remove(oldText.length - 1, 0); } else { - builder.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); - builder.remove(oldText.length - lastNewlinePos - 1, 0); + builder2.remove(lastNewlinePos, oldText.match(/\n/g).length - 1); + builder2.remove(oldText.length - lastNewlinePos - 1, 0); } const changeset = builder.toString(); diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 07f939fa7..52f470926 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -21,7 +21,16 @@ import AttributeMap from '../../static/js/AttributeMap'; import {getPad} from '../db/PadManager'; -import {} from '../../static/js/Changeset'; +import { + builder, + checkRep, cloneAText, compose, + deserializeOps, + follow, inverse, makeAText, + makeSplice, + moveOpsToNewPool, mutateAttributionLines, mutateTextLines, + oldLen, prepareForWire, splitAttributionLines, splitTextLines, + unpack +} from '../../static/js/Changeset'; import ChatMessage from '../../static/js/ChatMessage'; import {AttributePool} from '../../static/js/AttributePool'; import AttributeManager from '../../static/js/AttributeManager'; @@ -62,6 +71,7 @@ import {ErrorCaused} from "../models/ErrorCaused"; import {Pad} from "../db/Pad"; import {SessionInfo} from "../models/SessionInfo"; import {randomString} from "../utils/randomstring"; +import {identity} from "lodash"; const securityManager = require('../db/SecurityManager'); @@ -610,10 +620,10 @@ const handleUserChanges = async (socket, message) => { const pad = await getPad(thisSession.padId, null, thisSession.author); // Verify that the changeset has valid syntax and is in canonical form - Changeset.checkRep(changeset); + checkRep(changeset); // Validate all added 'author' attribs to be the same value as the current user - for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) { + for (const op of deserializeOps(unpack(changeset).ops)) { // + can add text with attribs // = can change or add attribs // - can have attribs, but they are discarded and don't show up in the attribs - @@ -632,7 +642,7 @@ const handleUserChanges = async (socket, message) => { // ex. adoptChangesetAttribs // Afaik, it copies the new attributes from the changeset, to the global Attribute Pool - let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool); + let rebasedChangeset = moveOpsToNewPool(changeset, wireApool, pad.pool); // ex. applyUserChanges let r = baseRev; @@ -645,21 +655,21 @@ const handleUserChanges = async (socket, message) => { const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r); if (changeset === c && thisSession.author === authorId) { // Assume this is a retransmission of an already applied changeset. - rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen); + rebasedChangeset = identity(unpack(changeset).oldLen); } // At this point, both "c" (from the pad) and "changeset" (from the // client) are relative to revision r - 1. The follow function // rebases "changeset" so that it is relative to revision r // and can be applied after "c". - rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool); + rebasedChangeset = follow(c, rebasedChangeset, false, pad.pool); } const prevText = pad.text(); - if (Changeset.oldLen(rebasedChangeset) !== prevText.length) { + if (oldLen(rebasedChangeset) !== prevText.length) { throw new Error( `Can't apply changeset ${rebasedChangeset} with oldLen ` + - `${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`); + `${oldLen(rebasedChangeset)} to document of length ${prevText.length}`); } const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author); @@ -674,7 +684,7 @@ const handleUserChanges = async (socket, message) => { // Make sure the pad always ends with an empty line. if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) { - const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); + const nlChangeset = makeSplice(pad.text(), pad.text().length - 1, 0, '\n'); await pad.appendRevision(nlChangeset, thisSession.author); } @@ -729,7 +739,7 @@ export const updatePadClients = async (pad) => { const revChangeset = revision.changeset; const currentTime = revision.meta.timestamp; - const forWire = Changeset.prepareForWire(revChangeset, pad.pool); + const forWire = prepareForWire(revChangeset, pad.pool); const msg = { type: 'COLLABROOM', data: { @@ -764,7 +774,7 @@ const _correctMarkersInPad = (atext, apool) => { // that aren't at the start of a line const badMarkers = []; let offset = 0; - for (const op of Changeset.deserializeOps(atext.attribs)) { + for (const op of deserializeOps(atext.attribs)) { const attribs = AttributeMap.fromString(op.attribs, apool); const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a)); if (hasMarker) { @@ -786,15 +796,15 @@ const _correctMarkersInPad = (atext, apool) => { // create changeset that removes these bad markers offset = 0; - const builder = Changeset.builder(text.length); + const builder2 = builder(text.length); badMarkers.forEach((pos) => { - builder.keepText(text.substring(offset, pos)); - builder.remove(1); + builder2.keepText(text.substring(offset, pos)); + builder2.remove(1); offset = pos + 1; }); - return builder.toString(); + return builder2.toString(); }; export let clientVars:any @@ -918,7 +928,7 @@ const handleClientReady = async (socket, message) => { // return pending changesets for (const r of revisionsNeeded) { - const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool); + const forWire = prepareForWire(changesets[r].changeset, pad.pool); const wireMsg = {type: 'COLLABROOM', data: {type: 'CLIENT_RECONNECT', headRev: pad.getHeadRevisionNumber(), @@ -943,8 +953,8 @@ const handleClientReady = async (socket, message) => { let apool; // prepare all values for the wire, there's a chance that this throws, if the pad is corrupted try { - atext = Changeset.cloneAText(pad.atext); - const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool); + atext = cloneAText(pad.atext); + const attribsForWire = prepareForWire(atext.attribs, pad.pool); apool = attribsForWire.pool.toJsonable(); atext.attribs = attribsForWire.translated; } catch (e) { @@ -1175,13 +1185,13 @@ const getChangesetInfo = async (pad: Pad, startNum: number, endNum: number, gran if (compositeEnd > endNum || compositeEnd > headRevision + 1) break; const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`]; - const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool()); + const backwards = inverse(forwards, lines.textlines, lines.alines, pad.apool()); - Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool()); - Changeset.mutateTextLines(forwards, lines.textlines); + mutateAttributionLines(forwards, lines.alines, pad.apool()); + mutateTextLines(forwards, lines.textlines); - const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool); - const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool); + const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool); + const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool); const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1]; const t2 = revisionDate[compositeEnd - 1]; @@ -1209,12 +1219,12 @@ const getPadLines = async (pad, revNum) => { if (revNum >= 0) { atext = await pad.getInternalRevisionAText(revNum); } else { - atext = Changeset.makeAText('\n'); + atext = makeAText('\n'); } return { - textlines: Changeset.splitTextLines(atext.text), - alines: Changeset.splitAttributionLines(atext.attribs, atext.text), + textlines: splitTextLines(atext.text), + alines: splitAttributionLines(atext.attribs, atext.text), }; }; @@ -1249,7 +1259,7 @@ const composePadChangesets = async (pad, startNum, endNum) => { for (r = startNum + 1; r < endNum; r++) { const cs = changesets[r]; - changeset = Changeset.compose(changeset, cs, pool); + changeset = compose(changeset, cs, pool); } return changeset; } catch (e) { diff --git a/src/node/hooks/i18n.ts b/src/node/hooks/i18n.ts index 7ce55f744..001e87bd0 100644 --- a/src/node/hooks/i18n.ts +++ b/src/node/hooks/i18n.ts @@ -50,7 +50,7 @@ const getAllLocales = () => { // Build a locale index (merge all locale data other than user-supplied overrides) const locales = {}; - _.each(locales2paths, (files, langcode) => { + _.each(locales2paths, (files:[], langcode) => { locales[langcode] = {}; files.forEach((file) => { diff --git a/src/node/utils/ExportHelper.ts b/src/node/utils/ExportHelper.ts index b4a5765ee..814ceef33 100644 --- a/src/node/utils/ExportHelper.ts +++ b/src/node/utils/ExportHelper.ts @@ -20,12 +20,12 @@ */ import AttributeMap from '../../static/js/AttributeMap'; -import Changeset from '../../static/js/Changeset'; +import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset'; export const getPadPlainText = (pad, revNum) => { const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(revNum) : pad.atext); const textLines = atext.text.slice(0, -1).split('\n'); - const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const attribLines = splitAttributionLines(atext.attribs, atext.text); const apool = pad.pool; const pieces = []; @@ -57,7 +57,7 @@ export const _analyzeLine = (text, aline, apool) => { let lineMarker = 0; line.listLevel = 0; if (aline) { - const [op] = Changeset.deserializeOps(aline); + const [op] = deserializeOps(aline); if (op != null) { const attribs = AttributeMap.fromString(op.attribs, apool); let listType = attribs.get('list'); @@ -77,7 +77,7 @@ export const _analyzeLine = (text, aline, apool) => { } if (lineMarker) { line.text = text.substring(1); - line.aline = Changeset.subattribution(aline, 1); + line.aline = subattribution(aline, 1); } else { line.text = text; line.aline = aline; diff --git a/src/node/utils/ExportHtml.ts b/src/node/utils/ExportHtml.ts index 8ec4fc104..5de740f55 100644 --- a/src/node/utils/ExportHtml.ts +++ b/src/node/utils/ExportHtml.ts @@ -15,14 +15,22 @@ * limitations under the License. */ -import Changeset from '../../static/js/Changeset'; -import attributes from "../../static/js/attributes"; +import { + deserializeOps, + splitAttributionLines, + stringAssembler, + stringIterator, + subattribution +} from '../../static/js/Changeset'; +import {decodeAttribString} from "../../static/js/attributes"; import {getPad} from "../db/PadManager"; import _ from "underscore"; -import Security from '../../static/js/security'; +// FIXME this is a hack to get around the fact that we don't have a good way +// @ts-ignore +import {escapeHTML,escapeHTMLAttribute} from '../../static/js/security'; import {aCallAll} from '../../static/js/pluginfw/hooks'; import {required} from '../eejs'; import {_analyzeLine, _encodeWhitespace} from "./ExportHelper"; @@ -44,7 +52,7 @@ const getPadHTML = async (pad, revNum) => { export const getHTMLFromAtext = async (pad, atext, authorColors?) => { const apool = pad.apool(); const textLines = atext.text.slice(0, -1).split('\n'); - const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text); + const attribLines = splitAttributionLines(atext.attribs, atext.text); const tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; @@ -127,8 +135,8 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => { // Just bold Bold and italics Just italics // becomes // Just bold Bold and italics Just italics - const taker = Changeset.stringIterator(text); - const assem = Changeset.stringAssembler(); + const taker = stringIterator(text); + const assem = stringAssembler(); const openTags = []; const getSpanClassFor = (i) => { @@ -200,7 +208,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => { return; } - const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars)); + const ops = deserializeOps(subattribution(attribs, idx, idx + numChars)); idx += numChars; // this iterates over every op string and decides which tags to open or to close @@ -209,7 +217,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => { const usedAttribs = []; // mark all attribs as used - for (const a of attributes.decodeAttribString(o.attribs)) { + for (const a of decodeAttribString(o.attribs)) { if (a in anumMap) { usedAttribs.push(anumMap[a]); // i = 0 => bold, etc. } @@ -249,7 +257,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => { // from but they break the abiword parser and are completly useless s = s.replace(String.fromCharCode(12), ''); - assem.append(_encodeWhitespace(Security.escapeHTML(s))); + assem.append(_encodeWhitespace(escapeHTML(s))); } // end iteration over spans in line // close all the tags that are open after the last op @@ -272,7 +280,7 @@ export const getHTMLFromAtext = async (pad, atext, authorColors?) => { // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener // https://mathiasbynens.github.io/rel-noopener/ // https://github.com/ether/etherpad-lite/pull/3636 - assem.append(``); + assem.append(``); processNextChars(urlLength); assem.append(''); }); @@ -477,7 +485,7 @@ export const getPadHTMLDocument = async (padId, revNum, readOnlyId?) => { return required('ep_etherpad-lite/templates/export_html.html', { body: html, - padId: Security.escapeHTML(readOnlyId || padId), + padId: escapeHTML(readOnlyId || padId), extraCSS: stylesForExportCSS, }); }; diff --git a/src/node/utils/ImportHtml.ts b/src/node/utils/ImportHtml.ts index 2e5de910a..34dfb05e4 100644 --- a/src/node/utils/ImportHtml.ts +++ b/src/node/utils/ImportHtml.ts @@ -16,8 +16,8 @@ */ import log4js from 'log4js'; -import Changeset from '../../static/js/Changeset'; -import contentcollector from '../../static/js/contentcollector'; +import {builder, deserializeOps} from '../../static/js/Changeset'; +import {makeContentCollector} from '../../static/js/contentcollector'; import jsdom from 'jsdom'; const apiLogger = log4js.getLogger('ImportHtml'); @@ -42,7 +42,7 @@ export const setPadHTML = async (pad, html, authorId = '') => { // Convert a dom tree into a list of lines and attribute liens // using the content collector object - const cc = contentcollector.makeContentCollector(true, null, pad.pool); + const cc = makeContentCollector(true, null, pad.pool); try { // we use a try here because if the HTML is bad it will blow up cc.collectContent(document.body); @@ -68,24 +68,24 @@ export const setPadHTML = async (pad, html, authorId = '') => { const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`; // create a new changeset with a helper builder object - const builder = Changeset.builder(1); + const builder2 = builder(1); // assemble each line into the builder let textIndex = 0; const newTextStart = 0; const newTextEnd = newText.length; - for (const op of Changeset.deserializeOps(newAttribs)) { + for (const op of deserializeOps(newAttribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { const start = Math.max(newTextStart, textIndex); const end = Math.min(newTextEnd, nextIndex); - builder.insert(newText.substring(start, end), op.attribs); + builder2.insert(newText.substring(start, end), op.attribs); } textIndex = nextIndex; } // the changeset is ready! - const theChangeset = builder.toString(); + const theChangeset = builder2.toString(); apiLogger.debug(`The changeset: ${theChangeset}`); await pad.setText('\n', authorId); diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts index a50597469..e12c14c7f 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -28,7 +28,7 @@ */ import exp from "constants"; - +import packageJSON from '../../../package.json' import {findEtherpadRoot, makeAbsolute, isSubdir} from './AbsolutePaths'; import deepEqual from 'fast-deep-equal/es6'; import fs from 'fs'; @@ -507,7 +507,7 @@ export const getGitCommit = () => { }; // Return etherpad version from package.json -export const getEpVersion = () => require('../../package.json').version; +export const getEpVersion = () => packageJSON.version; /** * Receives a settingsObj and, if the property name is a valid configuration diff --git a/src/node/utils/UpdateCheck.ts b/src/node/utils/UpdateCheck.ts index de18d2f17..b80bfb954 100644 --- a/src/node/utils/UpdateCheck.ts +++ b/src/node/utils/UpdateCheck.ts @@ -48,12 +48,11 @@ export const needsUpdate = (cb?:(arg0: boolean)=>void) => { } const check = () => { - const needsUpdate = ((needsUpdate: boolean) => { - if (needsUpdate && infos) { - console.warn(`Update available: Download the latest version ${infos.latestVersion}`); + needsUpdate((needsUpdate)=>{ + if (needsUpdate) { + console.warn(`Update available: Download the actual version ${infos.latestVersion}`); } }) - needsUpdate(infos.latestVersion > getEpVersion()); } export default {check, getLatestVersion} diff --git a/src/node/utils/padDiff.ts b/src/node/utils/padDiff.ts index 88dd3d4b4..737f83067 100644 --- a/src/node/utils/padDiff.ts +++ b/src/node/utils/padDiff.ts @@ -1,9 +1,18 @@ 'use strict'; import AttributeMap from '../../static/js/AttributeMap'; -import Changeset from '../../static/js/Changeset'; -import attributes from '../../static/js/attributes'; +import { + applyToAText, + builder, checkRep, + compose, + deserializeOps, + numToString, Op, + opAssembler, pack, splitAttributionLines, splitTextLines, stringAssembler, + unpack +} from '../../static/js/Changeset'; +import {attribsFromString} from '../../static/js/attributes'; import {getHTMLFromAtext} from './ExportHtml'; +// @ts-ignore import {PadDiffModel} from "ep_etherpad-lite/node/models/PadDiffModel"; export const PadDiff = (pad, fromRev, toRev)=> { @@ -30,7 +39,7 @@ export const PadDiff = (pad, fromRev, toRev)=> { PadDiff.prototype._isClearAuthorship = function (changeset) { // unpack - const unpacked = Changeset.unpack(changeset); + const unpacked = unpack(changeset); // check if there is nothing in the charBank if (unpacked.charBank !== '') { @@ -42,7 +51,7 @@ PadDiff.prototype._isClearAuthorship = function (changeset) { return false; } - const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops); + const [clearOperator, anotherOp] = deserializeOps(unpacked.ops); // check if there is only one operator if (anotherOp != null) return false; @@ -59,7 +68,7 @@ PadDiff.prototype._isClearAuthorship = function (changeset) { } const [appliedAttribute, anotherAttribute] = - attributes.attribsFromString(clearOperator.attribs, this._pad.pool); + attribsFromString(clearOperator.attribs, this._pad.pool); // Check that the operation has exactly one attribute. if (appliedAttribute == null || anotherAttribute != null) return false; @@ -76,9 +85,9 @@ PadDiff.prototype._createClearAuthorship = async function (rev) { const atext = await this._pad.getInternalRevisionAText(rev); // build clearAuthorship changeset - const builder = Changeset.builder(atext.text.length); - builder.keepText(atext.text, [['author', '']], this._pad.pool); - const changeset = builder.toString(); + const builder2 = builder(atext.text.length); + builder2.keepText(atext.text, [['author', '']], this._pad.pool); + const changeset = builder2.toString(); return changeset; }; @@ -91,7 +100,7 @@ PadDiff.prototype._createClearStartAtext = async function (rev) { const changeset = await this._createClearAuthorship(rev); // apply the clearAuthorship changeset - const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool); + const newAText = applyToAText(changeset, atext, this._pad.pool); return newAText; }; @@ -160,7 +169,7 @@ PadDiff.prototype._createDiffAtext = async function () { if (superChangeset == null) { superChangeset = changeset; } else { - superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool); + superChangeset = compose(superChangeset, changeset, this._pad.pool); } } @@ -174,10 +183,10 @@ PadDiff.prototype._createDiffAtext = async function () { const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool); // apply the superChangeset, which includes all addings - atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool); + atext = applyToAText(superChangeset, atext, this._pad.pool); // apply the deletionChangeset, which adds a deletions - atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool); + atext = applyToAText(deletionChangeset, atext, this._pad.pool); } return atext; @@ -217,22 +226,22 @@ PadDiff.prototype.getAuthors = async function () { PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { // unpack - const unpacked = Changeset.unpack(changeset); + const unpacked = unpack(changeset); - const assem = Changeset.opAssembler(); + const assem = opAssembler(); // create deleted attribs const authorAttrib = apool.putAttrib(['author', author || '']); const deletedAttrib = apool.putAttrib(['removed', true]); - const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`; + const attribs = `*${numToString(authorAttrib)}*${numToString(deletedAttrib)}`; - for (const operator of Changeset.deserializeOps(unpacked.ops)) { + for (const operator of deserializeOps(unpacked.ops)) { if (operator.opcode === '-') { // this is a delete operator, extend it with the author operator.attribs = attribs; } else if (operator.opcode === '=' && operator.attribs) { // this is operator changes only attributes, let's mark which author did that - operator.attribs += `*${Changeset.numToString(authorAttrib)}`; + operator.attribs += `*${numToString(authorAttrib)}`; } // append the new operator to our assembler @@ -240,14 +249,14 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { } // return the modified changeset - return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank); + return pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank); }; // this method is 80% like Changeset.inverse. I just changed so instead of reverting, // it adds deletions and attribute changes to to the atext. PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { - const lines = Changeset.splitTextLines(startAText.text); - const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text); + const lines = splitTextLines(startAText.text); + const alines = splitAttributionLines(startAText.attribs, startAText.text); // lines and alines are what the exports is meant to apply to. // They may be arrays or objects with .get(i) and .length methods. @@ -274,14 +283,14 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let curLineOps = null; let curLineOpsNext = null; let curLineOpsLine; - let curLineNextOp = new Changeset.Op('+'); + let curLineNextOp = new Op('+'); - const unpacked = Changeset.unpack(cs); - const builder = Changeset.builder(unpacked.newLen); + const unpacked = unpack(cs); + const builder2 = builder(unpacked.newLen); const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { if (!curLineOps || curLineOpsLine !== curLine) { - curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOps = deserializeOps(aLinesGet(curLine)); curLineOpsNext = curLineOps.next(); curLineOpsLine = curLine; let indexIntoLine = 0; @@ -302,13 +311,13 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { curChar = 0; curLineOpsLine = curLine; curLineNextOp.chars = 0; - curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOps = deserializeOps(aLinesGet(curLine)); curLineOpsNext = curLineOps.next(); } if (!curLineNextOp.chars) { if (curLineOpsNext.done) { - curLineNextOp = new Changeset.Op(); + curLineNextOp = new Op(); } else { curLineNextOp = curLineOpsNext.value; curLineOpsNext = curLineOps.next(); @@ -343,7 +352,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { const nextText = (numChars) => { let len = 0; - const assem = Changeset.stringAssembler(); + const assem = stringAssembler(); const firstString = linesGet(curLine).substring(curChar); len += firstString.length; assem.append(firstString); @@ -371,7 +380,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { }; }; - for (const csOp of Changeset.deserializeOps(unpacked.ops)) { + for (const csOp of deserializeOps(unpacked.ops)) { if (csOp.opcode === '=') { const textBank = nextText(csOp.chars); @@ -417,7 +426,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { textLeftToProcess = textLeftToProcess.substr(lengthToProcess); if (lineBreak) { - builder.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak + builder2.keep(1, 1); // just skip linebreaks, don't do a insert + keep for a linebreak // consume the attributes of this linebreak consumeAttribRuns(1, () => {}); @@ -429,31 +438,31 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { // get the old attributes back const oldAttribs = undoBackToAttribs(attribs); - builder.insert(processText.substr(textBankIndex, len), oldAttribs); + builder2.insert(processText.substr(textBankIndex, len), oldAttribs); textBankIndex += len; }); - builder.keep(lengthToProcess, 0); + builder2.keep(lengthToProcess, 0); } } } else { skip(csOp.chars, csOp.lines); - builder.keep(csOp.chars, csOp.lines); + builder2.keep(csOp.chars, csOp.lines); } } else if (csOp.opcode === '+') { - builder.keep(csOp.chars, csOp.lines); + builder2.keep(csOp.chars, csOp.lines); } else if (csOp.opcode === '-') { const textBank = nextText(csOp.chars); let textBankIndex = 0; consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => { - builder.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); + builder2.insert(textBank.substr(textBankIndex, len), attribs + csOp.attribs); textBankIndex += len; }); } } - return Changeset.checkRep(builder.toString()); + return checkRep(builder.toString()); }; // export the constructor diff --git a/src/package-lock.json b/src/package-lock.json index 47345ad47..4a9eb06ba 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -40,7 +40,7 @@ "rehype-minify-whitespace": "^5.0.1", "request": "2.88.2", "resolve": "1.22.2", - "security": "1.0.0", + "security": "^1.0.0", "semver": "^7.5.2", "socket.io": "^2.4.1", "superagent": "^8.0.9", @@ -62,6 +62,7 @@ "@types/jquery": "^3.5.16", "@types/js-cookie": "^3.0.3", "@types/node": "^20.3.1", + "@types/underscore": "^1.11.5", "concurrently": "^8.2.0", "eslint": "^8.14.0", "eslint-config-etherpad": "^3.0.13", @@ -1227,6 +1228,12 @@ "@types/node": "*" } }, + "node_modules/@types/underscore": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.5.tgz", + "integrity": "sha512-b8e//LrIlhoXaaBcMC0J/s2/lIF9y5VJYKqbW4nA+tW/nqqDk1Dacd1ULLT7zgGsKs7PGbSnqCPzqEniZ0RxYg==", + "dev": true + }, "node_modules/@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", diff --git a/src/package.json b/src/package.json index 9b8a08abe..df513c468 100644 --- a/src/package.json +++ b/src/package.json @@ -61,7 +61,7 @@ "rehype-minify-whitespace": "^5.0.1", "request": "2.88.2", "resolve": "1.22.2", - "security": "1.0.0", + "security": "^1.0.0", "semver": "^7.5.2", "socket.io": "^2.4.1", "superagent": "^8.0.9", @@ -83,6 +83,7 @@ "@types/jquery": "^3.5.16", "@types/js-cookie": "^3.0.3", "@types/node": "^20.3.1", + "@types/underscore": "^1.11.5", "concurrently": "^8.2.0", "eslint": "^8.14.0", "eslint-config-etherpad": "^3.0.13", diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.ts similarity index 99% rename from src/static/js/AttributeManager.js rename to src/static/js/AttributeManager.ts index 5734769f6..67a71c0ee 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.ts @@ -2,7 +2,9 @@ import AttributeMap from "./AttributeMap.js"; import * as Changeset from "./Changeset.js"; import * as ChangesetUtils from "./ChangesetUtils.js"; import * as attributes from "./attributes.js"; -import * as _ from "./underscore.js"; +import _ from "underscore"; +export { map, each, identity} from "underscore"; + 'use strict'; const lineMarkerAttribute = 'lmkr'; // Some of these attributes are kept for compatibility purposes. @@ -255,7 +257,7 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ @param lineNum: the number of the line to set the attribute for @param attributeKey: the name of the attribute to set, e.g. list @param attributeValue: an optional parameter to pass to the attribute (e.g. indention level) - + */ setAttributeOnLine(lineNum, attributeName, attributeValue) { let loc = [0, 0]; diff --git a/src/static/js/AttributeMap.js b/src/static/js/AttributeMap.ts similarity index 99% rename from src/static/js/AttributeMap.js rename to src/static/js/AttributeMap.ts index 4e55eb8a7..ae39f9a05 100644 --- a/src/static/js/AttributeMap.js +++ b/src/static/js/AttributeMap.ts @@ -17,6 +17,7 @@ import * as attributes from "./attributes.js"; * Convenience class to convert an Op's attribute string to/from a Map of key, value pairs. */ class AttributeMap extends Map { + pool: any; /** * Converts an attribute string into an AttributeMap. * diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.ts similarity index 98% rename from src/static/js/Changeset.js rename to src/static/js/Changeset.ts index c6fa8402e..db29738ab 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.ts @@ -1,7 +1,8 @@ import AttributeMap from "./AttributeMap.js"; -import AttributePool from "./AttributePool.js"; +import {AttributePool} from "./AttributePool"; import * as attributes from "./attributes.js"; import { padutils } from "./pad_utils.js"; +import {CustomError} from "../../node/utils/customError"; 'use strict'; /** * A `[key, value]` pair of strings describing a text attribute. @@ -22,7 +23,7 @@ import { padutils } from "./pad_utils.js"; * @param {string} msg - Just some message */ const error = (msg) => { - const e = new Error(msg); + const e = new CustomError(msg); e.easysync = true; throw e; }; @@ -42,6 +43,10 @@ const assert = (b, msg) => { * An operation to apply to a shared document. */ class Op { + chars: number; + opcode: string; + lines: number; + attribs: string; /** * @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property. */ @@ -113,6 +118,8 @@ class Op { * @deprecated Use `deserializeOps` instead. */ class OpIter { + private _gen: any; + private _next: any; /** * @param {string} ops - String encoding the change operations to iterate over. */ @@ -240,6 +247,12 @@ const opsFromText = function* (opcode, text, attribs = '', pool = null) { * with no newlines. */ class TextLinesMutator { + _lines: any; + // FIXME What is this for a type? + _curSplice: any[]; + _inSplice: boolean; + _curLine: number; + _curCol: number; /** * @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place). */ @@ -362,7 +375,7 @@ class TextLinesMutator { * @param {number} L - * @param {boolean} includeInSplice - Indicates that attributes are present. */ - skipLines(L, includeInSplice) { + skipLines(L, includeInSplice?) { if (!L) return; if (includeInSplice) { @@ -906,7 +919,7 @@ export const mergingOpAssembler = () => { /** * @param {boolean} [isEndDocument] */ - const flush = (isEndDocument) => { + const flush = (isEndDocument?: boolean) => { if (!bufOp.opcode) return; if (isEndDocument && bufOp.opcode === '=' && !bufOp.attribs) { @@ -1325,7 +1338,7 @@ export const attributeTester = (attribPair, pool) => { return (attribs) => re.test(attribs); }; export const identity = (N) => exports.pack(N, N, '', ''); -export const makeSplice = (orig, start, ndel, ins, attribs, pool) => { +export const makeSplice = (orig, start, ndel, ins, attribs?, pool?) => { if (start < 0) throw new RangeError(`start index must be non-negative (is ${start})`); if (ndel < 0) @@ -1346,7 +1359,7 @@ export const makeSplice = (orig, start, ndel, ins, attribs, pool) => { assem.endDocument(); return exports.pack(orig.length, orig.length + ins.length - ndel, assem.toString(), ins); }; -export const characterRangeFollow = (cs, startChar, endChar, insertionsAfter) => { +export const characterRangeFollow = (cs, startChar, endChar, insertionsAfter?) => { let newStartChar = startChar; let newEndChar = endChar; let lengthChangeSoFar = 0; @@ -1451,7 +1464,7 @@ export const mapAttribNumbers = (cs, func) => { }); return newUpToDollar + cs.substring(dollarPos); }; -export const makeAText = (text, attribs) => ({ +export const makeAText = (text, attribs?) => ({ text, attribs: (attribs || exports.makeAttribution(text)), }); @@ -1538,7 +1551,7 @@ export const builder = (oldLen) => { * attribute key, value pairs. * @returns {Builder} this */ - keep: (N, L, attribs, pool) => { + keep: (N, L, attribs?, pool?) => { o.opcode = '='; o.attribs = typeof attribs === 'string' ? attribs : new AttributeMap(pool).update(attribs || []).toString(); @@ -1555,7 +1568,7 @@ export const builder = (oldLen) => { * attribute key, value pairs. * @returns {Builder} this */ - keepText: (text, attribs, pool) => { + keepText: (text, attribs?, pool?) => { for (const op of opsFromText('=', text, attribs, pool)) assem.append(op); return self; @@ -1568,7 +1581,7 @@ export const builder = (oldLen) => { * attribute key, value pairs. * @returns {Builder} this */ - insert: (text, attribs, pool) => { + insert: (text, attribs, pool?) => { for (const op of opsFromText('+', text, attribs, pool)) assem.append(op); charBank.append(text); @@ -1580,7 +1593,7 @@ export const builder = (oldLen) => { * character must be a newline. * @returns {Builder} this */ - remove: (N, L) => { + remove: (N, L?) => { o.opcode = '-'; o.attribs = ''; o.chars = N; @@ -1605,7 +1618,7 @@ export const makeAttribsString = (opcode, attribs, pool) => { return attribs; return new AttributeMap(pool).update(attribs, opcode === '+').toString(); }; -export const subattribution = (astr, start, optEnd) => { +export const subattribution = (astr, start, optEnd?) => { const attOps = exports.deserializeOps(astr); let attOpsNext = attOps.next(); const assem = exports.smartOpAssembler(); diff --git a/src/static/js/ChangesetUtils.js b/src/static/js/ChangesetUtils.ts similarity index 98% rename from src/static/js/ChangesetUtils.js rename to src/static/js/ChangesetUtils.ts index b63481a51..a527f1e74 100644 --- a/src/static/js/ChangesetUtils.js +++ b/src/static/js/ChangesetUtils.ts @@ -10,7 +10,7 @@ export const buildRemoveRange = (rep, builder, start, end) => { builder.remove(end[1] - start[1]); } }; -export const buildKeepRange = (rep, builder, start, end, attribs, pool) => { +export const buildKeepRange = (rep, builder, start, end, attribs?, pool?) => { const startLineOffset = rep.lines.offsetOfIndex(start[0]); const endLineOffset = rep.lines.offsetOfIndex(end[0]); if (end[0] > start[0]) { diff --git a/src/static/js/ChatMessage.js b/src/static/js/ChatMessage.ts similarity index 92% rename from src/static/js/ChatMessage.js rename to src/static/js/ChatMessage.ts index c9cc87e28..3873e0000 100644 --- a/src/static/js/ChatMessage.js +++ b/src/static/js/ChatMessage.ts @@ -8,6 +8,10 @@ const { padutils: { warnDeprecated } } = { padutils }; * Supports serialization to JSON. */ class ChatMessage { + private text: any; + authorId: any; + private time: any; + displayName: null; static fromObject(obj) { // 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. @@ -83,9 +87,10 @@ class ChatMessage { // doesn't support authorId and displayName. toJSON() { const { authorId, displayName, ...obj } = this; - obj.userId = authorId; - obj.userName = displayName; - return obj; + let objExtendable = obj as any + objExtendable.userId = authorId; + objExtendable.userName = displayName; + return objExtendable; } } export default ChatMessage; diff --git a/src/static/js/ace.ts b/src/static/js/ace.ts index e6878c720..310171dd4 100644 --- a/src/static/js/ace.ts +++ b/src/static/js/ace.ts @@ -3,7 +3,8 @@ import { makeCSSManager as makeCSSManager$0 } from "./cssmanager.js"; import * as pluginUtils from "./pluginfw/shared.js"; import {Ace2EditorInfo, AceDocType} from "../module/Ace2EditorInfo"; import {clientVars} from "../../node/handler/PadMessageHandler"; -import {CustomWindow} from "../module/CustomWindow"; +import {CustomElementWithSheet, CustomWindow} from "../module/CustomWindow"; +import {required} from "../../node/eejs"; 'use strict'; const makeCSSManager = { makeCSSManager: makeCSSManager$0 }.makeCSSManager; @@ -244,17 +245,17 @@ const Ace2Editor = function () { require.setLibraryURI(absUrl('../javascripts/lib')); require.setGlobalKeyPath('require'); // intentially moved before requiring client_plugins to save a 307 - innerWindow.Ace2Inner = require('ep_etherpad-lite/static/js/ace2_inner'); - innerWindow.plugins = require('ep_etherpad-lite/static/js/pluginfw/client_plugins'); + innerWindow.Ace2Inner = required('ep_etherpad-lite/static/js/ace2_inner'); + innerWindow.plugins = required('ep_etherpad-lite/static/js/pluginfw/client_plugins'); innerWindow.plugins.adoptPluginsFromAncestorsOf(innerWindow); - innerWindow.$ = innerWindow.jQuery = require('ep_etherpad-lite/static/js/rjquery').jQuery; + innerWindow.$ = innerWindow.jQuery = required('ep_etherpad-lite/static/js/rjquery').jQuery; debugLog('Ace2Editor.init() waiting for plugins'); - await new Promise((resolve, reject) => innerWindow.plugins.ensure((err) => err != null ? reject(err) : resolve())); + await new Promise((resolve, reject) => innerWindow.plugins.ensure((err) => err != null ? reject(err) : resolve())); debugLog('Ace2Editor.init() waiting for Ace2Inner.init()'); await innerWindow.Ace2Inner.init(info, { inner: makeCSSManager(innerStyle.sheet), outer: makeCSSManager(outerStyle.sheet), - parent: makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet), + parent: makeCSSManager((document.querySelector('style[title="dynamicsyntax"]') as unknown as CustomElementWithSheet).sheet), }); debugLog('Ace2Editor.init() Ace2Inner.init() returned'); loaded = true; diff --git a/src/static/js/attributes.js b/src/static/js/attributes.ts similarity index 100% rename from src/static/js/attributes.js rename to src/static/js/attributes.ts diff --git a/src/static/js/basic_error_handler.js b/src/static/js/basic_error_handler.ts similarity index 94% rename from src/static/js/basic_error_handler.js rename to src/static/js/basic_error_handler.ts index 77f93f5d2..e162aad62 100644 --- a/src/static/js/basic_error_handler.js +++ b/src/static/js/basic_error_handler.ts @@ -20,7 +20,9 @@ const msgBlock = document.createElement('blockquote'); box.appendChild(msgBlock); msgBlock.style.fontWeight = 'bold'; - msgBlock.appendChild(document.createTextNode(msg)); + if (typeof msg === "string") { + msgBlock.appendChild(document.createTextNode(msg)); + } const loc = document.createElement('p'); box.appendChild(loc); loc.appendChild(document.createTextNode(`in ${url}`)); diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.ts similarity index 89% rename from src/static/js/broadcast.js rename to src/static/js/broadcast.ts index d891b0d81..0d8824c65 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.ts @@ -1,12 +1,15 @@ import { makeCSSManager as makeCSSManager$0 } from "./cssmanager.js"; import { domline as domline$0 } from "./domline.js"; -import AttribPool from "./AttributePool.js"; +import {AttributePool} from "./AttributePool.js"; import * as Changeset from "./Changeset.js"; import * as attributes from "./attributes.js"; import { linestylefilter as linestylefilter$0 } from "./linestylefilter.js"; import { colorutils as colorutils$0 } from "./colorutils.js"; -import * as _ from "./underscore.js"; +import * as _ from "underscore"; import * as hooks from "./pluginfw/hooks.js"; +import {clientVars} from "../../node/handler/PadMessageHandler"; +import {Author, AuthorData, CustomElementWithSheet, CustomWindow} from "../module/CustomWindow"; +import {i18nextvar} from "./vendors/i18next"; 'use strict'; /** * This code is mostly from the old Etherpad. Please help us to comment this code. @@ -53,9 +56,10 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro currentLines: Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text), currentDivs: null, // to be filled in once the dom loads - apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool), + apool: (new AttributePool()).fromJsonable(clientVars.collab_client_vars.apool), alines: Changeset.splitAttributionLines(clientVars.collab_client_vars.initialAttributedText.attribs, clientVars.collab_client_vars.initialAttributedText.text), // generates a jquery element containing HTML for a line + targetRevision: undefined, lineToElement(line, aline) { const element = document.createElement('div'); const emptyLine = (line === '\n'); @@ -64,7 +68,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro domInfo.prepareForAdd(); element.className = domInfo.node.className; element.innerHTML = domInfo.node.innerHTML; - element.id = Math.random(); + element.id = String(Math.random()); return $(element); }, // splice the lines @@ -172,11 +176,12 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro padContents.currentRevision = revision; padContents.currentTime += timeDelta * 1000; updateTimer(); + // @ts-ignore const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]); BroadcastSlider.setAuthors(authors); }; const loadedNewChangeset = (changesetForward, changesetBackward, revision, timeDelta) => { - const revisionInfo = window.revisionInfo; + const revisionInfo = (window as unknown as CustomWindow).revisionInfo; const broadcasting = (BroadcastSlider.getSliderPosition() === revisionInfo.latest); revisionInfo.addChangeset(revision, revision + 1, changesetForward, changesetBackward, timeDelta); BroadcastSlider.setSliderLength(revisionInfo.latest); @@ -203,7 +208,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro const hours = zpad(date.getHours(), 2); const minutes = zpad(date.getMinutes(), 2); const seconds = zpad(date.getSeconds(), 2); - return (html10n.get('timeslider.dateformat', { + return (i18nextvar('timeslider.dateformat', { day, month, year, @@ -213,21 +218,21 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro })); }; $('#timer').html(dateFormat()); - const revisionDate = html10n.get('timeslider.saved', { + const revisionDate = i18nextvar('timeslider.saved', { day: date.getDate(), month: [ - html10n.get('timeslider.month.january'), - html10n.get('timeslider.month.february'), - html10n.get('timeslider.month.march'), - html10n.get('timeslider.month.april'), - html10n.get('timeslider.month.may'), - html10n.get('timeslider.month.june'), - html10n.get('timeslider.month.july'), - html10n.get('timeslider.month.august'), - html10n.get('timeslider.month.september'), - html10n.get('timeslider.month.october'), - html10n.get('timeslider.month.november'), - html10n.get('timeslider.month.december'), + i18nextvar('timeslider.month.january'), + i18nextvar('timeslider.month.february'), + i18nextvar('timeslider.month.march'), + i18nextvar('timeslider.month.april'), + i18nextvar('timeslider.month.may'), + i18nextvar('timeslider.month.june'), + i18nextvar('timeslider.month.july'), + i18nextvar('timeslider.month.august'), + i18nextvar('timeslider.month.september'), + i18nextvar('timeslider.month.october'), + i18nextvar('timeslider.month.november'), + i18nextvar('timeslider.month.december'), ][date.getMonth()], year: date.getFullYear(), }); @@ -236,7 +241,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro updateTimer(); const goToRevision = (newRevision) => { padContents.targetRevision = newRevision; - const path = window.revisionInfo.getPath(padContents.currentRevision, newRevision); + const path = (window as unknown as CustomWindow).revisionInfo.getPath(padContents.currentRevision, newRevision); hooks.aCallAll('goToRevisionEvent', { rev: newRevision, }); @@ -273,10 +278,11 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // Loading changeset history for old revision (to make diff between old and new revision) loadChangesetsForRevision(padContents.currentRevision - 1); } + // @ts-ignore const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]); BroadcastSlider.setAuthors(authors); }; - const loadChangesetsForRevision = (revision, callback) => { + const loadChangesetsForRevision = (revision, callback?) => { if (BroadcastSlider.getSliderLength() > 10000) { const start = (Math.floor((revision) / 10000) * 10000); // revision 0 to 10 changesetLoader.queueUp(start, 100); @@ -348,7 +354,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro setTimeout(() => this.loadFromQueue(), 10); }, handleResponse: (data, start, granularity, callback) => { - const pool = (new AttribPool()).fromJsonable(data.apool); + const pool = (new AttributePool()).fromJsonable(data.apool); for (let i = 0; i < data.forwardsChangesets.length; i++) { const astart = start + i * granularity - 1; // rev -1 is a blank single line let aend = start + (i + 1) * granularity - 1; // totalRevs is the most recent revision @@ -357,7 +363,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // debugLog("adding changeset:", astart, aend); const forwardcs = Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool); const backwardcs = Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool); - window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]); + (window as unknown as CustomWindow).revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]); } if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1); @@ -366,15 +372,16 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro if (obj.type === 'COLLABROOM') { obj = obj.data; if (obj.type === 'NEW_CHANGES') { - const changeset = Changeset.moveOpsToNewPool(obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + const changeset = Changeset.moveOpsToNewPool(obj.changeset, (new AttributePool()).fromJsonable(obj.apool), padContents.apool); let changesetBack = Changeset.inverse(obj.changeset, padContents.currentLines, padContents.alines, padContents.apool); - changesetBack = Changeset.moveOpsToNewPool(changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool); + changesetBack = Changeset.moveOpsToNewPool(changesetBack, (new AttributePool()).fromJsonable(obj.apool), padContents.apool); loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta); } else if (obj.type === 'NEW_AUTHORDATA') { const authorMap = {}; authorMap[obj.author] = obj.data; receiveAuthorData(authorMap); + // @ts-ignore const authors = _.map(padContents.getActiveAuthors(), (name) => authorData[name]); BroadcastSlider.setAuthors(authors); } @@ -412,13 +419,15 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro goToRevisionIfEnabledCount--; } else { + //FIXME what to do? + // @ts-ignore goToRevision(...args); } - }; + } BroadcastSlider.onSlider(goToRevisionIfEnabled); - const dynamicCSS = makeCSSManager(document.querySelector('style[title="dynamicsyntax"]').sheet); + const dynamicCSS = makeCSSManager((document.querySelector('style[title="dynamicsyntax"]') as unknown as CustomElementWithSheet).sheet); const authorData = {}; - const receiveAuthorData = (newAuthorData) => { + const receiveAuthorData = (newAuthorData: Author) => { for (const [author, data] of Object.entries(newAuthorData)) { const bgcolor = typeof data.colorId === 'number' ? clientVars.colorPalette[data.colorId] : data.colorId; diff --git a/src/static/js/broadcast_revisions.js b/src/static/js/broadcast_revisions.ts similarity index 92% rename from src/static/js/broadcast_revisions.js rename to src/static/js/broadcast_revisions.ts index 9409cd978..f29e570b4 100644 --- a/src/static/js/broadcast_revisions.js +++ b/src/static/js/broadcast_revisions.ts @@ -4,6 +4,9 @@ * 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 {clientVars} from "../../node/handler/PadMessageHandler"; +import {CustomWindow} from "../module/CustomWindow"; + /** * Copyright 2009 Google Inc. * @@ -36,7 +39,12 @@ const loadBroadcastRevisionsJS = () => { this.changesets.push(changesetWrapper); this.changesets.sort((a, b) => (b.deltaRev - a.deltaRev)); }; - const revisionInfo = {}; + const revisionInfo = { + latest: undefined, + addChangeset: undefined, + createNew: undefined, + getPath: undefined + }; revisionInfo.addChangeset = function (fromIndex, toIndex, changeset, backChangeset, timeDelta) { const startRevision = this[fromIndex] || this.createNew(fromIndex); const endRevision = this[toIndex] || this.createNew(toIndex); @@ -95,6 +103,6 @@ const loadBroadcastRevisionsJS = () => { times, }; }; - window.revisionInfo = revisionInfo; + (window as unknown as CustomWindow).revisionInfo = revisionInfo; }; export { loadBroadcastRevisionsJS }; diff --git a/src/static/js/broadcast_slider.js b/src/static/js/broadcast_slider.ts similarity index 89% rename from src/static/js/broadcast_slider.js rename to src/static/js/broadcast_slider.ts index c2478ae48..46d6866d3 100644 --- a/src/static/js/broadcast_slider.js +++ b/src/static/js/broadcast_slider.ts @@ -1,13 +1,17 @@ -import * as _ from "./underscore.js"; +import _ from "underscore"; + import { padmodals as padmodals$0 } from "./pad_modals.js"; import { colorutils as colorutils$0 } from "./colorutils.js"; +import {i18nextvar} from "./vendors/i18next"; +import {clientVars} from "../../node/handler/PadMessageHandler"; +import {CustomElementWithSheet} from "../module/CustomWindow"; 'use strict'; const padmodals = { padmodals: padmodals$0 }.padmodals; const colorutils = { colorutils: colorutils$0 }.colorutils; const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { let BroadcastSlider; // Hack to ensure timeslider i18n values are in - $("[data-key='timeslider_returnToPad'] > a > span").html(html10n.get('timeslider.toolbar.returnbutton')); + $("[data-key='timeslider_returnToPad'] > a > span").html(i18nextvar('timeslider.toolbar.returnbutton')); (() => { let sliderLength = 1000; let sliderPos = 0; @@ -56,7 +60,7 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { $('a.tlink').map(function () { $(this).attr('href', $(this).attr('thref').replace('%revision%', newpos)); }); - $('#revision_label').html(html10n.get('timeslider.version', { version: newpos })); + $('#revision_label').html(i18nextvar('timeslider.version', { version: newpos })); $('#leftstar, #leftstep').toggleClass('disabled', newpos === 0); $('#rightstar, #rightstep').toggleClass('disabled', newpos === sliderLength); sliderPos = newpos; @@ -100,7 +104,7 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { } }); if (numAnonymous > 0) { - const anonymousAuthorString = html10n.get('timeslider.unnamedauthors', { num: numAnonymous }); + const anonymousAuthorString = i18nextvar('timeslider.unnamedauthors', { num: numAnonymous }); if (numNamed !== 0) { authorsList.append(` + ${anonymousAuthorString}`); } @@ -121,7 +125,7 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { } } if (authors.length === 0) { - authorsList.append(html10n.get('timeslider.toolbar.authorsList')); + authorsList.append(i18nextvar('timeslider.toolbar.authorsList')); } }; const playButtonUpdater = () => { @@ -163,7 +167,7 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { fireWhenAllScriptsAreLoaded.push(() => { $(document).keyup((e) => { if (!e) - e = window.event; + e = window.event as any const code = e.keyCode || e.which; if (code === 37) { // left if (e.shiftKey) { @@ -196,19 +200,19 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { }); // Slider dragging $('#ui-slider-handle').mousedown(function (evt) { - this.startLoc = evt.clientX; - this.currentLoc = parseInt($(this).css('left')); + (this as unknown as CustomElementWithSheet).startLoc = evt.clientX; + (this as unknown as CustomElementWithSheet).currentLoc = parseInt($(this).css('left')); sliderActive = true; $(document).mousemove((evt2) => { $(this).css('pointer', 'move'); - let newloc = this.currentLoc + (evt2.clientX - this.startLoc); + let newloc = (this as unknown as CustomElementWithSheet).currentLoc + (evt2.clientX - (this as unknown as CustomElementWithSheet).startLoc); if (newloc < 0) newloc = 0; const maxPos = $('#ui-slider-bar').width() - 2; if (newloc > maxPos) newloc = maxPos; const version = Math.floor(newloc * sliderLength / maxPos); - $('#revision_label').html(html10n.get('timeslider.version', { version })); + $('#revision_label').html(i18nextvar('timeslider.version', { version })); $(this).css('left', newloc); if (getSliderPosition() !== version) _callSliderCallbacks(version); @@ -217,7 +221,7 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { $(document).unbind('mousemove'); $(document).unbind('mouseup'); sliderActive = false; - let newloc = this.currentLoc + (evt2.clientX - this.startLoc); + let newloc = (this as unknown as CustomElementWithSheet).currentLoc + (evt2.clientX - (this as unknown as CustomElementWithSheet).startLoc); if (newloc < 0) newloc = 0; const maxPos = $('#ui-slider-bar').width() - 2; @@ -229,7 +233,7 @@ const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => { $(this).css('left', '2px'); } else { - this.currentLoc = parseInt($(this).css('left')); + (this as unknown as CustomElementWithSheet).currentLoc = parseInt($(this).css('left')); } }); }); diff --git a/src/static/js/caretPosition.js b/src/static/js/caretPosition.ts similarity index 97% rename from src/static/js/caretPosition.js rename to src/static/js/caretPosition.ts index 3a27b0d60..688bb8f87 100644 --- a/src/static/js/caretPosition.js +++ b/src/static/js/caretPosition.ts @@ -1,4 +1,6 @@ 'use strict'; +import {CustomElementWithSheet} from "../module/CustomWindow"; + const createSelectionRange = (range) => { const clonedRange = range.cloneRange(); // we set the selection start and end to avoid error when user selects a text bigger than @@ -122,7 +124,7 @@ const getSelectionRange = () => { }; export const getPosition = () => { const range = getSelectionRange(); - if (!range || $(range.endContainer).closest('body')[0].id !== 'innerdocbody') + if (!range || ($(range.endContainer).closest('body')[0] as unknown as CustomElementWithSheet).id !== 'innerdocbody') return null; // When there's a
or any element that has no height, we can't get the dimension of the // element where the caret is. As we can't get the element height, we create a text node to get diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.ts similarity index 97% rename from src/static/js/changesettracker.js rename to src/static/js/changesettracker.ts index 5db0fb192..e4f8ed029 100644 --- a/src/static/js/changesettracker.js +++ b/src/static/js/changesettracker.ts @@ -1,6 +1,7 @@ import AttributeMap from "./AttributeMap.js"; -import AttributePool from "./AttributePool.js"; +import {AttributePool} from "./AttributePool.js"; import * as Changeset from "./Changeset.js"; +import {CustomWindow} from "../module/CustomWindow"; 'use strict'; const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => { // latest official text from server @@ -111,7 +112,7 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => { } else { // Get my authorID - const authorId = parent.parent.pad.myUserInfo.userId; + const authorId = (parent.parent as unknown as CustomWindow).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); diff --git a/src/static/js/chat.js b/src/static/js/chat.ts old mode 100755 new mode 100644 similarity index 90% rename from src/static/js/chat.js rename to src/static/js/chat.ts index 74d451044..66257668c --- a/src/static/js/chat.js +++ b/src/static/js/chat.ts @@ -4,6 +4,9 @@ import { padcookie as padcookie$0 } from "./pad_cookie.js"; import Tinycon from "tinycon/tinycon"; import * as hooks from "./pluginfw/hooks.js"; import { padeditor as padeditor$0 } from "./pad_editor.js"; +import {CustomWindow, JQueryGritter} from "../module/CustomWindow"; +import {pad} from "./pad"; +import {i18nextvar} from "./vendors/i18next"; 'use strict'; const padutils = { padutils: padutils$0 }.padutils; const padcookie = { padcookie: padcookie$0 }.padcookie; @@ -15,6 +18,8 @@ export const chat = (() => { let userAndChat = false; let chatMentions = 0; return { + historyPointer: undefined, gotInitalMessages: false, + show() { $('#chaticon').removeClass('visible'); $('#chatbox').addClass('visible'); @@ -22,7 +27,7 @@ export const chat = (() => { chatMentions = 0; Tinycon.setBubble(0); $('.chat-gritter-msg').each(function () { - $.gritter.remove(this.id); + ($ as unknown as JQueryGritter).gritter.remove(this.id); }); }, focus: () => { @@ -31,7 +36,7 @@ export const chat = (() => { }, 100); }, // Make chat stick to right hand side of screen - stickToScreen(fromInitialCall) { + stickToScreen(fromInitialCall?) { if (pad.settings.hideChat) { return; } @@ -46,7 +51,7 @@ export const chat = (() => { padcookie.setPref('chatAlwaysVisible', isStuck); $('#options-stickychat').prop('checked', isStuck); }, - chatAndUsers(fromInitialCall) { + chatAndUsers(fromInitialCall?) { const toEnable = $('#options-chatandusers').is(':checked'); if (toEnable || !userAndChat || fromInitialCall) { this.stickToScreen(true); @@ -76,7 +81,7 @@ export const chat = (() => { $('#chatbox').removeClass('visible'); } }, - scrollDown(force) { + scrollDown(force?) { if ($('#chatbox').hasClass('visible')) { if (force || !this.lastMessage || !this.lastMessage.position() || this.lastMessage.position().top < ($('#chattext').outerHeight() + 20)) { @@ -89,7 +94,7 @@ export const chat = (() => { }, async send() { const text = $('#chatinput').val(); - if (text.replace(/\s+/, '').length === 0) + if (typeof text !== "string" || text?.replace(/\s+/, '').length === 0) return; const message = new ChatMessage(text); await hooks.aCallAll('chatSendMessage', Object.freeze({ message })); @@ -117,7 +122,7 @@ export const chat = (() => { })}`; // the hook args const ctx = { - authorName: msg.displayName != null ? msg.displayName : html10n.get('pad.userlist.unnamed'), + authorName: msg.displayName != null ? msg.displayName : i18nextvar('pad.userlist.unnamed'), author: msg.authorId, text: padutils.escapeHtmlWithClickableLinks(msg.text, '_blank'), message: msg, @@ -140,8 +145,8 @@ export const chat = (() => { // does the user already have the chatbox open? const chatOpen = $('#chatbox').hasClass('visible'); // does this message contain this user's name? (is the current user mentioned?) - const wasMentioned = msg.authorId !== window.clientVars.userId && - ctx.authorName !== html10n.get('pad.userlist.unnamed') && + const wasMentioned = msg.authorId !== (window as unknown as CustomWindow).clientVars.userId && + ctx.authorName !== i18nextvar('pad.userlist.unnamed') && normalize(ctx.text).includes(normalize(ctx.authorName)); // If the user was mentioned, make the message sticky if (wasMentioned && !alreadyFocused && !isHistoryAdd && !chatOpen) { @@ -169,7 +174,10 @@ export const chat = (() => { chatMsg.insertAfter('#chatloadmessagesbutton'); else $('#chattext').append(chatMsg); - chatMsg.each((i, e) => html10n.translateElement(html10n.translations, e)); + chatMsg.each((i, e) => { + //TODO Fix this and return the component + i18nextvar(e) + }); // should we increment the counter?? if (increment && !isHistoryAdd) { // Update the counter of unread messages @@ -182,8 +190,11 @@ export const chat = (() => { // ctx.text was HTML-escaped before calling the hook. Hook functions are trusted // to not introduce an XSS vulnerability by adding unescaped user input. .append($('
').html(ctx.text).contents()); - text.each((i, e) => html10n.translateElement(html10n.translations, e)); - $.gritter.add({ + text.each((i, e) => { + //TODO Fix this and return the component + i18nextvar(e.getAttribute('data-l10n-id')) + });; + ($ as unknown as JQueryGritter).gritter.add({ text, sticky: ctx.sticky, time: ctx.duration, diff --git a/src/static/js/collab_client.js b/src/static/js/collab_client.ts similarity index 94% rename from src/static/js/collab_client.js rename to src/static/js/collab_client.ts index 5652c331c..e29f33d7d 100644 --- a/src/static/js/collab_client.js +++ b/src/static/js/collab_client.ts @@ -1,6 +1,8 @@ import { chat as chat$0 } from "./chat.js"; import * as hooks from "./pluginfw/hooks.js"; import browser from "./vendors/browser.js"; +import {clientVars} from "../../node/handler/PadMessageHandler"; +import {AuthorData} from "../module/CustomWindow"; 'use strict'; /** * This code is mostly from the old Etherpad. Please help us to comment this code. @@ -46,14 +48,14 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) userSet[userId] = initialUserInfo; let isPendingRevision = false; const callbacks = { - onUserJoin: () => { }, - onUserLeave: () => { }, - onUpdateUserInfo: () => { }, - onChannelStateChange: () => { }, - onClientMessage: () => { }, - onInternalAction: () => { }, - onConnectionTrouble: () => { }, - onServerMessage: () => { }, + onUserJoin: (arg?) => { }, + onUserLeave: (arg?) => { }, + onUpdateUserInfo: (arg?) => { }, + onChannelStateChange: (arg?, addArg?) => { }, + onClientMessage: (arg?) => { }, + onInternalAction: (arg?) => { }, + onConnectionTrouble: (arg?) => { }, + onServerMessage: (arg?) => { }, }; if (browser.firefox) { // Prevent "escape" from taking effect and canceling a comet connection; @@ -151,6 +153,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) }); }; const serverMessageTaskQueue = new class { + private _promiseChain: Promise; constructor() { this._promiseChain = Promise.resolve(); } @@ -309,7 +312,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const tellAceActiveAuthorInfo = (userInfo) => { tellAceAuthorInfo(userInfo.userId, userInfo.colorId); }; - const tellAceAuthorInfo = (userId, colorId, inactive) => { + const tellAceAuthorInfo = (userId, colorId, inactive?) => { if (typeof colorId === 'number') { colorId = clientVars.colorPalette[colorId]; } @@ -333,11 +336,11 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) const tellAceAboutHistoricalAuthors = (hadata) => { for (const [author, data] of Object.entries(hadata)) { if (!userSet[author]) { - tellAceAuthorInfo(author, data.colorId, true); + tellAceAuthorInfo(author, (data as AuthorData).colorId, true); } } }; - const setChannelState = (newChannelState, moreInfo) => { + const setChannelState = (newChannelState, moreInfo?) => { if (newChannelState !== channelState) { channelState = newChannelState; callbacks.onChannelStateChange(channelState, moreInfo); @@ -353,7 +356,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) // We need to present a working interface even before the socket // is connected for the first time. let deferredActions = []; - const defer = (func, tag) => function (...args) { + const defer = (func, tag?) => function (...args) { const action = () => { func.call(this, ...args); }; @@ -365,7 +368,7 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) action(); } }; - const doDeferredActions = (tag) => { + const doDeferredActions = (tag?) => { const newArray = []; for (let i = 0; i < deferredActions.length; i++) { const a = deferredActions[i]; @@ -386,7 +389,14 @@ const getCollabClient = (ace2editor, serverVars, initialUserInfo, options, _pad) }; const getCurrentRevisionNumber = () => rev; const getMissedChanges = () => { - const obj = {}; + const obj = { + furtherChangeset: undefined, + furtherChangesetAPool: undefined, + committedChangesetAPool: undefined, + committedChangeset: undefined, + userInfo: undefined, + baseRev: undefined + }; obj.userInfo = userSet[userId]; obj.baseRev = rev; if (committing && stateMessage) { diff --git a/src/static/js/colorutils.js b/src/static/js/colorutils.js deleted file mode 100644 index 25d678728..000000000 --- a/src/static/js/colorutils.js +++ /dev/null @@ -1,102 +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 - */ -// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js -// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS -/** - * 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 colorutils = {}; -// Check that a given value is a css hex color value, e.g. -// "#ffffff" or "#fff" -colorutils.isCssHex = (cssColor) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor); -// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] -colorutils.css2triple = (cssColor) => { - const sixHex = colorutils.css2sixhex(cssColor); - const hexToFloat = (hh) => Number(`0x${hh}`) / 255; - return [ - hexToFloat(sixHex.substr(0, 2)), - hexToFloat(sixHex.substr(2, 2)), - hexToFloat(sixHex.substr(4, 2)), - ]; -}; -// "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff" -colorutils.css2sixhex = (cssColor) => { - let h = /[0-9a-fA-F]+/.exec(cssColor)[0]; - if (h.length !== 6) { - const a = h.charAt(0); - const b = h.charAt(1); - const c = h.charAt(2); - h = a + a + b + b + c + c; - } - return h; -}; -// [1.0, 1.0, 1.0] -> "#ffffff" -colorutils.triple2css = (triple) => { - const floatToHex = (n) => { - const n2 = colorutils.clamp(Math.round(n * 255), 0, 255); - return (`0${n2.toString(16)}`).slice(-2); - }; - return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`; -}; -colorutils.clamp = (v, bot, top) => v < bot ? bot : (v > top ? top : v); -colorutils.min3 = (a, b, c) => (a < b) ? (a < c ? a : c) : (b < c ? b : c); -colorutils.max3 = (a, b, c) => (a > b) ? (a > c ? a : c) : (b > c ? b : c); -colorutils.colorMin = (c) => colorutils.min3(c[0], c[1], c[2]); -colorutils.colorMax = (c) => colorutils.max3(c[0], c[1], c[2]); -colorutils.scale = (v, bot, top) => colorutils.clamp(bot + v * (top - bot), 0, 1); -colorutils.unscale = (v, bot, top) => colorutils.clamp((v - bot) / (top - bot), 0, 1); -colorutils.scaleColor = (c, bot, top) => [ - colorutils.scale(c[0], bot, top), - colorutils.scale(c[1], bot, top), - colorutils.scale(c[2], bot, top), -]; -colorutils.unscaleColor = (c, bot, top) => [ - colorutils.unscale(c[0], bot, top), - colorutils.unscale(c[1], bot, top), - colorutils.unscale(c[2], bot, top), -]; -// rule of thumb for RGB brightness; 1.0 is white -colorutils.luminosity = (c) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11; -colorutils.saturate = (c) => { - const min = colorutils.colorMin(c); - const max = colorutils.colorMax(c); - if (max - min <= 0) - return [1.0, 1.0, 1.0]; - return colorutils.unscaleColor(c, min, max); -}; -colorutils.blend = (c1, c2, t) => [ - colorutils.scale(t, c1[0], c2[0]), - colorutils.scale(t, c1[1], c2[1]), - colorutils.scale(t, c1[2], c2[2]), -]; -colorutils.invert = (c) => [1 - c[0], 1 - c[1], 1 - c[2]]; -colorutils.complementary = (c) => { - const inv = colorutils.invert(c); - return [ - (inv[0] >= c[0]) ? Math.min(inv[0] * 1.30, 1) : (c[0] * 0.30), - (inv[1] >= c[1]) ? Math.min(inv[1] * 1.59, 1) : (c[1] * 0.59), - (inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11), - ]; -}; -colorutils.textColorFromBackgroundColor = (bgcolor, skinName) => { - const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff'; - const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222'; - return colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5 ? white : black; -}; -export { colorutils }; diff --git a/src/static/js/colorutils.ts b/src/static/js/colorutils.ts new file mode 100644 index 000000000..a12db7d11 --- /dev/null +++ b/src/static/js/colorutils.ts @@ -0,0 +1,113 @@ +'use strict'; +/** + * This code is mostly from the old Etherpad. Please help us to comment this code. + * This helps other people to understand this code better and helps them to improve it. + * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED + */ +// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/colorutils.js +// THIS FILE IS ALSO SERVED AS CLIENT-SIDE JS +/** + * 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 colorutils = { + css2triple: (cssColor) => { + const sixHex = colorutils.css2sixhex(cssColor); + const hexToFloat = (hh) => Number(`0x${hh}`) / 255; + return [ + hexToFloat(sixHex.substr(0, 2)), + hexToFloat(sixHex.substr(2, 2)), + hexToFloat(sixHex.substr(4, 2)), + ]; + }, + // "#ffffff" or "#fff" or "ffffff" or "fff" to "ffffff" + css2sixhex: (cssColor) => { + let h = /[0-9a-fA-F]+/.exec(cssColor)[0]; + if (h.length !== 6) { + const a = h.charAt(0); + const b = h.charAt(1); + const c = h.charAt(2); + h = a + a + b + b + c + c; + } + return h; + }, + // [1.0, 1.0, 1.0] -> "#ffffff" + triple2css: (triple) => { + const floatToHex = (n) => { + const n2 = colorutils.clamp(Math.round(n * 255), 0, 255); + return (`0${n2.toString(16)}`).slice(-2); + }; + return `#${floatToHex(triple[0])}${floatToHex(triple[1])}${floatToHex(triple[2])}`; + }, + isCssHex: (cssColor) => /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(cssColor), + clamp: (v, bot, top) => v < bot ? bot : (v > top ? top : v), + min3: (a, b, c) => (a < b) ? (a < c ? a : c) : (b < c ? b : c), + max3: (a, b, c) => (a > b) ? (a > c ? a : c) : (b > c ? b : c), + colorMin: (c) => colorutils.min3(c[0], c[1], c[2]), + colorMax: (c) => colorutils.max3(c[0], c[1], c[2]), + scale: (v, bot, top) => colorutils.clamp(bot + v * (top - bot), 0, 1), + unscale: (v, bot, top) => colorutils.clamp((v - bot) / (top - bot), 0, 1), + scaleColor:(c, bot, top) => [ + colorutils.scale(c[0], bot, top), + colorutils.scale(c[1], bot, top), + colorutils.scale(c[2], bot, top), + ], + unscaleColor: (c, bot, top) => [ + colorutils.unscale(c[0], bot, top), + colorutils.unscale(c[1], bot, top), + colorutils.unscale(c[2], bot, top), + ], +// rule of thumb for RGB brightness; 1.0 is white + luminosity: (c) => c[0] * 0.30 + c[1] * 0.59 + c[2] * 0.11, + saturate: (c) => { + const min = colorutils.colorMin(c); + const max = colorutils.colorMax(c); + if (max - min <= 0) + return [1.0, 1.0, 1.0]; + return colorutils.unscaleColor(c, min, max); + }, + blend: (c1, c2, t) => [ + colorutils.scale(t, c1[0], c2[0]), + colorutils.scale(t, c1[1], c2[1]), + colorutils.scale(t, c1[2], c2[2]), + ], + invert: (c) => [1 - c[0], 1 - c[1], 1 - c[2]], + complementary: (c) => { + const inv = colorutils.invert(c); + return [ + (inv[0] >= c[0]) ? Math.min(inv[0] * 1.30, 1) : (c[0] * 0.30), + (inv[1] >= c[1]) ? Math.min(inv[1] * 1.59, 1) : (c[1] * 0.59), + (inv[2] >= c[2]) ? Math.min(inv[2] * 1.11, 1) : (c[2] * 0.11), + ]; + }, + textColorFromBackgroundColor: (bgcolor, skinName) => { + const white = skinName === 'colibris' ? 'var(--super-light-color)' : '#fff'; + const black = skinName === 'colibris' ? 'var(--super-dark-color)' : '#222'; + return colorutils.luminosity(colorutils.css2triple(bgcolor)) < 0.5 ? white : black; + }, +}; +// Check that a given value is a css hex color value, e.g. +// "#ffffff" or "#fff" +// "#ffffff" or "#fff" or "ffffff" or "fff" to [1.0, 1.0, 1.0] +colorutils.css2triple = (cssColor) => { + const sixHex = colorutils.css2sixhex(cssColor); + const hexToFloat = (hh) => Number(`0x${hh}`) / 255; + return [ + hexToFloat(sixHex.substr(0, 2)), + hexToFloat(sixHex.substr(2, 2)), + hexToFloat(sixHex.substr(4, 2)), + ]; +}; + +export { colorutils }; diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.ts similarity index 57% rename from src/static/js/contentcollector.js rename to src/static/js/contentcollector.ts index 41ec41c37..0b78b393f 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.ts @@ -1,3 +1,5 @@ +// @ts-nocheck +//TODO make this file compatible with Typescript import AttributeMap from "./AttributeMap.js"; import UNorm from "unorm"; import * as Changeset from "./Changeset.js"; @@ -51,7 +53,7 @@ const supportedElems = new Set([ 'u', 'ul', ]); -const makeContentCollector = (collectStyles, abrowser, apool, className2Author) => { +const makeContentCollector = (collectStyles, abrowser, apool?, className2Author?) => { const _blockElems = { div: 1, p: 1, @@ -101,7 +103,380 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) self.startNew(); return self; })(); - const cc = {}; + const cc = { + collectContent(body, state?) { + let unsupportedElements = null; + if (!state) { + state = { + flags: { /* name -> nesting counter*/}, + localAttribs: null, + attribs: { /* name -> nesting counter*/}, + attribString: '', + // lineAttributes maintain a map from attributes to attribute values set on a line + lineAttributes: { + /* + example: + 'list': 'bullet1', + */ + }, + unsupportedElements: new Set(), + }; + unsupportedElements = state.unsupportedElements; + } + const localAttribs = state.localAttribs; + state.localAttribs = null; + const isBlock = isBlockElement(node); + if (!isBlock && node.name && (node.name !== 'body')) { + if (!supportedElems.has(node.name)) + state.unsupportedElements.add(node.name); + } + const isEmpty = _isEmpty(node, state); + if (isBlock) + _ensureColumnZero(state); + const startLine = lines.length() - 1; + _reachBlockPoint(node, 0, state); + if (node.nodeType === node.TEXT_NODE) { + const tname = node.parentNode.getAttribute('name'); + const context = { cc: this, state, tname, node, text: node.nodeValue }; + // Hook functions may either return a string (deprecated) or modify context.text. If any hook + // function modifies context.text then all returned strings are ignored. If no hook functions + // modify context.text, the first hook function to return a string wins. + const [hookTxt] = hooks.callAll('collectContentLineText', context).filter((s) => typeof s === 'string'); + let txt = context.text === node.nodeValue && hookTxt != null ? hookTxt : context.text; + let rest = ''; + let x = 0; // offset into original text + if (txt.length === 0) { + if (startPoint && node === startPoint.node) { + selStart = _pointHere(0, state); + } + if (endPoint && node === endPoint.node) { + selEnd = _pointHere(0, state); + } + } + while (txt.length > 0) { + let consumed = 0; + if (state.flags.preMode) { + const firstLine = txt.split('\n', 1)[0]; + consumed = firstLine.length + 1; + rest = txt.substring(consumed); + txt = firstLine; + } + else { /* will only run this loop body once */ + } + if (startPoint && node === startPoint.node && startPoint.index - x <= txt.length) { + selStart = _pointHere(startPoint.index - x, state); + } + if (endPoint && node === endPoint.node && endPoint.index - x <= txt.length) { + selEnd = _pointHere(endPoint.index - x, state); + } + let txt2 = txt; + if ((!state.flags.preMode) && /^[\r\n]*$/.exec(txt)) { + // prevents textnodes containing just "\n" from being significant + // in safari when pasting text, now that we convert them to + // spaces instead of removing them, because in other cases + // removing "\n" from pasted HTML will collapse words together. + txt2 = ''; + } + const atBeginningOfLine = lines.textOfLine(lines.length() - 1).length === 0; + if (atBeginningOfLine) { + // newlines in the source mustn't become spaces at beginning of line box + txt2 = txt2.replace(/^\n*/, ''); + } + if (atBeginningOfLine && Object.keys(state.lineAttributes).length !== 0) { + _produceLineAttributesMarker(state); + } + lines.appendText(textify(txt2), state.attribString); + x += consumed; + txt = rest; + if (txt.length > 0) { + cc.startNewLine(state); + } + } + } + else if (node.nodeType === node.ELEMENT_NODE) { + const tname = tagName(node) || ''; + if (tname === 'img') { + hooks.callAll('collectContentImage', { + cc, + state, + tname, + styl: null, + cls: null, + node, + }); + } + else { + // THIS SEEMS VERY HACKY! -- Please submit a better fix! + delete state.lineAttributes.img; + } + if (tname === 'br') { + this.breakLine = true; + const tvalue = node.getAttribute('value'); + const [startNewLine = true] = hooks.callAll('collectContentLineBreak', { + cc: this, + state, + tname, + tvalue, + styl: null, + cls: null, + }); + if (startNewLine) { + cc.startNewLine(state); + } + } + else if (tname === 'script' || tname === 'style') { + // ignore + } + else if (!isEmpty) { + let styl = node.getAttribute('style'); + let cls = node.getAttribute('class'); + let isPre = (tname === 'pre'); + if ((!isPre) && abrowser && abrowser.safari) { + isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); + } + if (isPre) + cc.incrementFlag(state, 'preMode'); + let oldListTypeOrNull = null; + let oldAuthorOrNull = null; + // LibreOffice Writer puts in weird items during import or copy/paste, we should drop them. + if (cls === 'Numbering_20_Symbols' || cls === 'Bullet_20_Symbols') { + styl = null; + cls = null; + // We have to return here but this could break things in the future, + // for now it shows how to fix the problem + return; + } + if (collectStyles) { + hooks.callAll('collectContentPre', { + cc, + state, + tname, + styl, + cls, + }); + if (tname === 'b' || + (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || + tname === 'strong') { + cc.doAttrib(state, 'bold'); + } + if (tname === 'i' || + (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || + tname === 'em') { + cc.doAttrib(state, 'italic'); + } + if (tname === 'u' || + (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || + tname === 'ins') { + cc.doAttrib(state, 'underline'); + } + if (tname === 's' || + (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || + tname === 'del') { + cc.doAttrib(state, 'strikethrough'); + } + if (tname === 'ul' || tname === 'ol') { + let type = node.getAttribute('class'); + const rr = cls && /(?:^| )list-([a-z]+[0-9]+)\b/.exec(cls); + // lists do not need to have a type, so before we make a wrong guess + // check if we find a better hint within the node's children + if (!rr && !type) { + for (const child of node.childNodes) { + if (tagName(child) !== 'ul') + continue; + type = child.getAttribute('class'); + if (type) + break; + } + } + if (rr && rr[1]) { + type = rr[1]; + } + else { + if (tname === 'ul') { + const cls = node.getAttribute('class'); + if ((type && type.match('indent')) || (cls && cls.match('indent'))) { + type = 'indent'; + } + else { + type = 'bullet'; + } + } + else { + type = 'number'; + } + type += String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1)); + } + oldListTypeOrNull = (_enterList(state, type) || 'none'); + } + else if ((tname === 'div' || tname === 'p') && cls && cls.match(/(?:^| )ace-line\b/)) { + // This has undesirable behavior in Chrome but is right in other browsers. + // See https://github.com/ether/etherpad-lite/issues/2412 for reasoning + if (!abrowser.chrome) + oldListTypeOrNull = (_enterList(state, undefined) || 'none'); + } + else if (tname === 'li') { + state.lineAttributes.start = state.start || 0; + _recalcAttribString(state); + if (state.lineAttributes.list.indexOf('number') !== -1) { + /* + Nested OLs are not -->
  1. 1
    1. nested
+ They are -->
  1. 1
    1. nested
+ Note how the
    item has to be inside a
  1. + Because of this we don't increment the start number + */ + if (node.parentNode && tagName(node.parentNode) !== 'ol') { + /* + TODO: start number has to increment based on indentLevel(numberX) + This means we have to build an object IE + { + 1: 4 + 2: 3 + 3: 5 + } + But the browser seems to handle it fine using CSS.. Why can't we do the same + with exports? We can.. But let's leave this comment in because it might be useful + in the future.. + */ + state.start++; // not if it's parent is an OL or UL. + } + } + // UL list items never modify the start value. + if (node.parentNode && tagName(node.parentNode) === 'ul') { + state.start++; + // TODO, this is hacky. + // Because if the first item is an UL it will increment a list no? + // A much more graceful way would be to say, ul increases if it's within an OL + // But I don't know a way to do that because we're only aware of the previous Line + // As the concept of parent's doesn't exist when processing each domline... + } + } + else { + // Below needs more testin if it's neccesary as _exitList should take care of this. + // delete state.start; + // delete state.listNesting; + // _recalcAttribString(state); + } + if (className2Author && cls) { + const classes = cls.match(/\S+/g); + if (classes && classes.length > 0) { + for (let i = 0; i < classes.length; i++) { + const c = classes[i]; + const a = className2Author(c); + if (a) { + oldAuthorOrNull = (_enterAuthor(state, a) || 'none'); + break; + } + } + } + } + } + for (const c of node.childNodes) { + cc.collectContent(c, state); + } + if (collectStyles) { + hooks.callAll('collectContentPost', { + cc, + state, + tname, + styl, + cls, + }); + } + if (isPre) + cc.decrementFlag(state, 'preMode'); + if (state.localAttribs) { + for (let i = 0; i < state.localAttribs.length; i++) { + cc.decrementAttrib(state, state.localAttribs[i]); + } + } + if (oldListTypeOrNull) { + _exitList(state, oldListTypeOrNull); + } + if (oldAuthorOrNull) { + _exitAuthor(state, oldAuthorOrNull); + } + } + } + }, + finish() { + lines.flush(); + const lineAttribs = lines.attribLines(); + const lineStrings = cc.getLines(); + lineStrings.length--; + lineAttribs.length--; + const ss = getSelectionStart(); + const se = getSelectionEnd(); + const fixLongLines = () => { + // design mode does not deal with with really long lines! + const lineLimit = 2000; // chars + const buffer = 10; // chars allowed over before wrapping + let linesWrapped = 0; + let numLinesAfter = 0; + for (let i = lineStrings.length - 1; i >= 0; i--) { + let oldString = lineStrings[i]; + let oldAttribString = lineAttribs[i]; + if (oldString.length > lineLimit + buffer) { + const newStrings = []; + const newAttribStrings = []; + while (oldString.length > lineLimit) { + // var semiloc = oldString.lastIndexOf(';', lineLimit-1); + // var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit); + const lengthToTake = lineLimit; + newStrings.push(oldString.substring(0, lengthToTake)); + oldString = oldString.substring(lengthToTake); + newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake)); + oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake); + } + if (oldString.length > 0) { + newStrings.push(oldString); + newAttribStrings.push(oldAttribString); + } + const fixLineNumber = (lineChar) => { + if (lineChar[0] < 0) + return; + let n = lineChar[0]; + let c = lineChar[1]; + if (n > i) { + n += (newStrings.length - 1); + } + else if (n === i) { + let a = 0; + while (c > newStrings[a].length) { + c -= newStrings[a].length; + a++; + } + n += a; + } + lineChar[0] = n; + lineChar[1] = c; + }; + fixLineNumber(ss); + fixLineNumber(se); + linesWrapped++; + numLinesAfter += newStrings.length; + lineStrings.splice(i, 1, ...newStrings); + lineAttribs.splice(i, 1, ...newAttribStrings); + } + } + return { + linesWrapped, + numLinesAfter, + }; + }; + const wrapData = fixLongLines(); + return { + selStart: ss, + selEnd: se, + linesWrapped: wrapData.linesWrapped, + numLinesAfter: wrapData.numLinesAfter, + lines: lineStrings, + lineAttribs, + }; + } + + } + return cc; + } const _ensureColumnZero = (state) => { if (!lines.atColumnZero()) { cc.startNewLine(state); @@ -615,83 +990,6 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) // last line is complete (i.e. if a following span should be on a new line). // can be called at any point cc.getLines = () => lines.textLines(); - cc.finish = () => { - lines.flush(); - const lineAttribs = lines.attribLines(); - const lineStrings = cc.getLines(); - lineStrings.length--; - lineAttribs.length--; - const ss = getSelectionStart(); - const se = getSelectionEnd(); - const fixLongLines = () => { - // design mode does not deal with with really long lines! - const lineLimit = 2000; // chars - const buffer = 10; // chars allowed over before wrapping - let linesWrapped = 0; - let numLinesAfter = 0; - for (let i = lineStrings.length - 1; i >= 0; i--) { - let oldString = lineStrings[i]; - let oldAttribString = lineAttribs[i]; - if (oldString.length > lineLimit + buffer) { - const newStrings = []; - const newAttribStrings = []; - while (oldString.length > lineLimit) { - // var semiloc = oldString.lastIndexOf(';', lineLimit-1); - // var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit); - const lengthToTake = lineLimit; - newStrings.push(oldString.substring(0, lengthToTake)); - oldString = oldString.substring(lengthToTake); - newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake)); - oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake); - } - if (oldString.length > 0) { - newStrings.push(oldString); - newAttribStrings.push(oldAttribString); - } - const fixLineNumber = (lineChar) => { - if (lineChar[0] < 0) - return; - let n = lineChar[0]; - let c = lineChar[1]; - if (n > i) { - n += (newStrings.length - 1); - } - else if (n === i) { - let a = 0; - while (c > newStrings[a].length) { - c -= newStrings[a].length; - a++; - } - n += a; - } - lineChar[0] = n; - lineChar[1] = c; - }; - fixLineNumber(ss); - fixLineNumber(se); - linesWrapped++; - numLinesAfter += newStrings.length; - lineStrings.splice(i, 1, ...newStrings); - lineAttribs.splice(i, 1, ...newAttribStrings); - } - } - return { - linesWrapped, - numLinesAfter, - }; - }; - const wrapData = fixLongLines(); - return { - selStart: ss, - selEnd: se, - linesWrapped: wrapData.linesWrapped, - numLinesAfter: wrapData.numLinesAfter, - lines: lineStrings, - lineAttribs, - }; - }; - return cc; -}; export { sanitizeUnicode }; export { makeContentCollector }; export { supportedElems }; diff --git a/src/static/js/cssmanager.js b/src/static/js/cssmanager.ts similarity index 100% rename from src/static/js/cssmanager.js rename to src/static/js/cssmanager.ts diff --git a/src/static/js/domline.js b/src/static/js/domline.js deleted file mode 100644 index aea7f84fa..000000000 --- a/src/static/js/domline.js +++ /dev/null @@ -1,244 +0,0 @@ -import * as Security from "./security.js"; -import * as hooks from "./pluginfw/hooks.js"; -import * as _ from "./underscore.js"; -import { lineAttributeMarker as lineAttributeMarker$0 } from "./linestylefilter.js"; -'use strict'; -const lineAttributeMarker = { lineAttributeMarker: lineAttributeMarker$0 }.lineAttributeMarker; -const noop = () => { }; -const domline = {}; -domline.addToLineClass = (lineClass, cls) => { - // an "empty span" at any point can be used to add classes to - // the line, using line:className. otherwise, we ignore - // the span. - cls.replace(/\S+/g, (c) => { - if (c.indexOf('line:') === 0) { - // add class to line - lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5); - } - }); - return lineClass; -}; -// if "document" is falsy we don't create a DOM node, just -// an object with innerHTML and className -domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { - const result = { - node: null, - appendSpan: noop, - prepareForAdd: noop, - notifyAdded: noop, - clearSpans: noop, - finishUpdate: noop, - lineMarker: 0, - }; - const document = optDocument; - if (document) { - result.node = document.createElement('div'); - // JAWS and NVDA screen reader compatibility. Only needed if in a real browser. - result.node.setAttribute('aria-live', 'assertive'); - } - else { - result.node = { - innerHTML: '', - className: '', - }; - } - let html = []; - let preHtml = ''; - let postHtml = ''; - let curHTML = null; - const processSpaces = (s) => domline.processSpaces(s, doesWrap); - const perTextNodeProcess = (doesWrap ? _.identity : processSpaces); - const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); - let lineClass = 'ace-line'; - result.appendSpan = (txt, cls) => { - let processedMarker = false; - // Handle lineAttributeMarker, if present - if (cls.indexOf(lineAttributeMarker) >= 0) { - let listType = /(?:^| )list:(\S+)/.exec(cls); - const start = /(?:^| )start:(\S+)/.exec(cls); - _.map(hooks.callAll('aceDomLinePreProcessLineAttributes', { - domline, - cls, - }), (modifier) => { - preHtml += modifier.preHtml; - postHtml += modifier.postHtml; - processedMarker |= modifier.processedMarker; - }); - if (listType) { - listType = listType[1]; - if (listType) { - if (listType.indexOf('number') < 0) { - preHtml += `
    • `; - postHtml = `
    ${postHtml}`; - } - else { - if (start) { // is it a start of a list with more than one item in? - if (Number.parseInt(start[1]) === 1) { // if its the first one at this level? - // Add start class to DIV node - lineClass = `${lineClass} ` + `list-start-${listType}`; - } - preHtml += - `
    1. `; - } - else { - // Handles pasted contents into existing lists - preHtml += `
      1. `; - } - postHtml += '
      '; - } - } - processedMarker = true; - } - _.map(hooks.callAll('aceDomLineProcessLineAttributes', { - domline, - cls, - }), (modifier) => { - preHtml += modifier.preHtml; - postHtml += modifier.postHtml; - processedMarker |= modifier.processedMarker; - }); - if (processedMarker) { - result.lineMarker += txt.length; - return; // don't append any text - } - } - let href = null; - let simpleTags = null; - if (cls.indexOf('url') >= 0) { - cls = cls.replace(/(^| )url:(\S+)/g, (x0, space, url) => { - href = url; - return `${space}url`; - }); - } - if (cls.indexOf('tag') >= 0) { - cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => { - if (!simpleTags) - simpleTags = []; - simpleTags.push(tag.toLowerCase()); - return space + tag; - }); - } - let extraOpenTags = ''; - let extraCloseTags = ''; - _.map(hooks.callAll('aceCreateDomLine', { - domline, - cls, - }), (modifier) => { - cls = modifier.cls; - extraOpenTags += modifier.extraOpenTags; - extraCloseTags = modifier.extraCloseTags + extraCloseTags; - }); - if ((!txt) && cls) { - lineClass = domline.addToLineClass(lineClass, cls); - } - else if (txt) { - if (href) { - const urn_schemes = new RegExp('^(about|geo|mailto|tel):'); - // if the url doesn't include a protocol prefix, assume http - if (!~href.indexOf('://') && !urn_schemes.test(href)) { - href = `http://${href}`; - } - // Using rel="noreferrer" stops leaking the URL/location of the pad when - // clicking links in the document. - // Not all browsers understand this attribute, but it's part of the HTML5 standard. - // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer - // Additionally, we do rel="noopener" to ensure a higher level of referrer security. - // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener - // https://mathiasbynens.github.io/rel-noopener/ - // https://github.com/ether/etherpad-lite/pull/3636 - const escapedHref = Security.escapeHTMLAttribute(href); - extraOpenTags = `${extraOpenTags}`; - extraCloseTags = `${extraCloseTags}`; - } - if (simpleTags) { - simpleTags.sort(); - extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`; - simpleTags.reverse(); - extraCloseTags = `${extraCloseTags}`; - } - html.push('', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, ''); - } - }; - result.clearSpans = () => { - html = []; - lineClass = 'ace-line'; - result.lineMarker = 0; - }; - const writeHTML = () => { - let newHTML = perHtmlLineProcess(html.join('')); - if (!newHTML) { - if ((!document) || (!optBrowser)) { - newHTML += ' '; - } - else { - newHTML += '
      '; - } - } - if (nonEmpty) { - newHTML = (preHtml || '') + newHTML + (postHtml || ''); - } - html = preHtml = postHtml = ''; // free memory - if (newHTML !== curHTML) { - curHTML = newHTML; - result.node.innerHTML = curHTML; - } - if (lineClass != null) - result.node.className = lineClass; - hooks.callAll('acePostWriteDomLineHTML', { - node: result.node, - }); - }; - result.prepareForAdd = writeHTML; - result.finishUpdate = writeHTML; - return result; -}; -domline.processSpaces = (s, doesWrap) => { - if (s.indexOf('<') < 0 && !doesWrap) { - // short-cut - return s.replace(/ /g, ' '); - } - const parts = []; - s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { - parts.push(m); - }); - if (doesWrap) { - let endOfLine = true; - let beforeSpace = false; - // last space in a run is normal, others are nbsp, - // end of line is nbsp - for (let i = parts.length - 1; i >= 0; i--) { - const p = parts[i]; - if (p === ' ') { - if (endOfLine || beforeSpace) - parts[i] = ' '; - endOfLine = false; - beforeSpace = true; - } - else if (p.charAt(0) !== '<') { - endOfLine = false; - beforeSpace = false; - } - } - // beginning of line is nbsp - for (let i = 0; i < parts.length; i++) { - const p = parts[i]; - if (p === ' ') { - parts[i] = ' '; - break; - } - else if (p.charAt(0) !== '<') { - break; - } - } - } - else { - for (let i = 0; i < parts.length; i++) { - const p = parts[i]; - if (p === ' ') { - parts[i] = ' '; - } - } - } - return parts.join(''); -}; -export { domline }; diff --git a/src/static/js/domline.ts b/src/static/js/domline.ts new file mode 100644 index 000000000..6b62a2231 --- /dev/null +++ b/src/static/js/domline.ts @@ -0,0 +1,408 @@ +import * as Security from "./security.js"; +import * as hooks from "./pluginfw/hooks.js"; +import * as _ from "underscore"; +import { lineAttributeMarker as lineAttributeMarker$0 } from "./linestylefilter.js"; +'use strict'; +const lineAttributeMarker = { lineAttributeMarker: lineAttributeMarker$0 }.lineAttributeMarker; +const noop = () => { }; +const domline = { + processSpaces: (s, doesWrap) => { + if (s.indexOf('<') < 0 && !doesWrap) { + // short-cut + return s.replace(/ /g, ' '); + } + const parts = []; + s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { + parts.push(m); + }); + if (doesWrap) { + let endOfLine = true; + let beforeSpace = false; + // last space in a run is normal, others are nbsp, + // end of line is nbsp + for (let i = parts.length - 1; i >= 0; i--) { + const p = parts[i]; + if (p === ' ') { + if (endOfLine || beforeSpace) + parts[i] = ' '; + endOfLine = false; + beforeSpace = true; + } + else if (p.charAt(0) !== '<') { + endOfLine = false; + beforeSpace = false; + } + } + // beginning of line is nbsp + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + if (p === ' ') { + parts[i] = ' '; + break; + } + else if (p.charAt(0) !== '<') { + break; + } + } + } + else { + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + if (p === ' ') { + parts[i] = ' '; + } + } + } + return parts.join(''); + }, + addToLineClass: (lineClass, cls) => { + // an "empty span" at any point can be used to add classes to + // the line, using line:className. otherwise, we ignore + // the span. + cls.replace(/\S+/g, (c) => { + if (c.indexOf('line:') === 0) { + // add class to line + lineClass = (lineClass ? `${lineClass} ` : '') + c.substring(5); + } + }); + return lineClass; + }, + createDomLine: (nonEmpty, doesWrap, optBrowser?, optDocument?) => { + const result = { + node: null, + prepareForAdd: noop, + notifyAdded: noop, + finishUpdate: noop, + lineMarker: 0, + clearSpans: () => { + html = []; + lineClass = 'ace-line'; + result.lineMarker = 0; + }, + appendSpan: (txt, cls) => { + let processedMarker:any = false; + // Handle lineAttributeMarker, if present + if (cls.indexOf(lineAttributeMarker) >= 0) { + let listType:any = /(?:^| )list:(\S+)/.exec(cls); + const start = /(?:^| )start:(\S+)/.exec(cls); + _.map(hooks.callAll('aceDomLinePreProcessLineAttributes', { + domline, + cls, + }), (modifier) => { + preHtml += modifier.preHtml; + postHtml += modifier.postHtml; + processedMarker |= modifier.processedMarker; + }); + if (listType) { + listType = listType[1]; + if (listType) { + if (listType.indexOf('number') < 0) { + preHtml += `
      • `; + postHtml = `
      ${postHtml}`; + } else { + if (start) { // is it a start of a list with more than one item in? + if (Number.parseInt(start[1]) === 1) { // if its the first one at this level? + // Add start class to DIV node + lineClass = `${lineClass} ` + `list-start-${listType}`; + } + preHtml += + `
      1. `; + } else { + // Handles pasted contents into existing lists + preHtml += `
        1. `; + } + postHtml += '
        '; + } + } + processedMarker = true; + } + _.map(hooks.callAll('aceDomLineProcessLineAttributes', { + domline, + cls, + }), (modifier) => { + preHtml += modifier.preHtml; + postHtml += modifier.postHtml; + processedMarker |= modifier.processedMarker; + }); + if (processedMarker) { + result.lineMarker += txt.length; + return; // don't append any text + } + } + let href = null; + let simpleTags = null; + if (cls.indexOf('url') >= 0) { + cls = cls.replace(/(^| )url:(\S+)/g, (x0, space, url) => { + href = url; + return `${space}url`; + }); + } + if (cls.indexOf('tag') >= 0) { + cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => { + if (!simpleTags) + simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space + tag; + }); + } + let extraOpenTags = ''; + let extraCloseTags = ''; + _.map(hooks.callAll('aceCreateDomLine', { + domline, + cls, + }), (modifier) => { + cls = modifier.cls; + extraOpenTags += modifier.extraOpenTags; + extraCloseTags = modifier.extraCloseTags + extraCloseTags; + }); + if ((!txt) && cls) { + lineClass = domline.addToLineClass(lineClass, cls); + } else if (txt) { + if (href) { + const urn_schemes = new RegExp('^(about|geo|mailto|tel):'); + // if the url doesn't include a protocol prefix, assume http + if (!~href.indexOf('://') && !urn_schemes.test(href)) { + href = `http://${href}`; + } + // Using rel="noreferrer" stops leaking the URL/location of the pad when + // clicking links in the document. + // Not all browsers understand this attribute, but it's part of the HTML5 standard. + // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer + // Additionally, we do rel="noopener" to ensure a higher level of referrer security. + // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener + // https://mathiasbynens.github.io/rel-noopener/ + // https://github.com/ether/etherpad-lite/pull/3636 + const escapedHref = Security.escapeHTMLAttribute(href); + extraOpenTags = `${extraOpenTags}`; + extraCloseTags = `${extraCloseTags}`; + } + if (simpleTags) { + simpleTags.sort(); + extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`; + simpleTags.reverse(); + extraCloseTags = `${extraCloseTags}`; + } + (html as any[]).push('', extraOpenTags, + perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, ''); + } + } + + } + const document = optDocument; + if (document) { + result.node = document.createElement('div'); + // JAWS and NVDA screen reader compatibility. Only needed if in a real browser. + result.node.setAttribute('aria-live', 'assertive'); + } + else { + result.node = { + innerHTML: '', + className: '', + }; + } + let html:any[]|string = []; + let preHtml = ''; + let postHtml = ''; + let curHTML = null; + const processSpaces = (s) => domline.processSpaces(s, doesWrap); + const perTextNodeProcess = (doesWrap ? _.identity : processSpaces); + const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); + let lineClass = 'ace-line'; + const writeHTML = () => { + let newHTML = perHtmlLineProcess((html as any[]).join('')); + if (!newHTML) { + if ((!document) || (!optBrowser)) { + newHTML += ' '; + } + else { + newHTML += '
        '; + } + } + if (nonEmpty) { + newHTML = (preHtml || '') + newHTML + (postHtml || ''); + } + html = preHtml = postHtml = ''; // free memory + if (newHTML !== curHTML) { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass != null) + result.node.className = lineClass; + hooks.callAll('acePostWriteDomLineHTML', { + node: result.node, + }); + }; + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + return result; + } +} +// if "document" is falsy we don't create a DOM node, just +// an object with innerHTML and className +domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { + const result = { + node: null, + prepareForAdd: noop, + notifyAdded: noop, + clearSpans: noop, + finishUpdate: noop, + lineMarker: 0, + appendSpan: (txt, cls) => { + let processedMarker:any = false; + // Handle lineAttributeMarker, if present + if (cls.indexOf(lineAttributeMarker) >= 0) { + let listType:any = /(?:^| )list:(\S+)/.exec(cls); + const start = /(?:^| )start:(\S+)/.exec(cls); + _.map(hooks.callAll('aceDomLinePreProcessLineAttributes', { + domline, + cls, + }), (modifier) => { + preHtml += modifier.preHtml; + postHtml += modifier.postHtml; + processedMarker |= modifier.processedMarker; + }); + if (listType) { + listType = listType[1]; + if (listType) { + if (listType.indexOf('number') < 0) { + preHtml += `
        • `; + postHtml = `
        ${postHtml}`; + } + else { + if (start) { // is it a start of a list with more than one item in? + if (Number.parseInt(start[1]) === 1) { // if its the first one at this level? + // Add start class to DIV node + lineClass = `${lineClass} ` + `list-start-${listType}`; + } + preHtml += + `
        1. `; + } + else { + // Handles pasted contents into existing lists + preHtml += `
          1. `; + } + postHtml += '
          '; + } + } + processedMarker = true; + } + _.map(hooks.callAll('aceDomLineProcessLineAttributes', { + domline, + cls, + }), (modifier) => { + preHtml += modifier.preHtml; + postHtml += modifier.postHtml; + processedMarker |= modifier.processedMarker; + }); + if (processedMarker) { + result.lineMarker += txt.length; + return; // don't append any text + } + } + let href = null; + let simpleTags = null; + if (cls.indexOf('url') >= 0) { + cls = cls.replace(/(^| )url:(\S+)/g, (x0, space, url) => { + href = url; + return `${space}url`; + }); + } + if (cls.indexOf('tag') >= 0) { + cls = cls.replace(/(^| )tag:(\S+)/g, (x0, space, tag) => { + if (!simpleTags) + simpleTags = []; + simpleTags.push(tag.toLowerCase()); + return space + tag; + }); + } + let extraOpenTags = ''; + let extraCloseTags = ''; + _.map(hooks.callAll('aceCreateDomLine', { + domline, + cls, + }), (modifier) => { + cls = modifier.cls; + extraOpenTags += modifier.extraOpenTags; + extraCloseTags = modifier.extraCloseTags + extraCloseTags; + }); + if ((!txt) && cls) { + lineClass = domline.addToLineClass(lineClass, cls); + } + else if (txt) { + if (href) { + const urn_schemes = new RegExp('^(about|geo|mailto|tel):'); + // if the url doesn't include a protocol prefix, assume http + if (!~href.indexOf('://') && !urn_schemes.test(href)) { + href = `http://${href}`; + } + // Using rel="noreferrer" stops leaking the URL/location of the pad when + // clicking links in the document. + // Not all browsers understand this attribute, but it's part of the HTML5 standard. + // https://html.spec.whatwg.org/multipage/links.html#link-type-noreferrer + // Additionally, we do rel="noopener" to ensure a higher level of referrer security. + // https://html.spec.whatwg.org/multipage/links.html#link-type-noopener + // https://mathiasbynens.github.io/rel-noopener/ + // https://github.com/ether/etherpad-lite/pull/3636 + const escapedHref = Security.escapeHTMLAttribute(href); + extraOpenTags = `${extraOpenTags}`; + extraCloseTags = `${extraCloseTags}`; + } + if (simpleTags) { + simpleTags.sort(); + extraOpenTags = `${extraOpenTags}<${simpleTags.join('><')}>`; + simpleTags.reverse(); + extraCloseTags = `${extraCloseTags}`; + } + html.push('', extraOpenTags, perTextNodeProcess(Security.escapeHTML(txt)), extraCloseTags, ''); + } + } + }; + const document = optDocument; + if (document) { + result.node = document.createElement('div'); + // JAWS and NVDA screen reader compatibility. Only needed if in a real browser. + result.node.setAttribute('aria-live', 'assertive'); + } + else { + result.node = { + innerHTML: '', + className: '', + }; + } + let html:any|any[] = []; + let preHtml = ''; + let postHtml = ''; + let curHTML = null; + const processSpaces = (s) => domline.processSpaces(s, doesWrap); + const perTextNodeProcess = (doesWrap ? _.identity : processSpaces); + const perHtmlLineProcess = (doesWrap ? processSpaces : _.identity); + let lineClass = 'ace-line'; + const writeHTML = () => { + let newHTML = perHtmlLineProcess(html.join('')); + if (!newHTML) { + if ((!document) || (!optBrowser)) { + newHTML += ' '; + } + else { + newHTML += '
          '; + } + } + if (nonEmpty) { + newHTML = (preHtml || '') + newHTML + (postHtml || ''); + } + html = preHtml = postHtml = ''; // free memory + if (newHTML !== curHTML) { + curHTML = newHTML; + result.node.innerHTML = curHTML; + } + if (lineClass != null) + result.node.className = lineClass; + hooks.callAll('acePostWriteDomLineHTML', { + node: result.node, + }); + }; + result.prepareForAdd = writeHTML; + result.finishUpdate = writeHTML; + return result; +}; +export { domline }; diff --git a/src/static/js/index.js b/src/static/js/index.ts similarity index 83% rename from src/static/js/index.js rename to src/static/js/index.ts index bc7ae68f6..ef33b3e11 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.ts @@ -1,6 +1,7 @@ 'use strict'; /* eslint-disable-next-line max-len */ // @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0 + /** * Copyright 2011 Peter Martischka, Primary Technology. * Copyright 2020 Richard Hansen @@ -17,6 +18,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +// FIXME Why should this variable be a rename? +// @ts-ignore +// @ts-ignore const randomPadName = () => { // the number of distinct chars (64) is chosen to ensure that the selection will be uniform when // using the PRNG below @@ -26,7 +31,7 @@ const randomPadName = () => { // make room for 8-bit integer values that span from 0 to 255. const randomarray = new Uint8Array(stringLength); // use browser's PRNG to generate a "unique" sequence - const cryptoObj = window.crypto || window.msCrypto; // for IE 11 + const cryptoObj = window.crypto; // for IE 11 cryptoObj.getRandomValues(randomarray); let randomstring = ''; for (let i = 0; i < stringLength; i++) { @@ -40,8 +45,8 @@ const randomPadName = () => { $(() => { $('#go2Name').submit(() => { const padname = $('#padname').val(); - if (padname.length > 0) { - window.location = `p/${encodeURIComponent(padname.trim())}`; + if (typeof padname === "string" && padname.length > 0) { + window.location.href = `p/${encodeURIComponent(padname.trim())}`; } else { alert('Please enter a name'); @@ -49,9 +54,9 @@ $(() => { return false; }); $('#button').click(() => { - window.location = `p/${randomPadName()}`; + window.location.href = `p/${randomPadName()}`; }); // start the custom js - if (typeof window.customStart === 'function') + if ("customStart" in window && typeof window.customStart === 'function') window.customStart(); }); diff --git a/src/static/js/l10n.js b/src/static/js/l10n.js deleted file mode 100644 index 786749429..000000000 --- a/src/static/js/l10n.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; -((document) => { - // Set language for l10n - let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/); - if (language) - language = language[1]; - html10n.bind('indexed', () => { - html10n.localize([language, navigator.language, navigator.userLanguage, 'en']); - }); - html10n.bind('localized', () => { - document.documentElement.lang = html10n.getLanguage(); - document.documentElement.dir = html10n.getDirection(); - }); -})(document); diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.ts similarity index 51% rename from src/static/js/linestylefilter.js rename to src/static/js/linestylefilter.ts index b8cd5b6f5..499c2402a 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.ts @@ -4,23 +4,24 @@ import * as hooks from "./pluginfw/hooks.js"; import AttributeManager from "./AttributeManager.js"; import { padutils as padutils$0 } from "./pad_utils.js"; 'use strict'; -const linestylefilter = {}; + + const padutils = { padutils: padutils$0 }.padutils; -linestylefilter.ATTRIB_CLASSES = { - bold: 'tag:b', - italic: 'tag:i', - underline: 'tag:u', - strikethrough: 'tag:s', -}; -const lineAttributeMarker = 'lineAttribMarker'; -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) => { + +// @ts-ignore +const linestylefilter = { + ATTRIB_CLASSES: { + bold: 'tag:b', + italic: 'tag:i', + underline: 'tag:u', + strikethrough: 'tag:s', + }, + getAuthorClassName: (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => { + if (c === '.') + return '-'; + return `z${c.charCodeAt(0)}z`; + })}`, + 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); @@ -115,126 +116,132 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool }; })(); 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]; +}, + 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); } - 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}`; + if (!splitPoints) + return textAndClassFunc; + return linestylefilter.textAndClassFuncSplitter(textAndClassFunc, splitPoints); + }, + 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 = []; } - textAndClassFunc(txt, newCls); - curIndex += txtlen; + 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; }; - })(); - 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 && + 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); + }, + getURLFilter: ()=>{ + return linestylefilter.getRegexpFilter(padutils.urlRegex, 'url') + }, + 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; + nextPointIndex++; } - else { - const splitPoints = splitPointsOpt; - const pointLocInSpan = splitPoints[nextPointIndex] - idx; - const txtlen = txt.length; - if (pointLocInSpan >= txtlen) { + const spanHandler = (txt, cls) => { + if ((!splitPointsOpt) || nextPointIndex >= splitPointsOpt.length) { func(txt, cls); idx += txt.length; - if (pointLocInSpan === txtlen) { - nextPointIndex++; - } } else { - if (pointLocInSpan > 0) { - func(txt.substring(0, pointLocInSpan), cls); - idx += pointLocInSpan; + 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); } - nextPointIndex++; - // recurse - spanHandler(txt.substring(pointLocInSpan), cls); } + }; + return spanHandler; + }, + getFilterStack: (lineText, textAndClassFunc, abrowser?) => { + let func = linestylefilter.getURLFilter(); + const hookFilters = hooks.callAll('aceGetFilterStack', { + linestylefilter, + browser: abrowser, + }); + hookFilters.map((hookFilter) => { + func = hookFilter(lineText, func); + }); + return func; + }, + 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); } - }; - 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, ''); } - const textAndClassFunc = (tokenText, tokenClass) => { - domLineObj.appendSpan(tokenText, tokenClass); - }; - let func = linestylefilter.getFilterStack(text, textAndClassFunc); - func = linestylefilter.getLineStyleFilter(text.length, aline, func, apool); - func(text, ''); }; +const lineAttributeMarker = 'lineAttribMarker'; +// lineLength is without newline; aline includes newline, +// but may be falsy if lineLength == 0 +// domLineObj is like that returned by domline.createDomLine export { lineAttributeMarker }; export { linestylefilter }; diff --git a/src/static/js/pad.js b/src/static/js/pad.ts similarity index 99% rename from src/static/js/pad.js rename to src/static/js/pad.ts index eba61f78d..f19ee73c9 100644 --- a/src/static/js/pad.js +++ b/src/static/js/pad.ts @@ -1,3 +1,5 @@ +// @ts-nocheck +//FIXME this file needs some insights import "./vendors/jquery.js"; import "./vendors/farbtastic.js"; import "./vendors/gritter.js"; @@ -15,6 +17,8 @@ import { paduserlist as paduserlist$0 } from "./pad_userlist.js"; import { colorutils as colorutils$0 } from "./colorutils.js"; import * as socketio from "./socketio.js"; import * as hooks from "./pluginfw/hooks.js"; +import {clientVars} from "../../node/handler/PadMessageHandler"; +import {i18nextvar} from "./vendors/i18next"; 'use strict'; /** * This code is mostly from the old Etherpad. Please help us to comment this code. @@ -139,7 +143,7 @@ const getParameters = [ name: 'lang', checkVal: null, callback: (val) => { - window.html10n.localize([val, 'en']); + i18nextvar([val, 'en']); Cookies.set('language', val); }, }, @@ -347,6 +351,7 @@ const pad = { padOptions: {}, _messageQ: new MessageQueue(), // these don't require init; clientVars should all go through here + settings: undefined, getPadId: () => clientVars.padId, getClientIp: () => clientVars.clientIp, getColorPalette: () => clientVars.colorPalette, @@ -705,7 +710,8 @@ const settings = { useMonospaceFontGlobal: false, globalUserName: false, globalUserColor: false, - rtlIsTrue: false, + rtlIsTrue: false, hideChat: false + }; pad.settings = settings; export const baseURL = ''; diff --git a/src/static/js/pad_automatic_reconnect.js b/src/static/js/pad_automatic_reconnect.ts similarity index 96% rename from src/static/js/pad_automatic_reconnect.js rename to src/static/js/pad_automatic_reconnect.ts index b161c831c..2518713dd 100644 --- a/src/static/js/pad_automatic_reconnect.js +++ b/src/static/js/pad_automatic_reconnect.ts @@ -1,4 +1,7 @@ 'use strict'; +import {i18nextvar} from "./vendors/i18next"; +import {clientVars} from "../../node/handler/PadMessageHandler"; + const createCountDownElementsIfNecessary = ($modal) => { const elementsDoNotExist = $modal.find('#cancelreconnect').length === 0; if (elementsDoNotExist) { @@ -24,7 +27,7 @@ const createCountDownElementsIfNecessary = ($modal) => { } }; const localize = ($element) => { - html10n.translateElement(html10n.translations, $element.get(0)); + i18nextvar($element.get(0)); }; const createTimerForModal = ($modal, pad) => { const timeUntilReconnection = clientVars.automaticReconnectionTimeout * reconnectionTries.nextTry(); @@ -87,7 +90,7 @@ const reconnectionTries = { // duration: how many **seconds** until the timer ends // granularity (optional): how many **milliseconds** // between each 'tick' of timer. Default: 1000ms (1s) -const CountDownTimer = function (duration, granularity) { +const CountDownTimer = function (duration, granularity?) { this.duration = duration; this.granularity = granularity || 1000; this.running = false; diff --git a/src/static/js/pad_connectionstatus.js b/src/static/js/pad_connectionstatus.ts similarity index 95% rename from src/static/js/pad_connectionstatus.js rename to src/static/js/pad_connectionstatus.ts index a84f29244..9a71344d7 100644 --- a/src/static/js/pad_connectionstatus.js +++ b/src/static/js/pad_connectionstatus.ts @@ -20,10 +20,17 @@ import { padmodals as padmodals$0 } from "./pad_modals.js"; * See the License for the specific language governing permissions and * limitations under the License. */ +type Pad_connectionStatus = { + what: string, + why?: string +} + + const padmodals = { padmodals: padmodals$0 }.padmodals; const padconnectionstatus = (() => { - let status = { + let status:Pad_connectionStatus = { what: 'connecting', + why: undefined }; const self = { init: () => { diff --git a/src/static/js/pad_cookie.js b/src/static/js/pad_cookie.ts similarity index 87% rename from src/static/js/pad_cookie.js rename to src/static/js/pad_cookie.ts index 1a988719a..6b99981ab 100644 --- a/src/static/js/pad_cookie.js +++ b/src/static/js/pad_cookie.ts @@ -1,4 +1,6 @@ import * as padUtils from "./pad_utils.js"; +import {JQueryGritter} from "../module/CustomWindow"; +import {i18nextvar} from "./vendors/i18next"; 'use strict'; /** * Copyright 2009 Google Inc. @@ -17,6 +19,7 @@ import * as padUtils from "./pad_utils.js"; */ const Cookies = { Cookies: padUtils }.Cookies; export const padcookie = new class { + cookieName_: string; constructor() { this.cookieName_ = window.location.protocol === 'https:' ? 'prefs' : 'prefsHttp'; } @@ -28,9 +31,9 @@ export const padcookie = new class { this.writePrefs_(prefs); // Re-read the saved cookie to test if cookies are enabled. if (this.readPrefs_() == null) { - $.gritter.add({ + ($ as unknown as JQueryGritter).gritter.add({ title: 'Error', - text: html10n.get('pad.noCookie'), + text: i18nextvar('pad.noCookie'), sticky: true, class_name: 'error', }); @@ -38,6 +41,7 @@ export const padcookie = new class { } readPrefs_() { try { + // @ts-ignore const json = Cookies.get(this.cookieName_); if (json == null) return null; @@ -48,6 +52,7 @@ export const padcookie = new class { } } writePrefs_(prefs) { + // @ts-ignore Cookies.set(this.cookieName_, JSON.stringify(prefs), { expires: 365 * 100 }); } getPref(prefName) { diff --git a/src/static/js/pad_editbar.js b/src/static/js/pad_editbar.ts similarity index 99% rename from src/static/js/pad_editbar.js rename to src/static/js/pad_editbar.ts index 2554b689b..483e0bda3 100644 --- a/src/static/js/pad_editbar.js +++ b/src/static/js/pad_editbar.ts @@ -1,3 +1,5 @@ +// @ts-nocheck +//FIXME lots of errors import browser from "./vendors/browser.js"; import * as hooks from "./pluginfw/hooks.js"; import { padutils as padutils$0 } from "./pad_utils.js"; diff --git a/src/static/js/pad_editor.js b/src/static/js/pad_editor.ts similarity index 99% rename from src/static/js/pad_editor.js rename to src/static/js/pad_editor.ts index 2af32ffce..1e1045c54 100644 --- a/src/static/js/pad_editor.js +++ b/src/static/js/pad_editor.ts @@ -1,3 +1,5 @@ +// @ts-nocheck +//Lots of methods are set onto padutils import * as padUtils from "./pad_utils.js"; import { padcookie as padcookie$0 } from "./pad_cookie.js"; 'use strict'; diff --git a/src/static/js/pad_impexp.js b/src/static/js/pad_impexp.ts similarity index 85% rename from src/static/js/pad_impexp.js rename to src/static/js/pad_impexp.ts index 7bf9e1e88..2c45e6ff3 100644 --- a/src/static/js/pad_impexp.js +++ b/src/static/js/pad_impexp.ts @@ -4,6 +4,10 @@ * 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 {i18nextvar} from "./vendors/i18next"; +import {AjaxDirectDatabaseAccess} from "../module/CustomWindow"; +import {clientVars} from "../../node/handler/PadMessageHandler"; + /** * Copyright 2009 Google Inc. * @@ -39,14 +43,14 @@ const padimpexp = (() => { const fileInputSubmit = function (e) { e.preventDefault(); $('#importmessagefail').fadeOut('fast'); - if (!window.confirm(html10n.get('pad.impexp.confirmimport'))) + if (!window.confirm(i18nextvar('pad.impexp.confirmimport'))) return; - $('#importsubmitinput').attr({ disabled: true }).val(html10n.get('pad.impexp.importing')); + $('#importsubmitinput').attr({ disabled: true }).val(i18nextvar('pad.impexp.importing')); window.setTimeout(() => $('#importfileinput').attr({ disabled: true }), 0); $('#importarrow').stop(true, true).hide(); $('#importstatusball').show(); (async () => { - const { code, message, data: { directDatabaseAccess } = {} } = await $.ajax({ + const { code, message, data: { directDatabaseAccess } = {} }:AjaxDirectDatabaseAccess = await $.ajax({ url: `${window.location.href.split('?')[0].split('#')[0]}/import`, method: 'POST', data: new FormData(this), @@ -67,7 +71,7 @@ const padimpexp = (() => { if (directDatabaseAccess) window.location.reload(); } - $('#importsubmitinput').removeAttr('disabled').val(html10n.get('pad.impexp.importbutton')); + $('#importsubmitinput').removeAttr('disabled').val(i18nextvar('pad.impexp.importbutton')); window.setTimeout(() => $('#importfileinput').removeAttr('disabled'), 0); $('#importstatusball').hide(); addImportFrames(); @@ -81,12 +85,12 @@ const padimpexp = (() => { 'maxFileSize', 'permission', ]; - const msg = html10n.get(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`); - const showError = (fade) => { + const msg = i18nextvar(`pad.impexp.${known.indexOf(status) !== -1 ? status : 'copypaste'}`); + const showError = (fade?) => { const popup = $('#importmessagefail').empty() .append($('') .css('color', 'red') - .text(`${html10n.get('pad.impexp.importfailed')}: `)) + .text(`${i18nextvar('pad.impexp.importfailed')}: `)) .append(document.createTextNode(msg)); popup[(fade ? 'fadeIn' : 'show')](); }; @@ -100,7 +104,7 @@ const padimpexp = (() => { }; // /// export function cantExport() { - let type = $(this); + let type: JQuery|string = $(this); if (type.hasClass('exporthrefpdf')) { type = 'PDF'; } @@ -113,7 +117,7 @@ const padimpexp = (() => { else { type = 'this file'; } - alert(html10n.get('pad.impexp.exportdisabled', { type })); + alert(i18nextvar('pad.impexp.exportdisabled', { type })); return false; } // /// @@ -124,9 +128,9 @@ const padimpexp = (() => { // if /p/ isn't available due to a rewrite we use the clientVars padId const padRootPath = /.*\/p\/[^/]+/.exec(document.location.pathname) || clientVars.padId; // i10l buttom import - $('#importsubmitinput').val(html10n.get('pad.impexp.importbutton')); - html10n.bind('localized', () => { - $('#importsubmitinput').val(html10n.get('pad.impexp.importbutton')); + $('#importsubmitinput').val(i18nextvar('pad.impexp.importbutton')); + i18nextvar.bind('localized', () => { + $('#importsubmitinput').val(i18nextvar('pad.impexp.importbutton')); }); // build the export links $('#exporthtmla').attr('href', `${padRootPath}/export/html`); diff --git a/src/static/js/pad_modals.js b/src/static/js/pad_modals.ts similarity index 100% rename from src/static/js/pad_modals.js rename to src/static/js/pad_modals.ts diff --git a/src/static/js/pad_savedrevs.js b/src/static/js/pad_savedrevs.ts similarity index 81% rename from src/static/js/pad_savedrevs.js rename to src/static/js/pad_savedrevs.ts index 684dd7a12..7340873e1 100644 --- a/src/static/js/pad_savedrevs.js +++ b/src/static/js/pad_savedrevs.ts @@ -1,4 +1,7 @@ 'use strict'; +import {JQueryGritter} from "../module/CustomWindow"; +import {i18nextvar} from "./vendors/i18next"; + /** * Copyright 2012 Peter 'Pita' Martischka * @@ -17,11 +20,11 @@ let pad; export const saveNow = () => { pad.collabClient.sendMessage({ type: 'SAVE_REVISION' }); - $.gritter.add({ + ($ as unknown as JQueryGritter).gritter.add({ // (string | mandatory) the heading of the notification - title: html10n.get('pad.savedrevs.marked'), + title: i18nextvar('pad.savedrevs.marked'), // (string | mandatory) the text inside the notification - text: html10n.get('pad.savedrevs.timeslider') || + text: i18nextvar('pad.savedrevs.timeslider') || 'You can view saved revisions in the timeslider', // (bool | optional) if you want it to fade out on its own or just sit there sticky: false, diff --git a/src/static/js/pad_userlist.js b/src/static/js/pad_userlist.ts similarity index 94% rename from src/static/js/pad_userlist.js rename to src/static/js/pad_userlist.ts index 531080d32..04941175a 100644 --- a/src/static/js/pad_userlist.js +++ b/src/static/js/pad_userlist.ts @@ -1,5 +1,9 @@ import { padutils as padutils$0 } from "./pad_utils.js"; import * as hooks from "./pluginfw/hooks.js"; +import {i18nextvar} from "./vendors/i18next"; +import {clientVars} from "../../node/handler/PadMessageHandler"; +import {HistoricalAuthorData, JQueryGritter} from "../module/CustomWindow"; +import {pad} from "./pad"; 'use strict'; /** * Copyright 2009 Google Inc. @@ -17,7 +21,12 @@ import * as hooks from "./pluginfw/hooks.js"; * limitations under the License. */ const padutils = { padutils: padutils$0 }.padutils; -let myUserInfo = {}; +let myUserInfo = { + name: undefined, userId: undefined, + colorId: undefined + + +}; let colorPickerOpen = false; let colorPickerSetup = false; const paduserlist = (() => { @@ -83,7 +92,7 @@ const paduserlist = (() => { handleOtherUserInputs(); return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do }; - const getAnimationHeight = (step, power) => { + const getAnimationHeight = (step, power?) => { let a = Math.abs(step / 12); if (power === 2) a **= 2; @@ -138,7 +147,7 @@ const paduserlist = (() => { .attr('type', 'text') .addClass('editempty') .addClass('newinput') - .attr('value', html10n.get('pad.userlist.unnamed')); + .attr('value', i18nextvar('pad.userlist.unnamed')); if (isNameEditable(data)) name.attr('disabled', 'disabled'); } @@ -148,7 +157,7 @@ const paduserlist = (() => { .addClass('usertdswatch') .append($('
          ') .addClass('swatch') - .css('background', padutils.escapeHtml(data.color)) + .css('background', padutils.escapeHtml(data.color) as unknown as string) .html(' '))) .add($('') .css('height', `${height}px`) @@ -187,7 +196,7 @@ const paduserlist = (() => { }).removeClass('newinput'); }; // animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc. - const insertRow = (position, data, animationPower) => { + const insertRow = (position, data, animationPower?) => { position = Math.max(0, Math.min(rowsPresent.length, position)); animationPower = (animationPower === undefined ? 4 : animationPower); const domId = nextRowId(); @@ -237,7 +246,7 @@ const paduserlist = (() => { } } }; - const removeRow = (position, animationPower) => { + const removeRow = (position, animationPower?) => { animationPower = (animationPower === undefined ? 4 : animationPower); const row = rowsPresent[position]; if (row) { @@ -257,7 +266,7 @@ const paduserlist = (() => { } }; // newPosition is position after the row has been removed - const moveRow = (oldPosition, newPosition, animationPower) => { + const moveRow = (oldPosition, newPosition, animationPower?) => { animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best const row = rowsPresent[oldPosition]; if (row && oldPosition !== newPosition) { @@ -288,7 +297,7 @@ const paduserlist = (() => { }, (newName) => { if (!newName) { jnode.addClass('editempty'); - jnode.val(html10n.get('pad.userlist.unnamed')); + jnode.val(i18nextvar('pad.userlist.unnamed')); } else { jnode.attr('disabled', 'disabled'); @@ -372,11 +381,12 @@ const paduserlist = (() => { const userList = self.usersOnline(); // Now we add historical authors const historical = clientVars.collab_client_vars.historicalAuthorData; - for (const [key, { userId }] of Object.entries(historical)) { + for (const [key, res] of Object.entries(historical)) { + const resMapped = res as HistoricalAuthorData // Check we don't already have this author in our array let exists = false; userList.forEach((user) => { - if (user.userId === userId) + if (user.userId === resMapped.userId) exists = true; }); if (exists === false) { @@ -401,18 +411,20 @@ const paduserlist = (() => { hooks.callAll('userJoinOrUpdate', { userInfo: info, }); - const userData = {}; - userData.color = typeof info.colorId === 'number' - ? clientVars.colorPalette[info.colorId] : info.colorId; - userData.name = info.name; - userData.status = ''; - userData.activity = ''; - userData.id = info.userId; + const userData = { + status: '', + name: info.name, + activity: '', + id: info.userId, + color: typeof info.colorId === 'number' + ? clientVars.colorPalette[info.colorId] : info.colorId + }; const existingIndex = findExistingIndex(info.userId); let numUsersBesides = otherUsersInfo.length; if (existingIndex >= 0) { numUsersBesides--; } + //@ts-ignore const newIndex = padutils.binarySearch(numUsersBesides, (n) => { if (existingIndex >= 0 && n >= existingIndex) { // pretend existingIndex isn't there @@ -495,7 +507,7 @@ const paduserlist = (() => { $('#myusernameedit').removeClass('editempty').val(myUserInfo.name); } else { - $('#myusernameedit').attr('placeholder', html10n.get('pad.userlist.entername')); + $('#myusernameedit').attr('placeholder', i18nextvar('pad.userlist.entername')); } if (colorPickerOpen) { $('#myswatchbox').addClass('myswatchboxunhoverable').removeClass('myswatchboxhoverable'); @@ -536,7 +548,7 @@ const closeColorPicker = (accept) => { $('#mycolorpicker').removeClass('popup-show'); }; const showColorPicker = () => { - $.farbtastic('#colorpicker').setColor(myUserInfo.colorId); + ($ as unknown as JQueryGritter).farbtastic('#colorpicker').setColor(myUserInfo.colorId); if (!colorPickerOpen) { const palette = pad.getColorPalette(); if (!colorPickerSetup) { diff --git a/src/static/js/pad_utils.ts b/src/static/js/pad_utils.ts index 537f99169..29995876d 100644 --- a/src/static/js/pad_utils.ts +++ b/src/static/js/pad_utils.ts @@ -1,3 +1,6 @@ + + + 'use strict'; /** @@ -22,7 +25,9 @@ * limitations under the License. */ -import Security from './security'; +//FIXME Security does not have a typescript file +// @ts-ignore +import {escapeHTML,escapeHTMLAttribute} from './security'; /** * Generates a random String with the given length. Is needed to generate the Author, Group, @@ -137,7 +142,7 @@ export const padutils = { } }, - escapeHtml: (x) => Security.escapeHTML(String(x)), + escapeHtml: (x) => escapeHTML(String(x)), uniqueId: () => { const pad = require('./pad').pad; // Sidestep circular dependency // returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits @@ -198,7 +203,7 @@ export const padutils = { const advanceTo = (i) => { if (i > idx) { - pieces.push(Security.escapeHTML(text.substring(idx, i))); + pieces.push(escapeHTML(text.substring(idx, i))); idx = i; } }; @@ -216,9 +221,9 @@ export const padutils = { // https://github.com/ether/etherpad-lite/pull/3636 pieces.push( ''); advanceTo(startIndex + href.length); pieces.push(''); diff --git a/src/static/js/pluginfw/client_plugins.js b/src/static/js/pluginfw/client_plugins.ts similarity index 99% rename from src/static/js/pluginfw/client_plugins.js rename to src/static/js/pluginfw/client_plugins.ts index e8ac41e21..0f5236763 100644 --- a/src/static/js/pluginfw/client_plugins.js +++ b/src/static/js/pluginfw/client_plugins.ts @@ -1,6 +1,8 @@ +// @ts-nocheck import * as pluginUtils from "./shared.js"; import * as defs from "./plugin_defs.js"; 'use strict'; + const adoptPluginsFromAncestorsOf = (frame) => { // Bind plugins with parent; let parentRequire = null; diff --git a/src/static/js/pluginfw/plugin_defs.ts b/src/static/js/pluginfw/plugin_defs.ts index 1a7553757..5a5dbee3f 100644 --- a/src/static/js/pluginfw/plugin_defs.ts +++ b/src/static/js/pluginfw/plugin_defs.ts @@ -8,13 +8,13 @@ // * hook_fn: Plugin-supplied hook function. // * hook_fn_name: Name of the hook function, with the form :. // * part: The ep.json part object that declared the hook. See exports.plugins. -export const hooks = {}; +export let hooks = {}; // Whether the plugins have been loaded. export let loaded = false; // Topologically sorted list of parts from exports.plugins. -export const parts = []; +export let parts = []; // Maps the name of a plugin to the plugin's definition provided in ep.json. The ep.json object is // augmented with additional metadata: diff --git a/src/static/js/pluginfw/plugins.ts b/src/static/js/pluginfw/plugins.ts index ddb9f3c29..d4ca42735 100644 --- a/src/static/js/pluginfw/plugins.ts +++ b/src/static/js/pluginfw/plugins.ts @@ -94,7 +94,7 @@ export const pathNormalization = (part, hookFnName, hookName) => { }; export const update = async () => { - const packages = await exports.getPackages(); + const packages = await getPackages(); let parts:{[keys: string]:any} = {}; // Key is full name. sortParts converts this into a topologically sorted array. let plugins = {}; @@ -107,7 +107,7 @@ export const update = async () => { setPlugins(plugins); setParts(sortParts(parts)) - setHooks(extractHooks(parts, 'hooks', exports.pathNormalization)); + setHooks(extractHooks(parts, 'hooks', pathNormalization)); setLoaded(true) await Promise.all(Object.keys(plugins).map(async (p) => { const logger = log4js.getLogger(`plugin:${p}`); @@ -115,7 +115,7 @@ export const update = async () => { })); }; -exports.getPackages = async () => { +const getPackages = async () => { logger.info('Running npm to get a list of installed plugins...'); // Notes: // * Do not pass `--prod` otherwise `npm ls` will fail if there is no `package.json`. @@ -125,7 +125,7 @@ exports.getPackages = async () => { const cmd = ['npm', 'ls', '--long', '--json', '--depth=0', '--no-production']; const {dependencies = {}} = JSON.parse(await exportCMD(cmd, {stdio: [null, 'string']}) as unknown as string); await Promise.all(Object.entries(dependencies).map(async ([pkg, info]) => { - if (!pkg.startsWith(exports.prefix)) { + if (!pkg.startsWith(prefix)) { delete dependencies[pkg]; return; } diff --git a/src/static/js/rjquery.js b/src/static/js/rjquery.ts similarity index 73% rename from src/static/js/rjquery.js rename to src/static/js/rjquery.ts index 6bd4bbe5f..130a46f60 100644 --- a/src/static/js/rjquery.js +++ b/src/static/js/rjquery.ts @@ -1,3 +1,5 @@ +// @ts-nocheck +//FIXME Could be interesting to have a look at this 'use strict'; // Provides a require'able version of jQuery without leaking $ and jQuery; window.$ = require('./vendors/jquery'); diff --git a/src/static/js/scroll.js b/src/static/js/scroll.ts similarity index 97% rename from src/static/js/scroll.js rename to src/static/js/scroll.ts index eb7edbe4c..11120ad24 100644 --- a/src/static/js/scroll.js +++ b/src/static/js/scroll.ts @@ -1,8 +1,9 @@ import * as caretPosition from "./caretPosition.js"; +import {CustomWindow} from "../module/CustomWindow"; 'use strict'; function Scroll(outerWin) { // scroll settings - this.scrollSettings = parent.parent.clientVars.scrollWhenFocusLineIsOutOfViewport; + this.scrollSettings = (parent.parent as unknown as CustomWindow).clientVars.scrollWhenFocusLineIsOutOfViewport; // DOM reference this.outerWin = outerWin; this.doc = this.outerWin.document; @@ -166,7 +167,7 @@ Scroll.prototype._getPixelsRelativeToPercentageOfViewport = let pixels = 0; const scrollPercentageRelativeToViewport = this._getPercentageToScroll(aboveOfViewport); if (scrollPercentageRelativeToViewport > 0 && scrollPercentageRelativeToViewport <= 1) { - pixels = parseInt(innerHeight * scrollPercentageRelativeToViewport); + pixels = parseInt(String(innerHeight * scrollPercentageRelativeToViewport)); } return pixels; }; @@ -183,7 +184,7 @@ Scroll.prototype._getPixelsToScrollWhenUserPressesArrowUp = function (innerHeigh let pixels = 0; const percentageToScrollUp = this.scrollSettings.percentageToScrollWhenUserPressesArrowUp; if (percentageToScrollUp > 0 && percentageToScrollUp <= 1) { - pixels = parseInt(innerHeight * percentageToScrollUp); + pixels = parseInt(String(innerHeight * percentageToScrollUp)); } return pixels; }; diff --git a/src/static/js/security.js b/src/static/js/security.js deleted file mode 100644 index 869914a6d..000000000 --- a/src/static/js/security.js +++ /dev/null @@ -1,2 +0,0 @@ -'use strict'; -export * from "security"; diff --git a/src/static/js/security.ts b/src/static/js/security.ts new file mode 100644 index 000000000..90de9b6bf --- /dev/null +++ b/src/static/js/security.ts @@ -0,0 +1,10 @@ +'use strict'; +export * from "security"; + +export function escapeHTMLAttribute(href) { + throw new Error("Function not implemented."); +} +export function escapeHTML(txt) { + throw new Error("Function not implemented."); +} + diff --git a/src/static/js/skin_variants.js b/src/static/js/skin_variants.ts similarity index 100% rename from src/static/js/skin_variants.js rename to src/static/js/skin_variants.ts diff --git a/src/static/js/skiplist.js b/src/static/js/skiplist.ts similarity index 96% rename from src/static/js/skiplist.js rename to src/static/js/skiplist.ts index d64fd20e3..ce73d4b59 100644 --- a/src/static/js/skiplist.js +++ b/src/static/js/skiplist.ts @@ -21,6 +21,13 @@ */ const _entryWidth = (e) => (e && e.width) || 0; class Node { + key: any; + private entry: any; + levels: number; + upPtrs: any[]; + downPtrs: any[]; + downSkips: any[]; + downSkipWidths: any[]; constructor(entry, levels = 0, downSkips = 1, downSkipWidths = 0) { this.key = entry != null ? entry.key : null; this.entry = entry; @@ -51,6 +58,11 @@ class Node { // is still valid and points to the same index in the skiplist. Other operations with other points // invalidate this point. class Point { + private _skipList: any; + private loc: any; + private idxs: any[]; + private nodes: any[]; + private widthSkips: any[]; constructor(skipList, loc) { this._skipList = skipList; this.loc = loc; @@ -172,6 +184,10 @@ class Point { * property that is a string. */ class SkipList { + private _keyToNodeMap: Map; + private _start: Node; + private _end: Node; + private _totalWidth: number; constructor() { // if there are N elements in the skiplist, "start" is element -1 and "end" is element N this._start = new Node(null, 1); @@ -199,7 +215,7 @@ class SkipList { } return n; } - _getNodeIndex(node, byWidth) { + _getNodeIndex(node, byWidth?) { let dist = (byWidth ? 0 : -1); let n = node; while (n !== this._start) { diff --git a/src/static/js/socketio.js b/src/static/js/socketio.ts similarity index 84% rename from src/static/js/socketio.js rename to src/static/js/socketio.ts index 6d8e41850..5271cca79 100644 --- a/src/static/js/socketio.js +++ b/src/static/js/socketio.ts @@ -1,4 +1,6 @@ 'use strict'; +import {CustomWindow} from "../module/CustomWindow"; + /** * Creates a socket.io connection. * @param etherpadBaseUrl - Etherpad URL. If relative, it is assumed to be relative to @@ -8,6 +10,8 @@ * https://socket.io/docs/v2/client-api/#new-Manager-url-options * @return socket.io Socket object */ + +import io from 'socket.io-client'; const connect = (etherpadBaseUrl, namespace = '/', options = {}) => { // 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 @@ -15,7 +19,7 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => { // parameters, if present) is combined with the `path` option (which defaults to '/socket.io', but // is overridden here to allow users to host Etherpad at something like '/etherpad') to get the // URL of the socket.io endpoint. - const baseUrl = new URL(etherpadBaseUrl, window.location); + const baseUrl = new URL(etherpadBaseUrl, window.location.href); const socketioUrl = new URL('socket.io', baseUrl); const namespaceUrl = new URL(namespace, new URL('/', baseUrl)); return io(namespaceUrl.href, Object.assign({ path: socketioUrl.pathname }, options)); @@ -23,6 +27,6 @@ const connect = (etherpadBaseUrl, namespace = '/', options = {}) => { if (typeof exports === 'object') { } else { - window.socketio = { connect }; + (window as unknown as CustomWindow).socketio = { connect }; } export { connect }; diff --git a/src/static/js/timeslider.js b/src/static/js/timeslider.ts similarity index 98% rename from src/static/js/timeslider.js rename to src/static/js/timeslider.ts index 9ac1163e7..85b90c279 100644 --- a/src/static/js/timeslider.js +++ b/src/static/js/timeslider.ts @@ -1,3 +1,5 @@ +// @ts-nocheck +//FIXME Could be interesting to have a look at this import "./vendors/jquery.js"; import * as padUtils from "./pad_utils.js"; import * as hooks from "./pluginfw/hooks.js"; diff --git a/src/static/js/underscore.js b/src/static/js/underscore.js deleted file mode 100644 index 6a5f1c1ed..000000000 --- a/src/static/js/underscore.js +++ /dev/null @@ -1,2 +0,0 @@ -'use strict'; -export { default } from "underscore"; diff --git a/src/static/js/undomodule.js b/src/static/js/undomodule.ts similarity index 99% rename from src/static/js/undomodule.js rename to src/static/js/undomodule.ts index 96d7bca6a..2080a8603 100644 --- a/src/static/js/undomodule.js +++ b/src/static/js/undomodule.ts @@ -1,5 +1,5 @@ import * as Changeset from "./Changeset.js"; -import * as _ from "./underscore.js"; +import _ from "underscore"; 'use strict'; const undoModule = (() => { const stack = (() => { diff --git a/src/static/js/vendors/html10n.js b/src/static/js/vendors/html10n.js deleted file mode 100644 index 73db3597a..000000000 --- a/src/static/js/vendors/html10n.js +++ /dev/null @@ -1,943 +0,0 @@ -// WARNING: This file has been modified from the Original -/** - * Copyright (c) 2012 Marcel Klehr - * Copyright (c) 2011-2012 Fabien Cazenave, Mozilla - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ -window.html10n = (function (window, document, undefined) { - // fix console - (function () { - var noop = function () { }; - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; - var console = (window.console = window.console || {}); - for (var i = 0; i < names.length; ++i) { - if (!console[names[i]]) { - console[names[i]] = noop; - } - } - }()); - // fix Array#forEach in IE - // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach - if (!Array.prototype.forEach) { - Array.prototype.forEach = function (fn, scope) { - for (var i = 0, len = this.length; i < len; ++i) { - if (i in this) { - fn.call(scope, this[i], i, this); - } - } - }; - } - // fix Array#indexOf in, guess what, IE! <3 - // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (searchElement /*, fromIndex */) { - "use strict"; - if (this == null) { - throw new TypeError(); - } - var t = Object(this); - var len = t.length >>> 0; - if (len === 0) { - return -1; - } - var n = 0; - if (arguments.length > 1) { - n = Number(arguments[1]); - if (n != n) { // shortcut for verifying if it's NaN - n = 0; - } - else if (n != 0 && n != Infinity && n != -Infinity) { - n = (n > 0 || -1) * Math.floor(Math.abs(n)); - } - } - if (n >= len) { - return -1; - } - var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); - for (; k < len; k++) { - if (k in t && t[k] === searchElement) { - return k; - } - } - return -1; - }; - } - /** - * MicroEvent - to make any js object an event emitter (server or browser) - */ - var MicroEvent = function () { }; - MicroEvent.prototype = { - bind: function (event, fct) { - this._events = this._events || {}; - this._events[event] = this._events[event] || []; - this._events[event].push(fct); - }, - unbind: function (event, fct) { - this._events = this._events || {}; - if (event in this._events === false) - return; - this._events[event].splice(this._events[event].indexOf(fct), 1); - }, - trigger: function (event /* , args... */) { - this._events = this._events || {}; - if (event in this._events === false) - return; - for (var i = 0; i < this._events[event].length; i++) { - this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - }; - /** - * mixin will delegate all MicroEvent.js function in the destination object - * @param {Object} the object which will support MicroEvent - */ - MicroEvent.mixin = function (destObject) { - var props = ['bind', 'unbind', 'trigger']; - if (!destObject) - return; - for (var i = 0; i < props.length; i++) { - destObject[props[i]] = MicroEvent.prototype[props[i]]; - } - }; - /** - * Loader - * The loader is responsible for loading - * and caching all necessary resources - */ - function Loader(resources) { - this.resources = resources; - this.cache = {}; // file => contents - this.langs = {}; // lang => strings - } - Loader.prototype.load = function (lang, cb) { - if (this.langs[lang]) - return cb(); - if (this.resources.length > 0) { - var reqs = 0; - for (var i = 0, n = this.resources.length; i < n; i++) { - this.fetch(this.resources[i], lang, function (e) { - reqs++; - if (e) - console.warn(e); - if (reqs < n) - return; // Call back once all reqs are completed - cb && cb(); - }); - } - } - }; - Loader.prototype.fetch = function (href, lang, cb) { - var that = this; - if (this.cache[href]) { - this.parse(lang, href, this.cache[href], cb); - return; - } - var xhr = new XMLHttpRequest(); - xhr.open('GET', href, /*async: */ true); - if (xhr.overrideMimeType) { - xhr.overrideMimeType('application/json; charset=utf-8'); - } - xhr.onreadystatechange = function () { - if (xhr.readyState == 4) { - if (xhr.status == 200 || xhr.status === 0) { - var data = JSON.parse(xhr.responseText); - that.cache[href] = data; - // Pass on the contents for parsing - that.parse(lang, href, data, cb); - } - else { - cb(new Error('Failed to load ' + href)); - } - } - }; - xhr.send(null); - }; - Loader.prototype.parse = function (lang, currHref, data, cb) { - if ('object' != typeof data) { - cb(new Error('A file couldn\'t be parsed as json.')); - return; - } - // Check if lang exists - if (!data[lang]) { - // lang not found - // This may be due to formatting (expected 'ru' but browser sent 'ru-RU') - // Set err msg before mutating lang (we may need this later) - var msg = 'Couldn\'t find translations for ' + lang; - // Check for '-' ('ROOT-VARIANT') - if (lang.indexOf('-') > -1) { - // ROOT-VARIANT formatting detected - lang = lang.split('-')[0]; // set lang to ROOT lang - } - // Check if ROOT lang exists (e.g 'ru') - if (!data[lang]) { - // ROOT lang not found. (e.g 'zh') - // Loop through langs data. Maybe we have a variant? e.g (zh-hans) - var l; // langs item. Declare outside of loop - for (l in data) { - // Is not ROOT? - // And index of ROOT equals 0? - // And is known lang? - if (lang != l && l.indexOf(lang) === 0 && data[l]) { - lang = l; // set lang to ROOT-VARIANT (e.g 'zh-hans') - break; - } - } - // Did we find a variant? If not, return err. - if (lang != l) { - return cb(new Error(msg)); - } - } - } - if ('string' == typeof data[lang]) { - // Import rule - // absolute path - var importUrl = data[lang]; - // relative path - if (data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) { - importUrl = currHref + "/../" + data[lang]; - } - this.fetch(importUrl, lang, cb); - return; - } - if ('object' != typeof data[lang]) { - cb(new Error('Translations should be specified as JSON objects!')); - return; - } - this.langs[lang] = data[lang]; - // TODO: Also store accompanying langs - cb(); - }; - /** - * The html10n object - */ - var html10n = { language: null - }; - MicroEvent.mixin(html10n); - html10n.macros = {}; - html10n.rtl = ["ar", "dv", "fa", "ha", "he", "ks", "ku", "ps", "ur", "yi"]; - /** - * Get rules for plural forms (shared with JetPack), see: - * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html - * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p - * - * @param {string} lang - * locale (language) used. - * - * @return {Function} - * returns a function that gives the plural form name for a given integer: - * var fun = getPluralRules('en'); - * fun(1) -> 'one' - * fun(0) -> 'other' - * fun(1000) -> 'other'. - */ - function getPluralRules(lang) { - var locales2rules = { - 'af': 3, - 'ak': 4, - 'am': 4, - 'ar': 1, - 'asa': 3, - 'az': 0, - 'be': 11, - 'bem': 3, - 'bez': 3, - 'bg': 3, - 'bh': 4, - 'bm': 0, - 'bn': 3, - 'bo': 0, - 'br': 20, - 'brx': 3, - 'bs': 11, - 'ca': 3, - 'cgg': 3, - 'chr': 3, - 'cs': 12, - 'cy': 17, - 'da': 3, - 'de': 3, - 'dv': 3, - 'dz': 0, - 'ee': 3, - 'el': 3, - 'en': 3, - 'eo': 3, - 'es': 3, - 'et': 3, - 'eu': 3, - 'fa': 0, - 'ff': 5, - 'fi': 3, - 'fil': 4, - 'fo': 3, - 'fr': 5, - 'fur': 3, - 'fy': 3, - 'ga': 8, - 'gd': 24, - 'gl': 3, - 'gsw': 3, - 'gu': 3, - 'guw': 4, - 'gv': 23, - 'ha': 3, - 'haw': 3, - 'he': 2, - 'hi': 4, - 'hr': 11, - 'hu': 0, - 'id': 0, - 'ig': 0, - 'ii': 0, - 'is': 3, - 'it': 3, - 'iu': 7, - 'ja': 0, - 'jmc': 3, - 'jv': 0, - 'ka': 0, - 'kab': 5, - 'kaj': 3, - 'kcg': 3, - 'kde': 0, - 'kea': 0, - 'kk': 3, - 'kl': 3, - 'km': 0, - 'kn': 0, - 'ko': 0, - 'ksb': 3, - 'ksh': 21, - 'ku': 3, - 'kw': 7, - 'lag': 18, - 'lb': 3, - 'lg': 3, - 'ln': 4, - 'lo': 0, - 'lt': 10, - 'lv': 6, - 'mas': 3, - 'mg': 4, - 'mk': 16, - 'ml': 3, - 'mn': 3, - 'mo': 9, - 'mr': 3, - 'ms': 0, - 'mt': 15, - 'my': 0, - 'nah': 3, - 'naq': 7, - 'nb': 3, - 'nd': 3, - 'ne': 3, - 'nl': 3, - 'nn': 3, - 'no': 3, - 'nr': 3, - 'nso': 4, - 'ny': 3, - 'nyn': 3, - 'om': 3, - 'or': 3, - 'pa': 3, - 'pap': 3, - 'pl': 13, - 'ps': 3, - 'pt': 3, - 'rm': 3, - 'ro': 9, - 'rof': 3, - 'ru': 11, - 'rwk': 3, - 'sah': 0, - 'saq': 3, - 'se': 7, - 'seh': 3, - 'ses': 0, - 'sg': 0, - 'sh': 11, - 'shi': 19, - 'sk': 12, - 'sl': 14, - 'sma': 7, - 'smi': 7, - 'smj': 7, - 'smn': 7, - 'sms': 7, - 'sn': 3, - 'so': 3, - 'sq': 3, - 'sr': 11, - 'ss': 3, - 'ssy': 3, - 'st': 3, - 'sv': 3, - 'sw': 3, - 'syr': 3, - 'ta': 3, - 'te': 3, - 'teo': 3, - 'th': 0, - 'ti': 4, - 'tig': 3, - 'tk': 3, - 'tl': 4, - 'tn': 3, - 'to': 0, - 'tr': 0, - 'ts': 3, - 'tzm': 22, - 'uk': 11, - 'ur': 3, - 've': 3, - 'vi': 0, - 'vun': 3, - 'wa': 4, - 'wae': 3, - 'wo': 0, - 'xh': 3, - 'xog': 3, - 'yo': 0, - 'zh': 0, - 'zu': 3 - }; - // utility functions for plural rules methods - function isIn(n, list) { - return list.indexOf(n) !== -1; - } - function isBetween(n, start, end) { - return start <= n && n <= end; - } - // list of all plural rules methods: - // map an integer to the plural form name to use - var pluralRules = { - '0': function (n) { - return 'other'; - }, - '1': function (n) { - if ((isBetween((n % 100), 3, 10))) - return 'few'; - if (n === 0) - return 'zero'; - if ((isBetween((n % 100), 11, 99))) - return 'many'; - if (n == 2) - return 'two'; - if (n == 1) - return 'one'; - return 'other'; - }, - '2': function (n) { - if (n !== 0 && (n % 10) === 0) - return 'many'; - if (n == 2) - return 'two'; - if (n == 1) - return 'one'; - return 'other'; - }, - '3': function (n) { - if (n == 1) - return 'one'; - return 'other'; - }, - '4': function (n) { - if ((isBetween(n, 0, 1))) - return 'one'; - return 'other'; - }, - '5': function (n) { - if ((isBetween(n, 0, 2)) && n != 2) - return 'one'; - return 'other'; - }, - '6': function (n) { - if (n === 0) - return 'zero'; - if ((n % 10) == 1 && (n % 100) != 11) - return 'one'; - return 'other'; - }, - '7': function (n) { - if (n == 2) - return 'two'; - if (n == 1) - return 'one'; - return 'other'; - }, - '8': function (n) { - if ((isBetween(n, 3, 6))) - return 'few'; - if ((isBetween(n, 7, 10))) - return 'many'; - if (n == 2) - return 'two'; - if (n == 1) - return 'one'; - return 'other'; - }, - '9': function (n) { - if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19))) - return 'few'; - if (n == 1) - return 'one'; - return 'other'; - }, - '10': function (n) { - if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) - return 'few'; - if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19))) - return 'one'; - return 'other'; - }, - '11': function (n) { - if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) - return 'few'; - if ((n % 10) === 0 || - (isBetween((n % 10), 5, 9)) || - (isBetween((n % 100), 11, 14))) - return 'many'; - if ((n % 10) == 1 && (n % 100) != 11) - return 'one'; - return 'other'; - }, - '12': function (n) { - if ((isBetween(n, 2, 4))) - return 'few'; - if (n == 1) - return 'one'; - return 'other'; - }, - '13': function (n) { - if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) - return 'few'; - if (n != 1 && (isBetween((n % 10), 0, 1)) || - (isBetween((n % 10), 5, 9)) || - (isBetween((n % 100), 12, 14))) - return 'many'; - if (n == 1) - return 'one'; - return 'other'; - }, - '14': function (n) { - if ((isBetween((n % 100), 3, 4))) - return 'few'; - if ((n % 100) == 2) - return 'two'; - if ((n % 100) == 1) - return 'one'; - return 'other'; - }, - '15': function (n) { - if (n === 0 || (isBetween((n % 100), 2, 10))) - return 'few'; - if ((isBetween((n % 100), 11, 19))) - return 'many'; - if (n == 1) - return 'one'; - return 'other'; - }, - '16': function (n) { - if ((n % 10) == 1 && n != 11) - return 'one'; - return 'other'; - }, - '17': function (n) { - if (n == 3) - return 'few'; - if (n === 0) - return 'zero'; - if (n == 6) - return 'many'; - if (n == 2) - return 'two'; - if (n == 1) - return 'one'; - return 'other'; - }, - '18': function (n) { - if (n === 0) - return 'zero'; - if ((isBetween(n, 0, 2)) && n !== 0 && n != 2) - return 'one'; - return 'other'; - }, - '19': function (n) { - if ((isBetween(n, 2, 10))) - return 'few'; - if ((isBetween(n, 0, 1))) - return 'one'; - return 'other'; - }, - '20': function (n) { - if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(isBetween((n % 100), 10, 19) || - isBetween((n % 100), 70, 79) || - isBetween((n % 100), 90, 99))) - return 'few'; - if ((n % 1000000) === 0 && n !== 0) - return 'many'; - if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92])) - return 'two'; - if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91])) - return 'one'; - return 'other'; - }, - '21': function (n) { - if (n === 0) - return 'zero'; - if (n == 1) - return 'one'; - return 'other'; - }, - '22': function (n) { - if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) - return 'one'; - return 'other'; - }, - '23': function (n) { - if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) - return 'one'; - return 'other'; - }, - '24': function (n) { - if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) - return 'few'; - if (isIn(n, [2, 12])) - return 'two'; - if (isIn(n, [1, 11])) - return 'one'; - return 'other'; - } - }; - // return a function that gives the plural form name for a given integer - var index = locales2rules[lang.replace(/-.*$/, '')]; - if (!(index in pluralRules)) { - console.warn('plural form unknown for [' + lang + ']'); - return function () { return 'other'; }; - } - return pluralRules[index]; - } - /** - * pre-defined 'plural' macro - */ - html10n.macros.plural = function (key, param, opts) { - var str, n = parseFloat(param); - if (isNaN(n)) - return; - // initialize _pluralRules - if (!this._pluralRules) - this._pluralRules = getPluralRules(html10n.language); - var index = this._pluralRules(n); - // try to find a [zero|one|two] key if it's defined - if (n === 0 && ('zero') in opts) { - str = opts['zero']; - } - else if (n == 1 && ('one') in opts) { - str = opts['one']; - } - else if (n == 2 && ('two') in opts) { - str = opts['two']; - } - else if (index in opts) { - str = opts[index]; - } - return str; - }; - /** - * Localize a document - * @param langs An array of lang codes defining fallbacks - */ - html10n.localize = function (langs) { - var that = this; - // if only one string => create an array - if ('string' == typeof langs) - langs = [langs]; - // Expand two-part locale specs - var i = 0; - langs.forEach(function (lang) { - if (!lang) - return; - langs[i++] = lang; - if (~lang.indexOf('-')) - langs[i++] = lang.substr(0, lang.indexOf('-')); - }); - this.build(langs, function (er, translations) { - html10n.translations = translations; - html10n.translateElement(translations); - that.trigger('localized'); - }); - }; - /** - * Triggers the translation process - * for an element - * @param translations A hash of all translation strings - * @param element A DOM element, if omitted, the document element will be used - */ - html10n.translateElement = function (translations, element) { - element = element || document.documentElement; - var children = element ? getTranslatableChildren(element) : document.childNodes; - for (var i = 0, n = children.length; i < n; i++) { - this.translateNode(translations, children[i]); - } - // translate element itself if necessary - this.translateNode(translations, element); - }; - function asyncForEach(list, iterator, cb) { - var i = 0, n = list.length; - iterator(list[i], i, function each(err) { - if (err) - console.error(err); - i++; - if (i < n) - return iterator(list[i], i, each); - cb(); - }); - } - function getTranslatableChildren(element) { - if (!document.querySelectorAll) { - if (!element) - return []; - var nodes = element.getElementsByTagName('*'), l10nElements = []; - for (var i = 0, n = nodes.length; i < n; i++) { - if (nodes[i].getAttribute('data-l10n-id')) - l10nElements.push(nodes[i]); - } - return l10nElements; - } - return element.querySelectorAll('*[data-l10n-id]'); - } - html10n.get = function (id, args) { - var translations = html10n.translations; - if (!translations) - return console.warn('No translations available (yet)'); - if (!translations[id]) - return console.warn('Could not find string ' + id); - // apply macros - var str = translations[id]; - str = substMacros(id, str, args); - // apply args - str = substArguments(str, args); - return str; - }; - // replace {{arguments}} with their values or the - // associated translation string (based on its key) - function substArguments(str, args) { - var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/, match; - var translations = html10n.translations; - while (match = reArgs.exec(str)) { - if (!match || match.length < 2) - return str; // argument key not found - var arg = match[1], sub = ''; - if (args && arg in args) { - sub = args[arg]; - } - else if (translations && arg in translations) { - sub = translations[arg]; - } - else { - console.warn('Could not find argument {{' + arg + '}}'); - return str; - } - str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length); - } - return str; - } - // replace {[macros]} with their values - function substMacros(key, str, args) { - var regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}') - , match; - while (match = regex.exec(str)) { - // a macro has been found - // Note: at the moment, only one parameter is supported - var macroName = match[1], paramName = match[2], optv = match[3], opts = {}; - if (!(macroName in html10n.macros)) - continue; - if (optv) { - optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g).forEach(function (arg) { - var parts = arg.split(':'), name = parts[0], value = parts[1].trim(); - opts[name] = value; - }); - } - var param; - if (args && paramName in args) { - param = args[paramName]; - } - else if (paramName in html10n.translations) { - param = translations[paramName]; - } - // there's no macro parser: it has to be defined in html10n.macros - var macro = html10n.macros[macroName]; - str = str.substr(0, match.index) + macro(key, param, opts) + str.substr(match.index + match[0].length); - } - return str; - } - /** - * Applies translations to a DOM node (recursive) - */ - html10n.translateNode = function (translations, node) { - var str = {}; - // get id - str.id = node.getAttribute('data-l10n-id'); - if (!str.id) - return; - if (!translations[str.id]) - return console.warn('Couldn\'t find translation key ' + str.id); - // get args - if (window.JSON) { - str.args = JSON.parse(node.getAttribute('data-l10n-args')); - } - else { - try { - str.args = eval(node.getAttribute('data-l10n-args')); - } - catch (e) { - console.warn('Couldn\'t parse args for ' + str.id); - } - } - str.str = html10n.get(str.id, str.args); - // get attribute name to apply str to - var prop, index = str.id.lastIndexOf('.'), attrList = // allowed attributes - { "title": 1, - "innerHTML": 1, - "alt": 1, - "textContent": 1, - "value": 1, - "placeholder": 1 - }; - if (index > 0 && str.id.substr(index + 1) in attrList) { - // an attribute has been specified (example: "my_translation_key.placeholder") - prop = str.id.substr(index + 1); - } - else { // no attribute: assuming text content by default - prop = document.body.textContent ? 'textContent' : 'innerText'; - } - // Apply translation - if (node.children.length === 0 || prop != 'textContent') { - node[prop] = str.str; - node.setAttribute("aria-label", str.str); // Sets the aria-label - // The idea of the above is that we always have an aria value - // This might be a bit of an abrupt solution but let's see how it goes - } - else { - var children = node.childNodes, found = false; - for (var i = 0, n = children.length; i < n; i++) { - if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) { - if (!found) { - children[i].nodeValue = str.str; - found = true; - } - else { - children[i].nodeValue = ''; - } - } - } - if (!found) { - console.warn('Unexpected error: could not translate element content for key ' + str.id, node); - } - } - }; - /** - * Builds a translation object from a list of langs (loads the necessary translations) - * @param langs Array - a list of langs sorted by priority (default langs should go last) - */ - html10n.build = function (langs, cb) { - var that = this, build = {}; - asyncForEach(langs, function (lang, i, next) { - if (!lang) - return next(); - that.loader.load(lang, next); - }, function () { - var lang; - langs.reverse(); - // loop through the priority array... - for (var i = 0, n = langs.length; i < n; i++) { - lang = langs[i]; - if (!lang) - continue; - if (!(lang in that.loader.langs)) { // uh, we don't have this lang availbable.. - // then check for related langs - if (~lang.indexOf('-')) - lang = lang.split('-')[0]; - for (var l in that.loader.langs) { - if (lang != l && l.indexOf(lang) === 0) { - lang = l; - break; - } - } - if (lang != l) - continue; - } - // ... and apply all strings of the current lang in the list - // to our build object - for (var string in that.loader.langs[lang]) { - build[string] = that.loader.langs[lang][string]; - } - // the last applied lang will be exposed as the - // lang the page was translated to - that.language = lang; - } - cb(null, build); - }); - }; - /** - * Returns the language that was last applied to the translations hash - * thus overriding most of the formerly applied langs - */ - html10n.getLanguage = function () { - return this.language; - }; - /** - * Returns the direction of the language returned be html10n#getLanguage - */ - html10n.getDirection = function () { - if (!this.language) - return; - var langCode = this.language.indexOf('-') == -1 ? this.language : this.language.substr(0, this.language.indexOf('-')); - return html10n.rtl.indexOf(langCode) == -1 ? 'ltr' : 'rtl'; - }; - /** - * Index all s - */ - html10n.index = function () { - // Find all s - var links = document.getElementsByTagName('link'), resources = []; - for (var i = 0, n = links.length; i < n; i++) { - if (links[i].type != 'application/l10n+json') - continue; - resources.push(links[i].href); - } - this.loader = new Loader(resources); - this.trigger('indexed'); - }; - if (document.addEventListener) // modern browsers and IE9+ - document.addEventListener('DOMContentLoaded', function () { - html10n.index(); - }, false); - else if (window.attachEvent) - window.attachEvent('onload', function () { - html10n.index(); - }, false); - // gettext-like shortcut - if (window._ === undefined) - window._ = html10n.get; - return html10n; -})(window, document); diff --git a/src/static/js/vendors/i18next.ts b/src/static/js/vendors/i18next.ts new file mode 100644 index 000000000..2311d544d --- /dev/null +++ b/src/static/js/vendors/i18next.ts @@ -0,0 +1,15 @@ +import i18next, {TFunction} from 'i18next' +import Backend from 'i18next-fs-backend'; + + +export let i18nextvar: TFunction +const i18next2 = i18next + .use(Backend) + .init({ + fallbackLng: 'en', // Default language fallback + backend: { + loadPath: 'locales/{{lng}}.json', // Path pattern to load locale files + }, + }).then(t=>{ + i18nextvar = t + }); diff --git a/src/static/module/CustomWindow.ts b/src/static/module/CustomWindow.ts index 40711b98d..255b536aa 100644 --- a/src/static/module/CustomWindow.ts +++ b/src/static/module/CustomWindow.ts @@ -1,9 +1,16 @@ +import {extend} from "jquery"; + export type CustomWindow = { + revisionInfo: any; + $: any; require: RequireFunction, Ace2Inner:any, plugins: any, jQuery: any, document: any, + pad: any, + clientVars: any, + socketio: any, } @@ -12,3 +19,36 @@ export type RequireFunction= { setLibraryURI: (url: string)=>void, setGlobalKeyPath: (path: string)=>void, } + +export type CustomElementWithSheet = { + sheet: CSSStyleSheet, + startLoc: number + currentLoc: number, + id +} + + +export type AuthorData = { + colorId: number|string +} + +export type Author = { + [author:string]:AuthorData +} + +export type JQueryGritter = { + gritter: any; + farbtastic: any; +} + +export type AjaxDirectDatabaseAccess = { + code : number + message: string + data: { + directDatabaseAccess?: boolean + } +} + +export type HistoricalAuthorData = { + userId: string, +} diff --git a/src/tsconfig.json b/src/tsconfig.json index c2b23ed42..9ad402434 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,13 +1,14 @@ { "compilerOptions": { + "resolveJsonModule": true, "allowJs": true, "typeRoots": ["node_modules/@types"], "types": ["node", "jquery"], "module": "commonjs", "esModuleInterop": true, - "target": "es6", "moduleResolution": "node", "sourceMap": true, + "target": "ES2015", "outDir": "dist" }, "lib": ["es2015"]