mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Merge branch 'develop'
This commit is contained in:
commit
dd83164b22
159 changed files with 4971 additions and 24500 deletions
18
.github/workflows/backend-tests.yml
vendored
18
.github/workflows/backend-tests.yml
vendored
|
@ -69,6 +69,9 @@ jobs:
|
|||
-
|
||||
name: Run the backend tests
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
||||
withpluginsLinux:
|
||||
# run on pushes to any branch
|
||||
|
@ -142,6 +145,9 @@ jobs:
|
|||
-
|
||||
name: Run the backend tests
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
||||
withoutpluginsWindows:
|
||||
# run on pushes to any branch
|
||||
|
@ -193,7 +199,11 @@ jobs:
|
|||
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
|
||||
-
|
||||
name: Run the backend tests
|
||||
run: cd src && pnpm test
|
||||
working-directory: src
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
||||
withpluginsWindows:
|
||||
# run on pushes to any branch
|
||||
|
@ -273,4 +283,8 @@ jobs:
|
|||
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
|
||||
-
|
||||
name: Run the backend tests
|
||||
run: cd src && pnpm test
|
||||
working-directory: src
|
||||
run: pnpm test
|
||||
- name: Run the new vitest tests
|
||||
working-directory: src
|
||||
run: pnpm run test:vitest
|
||||
|
|
2
.github/workflows/frontend-admin-tests.yml
vendored
2
.github/workflows/frontend-admin-tests.yml
vendored
|
@ -50,7 +50,7 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
|
6
.github/workflows/frontend-tests.yml
vendored
6
.github/workflows/frontend-tests.yml
vendored
|
@ -57,7 +57,7 @@ jobs:
|
|||
name: Create settings.json
|
||||
run: cp ./src/tests/settings.json settings.json
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
@ -127,7 +127,7 @@ jobs:
|
|||
- name: Create settings.json
|
||||
run: cp ./src/tests/settings.json settings.json
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
@ -175,7 +175,7 @@ jobs:
|
|||
with:
|
||||
node-version: 22
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,3 +1,14 @@
|
|||
# 2.2.3
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
||||
- Introduced a new in process database `rustydb` that represents a fast key value store written in Rust.
|
||||
- Readded window._ as a shortcut for getting text
|
||||
- Added support for migrating any ueberdb database to another. You can now switch as you please. See here: https://docs.etherpad.org/cli.html
|
||||
- Further Typescript movements
|
||||
- A lot of security issues fixed and reviewed in this release. Please update.
|
||||
|
||||
|
||||
# 2.2.2
|
||||
|
||||
### Notable enhancements and fixes
|
||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -49,6 +49,14 @@ ARG ETHERPAD_PLUGINS=
|
|||
# ETHERPAD_LOCAL_PLUGINS="../ep_my_plugin ../ep_another_plugin"
|
||||
ARG ETHERPAD_LOCAL_PLUGINS=
|
||||
|
||||
# github plugins to install while building the container. By default no plugins are
|
||||
# installed.
|
||||
# If given a value, it has to be a space-separated, quoted list of plugin names.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# ETHERPAD_GITHUB_PLUGINS="ether/ep_plugin"
|
||||
ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
|
||||
# Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats.
|
||||
# By default, it is not installed.
|
||||
# If given any value, abiword will be installed.
|
||||
|
@ -114,13 +122,13 @@ COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./
|
|||
|
||||
FROM build AS development
|
||||
|
||||
COPY --chown=etherpad:etherpad ./src/package.json .npmrc ./src/
|
||||
COPY --chown=etherpad:etherpad ./src/ ./src/
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/ templates/admin./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||
|
||||
RUN bin/installDeps.sh && \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
|
||||
fi
|
||||
|
||||
|
||||
|
@ -134,11 +142,10 @@ COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/template
|
|||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||
|
||||
RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}}; \
|
||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ] || [ ! -z "${ETHERPAD_GITHUB_PLUGINS}" ]; then \
|
||||
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
|
||||
fi
|
||||
|
||||
|
||||
# Copy the configuration file.
|
||||
COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "admin",
|
||||
"private": true,
|
||||
"version": "2.2.2",
|
||||
"version": "2.2.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
@ -16,27 +16,27 @@
|
|||
"devDependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@types/react": "^18.3.2",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
||||
"@typescript-eslint/parser": "^8.4.0",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"i18next": "^23.12.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"lucide-react": "^0.426.0",
|
||||
"lucide-react": "^0.438.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.0",
|
||||
"vite": "^5.4.3",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"zustand": "^4.5.4"
|
||||
"zustand": "^4.5.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -802,3 +802,12 @@ input, button, select, optgroup, textarea {
|
|||
background-color: var(--etherpad-color);
|
||||
color: white
|
||||
}
|
||||
|
||||
.search-pads{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search-pads-body tr td:last-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
@ -6,14 +6,51 @@ import {Trans, useTranslation} from "react-i18next";
|
|||
import {SearchField} from "../components/SearchField.tsx";
|
||||
import {Download, Trash} from "lucide-react";
|
||||
import {IconButton} from "../components/IconButton.tsx";
|
||||
import {determineSorting} from "../utils/sorting.ts";
|
||||
|
||||
|
||||
export const HomePage = () => {
|
||||
const pluginsSocket = useStore(state=>state.pluginsSocket)
|
||||
const [plugins,setPlugins] = useState<PluginDef[]>([])
|
||||
const [installedPlugins, setInstalledPlugins] = useState<InstalledPlugin[]>([])
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
offset: 0,
|
||||
limit: 99999,
|
||||
sortBy: 'name',
|
||||
sortDir: 'asc',
|
||||
searchTerm: ''
|
||||
})
|
||||
|
||||
const filteredInstallablePlugins = useMemo(()=>{
|
||||
return plugins.sort((a, b)=>{
|
||||
if(searchParams.sortBy === "version"){
|
||||
if(searchParams.sortDir === "asc"){
|
||||
return a.version.localeCompare(b.version)
|
||||
}
|
||||
return b.version.localeCompare(a.version)
|
||||
}
|
||||
|
||||
if(searchParams.sortBy === "last-updated"){
|
||||
if(searchParams.sortDir === "asc"){
|
||||
return a.time.localeCompare(b.time)
|
||||
}
|
||||
return b.time.localeCompare(a.time)
|
||||
}
|
||||
|
||||
|
||||
if (searchParams.sortBy === "name") {
|
||||
if(searchParams.sortDir === "asc"){
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
return b.name.localeCompare(a.name)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}, [plugins, searchParams])
|
||||
|
||||
const sortedInstalledPlugins = useMemo(()=>{
|
||||
return installedPlugins.sort((a, b)=>{
|
||||
|
||||
if(a.name < b.name){
|
||||
return -1
|
||||
}
|
||||
|
@ -23,14 +60,8 @@ export const HomePage = () => {
|
|||
return 0
|
||||
})
|
||||
|
||||
} ,[installedPlugins])
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
offset: 0,
|
||||
limit: 99999,
|
||||
sortBy: 'name',
|
||||
sortDir: 'asc',
|
||||
searchTerm: ''
|
||||
})
|
||||
} ,[installedPlugins, searchParams])
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const {t} = useTranslation()
|
||||
|
||||
|
@ -165,16 +196,35 @@ export const HomePage = () => {
|
|||
<table id="available-plugins">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><Trans i18nKey="admin_plugins.name"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'name')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'name',
|
||||
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
|
||||
})
|
||||
}}>
|
||||
<Trans i18nKey="admin_plugins.name" /></th>
|
||||
<th style={{width: '30%'}}><Trans i18nKey="admin_plugins.description"/></th>
|
||||
<th><Trans i18nKey="admin_plugins.version"/></th>
|
||||
<th><Trans i18nKey="admin_plugins.last-update"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'version')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'version',
|
||||
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
|
||||
})
|
||||
}}><Trans i18nKey="admin_plugins.version"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'last-updated')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'last-updated',
|
||||
sortDir: searchParams.sortDir === "asc"? "desc": "asc"
|
||||
})
|
||||
}}><Trans i18nKey="admin_plugins.last-update"/></th>
|
||||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style={{overflow: 'auto'}}>
|
||||
{(plugins.length > 0) ?
|
||||
plugins.map((plugin) => {
|
||||
{(filteredInstallablePlugins.length > 0) ?
|
||||
filteredInstallablePlugins.map((plugin) => {
|
||||
return <tr key={plugin.name}>
|
||||
<td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
|
||||
<td>{plugin.description}</td>
|
||||
|
|
|
@ -104,7 +104,7 @@ export const PadPage = ()=>{
|
|||
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<tr className="search-pads">
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
|
@ -136,7 +136,7 @@ export const PadPage = ()=>{
|
|||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody className="search-pads-body">
|
||||
{
|
||||
pads?.results?.map((pad)=>{
|
||||
return <tr key={pad.padName}>
|
||||
|
|
|
@ -20,7 +20,7 @@ export type SearchParams = {
|
|||
searchTerm: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
sortBy: 'name'|'version',
|
||||
sortBy: 'name'|'version'|'last-updated',
|
||||
sortDir: 'asc'|'desc'
|
||||
}
|
||||
|
||||
|
|
63
bin/make_docs.ts
Normal file
63
bin/make_docs.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import {exec} from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import pjson from '../src/package.json'
|
||||
|
||||
const VERSION=pjson.version
|
||||
console.log(`Building docs for version ${VERSION}`)
|
||||
|
||||
const createDirIfNotExists = (dir: fs.PathLike) => {
|
||||
if (!fs.existsSync(dir)){
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function copyFolderSync(from: fs.PathLike, to: fs.PathLike) {
|
||||
if(fs.existsSync(to)){
|
||||
const stat = fs.lstatSync(to)
|
||||
if (stat.isDirectory()){
|
||||
fs.rmSync(to, { recursive: true })
|
||||
}
|
||||
else{
|
||||
fs.rmSync(to)
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(to);
|
||||
fs.readdirSync(from).forEach(element => {
|
||||
if (fs.lstatSync(path.join(<string>from, element)).isFile()) {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
fs.copyFileSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
copyFolderSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exec('asciidoctor -v', (err,stdout)=>{
|
||||
if (err){
|
||||
console.log('Please install asciidoctor')
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/')
|
||||
process.exit(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
createDirIfNotExists('../out')
|
||||
createDirIfNotExists('../out/doc')
|
||||
createDirIfNotExists('../out/doc/api')
|
||||
|
||||
|
||||
|
||||
exec(`asciidoctor -D ../out/doc ../doc/index.adoc */**.adoc -a VERSION=${VERSION}`)
|
||||
exec(`asciidoctor -D ../out/doc/api ../doc/api/*.adoc -a VERSION=${VERSION}`)
|
||||
|
||||
copyFolderSync('../doc/public/', '../out/doc/')
|
83
bin/migrateDB.ts
Normal file
83
bin/migrateDB.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
// DB migration
|
||||
import {readFileSync} from 'node:fs'
|
||||
import {Database, DatabaseType} from "ueberdb2";
|
||||
import path from "node:path";
|
||||
const settings = require('ep_etherpad-lite/node/utils/Settings');
|
||||
|
||||
|
||||
// file1 = source, file2 = target
|
||||
// pnpm run migrateDB --file1 <db1.json> --file2 <db2.json>
|
||||
const arg = process.argv.slice(2);
|
||||
|
||||
if (arg.length != 4) {
|
||||
console.error('Wrong number of arguments!. Call with pnpm run migrateDB --file1 source.json target.json')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
type SettingsConfig = {
|
||||
dbType: string,
|
||||
dbSettings: any
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"dbType": "<your-db-type>",
|
||||
"dbSettings": {
|
||||
<your-db-settings>
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
let firstDBSettingsFile: string
|
||||
let secondDBSettingsFile: string
|
||||
|
||||
|
||||
if (arg[0] == "--file1") {
|
||||
firstDBSettingsFile = arg[1]
|
||||
} else if (arg[0] === "--file2") {
|
||||
secondDBSettingsFile = arg[1]
|
||||
}
|
||||
|
||||
if (arg[2] == "--file1") {
|
||||
firstDBSettingsFile = arg[3]
|
||||
} else if (arg[2] === "--file2") {
|
||||
secondDBSettingsFile = arg[3]
|
||||
}
|
||||
|
||||
|
||||
|
||||
const settingsfile = JSON.parse(readFileSync(path.join(settings.root,firstDBSettingsFile!)).toString()) as SettingsConfig
|
||||
const settingsfile2 = JSON.parse(readFileSync(path.join(settings.root,secondDBSettingsFile!)).toString()) as SettingsConfig
|
||||
|
||||
console.log(settingsfile2)
|
||||
if ("filename" in settingsfile.dbSettings) {
|
||||
settingsfile.dbSettings.filename = path.join(settings.root, settingsfile.dbSettings.filename)
|
||||
console.log(settingsfile.dbType + " location is "+ settingsfile.dbSettings.filename)
|
||||
}
|
||||
|
||||
if ("filename" in settingsfile2.dbSettings) {
|
||||
settingsfile2.dbSettings.filename = path.join(settings.root, settingsfile2.dbSettings.filename)
|
||||
console.log(settingsfile2.dbType + " location is "+ settingsfile2.dbSettings.filename)
|
||||
}
|
||||
|
||||
const ueberdb1 = new Database(settingsfile.dbType as DatabaseType, settingsfile.dbSettings)
|
||||
const ueberdb2 = new Database(settingsfile2.dbType as DatabaseType, settingsfile2.dbSettings)
|
||||
|
||||
const handleSync = async ()=>{
|
||||
await ueberdb1.init()
|
||||
await ueberdb2.init()
|
||||
|
||||
const allKeys = await ueberdb1.findKeys('*','')
|
||||
for (const key of allKeys) {
|
||||
const foundVal = await ueberdb1.get(key)!
|
||||
await ueberdb2.set(key, foundVal)
|
||||
}
|
||||
}
|
||||
|
||||
handleSync().then(()=>{
|
||||
console.log("Done syncing dbs")
|
||||
}).catch(e=>{
|
||||
console.log(`Error syncing db ${e}`)
|
||||
})
|
||||
|
||||
|
|
@ -1,25 +1,26 @@
|
|||
{
|
||||
"name": "bin",
|
||||
"version": "2.2.2",
|
||||
"version": "2.2.3",
|
||||
"description": "",
|
||||
"main": "checkAllPads.js",
|
||||
"directories": {
|
||||
"doc": "doc"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.3",
|
||||
"axios": "^1.7.7",
|
||||
"ep_etherpad-lite": "workspace:../src",
|
||||
"log4js": "^6.9.1",
|
||||
"semver": "^7.6.3",
|
||||
"tsx": "^4.17.0",
|
||||
"ueberdb2": "^4.2.92"
|
||||
"tsx": "^4.19.0",
|
||||
"ueberdb2": "^4.2.100"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/semver": "^7.5.8",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"scripts": {
|
||||
"makeDocs": "node --import tsx make_docs.ts",
|
||||
"checkPad": "node --import tsx checkPad.ts",
|
||||
"checkAllPads": "node --import tsx checkAllPads.ts",
|
||||
"createUserSession": "node --import tsx createUserSession.ts",
|
||||
|
@ -33,7 +34,8 @@
|
|||
"stalePlugins": "node --import tsx ./plugins/stalePlugins.ts",
|
||||
"checkPlugin": "node --import tsx ./plugins/checkPlugin.ts",
|
||||
"plugins": "node --import tsx ./plugins.ts",
|
||||
"generateChangelog": "node --import tsx generateReleaseNotes.ts"
|
||||
"generateChangelog": "node --import tsx generateReleaseNotes.ts",
|
||||
"migrateDB": "node --import tsx migrateDB.ts"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
|
|
|
@ -23,17 +23,13 @@ const possibleActions = [
|
|||
]
|
||||
|
||||
const install = ()=> {
|
||||
|
||||
let registryPlugins: string[] = [];
|
||||
let localPlugins: string[] = [];
|
||||
|
||||
if (args.indexOf('--path') !== -1) {
|
||||
const indexToSplit = args.indexOf('--path');
|
||||
registryPlugins = args.slice(1, indexToSplit);
|
||||
localPlugins = args.slice(indexToSplit + 1);
|
||||
} else {
|
||||
registryPlugins = args;
|
||||
}
|
||||
const argsAsString: string = args.join(" ");
|
||||
const regexRegistryPlugins = /(?<=i\s)(.*?)(?=--github|--path|$)/;
|
||||
const regexLocalPlugins = /(?<=--path\s)(.*?)(?=--github|$)/;
|
||||
const regexGithubPlugins = /(?<=--github\s)(.*?)(?=--path|$)/;
|
||||
const registryPlugins = argsAsString.match(regexRegistryPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||
const localPlugins = argsAsString.match(regexLocalPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||
const githubPlugins = argsAsString.match(regexGithubPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||
|
||||
async function run() {
|
||||
for (const plugin of registryPlugins) {
|
||||
|
@ -53,6 +49,11 @@ const install = ()=> {
|
|||
console.log(`Installing plugin from path: ${plugin}`);
|
||||
await linkInstaller.installFromPath(plugin);
|
||||
}
|
||||
|
||||
for (const plugin of githubPlugins) {
|
||||
console.log(`Installing plugin from github: ${plugin}`);
|
||||
await linkInstaller.installFromGitHub(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
|
|
|
@ -197,7 +197,7 @@ try {
|
|||
|
||||
try {
|
||||
console.log('Building documentation...');
|
||||
run('node ./make_docs.js');
|
||||
run('pnpm run makeDocs');
|
||||
console.log('Updating ether.github.com master branch...');
|
||||
run('git pull --ff-only', {cwd: '../ether.github.com/'});
|
||||
console.log('Committing documentation...');
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
"resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
|
|
29
doc/cli.md
Normal file
29
doc/cli.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# CLI
|
||||
|
||||
You can find different tools for migrating things, checking your Etherpad health in the bin directory.
|
||||
One of these is the migrateDB command. It takes two settings.json files and copies data from one source to another one.
|
||||
In this example we migrate from the old dirty db to the new rustydb engine. So we copy these files to the root of the etherpad-directory.
|
||||
|
||||
````json
|
||||
{
|
||||
"dbType": "dirty",
|
||||
"dbSettings": {
|
||||
"filename": "./var/rusty.db"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
|
||||
````json
|
||||
{
|
||||
"dbType": "rustydb",
|
||||
"dbSettings": {
|
||||
"filename": "./var/rusty2.db"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
After that we need to move the data from dirty to rustydb.
|
||||
Therefore, we call `pnpm run migrateDB --file1 test1.json --file2 test2.json` with these two files in our root directories. After some time the data should be copied over to the new database.
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.3.2"
|
||||
"vitepress": "^1.3.4"
|
||||
},
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
|
|
|
@ -9,6 +9,7 @@ services:
|
|||
build:
|
||||
context: .
|
||||
args:
|
||||
# Attention: installed plugins in the node_modules folder get overwritten during volume mount in dev
|
||||
ETHERPAD_PLUGINS:
|
||||
# change from development to production if needed
|
||||
target: development
|
||||
|
|
55
make_docs.js
55
make_docs.js
|
@ -1,55 +0,0 @@
|
|||
import {exec} from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import pjson from './src/package.json' assert {type: "json"}
|
||||
|
||||
const VERSION=pjson.version
|
||||
console.log(`Building docs for version ${VERSION}`)
|
||||
|
||||
const createDirIfNotExists = (dir) => {
|
||||
if (!fs.existsSync(dir)){
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function copyFolderSync(from, to) {
|
||||
if(fs.existsSync(to)){
|
||||
const stat = fs.lstatSync(to)
|
||||
if (stat.isDirectory()){
|
||||
fs.rmSync(to, { recursive: true })
|
||||
}
|
||||
else{
|
||||
fs.rmSync(to)
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(to);
|
||||
fs.readdirSync(from).forEach(element => {
|
||||
if (fs.lstatSync(path.join(from, element)).isFile()) {
|
||||
fs.copyFileSync(path.join(from, element), path.join(to, element))
|
||||
} else {
|
||||
copyFolderSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exec('asciidoctor -v', (err,stdout)=>{
|
||||
if (err){
|
||||
console.log('Please install asciidoctor')
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/')
|
||||
process.exit(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
createDirIfNotExists('./out')
|
||||
createDirIfNotExists('./out/doc')
|
||||
createDirIfNotExists('./out/doc/api')
|
||||
|
||||
|
||||
|
||||
exec(`asciidoctor -D out/doc doc/index.adoc */**.adoc -a VERSION=${VERSION}`)
|
||||
exec(`asciidoctor -D out/doc/api ./doc/api/*.adoc -a VERSION=${VERSION}`)
|
||||
|
||||
copyFolderSync('./doc/public/', './out/doc/')
|
|
@ -30,7 +30,8 @@
|
|||
"remove-plugins": "pnpm --filter bin run remove-plugins",
|
||||
"list-plugins": "pnpm --filter bin run list-plugins",
|
||||
"build:etherpad": "pnpm --filter admin run build-copy && pnpm --filter ui run build-copy",
|
||||
"build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy"
|
||||
"build:ui": "pnpm --filter ui run build-copy && pnpm --filter admin run build-copy",
|
||||
"makeDocs": "pnpm --filter bin run makeDocs"
|
||||
},
|
||||
"dependencies": {
|
||||
"ep_etherpad-lite": "workspace:./src"
|
||||
|
@ -49,6 +50,6 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/ether/etherpad-lite.git"
|
||||
},
|
||||
"version": "2.2.2",
|
||||
"version": "2.2.3",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
1997
pnpm-lock.yaml
1997
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -35,6 +35,10 @@
|
|||
"admin_plugins_info.version_number": "Нумар вэрсіі",
|
||||
"admin_settings": "Налады",
|
||||
"admin_settings.current": "Цяперашняя канфігурацыя",
|
||||
"admin_settings.current_example-devel": "Прыклад шаблёну наладаў распрацоўкі",
|
||||
"admin_settings.current_example-prod": "Прыклад шаблёну наладаў вытворчасьці",
|
||||
"admin_settings.current_restart.value": "Перазапуск Etherpad",
|
||||
"admin_settings.current_save.value": "Захаваць налады",
|
||||
"admin_settings.page-title": "Налады — Etherpad",
|
||||
"index.newPad": "Стварыць",
|
||||
"index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:",
|
||||
|
@ -72,6 +76,7 @@
|
|||
"pad.settings.fontType.normal": "Звычайны",
|
||||
"pad.settings.language": "Мова:",
|
||||
"pad.settings.about": "Пра",
|
||||
"pad.settings.poweredBy": "Працуе на",
|
||||
"pad.importExport.import_export": "Імпарт/Экспарт",
|
||||
"pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты",
|
||||
"pad.importExport.importSuccessful": "Пасьпяхова!",
|
||||
|
@ -106,6 +111,9 @@
|
|||
"pad.modals.corruptPad.cause": "Гэта можа быць выклікана няправільнай канфігурацыяй сэрвэру або іншымі нечаканымі дзеяньнямі. Калі ласка, скантактуйцеся з адміністратарам службы.",
|
||||
"pad.modals.deleted": "Выдалены.",
|
||||
"pad.modals.deleted.explanation": "Гэты дакумэнт быў выдалены.",
|
||||
"pad.modals.rateLimited": "Хуткасьць абмежаваная.",
|
||||
"pad.modals.rateLimited.explanation": "Вы адаслалі так шмат паведамленьняў, што гэты дакумэнт вас адключыў.",
|
||||
"pad.modals.rejected.explanation": "Сэрвэр адхіліў паведамленьне, адасланае вашым броўзэрам.",
|
||||
"pad.modals.disconnected": "Вы былі адключаныя.",
|
||||
"pad.modals.disconnected.explanation": "Злучэньне з сэрвэрам было страчанае",
|
||||
"pad.modals.disconnected.cause": "Магчыма, сэрвэр недаступны. Калі ласка, паведаміце адміністратару службы, калі праблема будзе паўтарацца.",
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"authors": [
|
||||
"Aefgh39622",
|
||||
"Andibecker",
|
||||
"Ekminarin",
|
||||
"Patsagorn Y.",
|
||||
"Trisorn Triboon"
|
||||
]
|
||||
|
@ -121,7 +122,7 @@
|
|||
"pad.share.readonly": "อ่านเท่านั้น",
|
||||
"pad.share.link": "ลิงก์",
|
||||
"pad.share.emebdcode": "URL แบบฝังตัว",
|
||||
"pad.chat": "แชท",
|
||||
"pad.chat": "แชต",
|
||||
"pad.chat.title": "เปิดการแชทสำหรับแผ่นจดบันทึกนี้",
|
||||
"pad.chat.loadmessages": "โหลดข้อความเพิ่มเติม",
|
||||
"pad.chat.stick.title": "ปักการสนทนาไว้บนหน้าจอ",
|
||||
|
|
|
@ -19,8 +19,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
import {deserializeOps} from '../../static/js/Changeset';
|
||||
import ChatMessage from '../../static/js/ChatMessage';
|
||||
import {Builder} from "../../static/js/Builder";
|
||||
import {Attribute} from "../../static/js/types/Attribute";
|
||||
const CustomError = require('../utils/customError');
|
||||
const padManager = require('./PadManager');
|
||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||
|
@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
|||
const oldText = pad.text();
|
||||
atext.text += '\n';
|
||||
|
||||
const eachAttribRun = (attribs: string[], func:Function) => {
|
||||
const eachAttribRun = (attribs: string, func:Function) => {
|
||||
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);
|
||||
|
@ -577,10 +579,10 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
|||
};
|
||||
|
||||
// create a new changeset with a helper builder object
|
||||
const builder = Changeset.builder(oldText.length);
|
||||
const builder = new Builder(oldText.length);
|
||||
|
||||
// assemble each line into the builder
|
||||
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
|
||||
eachAttribRun(atext.attribs, (start: number, end: number, attribs:Attribute[]) => {
|
||||
builder.insert(atext.text.substring(start, end), attribs);
|
||||
});
|
||||
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
|
||||
const db = require('./DB');
|
||||
const CustomError = require('../utils/customError');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
import padutils, {randomString} from "../../static/js/pad_utils";
|
||||
|
||||
exports.getColorPalette = () => [
|
||||
'#ffc7c7',
|
||||
|
@ -169,7 +169,7 @@ exports.getAuthorId = async (token: string, user: object) => {
|
|||
* @param {String} token The token
|
||||
*/
|
||||
exports.getAuthor4Token = async (token: string) => {
|
||||
warnDeprecated(
|
||||
padutils.warnDeprecated(
|
||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
||||
return await getAuthor4Token(token);
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const randomString = require('../../static/js/pad_utils').randomString;
|
||||
import {randomString} from "../../static/js/pad_utils";
|
||||
const db = require('./DB');
|
||||
const padManager = require('./PadManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
|
|
|
@ -7,10 +7,10 @@ import {MapArrayType} from "../types/MapType";
|
|||
* The pad object, defined with joose
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import {applyToAText, checkRep, copyAText, deserializeOps, makeAText, makeSplice, opsFromAText, pack, unpack} from '../../static/js/Changeset';
|
||||
import ChatMessage from '../../static/js/ChatMessage';
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const Stream = require('../utils/Stream');
|
||||
const assert = require('assert').strict;
|
||||
const db = require('./DB');
|
||||
|
@ -23,8 +23,10 @@ const CustomError = require('../utils/customError');
|
|||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const randomString = require('../utils/randomstring');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
||||
const promises = require('../utils/promises');
|
||||
import pad_utils from "../../static/js/pad_utils";
|
||||
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
|
||||
import {} from '../utils/promises';
|
||||
import {timesLimit} from "async";
|
||||
|
||||
/**
|
||||
* Copied from the Etherpad source code. It converts Windows line breaks to Unix
|
||||
|
@ -40,7 +42,7 @@ exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n')
|
|||
class Pad {
|
||||
private db: Database;
|
||||
private atext: AText;
|
||||
private pool: APool;
|
||||
private pool: AttributePool;
|
||||
private head: number;
|
||||
private chatHead: number;
|
||||
private publicStatus: boolean;
|
||||
|
@ -56,7 +58,7 @@ class Pad {
|
|||
*/
|
||||
constructor(id:string, database = db) {
|
||||
this.db = database;
|
||||
this.atext = Changeset.makeAText('\n');
|
||||
this.atext = makeAText('\n');
|
||||
this.pool = new AttributePool();
|
||||
this.head = -1;
|
||||
this.chatHead = -1;
|
||||
|
@ -93,13 +95,13 @@ class Pad {
|
|||
* @param {String} authorId The id of the author
|
||||
* @return {Promise<number|string>}
|
||||
*/
|
||||
async appendRevision(aChangeset:AChangeSet, authorId = '') {
|
||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
||||
async appendRevision(aChangeset:string, authorId = '') {
|
||||
const newAText = applyToAText(aChangeset, this.atext, this.pool);
|
||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
||||
this.head !== -1) {
|
||||
return this.head;
|
||||
}
|
||||
Changeset.copyAText(newAText, this.atext);
|
||||
copyAText(newAText, this.atext);
|
||||
|
||||
const newRev = ++this.head;
|
||||
|
||||
|
@ -126,11 +128,11 @@ class Pad {
|
|||
pad: this,
|
||||
authorId,
|
||||
get author() {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
pad_utils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
return this.authorId;
|
||||
},
|
||||
set author(authorId) {
|
||||
warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
pad_utils.warnDeprecated(`${hook} hook author context is deprecated; use authorId instead`);
|
||||
this.authorId = authorId;
|
||||
},
|
||||
...this.head === 0 ? {} : {
|
||||
|
@ -215,7 +217,7 @@ class Pad {
|
|||
]);
|
||||
const apool = this.apool();
|
||||
let atext = keyAText;
|
||||
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
|
||||
for (const cs of changesets) atext = applyToAText(cs, atext, apool);
|
||||
return atext;
|
||||
}
|
||||
|
||||
|
@ -293,7 +295,7 @@ class Pad {
|
|||
(!ins && start > 0 && orig[start - 1] === '\n');
|
||||
if (!willEndWithNewline) ins += '\n';
|
||||
if (ndel === 0 && ins.length === 0) return;
|
||||
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
|
||||
const changeset = makeSplice(orig, start, ndel, ins);
|
||||
await this.appendRevision(changeset, authorId);
|
||||
}
|
||||
|
||||
|
@ -330,7 +332,7 @@ class Pad {
|
|||
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
||||
* `msgOrText.time` instead.
|
||||
*/
|
||||
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
|
||||
async appendChatMessage(msgOrText: string| ChatMessage, authorId = null, time = null) {
|
||||
const msg =
|
||||
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
||||
this.chatHead++;
|
||||
|
@ -393,7 +395,7 @@ class Pad {
|
|||
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
||||
text = exports.cleanText(context.content);
|
||||
}
|
||||
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
|
||||
const firstChangeset = makeSplice('\n', 0, 0, text);
|
||||
await this.appendRevision(firstChangeset, authorId);
|
||||
}
|
||||
await hooks.aCallAll('padLoad', {pad: this});
|
||||
|
@ -437,11 +439,11 @@ class Pad {
|
|||
// let the plugins know the pad was copied
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
pad_utils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
pad_utils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -520,8 +522,8 @@ class Pad {
|
|||
const oldAText = this.atext;
|
||||
|
||||
// based on Changeset.makeSplice
|
||||
const assem = Changeset.smartOpAssembler();
|
||||
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
|
||||
const assem = new SmartOpAssembler();
|
||||
for (const op of opsFromAText(oldAText)) assem.append(op);
|
||||
assem.endDocument();
|
||||
|
||||
// although we have instantiated the dstPad with '\n', an additional '\n' is
|
||||
|
@ -533,16 +535,16 @@ class Pad {
|
|||
|
||||
// create a changeset that removes the previous text and add the newText with
|
||||
// all atributes present on the source pad
|
||||
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
|
||||
const changeset = pack(oldLength, newLength, assem.toString(), newText);
|
||||
dstPad.appendRevision(changeset, authorId);
|
||||
|
||||
await hooks.aCallAll('padCopy', {
|
||||
get originalPad() {
|
||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
pad_utils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||
return this.srcPad;
|
||||
},
|
||||
get destinationID() {
|
||||
warnDeprecated(
|
||||
pad_utils.warnDeprecated(
|
||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||
return this.dstPad.id;
|
||||
},
|
||||
|
@ -585,12 +587,14 @@ class Pad {
|
|||
p.push(db.remove(`pad2readonly:${padID}`));
|
||||
|
||||
// delete all chat messages
|
||||
p.push(promises.timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
||||
// @ts-ignore
|
||||
p.push(timesLimit(this.chatHead + 1, 500, async (i: string) => {
|
||||
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
|
||||
}));
|
||||
|
||||
// delete all revisions
|
||||
p.push(promises.timesLimit(this.head + 1, 500, async (i: string) => {
|
||||
// @ts-ignore
|
||||
p.push(timesLimit(this.head + 1, 500, async (i: string) => {
|
||||
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
|
||||
}));
|
||||
|
||||
|
@ -603,7 +607,7 @@ class Pad {
|
|||
p.push(padManager.removePad(padID));
|
||||
p.push(hooks.aCallAll('padRemove', {
|
||||
get padID() {
|
||||
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
pad_utils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||
return this.pad.id;
|
||||
},
|
||||
pad: this,
|
||||
|
@ -706,7 +710,7 @@ class Pad {
|
|||
}
|
||||
})
|
||||
.batch(100).buffer(99);
|
||||
let atext = Changeset.makeAText('\n');
|
||||
let atext = makeAText('\n');
|
||||
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
||||
try {
|
||||
assert(authorId != null);
|
||||
|
@ -717,10 +721,10 @@ class Pad {
|
|||
assert(timestamp > 0);
|
||||
assert(changeset != null);
|
||||
assert.equal(typeof changeset, 'string');
|
||||
Changeset.checkRep(changeset);
|
||||
const unpacked = Changeset.unpack(changeset);
|
||||
checkRep(changeset);
|
||||
const unpacked = unpack(changeset);
|
||||
let text = atext.text;
|
||||
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
||||
for (const op of deserializeOps(unpacked.ops)) {
|
||||
if (['=', '-'].includes(op.opcode)) {
|
||||
assert(text.length >= op.chars);
|
||||
const consumed = text.slice(0, op.chars);
|
||||
|
@ -731,7 +735,7 @@ class Pad {
|
|||
}
|
||||
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
|
||||
}
|
||||
atext = Changeset.applyToAText(changeset, atext, pool);
|
||||
atext = applyToAText(changeset, atext, pool);
|
||||
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
||||
} catch (err:any) {
|
||||
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
import {UserSettingsObject} from "../types/UserSettingsObject";
|
||||
|
||||
const authorManager = require('./AuthorManager');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const padManager = require('./PadManager');
|
||||
const readOnlyManager = require('./ReadOnlyManager');
|
||||
const sessionManager = require('./SessionManager');
|
||||
|
@ -30,7 +30,7 @@ const settings = require('../utils/Settings');
|
|||
const webaccess = require('../hooks/express/webaccess');
|
||||
const log4js = require('log4js');
|
||||
const authLogger = log4js.getLogger('auth');
|
||||
const {padutils} = require('../../static/js/pad_utils');
|
||||
import padutils from '../../static/js/pad_utils'
|
||||
|
||||
const DENY = Object.freeze({accessStatus: 'deny'});
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
*/
|
||||
|
||||
const CustomError = require('../utils/customError');
|
||||
const promises = require('../utils/promises');
|
||||
import {firstSatisfies} from '../utils/promises';
|
||||
const randomString = require('../utils/randomstring');
|
||||
const db = require('./DB');
|
||||
const groupManager = require('./GroupManager');
|
||||
|
@ -79,7 +79,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
|||
groupID: string;
|
||||
validUntil: number;
|
||||
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
||||
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
|
||||
const sessionInfo = await firstSatisfies(sessionInfoPromises, isMatch) as any;
|
||||
if (sessionInfo == null) return undefined;
|
||||
return sessionInfo.authorID;
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
const ejs = require('ejs');
|
||||
const fs = require('fs');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const path = require('path');
|
||||
const resolve = require('resolve');
|
||||
const settings = require('../utils/Settings');
|
||||
|
|
|
@ -31,7 +31,7 @@ import os from 'os';
|
|||
const importHtml = require('../utils/ImportHtml');
|
||||
const importEtherpad = require('../utils/ImportEtherpad');
|
||||
import log4js from 'log4js';
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
|
||||
const logger = log4js.getLogger('ImportHandler');
|
||||
|
||||
|
|
|
@ -21,28 +21,30 @@
|
|||
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
const padManager = require('../db/PadManager');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
const ChatMessage = require('../../static/js/ChatMessage');
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import {checkRep, cloneAText, compose, deserializeOps, follow, identity, 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';
|
||||
const AttributeManager = require('../../static/js/AttributeManager');
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
const {padutils} = require('../../static/js/pad_utils');
|
||||
import padutils from '../../static/js/pad_utils';
|
||||
const readOnlyManager = require('../db/ReadOnlyManager');
|
||||
const settings = require('../utils/Settings');
|
||||
const securityManager = require('../db/SecurityManager');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs.js');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
import log4js from 'log4js';
|
||||
const messageLogger = log4js.getLogger('message');
|
||||
const accessLogger = log4js.getLogger('access');
|
||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
||||
const hooks = require('../../static/js/pluginfw/hooks');
|
||||
const stats = require('../stats')
|
||||
const assert = require('assert').strict;
|
||||
import {RateLimiterMemory} from 'rate-limiter-flexible';
|
||||
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
|
||||
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
|
||||
import {ChangeSet} from "../types/ChangeSet";
|
||||
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
|
||||
import {Builder} from "../../static/js/Builder";
|
||||
const webaccess = require('../hooks/express/webaccess');
|
||||
const { checkValidRev } = require('../utils/checkValidRev');
|
||||
|
||||
|
@ -214,7 +216,7 @@ exports.handleDisconnect = async (socket:any) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||
exports.handleMessage = async (socket:any, message: ClientVarMessage) => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
if (env === 'production') {
|
||||
|
@ -348,15 +350,15 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
|||
stats.counter('pendingEdits').inc();
|
||||
await padChannels.enqueue(thisSession.padId, {socket, message});
|
||||
break;
|
||||
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
|
||||
case 'CHAT_MESSAGE': await handleChatMessage(socket, message); break;
|
||||
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message as unknown as UserNewInfoMessage); break;
|
||||
case 'CHAT_MESSAGE': await handleChatMessage(socket, message as unknown as ChatMessageMessage); break;
|
||||
case 'GET_CHAT_MESSAGES': await handleGetChatMessages(socket, message); break;
|
||||
case 'SAVE_REVISION': await handleSaveRevisionMessage(socket, message); break;
|
||||
case 'SAVE_REVISION': await handleSaveRevisionMessage(socket, message as unknown as ClientSaveRevisionMessage); break;
|
||||
case 'CLIENT_MESSAGE': {
|
||||
const {type} = message.data.payload;
|
||||
try {
|
||||
switch (type) {
|
||||
case 'suggestUserName': handleSuggestUserName(socket, message); break;
|
||||
case 'suggestUserName': handleSuggestUserName(socket, message as unknown as ClientSuggestUserName); break;
|
||||
default: throw new Error('unknown message type');
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -384,7 +386,7 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleSaveRevisionMessage = async (socket:any, message: string) => {
|
||||
const handleSaveRevisionMessage = async (socket:any, message: ClientSaveRevisionMessage) => {
|
||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||
const pad = await padManager.getPad(padId, null, authorId);
|
||||
await pad.addSavedRevision(pad.head, authorId);
|
||||
|
@ -397,7 +399,7 @@ const handleSaveRevisionMessage = async (socket:any, message: string) => {
|
|||
* @param msg {Object} the message we're sending
|
||||
* @param sessionID {string} the socketIO session to which we're sending this message
|
||||
*/
|
||||
exports.handleCustomObjectMessage = (msg: typeof ChatMessage, sessionID: string) => {
|
||||
exports.handleCustomObjectMessage = (msg: CustomMessage, sessionID: string) => {
|
||||
if (msg.data.type === 'CUSTOM') {
|
||||
if (sessionID) {
|
||||
// a sessionID is targeted: directly to this sessionID
|
||||
|
@ -432,7 +434,7 @@ exports.handleCustomMessage = (padID: string, msgString:string) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
|
||||
const handleChatMessage = async (socket:any, message: ChatMessageMessage) => {
|
||||
const chatMessage = ChatMessage.fromObject(message.data.message);
|
||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||
// Don't trust the user-supplied values.
|
||||
|
@ -452,7 +454,7 @@ const handleChatMessage = async (socket:any, message: typeof ChatMessage) => {
|
|||
* @param {string} [padId] - The destination pad ID. Deprecated; pass a chat message
|
||||
* object as the first argument and the destination pad ID as the second argument instead.
|
||||
*/
|
||||
exports.sendChatMessageToPadClients = async (mt: typeof ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
|
||||
exports.sendChatMessageToPadClients = async (mt: ChatMessage|number, puId: string, text:string|null = null, padId:string|null = null) => {
|
||||
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
|
||||
padId = mt instanceof ChatMessage ? puId : padId;
|
||||
const pad = await padManager.getPad(padId, null, message.authorId);
|
||||
|
@ -499,7 +501,7 @@ const handleGetChatMessages = async (socket:any, {data: {start, end}}:any) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => {
|
||||
const handleSuggestUserName = (socket:any, message: ClientSuggestUserName) => {
|
||||
const {newName, unnamedId} = message.data.payload;
|
||||
if (newName == null) throw new Error('missing newName');
|
||||
if (unnamedId == null) throw new Error('missing unnamedId');
|
||||
|
@ -519,7 +521,7 @@ const handleSuggestUserName = (socket:any, message: typeof ChatMessage) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId}}}: PadUserInfo) => {
|
||||
const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId}}}: UserNewInfoMessage) => {
|
||||
if (colorId == null) throw new Error('missing colorId');
|
||||
if (!name) name = null;
|
||||
const session = sessioninfos[socket.id];
|
||||
|
@ -567,7 +569,9 @@ const handleUserInfoUpdate = async (socket:any, {data: {userInfo: {name, colorId
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
||||
const handleUserChanges = async (socket:any, message: {
|
||||
data: ClientUserChangesMessage
|
||||
}) => {
|
||||
// This one's no longer pending, as we're gonna process it now
|
||||
stats.counter('pendingEdits').dec();
|
||||
|
||||
|
@ -591,10 +595,10 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
const pad = await padManager.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 -
|
||||
|
@ -613,7 +617,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
// 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;
|
||||
|
@ -626,21 +630,21 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
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);
|
||||
|
@ -655,7 +659,7 @@ const handleUserChanges = async (socket:any, message: typeof ChatMessage) => {
|
|||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
@ -710,7 +714,7 @@ exports.updatePadClients = async (pad: PadType) => {
|
|||
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: {
|
||||
|
@ -738,14 +742,14 @@ exports.updatePadClients = async (pad: PadType) => {
|
|||
/**
|
||||
* Copied from the Etherpad Source Code. Don't know what this method does excatly...
|
||||
*/
|
||||
const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
||||
const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||
const text = atext.text;
|
||||
|
||||
// collect char positions of line markers (e.g. bullets) in new atext
|
||||
// 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: string) => attribs.has(a));
|
||||
if (hasMarker) {
|
||||
|
@ -767,7 +771,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
|||
// create changeset that removes these bad markers
|
||||
offset = 0;
|
||||
|
||||
const builder = Changeset.builder(text.length);
|
||||
const builder = new Builder(text.length);
|
||||
|
||||
badMarkers.forEach((pos) => {
|
||||
builder.keepText(text.substring(offset, pos));
|
||||
|
@ -785,7 +789,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
|||
* @param socket the socket.io Socket object for the client
|
||||
* @param message the message from the client
|
||||
*/
|
||||
const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||
const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
|
||||
const sessionInfo = sessioninfos[socket.id];
|
||||
if (sessionInfo == null) throw new Error('client disconnected');
|
||||
assert(sessionInfo.author);
|
||||
|
@ -793,8 +797,9 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context.
|
||||
|
||||
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
|
||||
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId)) {
|
||||
if (authorColorId && !/^#(?:[0-9A-F]{3}){1,2}$/i.test(authorColorId as string)) {
|
||||
messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`);
|
||||
// @ts-ignore
|
||||
authorColorId = null;
|
||||
}
|
||||
await Promise.all([
|
||||
|
@ -872,7 +877,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
const revisionsNeeded = [];
|
||||
const changesets:MapArrayType<any> = {};
|
||||
|
||||
let startNum = message.client_rev + 1;
|
||||
let startNum = message.client_rev! + 1;
|
||||
let endNum = pad.getHeadRevisionNumber() + 1;
|
||||
|
||||
const headNum = pad.getHeadRevisionNumber();
|
||||
|
@ -901,7 +906,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
|
||||
// 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(),
|
||||
|
@ -926,8 +931,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
|||
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:any) {
|
||||
|
@ -1163,13 +1168,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
|||
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];
|
||||
|
@ -1195,12 +1200,12 @@ const getPadLines = async (pad: PadType, revNum: number) => {
|
|||
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),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1235,7 +1240,7 @@ const composePadChangesets = async (pad: PadType, startNum: number, endNum: numb
|
|||
|
||||
for (r = startNum + 1; r < endNum; r++) {
|
||||
const cs = changesets[r];
|
||||
changeset = Changeset.compose(changeset, cs, pool);
|
||||
changeset = compose(changeset as string, cs as string, pool);
|
||||
}
|
||||
return changeset;
|
||||
} catch (e) {
|
||||
|
|
|
@ -324,7 +324,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
|
|||
|
||||
// serve index.html under /
|
||||
args.app.get('/', (req: any, res: any) => {
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, settings, entrypoint: "/"+fileNameIndex}));
|
||||
res.send(eejs.require('ep_etherpad-lite/templates/index.html', {req, settings, entrypoint: "./"+fileNameIndex}));
|
||||
});
|
||||
|
||||
|
||||
|
@ -342,7 +342,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
|
|||
req,
|
||||
toolbar,
|
||||
isReadOnly,
|
||||
entrypoint: "/"+fileNamePad
|
||||
entrypoint: "../"+fileNamePad
|
||||
})
|
||||
res.send(content);
|
||||
});
|
||||
|
@ -356,7 +356,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
|
|||
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||
req,
|
||||
toolbar,
|
||||
entrypoint: "/"+fileNameTimeSlider
|
||||
entrypoint: "../../"+fileNameTimeSlider
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -4,11 +4,10 @@ import {MapArrayType} from "../../types/MapType";
|
|||
import {PartType} from "../../types/PartType";
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const minify = require('../../utils/Minify');
|
||||
const path = require('path');
|
||||
import {minify} from '../../utils/Minify';
|
||||
import path from 'node:path';
|
||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const settings = require('../../utils/Settings');
|
||||
import CachingMiddleware from '../../utils/caching_middleware';
|
||||
|
||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
||||
const getTar = async () => {
|
||||
|
@ -32,15 +31,10 @@ const getTar = async () => {
|
|||
};
|
||||
|
||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
||||
// Cache both minified and static.
|
||||
const assetCache = new CachingMiddleware();
|
||||
// Cache static assets
|
||||
app.all(/\/js\/(.*)/, assetCache.handle.bind(assetCache));
|
||||
app.all(/\/css\/(.*)/, assetCache.handle.bind(assetCache));
|
||||
|
||||
// Minify will serve static files compressed (minify enabled). It also has
|
||||
// file-specific hacks for ace/require-kernel/etc.
|
||||
app.all('/static/:filename(*)', minify.minify);
|
||||
app.all('/static/:filename(*)', minify);
|
||||
|
||||
// serve plugin definitions
|
||||
// not very static, but served here so that client can do
|
||||
|
|
|
@ -177,6 +177,10 @@ const checkAccess = async (req:any, res:any, next: Function) => {
|
|||
res.status(401).send('Authentication Required');
|
||||
return;
|
||||
}
|
||||
if (ctx.username === '__proto__' || ctx.username === 'constructor' || ctx.username === 'prototype') {
|
||||
res.end(403);
|
||||
return;
|
||||
}
|
||||
settings.users[ctx.username].username = ctx.username;
|
||||
// Make a shallow copy so that the password property can be deleted (to prevent it from
|
||||
// appearing in logs or in the database) without breaking future authentication attempts.
|
||||
|
|
|
@ -7,8 +7,8 @@ const languages = require('languages4translatewiki');
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const _ = require('underscore');
|
||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
|
||||
const existsSync = require('../utils/path_exists');
|
||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs');
|
||||
import existsSync from '../utils/path_exists';
|
||||
const settings = require('../utils/Settings');
|
||||
|
||||
// returns all existing messages merged together and grouped by langcode
|
||||
|
|
|
@ -74,7 +74,7 @@ const express = require('./hooks/express');
|
|||
const hooks = require('../static/js/pluginfw/hooks');
|
||||
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
||||
const plugins = require('../static/js/pluginfw/plugins');
|
||||
const {Gate} = require('./utils/promises');
|
||||
import {Gate} from './utils/promises';
|
||||
const stats = require('./stats')
|
||||
|
||||
const logger = log4js.getLogger('server');
|
||||
|
@ -100,7 +100,7 @@ const removeSignalListener = (signal: NodeJS.Signals, listener: NodeJS.SignalsLi
|
|||
};
|
||||
|
||||
|
||||
let startDoneGate: { resolve: () => void; }
|
||||
let startDoneGate: Gate<unknown>
|
||||
exports.start = async () => {
|
||||
switch (state) {
|
||||
case State.INITIAL:
|
||||
|
@ -181,12 +181,14 @@ exports.start = async () => {
|
|||
} catch (err) {
|
||||
logger.error('Error occurred while starting Etherpad');
|
||||
state = State.STATE_TRANSITION_FAILED;
|
||||
// @ts-ignore
|
||||
startDoneGate.resolve();
|
||||
return await exports.exit(err);
|
||||
}
|
||||
|
||||
logger.info('Etherpad is running');
|
||||
state = State.RUNNING;
|
||||
// @ts-ignore
|
||||
startDoneGate.resolve();
|
||||
|
||||
// Return the HTTP server to make it easier to write tests.
|
||||
|
@ -228,11 +230,13 @@ exports.stop = async () => {
|
|||
} catch (err) {
|
||||
logger.error('Error occurred while stopping Etherpad');
|
||||
state = State.STATE_TRANSITION_FAILED;
|
||||
// @ts-ignore
|
||||
stopDoneGate.resolve();
|
||||
return await exports.exit(err);
|
||||
}
|
||||
logger.info('Etherpad stopped');
|
||||
state = State.STOPPED;
|
||||
// @ts-ignore
|
||||
stopDoneGate.resolve();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {MapArrayType} from "./MapType";
|
||||
import AttributePool from "../../static/js/AttributePool";
|
||||
|
||||
export type PadType = {
|
||||
id: string,
|
||||
apool: ()=>APool,
|
||||
apool: ()=>AttributePool,
|
||||
atext: AText,
|
||||
pool: APool,
|
||||
pool: AttributePool,
|
||||
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
|
||||
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
|
||||
getRevisionAuthor: (rev: number)=>Promise<string>,
|
||||
|
@ -35,6 +36,7 @@ export type APool = {
|
|||
clone: ()=>APool,
|
||||
check: ()=>Promise<void>,
|
||||
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
|
||||
getAttrib: (key: number)=>any,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -19,8 +19,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import AttributePool from "../../static/js/AttributePool";
|
||||
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||
const { checkValidRev } = require('./checkValidRev');
|
||||
|
||||
/*
|
||||
|
@ -30,7 +31,7 @@ exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any;
|
|||
const _analyzeLine = exports._analyzeLine;
|
||||
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(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 = [];
|
||||
|
@ -51,14 +52,14 @@ type LineModel = {
|
|||
[id:string]:string|number|LineModel
|
||||
}
|
||||
|
||||
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
||||
exports._analyzeLine = (text:string, aline: string, apool: AttributePool) => {
|
||||
const line: LineModel = {};
|
||||
|
||||
// identify list
|
||||
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');
|
||||
|
@ -78,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
|||
}
|
||||
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;
|
||||
|
|
|
@ -18,7 +18,7 @@ import {MapArrayType} from "../types/MapType";
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const padManager = require('../db/PadManager');
|
||||
const _ = require('underscore');
|
||||
|
@ -27,7 +27,9 @@ const hooks = require('../../static/js/pluginfw/hooks');
|
|||
const eejs = require('../eejs');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
||||
const padutils = require('../../static/js/pad_utils').padutils;
|
||||
import padutils from "../../static/js/pad_utils";
|
||||
import {StringIterator} from "../../static/js/StringIterator";
|
||||
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||
|
||||
const getPadHTML = async (pad: PadType, revNum: string) => {
|
||||
let atext = pad.atext;
|
||||
|
@ -44,7 +46,7 @@ const getPadHTML = async (pad: PadType, revNum: string) => {
|
|||
const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string[]) => {
|
||||
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'];
|
||||
|
@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
css += '<style>\n';
|
||||
|
||||
for (const a of Object.keys(apool.numToAttrib)) {
|
||||
// @ts-ignore
|
||||
const attr = apool.numToAttrib[a];
|
||||
|
||||
// skip non author attributes
|
||||
|
@ -115,6 +118,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
// see hook exportHtmlAdditionalTagsWithData
|
||||
attrib = propName;
|
||||
}
|
||||
// @ts-ignore
|
||||
const propTrueNum = apool.putAttrib(attrib, true);
|
||||
if (propTrueNum >= 0) {
|
||||
anumMap[propTrueNum] = i;
|
||||
|
@ -127,8 +131,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||
// becomes
|
||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||
const taker = Changeset.stringIterator(text);
|
||||
const assem = Changeset.stringAssembler();
|
||||
const taker = new StringIterator(text);
|
||||
const assem = new StringAssembler();
|
||||
const openTags:string[] = [];
|
||||
|
||||
const getSpanClassFor = (i: string) => {
|
||||
|
@ -204,7 +208,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
|||
return;
|
||||
}
|
||||
|
||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
||||
// @ts-ignore
|
||||
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
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
import {AText, PadType} from "../types/PadType";
|
||||
import {MapType} from "../types/MapType";
|
||||
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||
import {StringIterator} from "../../static/js/StringIterator";
|
||||
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const padManager = require('../db/PadManager');
|
||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||
|
@ -45,13 +47,14 @@ const getPadTXT = async (pad: PadType, revNum: string) => {
|
|||
const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||
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 props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||
const anumMap: MapType = {};
|
||||
const css = '';
|
||||
|
||||
props.forEach((propName, i) => {
|
||||
// @ts-ignore
|
||||
const propTrueNum = apool.putAttrib([propName, true], true);
|
||||
if (propTrueNum >= 0) {
|
||||
anumMap[propTrueNum] = i;
|
||||
|
@ -69,8 +72,8 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
|||
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||
// becomes
|
||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||
const taker = Changeset.stringIterator(text);
|
||||
const assem = Changeset.stringAssembler();
|
||||
const taker = new StringIterator(text);
|
||||
const assem = new StringAssembler();
|
||||
|
||||
let idx = 0;
|
||||
|
||||
|
@ -79,7 +82,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
||||
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||
idx += numChars;
|
||||
|
||||
for (const o of ops) {
|
||||
|
|
|
@ -18,7 +18,7 @@ import {APool} from "../types/PadType";
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributePool = require('../../static/js/AttributePool');
|
||||
import AttributePool from '../../static/js/AttributePool';
|
||||
const {Pad} = require('../db/Pad');
|
||||
const Stream = require('./Stream');
|
||||
const authorManager = require('../db/AuthorManager');
|
||||
|
@ -61,7 +61,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
|||
try {
|
||||
const processRecord = async (key:string, value: null|{
|
||||
padIDs: string|Record<string, unknown>,
|
||||
pool: APool
|
||||
pool: AttributePool
|
||||
}) => {
|
||||
if (!value) return;
|
||||
const keyParts = key.split(':');
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
*/
|
||||
|
||||
import log4js from 'log4js';
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import {deserializeOps} from '../../static/js/Changeset';
|
||||
const contentcollector = require('../../static/js/contentcollector');
|
||||
import jsdom from 'jsdom';
|
||||
import {PadType} from "../types/PadType";
|
||||
import {Builder} from "../../static/js/Builder";
|
||||
|
||||
const apiLogger = log4js.getLogger('ImportHtml');
|
||||
let processor:any;
|
||||
|
@ -69,13 +70,13 @@ exports.setPadHTML = async (pad: PadType, html:string, 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 builder = new 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);
|
||||
|
|
|
@ -21,20 +21,20 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const settings = require('./Settings');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
const mime = require('mime-types');
|
||||
const Threads = require('threads');
|
||||
const log4js = require('log4js');
|
||||
const sanitizePathname = require('./sanitizePathname');
|
||||
import {TransformResult} from "esbuild";
|
||||
import mime from 'mime-types';
|
||||
import log4js from 'log4js';
|
||||
import {compressCSS, compressJS} from './MinifyWorker'
|
||||
|
||||
const settings = require('./Settings');
|
||||
import {promises as fs} from 'fs';
|
||||
import path from 'node:path';
|
||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
||||
import sanitizePathname from './sanitizePathname';
|
||||
const logger = log4js.getLogger('Minify');
|
||||
|
||||
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
||||
|
||||
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
||||
|
||||
const LIBRARY_WHITELIST = [
|
||||
'async',
|
||||
|
@ -48,10 +48,10 @@ const LIBRARY_WHITELIST = [
|
|||
|
||||
// What follows is a terrible hack to avoid loop-back within the server.
|
||||
// TODO: Serve files from another service, or directly from the file system.
|
||||
const requestURI = async (url, method, headers) => {
|
||||
const requestURI = async (url: string | URL, method: any, headers: { [x: string]: any; }) => {
|
||||
const parsedUrl = new URL(url);
|
||||
let status = 500;
|
||||
const content = [];
|
||||
const content: any[] = [];
|
||||
const mockRequest = {
|
||||
url,
|
||||
method,
|
||||
|
@ -61,7 +61,7 @@ const requestURI = async (url, method, headers) => {
|
|||
let mockResponse;
|
||||
const p = new Promise((resolve) => {
|
||||
mockResponse = {
|
||||
writeHead: (_status, _headers) => {
|
||||
writeHead: (_status: number, _headers: { [x: string]: any; }) => {
|
||||
status = _status;
|
||||
for (const header in _headers) {
|
||||
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
|
||||
|
@ -69,37 +69,63 @@ const requestURI = async (url, method, headers) => {
|
|||
}
|
||||
}
|
||||
},
|
||||
setHeader: (header, value) => {
|
||||
setHeader: (header: string, value: { toString: () => any; }) => {
|
||||
headers[header.toLowerCase()] = value.toString();
|
||||
},
|
||||
header: (header, value) => {
|
||||
header: (header: string, value: { toString: () => any; }) => {
|
||||
headers[header.toLowerCase()] = value.toString();
|
||||
},
|
||||
write: (_content) => {
|
||||
write: (_content: any) => {
|
||||
_content && content.push(_content);
|
||||
},
|
||||
end: (_content) => {
|
||||
end: (_content: any) => {
|
||||
_content && content.push(_content);
|
||||
resolve([status, headers, content.join('')]);
|
||||
},
|
||||
};
|
||||
});
|
||||
await minify(mockRequest, mockResponse);
|
||||
await _minify(mockRequest, mockResponse);
|
||||
return await p;
|
||||
};
|
||||
|
||||
const requestURIs = (locations, method, headers, callback) => {
|
||||
const _requestURIs = (locations: any[], method: any, headers: {
|
||||
[x: string]:
|
||||
/**
|
||||
* This Module manages all /minified/* requests. It controls the
|
||||
* minification && compression of Javascript and CSS.
|
||||
*/
|
||||
/*
|
||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
any;
|
||||
}, callback: (arg0: any[], arg1: any[], arg2: any[]) => void) => {
|
||||
Promise.all(locations.map(async (loc) => {
|
||||
try {
|
||||
return await requestURI(loc, method, headers);
|
||||
} catch (err) {
|
||||
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
|
||||
// @ts-ignore
|
||||
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
|
||||
return [500, headers, ''];
|
||||
}
|
||||
})).then((responses) => {
|
||||
// @ts-ignore
|
||||
const statuss = responses.map((x) => x[0]);
|
||||
// @ts-ignore
|
||||
const headerss = responses.map((x) => x[1]);
|
||||
// @ts-ignore
|
||||
const contentss = responses.map((x) => x[2]);
|
||||
callback(statuss, headerss, contentss);
|
||||
});
|
||||
|
@ -119,11 +145,12 @@ const compatPaths = {
|
|||
* @param req the Express request
|
||||
* @param res the Express response
|
||||
*/
|
||||
const minify = async (req, res) => {
|
||||
const _minify = async (req:any, res:any) => {
|
||||
let filename = req.params.filename;
|
||||
try {
|
||||
filename = sanitizePathname(filename);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
|
||||
res.writeHead(404, {});
|
||||
res.end();
|
||||
|
@ -131,6 +158,7 @@ const minify = async (req, res) => {
|
|||
}
|
||||
|
||||
// Backward compatibility for plugins that require() files from old paths.
|
||||
// @ts-ignore
|
||||
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
|
||||
if (newLocation != null) {
|
||||
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
|
||||
|
@ -193,7 +221,7 @@ const minify = async (req, res) => {
|
|||
res.writeHead(200, {});
|
||||
res.end();
|
||||
} else if (req.method === 'GET') {
|
||||
const content = await getFileCompressed(filename, contentType);
|
||||
const content = await getFileCompressed(filename, contentType as string);
|
||||
res.header('Content-Type', contentType);
|
||||
res.writeHead(200, {});
|
||||
res.write(content);
|
||||
|
@ -205,7 +233,7 @@ const minify = async (req, res) => {
|
|||
};
|
||||
|
||||
// Check for the existance of the file and get the last modification date.
|
||||
const statFile = async (filename, dirStatLimit) => {
|
||||
const statFile = async (filename: string, dirStatLimit: number):Promise<(any | boolean)[]> => {
|
||||
/*
|
||||
* The only external call to this function provides an explicit value for
|
||||
* dirStatLimit: this check could be removed.
|
||||
|
@ -221,6 +249,7 @@ const statFile = async (filename, dirStatLimit) => {
|
|||
try {
|
||||
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
|
||||
// Stat the directory instead.
|
||||
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
|
||||
|
@ -234,26 +263,25 @@ const statFile = async (filename, dirStatLimit) => {
|
|||
|
||||
let contentCache = new Map();
|
||||
|
||||
const getFileCompressed = async (filename, contentType) => {
|
||||
const getFileCompressed = async (filename: any, contentType: string) => {
|
||||
if (contentCache.has(filename)) {
|
||||
return contentCache.get(filename);
|
||||
}
|
||||
let content = await getFile(filename);
|
||||
let content: Buffer|string = await getFile(filename);
|
||||
if (!content || !settings.minify) {
|
||||
return content;
|
||||
} else if (contentType === 'application/javascript') {
|
||||
return await new Promise((resolve) => {
|
||||
threadsPool.queue(async ({compressJS}) => {
|
||||
return await new Promise(async (resolve) => {
|
||||
try {
|
||||
logger.info('Compress JS file %s.', filename);
|
||||
|
||||
content = content.toString();
|
||||
const compressResult = await compressJS(content);
|
||||
|
||||
if (compressResult.error) {
|
||||
console.error(`Error compressing JS (${filename}) using terser`, compressResult.error);
|
||||
} else {
|
||||
try {
|
||||
let compressResult: TransformResult<{ minify: boolean }>
|
||||
compressResult = await compressJS(content);
|
||||
content = compressResult.code.toString(); // Convert content obj code to string
|
||||
} catch (error) {
|
||||
console.error(`Error compressing JS (${filename}) using esbuild`, error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('getFile() returned an error in ' +
|
||||
|
@ -262,41 +290,37 @@ const getFileCompressed = async (filename, contentType) => {
|
|||
contentCache.set(filename, content);
|
||||
resolve(content);
|
||||
});
|
||||
});
|
||||
} else if (contentType === 'text/css') {
|
||||
return await new Promise((resolve) => {
|
||||
threadsPool.queue(async ({compressCSS}) => {
|
||||
return await new Promise(async (resolve) => {
|
||||
try {
|
||||
logger.info('Compress CSS file %s.', filename);
|
||||
|
||||
const compressResult = await compressCSS(path.resolve(ROOT_DIR,filename));
|
||||
|
||||
if (compressResult.error) {
|
||||
console.error(`Error compressing CSS (${filename}) using terser`, compressResult.error);
|
||||
} else {
|
||||
content = compressResult
|
||||
}
|
||||
try {
|
||||
content = await compressCSS(path.resolve(ROOT_DIR, filename));
|
||||
} catch (error) {
|
||||
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
||||
}
|
||||
contentCache.set(filename, content);
|
||||
resolve(content);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('getFile() returned an error in ' +
|
||||
`getFileCompressed(${filename}, ${contentType}): ${e}`);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
contentCache.set(filename, content);
|
||||
return content;
|
||||
}
|
||||
};
|
||||
|
||||
const getFile = async (filename) => {
|
||||
const getFile = async (filename: any) => {
|
||||
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
||||
};
|
||||
|
||||
exports.minify = (req, res, next) => minify(req, res).catch((err) => next(err || new Error(err)));
|
||||
export const minify = (req:any, res:any, next:Function) => _minify(req, res).catch((err) => next(err || new Error(err)));
|
||||
|
||||
exports.requestURIs = requestURIs;
|
||||
export const requestURIs = _requestURIs;
|
||||
|
||||
exports.shutdown = async (hookName, context) => {
|
||||
await threadsPool.terminate();
|
||||
export const shutdown = async (hookName: string, context:any) => {
|
||||
contentCache = new Map();
|
||||
};
|
|
@ -3,14 +3,13 @@
|
|||
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
||||
*/
|
||||
|
||||
import {expose} from 'threads'
|
||||
import {build, transform} from 'esbuild';
|
||||
|
||||
/*
|
||||
* Minify JS content
|
||||
* @param {string} content - JS content to minify
|
||||
*/
|
||||
const compressJS = async (content) => {
|
||||
export const compressJS = async (content: string) => {
|
||||
return await transform(content, {minify: true});
|
||||
}
|
||||
|
||||
|
@ -19,7 +18,7 @@ const compressJS = async (content) => {
|
|||
* @param {string} filename - name of the file
|
||||
* @param {string} ROOT_DIR - the root dir of Etherpad
|
||||
*/
|
||||
const compressCSS = async (content) => {
|
||||
export const compressCSS = async (content: string) => {
|
||||
const transformedCSS = await build(
|
||||
{
|
||||
entryPoints: [content],
|
||||
|
@ -41,8 +40,3 @@ const compressCSS = async (content) => {
|
|||
)
|
||||
return transformedCSS.outputFiles[0].text
|
||||
};
|
||||
|
||||
expose({
|
||||
compressJS: compressJS,
|
||||
compressCSS,
|
||||
});
|
|
@ -169,11 +169,11 @@ exports.authenticationMethod = 'sso'
|
|||
/*
|
||||
* The Type of the database
|
||||
*/
|
||||
exports.dbType = 'dirty';
|
||||
exports.dbType = 'rustydb';
|
||||
/**
|
||||
* This setting is passed with dbType to ueberDB to set up the database
|
||||
*/
|
||||
exports.dbSettings = {filename: path.join(exports.root, 'var/dirty.db')};
|
||||
exports.dbSettings = {filename: path.join(exports.root, 'var/rusty.db')};
|
||||
|
||||
/**
|
||||
* The default Text of a new pad
|
||||
|
@ -837,7 +837,7 @@ exports.reloadSettings = () => {
|
|||
exports.skinName = 'colibris';
|
||||
}
|
||||
|
||||
if (!exports.socketTransportProtocols.includes("websocket") || exports.socketTransportProtocols.includes("polling")) {
|
||||
if (!exports.socketTransportProtocols.includes("websocket") || !exports.socketTransportProtocols.includes("polling")) {
|
||||
logger.warn("Invalid socketTransportProtocols setting. Please check out settings.json.template and update your settings.json. Falling back to the default ['websocket', 'polling'].");
|
||||
exports.socketTransportProtocols = ['websocket', 'polling'];
|
||||
}
|
||||
|
@ -941,6 +941,11 @@ exports.reloadSettings = () => {
|
|||
logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`);
|
||||
}
|
||||
|
||||
if (exports.dbType === 'rustydb') {
|
||||
exports.dbSettings.filename = absolutePaths.makeAbsolute(exports.dbSettings.filename);
|
||||
logger.warn(`File location: ${exports.dbSettings.filename}`);
|
||||
}
|
||||
|
||||
if (exports.ip === '') {
|
||||
// using Unix socket for connectivity
|
||||
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
|
||||
|
|
|
@ -1,211 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/*
|
||||
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS-IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Buffer} from 'node:buffer'
|
||||
import fs from 'fs';
|
||||
const fsp = fs.promises;
|
||||
import path from 'path';
|
||||
import zlib from 'zlib';
|
||||
const settings = require('./Settings');
|
||||
const existsSync = require('./path_exists');
|
||||
import util from 'util';
|
||||
|
||||
/*
|
||||
* The crypto module can be absent on reduced node installations.
|
||||
*
|
||||
* Here we copy the approach TypeScript guys used for https://github.com/microsoft/TypeScript/issues/19100
|
||||
* If importing crypto fails at runtime, we replace sha256 with djb2, which is
|
||||
* weaker, but works for our case.
|
||||
*
|
||||
* djb2 was written in 1991 by Daniel J. Bernstein.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
import _crypto from 'crypto';
|
||||
|
||||
|
||||
let CACHE_DIR: string|undefined = path.join(settings.root, 'var/');
|
||||
CACHE_DIR = existsSync(CACHE_DIR) ? CACHE_DIR : undefined;
|
||||
|
||||
type Headers = {
|
||||
[id: string]: string
|
||||
}
|
||||
|
||||
type ResponseCache = {
|
||||
[id: string]: {
|
||||
statusCode: number
|
||||
headers: Headers
|
||||
}
|
||||
}
|
||||
|
||||
const responseCache: ResponseCache = {};
|
||||
|
||||
const djb2Hash = (data: string) => {
|
||||
const chars = data.split('').map((str) => str.charCodeAt(0));
|
||||
return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`;
|
||||
};
|
||||
|
||||
const generateCacheKeyWithSha256 =
|
||||
(path: string) => _crypto.createHash('sha256').update(path).digest('hex');
|
||||
|
||||
const generateCacheKeyWithDjb2 =
|
||||
(path: string) => Buffer.from(djb2Hash(path)).toString('hex');
|
||||
|
||||
let generateCacheKey: (path: string)=>string;
|
||||
|
||||
if (_crypto) {
|
||||
generateCacheKey = generateCacheKeyWithSha256;
|
||||
} else {
|
||||
generateCacheKey = generateCacheKeyWithDjb2;
|
||||
console.warn('No crypto support in this nodejs runtime. Djb2 (weaker) will be used.');
|
||||
}
|
||||
|
||||
// MIMIC https://github.com/microsoft/TypeScript/commit/9677b0641cc5ba7d8b701b4f892ed7e54ceaee9a - END
|
||||
|
||||
/*
|
||||
This caches and compresses 200 and 404 responses to GET and HEAD requests.
|
||||
TODO: Caching and compressing are solved problems, a middleware configuration
|
||||
should replace this.
|
||||
*/
|
||||
|
||||
export default class CachingMiddleware {
|
||||
handle(req: any, res: any, next: any) {
|
||||
this._handle(req, res, next).catch((err) => next(err || new Error(err)));
|
||||
}
|
||||
|
||||
async _handle(req: any, res: any, next: any) {
|
||||
if (!(req.method === 'GET' || req.method === 'HEAD') || !CACHE_DIR) {
|
||||
return next(undefined, req, res);
|
||||
}
|
||||
|
||||
const oldReq:ResponseCache = {};
|
||||
const oldRes:ResponseCache = {};
|
||||
|
||||
const supportsGzip =
|
||||
(req.get('Accept-Encoding') || '').indexOf('gzip') !== -1;
|
||||
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const cacheKey = generateCacheKey(url.pathname + url.search);
|
||||
|
||||
const stats = await fsp.stat(`${CACHE_DIR}minified_${cacheKey}`).catch(() => {});
|
||||
const modifiedSince =
|
||||
req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']);
|
||||
if (stats != null && stats.mtime && responseCache[cacheKey]) {
|
||||
req.headers['if-modified-since'] = stats.mtime.toUTCString();
|
||||
} else {
|
||||
delete req.headers['if-modified-since'];
|
||||
}
|
||||
|
||||
// Always issue get to downstream.
|
||||
oldReq.method = req.method;
|
||||
req.method = 'GET';
|
||||
|
||||
// This handles read/write synchronization as well as its predecessor,
|
||||
// which is to say, not at all.
|
||||
// TODO: Implement locking on write or ditch caching of gzip and use
|
||||
// existing middlewares.
|
||||
const respond = () => {
|
||||
req.method = oldReq.method || req.method;
|
||||
res.write = oldRes.write || res.write;
|
||||
res.end = oldRes.end || res.end;
|
||||
|
||||
const headers: Headers = {};
|
||||
Object.assign(headers, (responseCache[cacheKey].headers || {}));
|
||||
const statusCode = responseCache[cacheKey].statusCode;
|
||||
|
||||
let pathStr = `${CACHE_DIR}minified_${cacheKey}`;
|
||||
if (supportsGzip && /application\/javascript/.test(headers['content-type'])) {
|
||||
pathStr += '.gz';
|
||||
headers['content-encoding'] = 'gzip';
|
||||
}
|
||||
|
||||
const lastModified = headers['last-modified'] && new Date(headers['last-modified']);
|
||||
|
||||
if (statusCode === 200 && lastModified <= modifiedSince) {
|
||||
res.writeHead(304, headers);
|
||||
res.end();
|
||||
} else if (req.method === 'GET') {
|
||||
const readStream = fs.createReadStream(pathStr);
|
||||
res.writeHead(statusCode, headers);
|
||||
readStream.pipe(res);
|
||||
} else {
|
||||
res.writeHead(statusCode, headers);
|
||||
res.end();
|
||||
}
|
||||
};
|
||||
|
||||
const expirationDate = new Date(((responseCache[cacheKey] || {}).headers || {}).expires);
|
||||
if (expirationDate > new Date()) {
|
||||
// Our cached version is still valid.
|
||||
return respond();
|
||||
}
|
||||
|
||||
const _headers:Headers = {};
|
||||
oldRes.setHeader = res.setHeader;
|
||||
res.setHeader = (key: string, value: string) => {
|
||||
// Don't set cookies, see issue #707
|
||||
if (key.toLowerCase() === 'set-cookie') return;
|
||||
|
||||
_headers[key.toLowerCase()] = value;
|
||||
// @ts-ignore
|
||||
oldRes.setHeader.call(res, key, value);
|
||||
};
|
||||
|
||||
oldRes.writeHead = res.writeHead;
|
||||
res.writeHead = (status: number, headers: Headers) => {
|
||||
res.writeHead = oldRes.writeHead;
|
||||
if (status === 200) {
|
||||
// Update cache
|
||||
let buffer = '';
|
||||
|
||||
Object.keys(headers || {}).forEach((key) => {
|
||||
res.setHeader(key, headers[key]);
|
||||
});
|
||||
headers = _headers;
|
||||
|
||||
oldRes.write = res.write;
|
||||
oldRes.end = res.end;
|
||||
res.write = (data: number, encoding: number) => {
|
||||
buffer += data.toString(encoding);
|
||||
};
|
||||
res.end = async (data: number, encoding: number) => {
|
||||
await Promise.all([
|
||||
fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}`, buffer).catch(() => {}),
|
||||
util.promisify(zlib.gzip)(buffer)
|
||||
// @ts-ignore
|
||||
.then((content: string) => fsp.writeFile(`${CACHE_DIR}minified_${cacheKey}.gz`, content))
|
||||
.catch(() => {}),
|
||||
]);
|
||||
responseCache[cacheKey] = {statusCode: status, headers};
|
||||
respond();
|
||||
};
|
||||
} else if (status === 304) {
|
||||
// Nothing new changed from the cached version.
|
||||
oldRes.write = res.write;
|
||||
oldRes.end = res.end;
|
||||
res.write = (data: number, encoding: number) => {};
|
||||
res.end = (data: number, encoding: number) => { respond(); };
|
||||
} else {
|
||||
res.writeHead(status, headers);
|
||||
}
|
||||
};
|
||||
|
||||
next(undefined, req, res);
|
||||
}
|
||||
};
|
|
@ -3,8 +3,13 @@
|
|||
import {PadAuthor, PadType} from "../types/PadType";
|
||||
import {MapArrayType} from "../types/MapType";
|
||||
|
||||
const AttributeMap = require('../../static/js/AttributeMap');
|
||||
const Changeset = require('../../static/js/Changeset');
|
||||
import AttributeMap from '../../static/js/AttributeMap';
|
||||
import {applyToAText, checkRep, compose, deserializeOps, pack, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
|
||||
import {Builder} from "../../static/js/Builder";
|
||||
import {OpAssembler} from "../../static/js/OpAssembler";
|
||||
import {numToString} from "../../static/js/ChangesetUtils";
|
||||
import Op from "../../static/js/Op";
|
||||
import {StringAssembler} from "../../static/js/StringAssembler";
|
||||
const attributes = require('../../static/js/attributes');
|
||||
const exportHtml = require('./ExportHtml');
|
||||
|
||||
|
@ -33,7 +38,7 @@ class PadDiff {
|
|||
}
|
||||
_isClearAuthorship(changeset: any){
|
||||
// unpack
|
||||
const unpacked = Changeset.unpack(changeset);
|
||||
const unpacked = unpack(changeset);
|
||||
|
||||
// check if there is nothing in the charBank
|
||||
if (unpacked.charBank !== '') {
|
||||
|
@ -45,7 +50,7 @@ class PadDiff {
|
|||
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;
|
||||
|
@ -78,7 +83,7 @@ class PadDiff {
|
|||
const atext = await this._pad.getInternalRevisionAText(rev);
|
||||
|
||||
// build clearAuthorship changeset
|
||||
const builder = Changeset.builder(atext.text.length);
|
||||
const builder = new Builder(atext.text.length);
|
||||
builder.keepText(atext.text, [['author', '']], this._pad.pool);
|
||||
const changeset = builder.toString();
|
||||
|
||||
|
@ -93,7 +98,7 @@ class PadDiff {
|
|||
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;
|
||||
}
|
||||
|
@ -157,7 +162,7 @@ class PadDiff {
|
|||
if (superChangeset == null) {
|
||||
superChangeset = changeset;
|
||||
} else {
|
||||
superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool);
|
||||
superChangeset = compose(superChangeset, changeset, this._pad.pool);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,10 +176,10 @@ class PadDiff {
|
|||
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;
|
||||
|
@ -209,22 +214,22 @@ class PadDiff {
|
|||
|
||||
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
|
||||
// unpack
|
||||
const unpacked = Changeset.unpack(changeset);
|
||||
const unpacked = unpack(changeset);
|
||||
|
||||
const assem = Changeset.opAssembler();
|
||||
const assem = new 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
|
||||
|
@ -232,26 +237,31 @@ class PadDiff {
|
|||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
_createDeletionChangeset(cs: any, startAText: any, apool: any){
|
||||
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.
|
||||
// They include final newlines on lines.
|
||||
|
||||
const linesGet = (idx: number) => {
|
||||
// @ts-ignore
|
||||
if (lines.get) {
|
||||
// @ts-ignore
|
||||
return lines.get(idx);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return lines[idx];
|
||||
}
|
||||
};
|
||||
|
||||
const aLinesGet = (idx: number) => {
|
||||
// @ts-ignore
|
||||
if (alines.get) {
|
||||
// @ts-ignore
|
||||
return alines.get(idx);
|
||||
} else {
|
||||
return alines[idx];
|
||||
|
@ -263,14 +273,14 @@ class PadDiff {
|
|||
let curLineOps: { next: () => any; } | null = null;
|
||||
let curLineOpsNext: { done: any; value: any; } | null = null;
|
||||
let curLineOpsLine: number;
|
||||
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 builder = new Builder(unpacked.newLen);
|
||||
|
||||
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
|
||||
if (!curLineOps || curLineOpsLine !== curLine) {
|
||||
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
|
||||
curLineOps = deserializeOps(aLinesGet(curLine));
|
||||
curLineOpsNext = curLineOps!.next();
|
||||
curLineOpsLine = curLine;
|
||||
let indexIntoLine = 0;
|
||||
|
@ -291,13 +301,13 @@ class PadDiff {
|
|||
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();
|
||||
|
@ -332,7 +342,7 @@ class PadDiff {
|
|||
|
||||
const nextText = (numChars: number) => {
|
||||
let len = 0;
|
||||
const assem = Changeset.stringAssembler();
|
||||
const assem = new StringAssembler();
|
||||
const firstString = linesGet(curLine).substring(curChar);
|
||||
len += firstString.length;
|
||||
assem.append(firstString);
|
||||
|
@ -360,7 +370,7 @@ class PadDiff {
|
|||
};
|
||||
};
|
||||
|
||||
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
|
||||
for (const csOp of deserializeOps(unpacked.ops)) {
|
||||
if (csOp.opcode === '=') {
|
||||
const textBank = nextText(csOp.chars);
|
||||
|
||||
|
@ -442,7 +452,7 @@ class PadDiff {
|
|||
}
|
||||
}
|
||||
|
||||
return Changeset.checkRep(builder.toString());
|
||||
return checkRep(builder.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -450,6 +460,7 @@ class PadDiff {
|
|||
|
||||
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
|
||||
// it adds deletions and attribute changes to the atext.
|
||||
// @ts-ignore
|
||||
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
||||
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'use strict';
|
||||
const fs = require('fs');
|
||||
import fs from 'node:fs';
|
||||
|
||||
const check = (path:string) => {
|
||||
const existsSync = fs.statSync || fs.existsSync;
|
||||
|
@ -13,4 +13,4 @@ const check = (path:string) => {
|
|||
return result;
|
||||
};
|
||||
|
||||
module.exports = check;
|
||||
export default check;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// `predicate`. Resolves to `undefined` if none of the Promises satisfy `predicate`, or if
|
||||
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
|
||||
// the predicate.
|
||||
exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
|
||||
export const firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
|
||||
if (predicate == null) {
|
||||
predicate = (x: any) => x;
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) =
|
|||
// `total` is greater than `concurrency`, then `concurrency` Promises will be created right away,
|
||||
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
|
||||
// function resolves once all `total` Promises have resolved.
|
||||
exports.timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
|
||||
export const timesLimit = async (total: number, concurrency: number, promiseCreator: Function) => {
|
||||
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
|
||||
let next = 0;
|
||||
const addAnother = () => promiseCreator(next++).finally(() => {
|
||||
|
@ -61,7 +61,7 @@ exports.timesLimit = async (total: number, concurrency: number, promiseCreator:
|
|||
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
|
||||
* properties.
|
||||
*/
|
||||
class Gate<T> extends Promise<T> {
|
||||
export class Gate<T> extends Promise<T> {
|
||||
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
|
||||
// https://stackoverflow.com/a/65669070 for the rationale.
|
||||
static get [Symbol.species]() { return Promise; }
|
||||
|
@ -75,4 +75,3 @@ class Gate<T> extends Promise<T> {
|
|||
Object.assign(this, props);
|
||||
}
|
||||
}
|
||||
exports.Gate = Gate;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
import path from 'path';
|
||||
|
||||
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
||||
// https://nvd.nist.gov/vuln/detail/CVE-2015-3297 for additional context.
|
||||
module.exports = (p: string, pathApi = path) => {
|
||||
const sanitizeRoot = (p: string, pathApi = path) => {
|
||||
// The documentation for path.normalize() says that it resolves '..' and '.' segments. The word
|
||||
// "resolve" implies that it examines the filesystem to resolve symbolic links, so 'a/../b' might
|
||||
// not be the same thing as 'b'. Most path normalization functions from other libraries (e.g.,
|
||||
|
@ -21,3 +19,5 @@ module.exports = (p: string, pathApi = path) => {
|
|||
if (pathApi.sep === '\\') p = p.replace(/\\/g, '/');
|
||||
return p;
|
||||
};
|
||||
|
||||
export default sanitizeRoot
|
||||
|
|
|
@ -30,23 +30,23 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@etherpad/express-session": "^1.18.2",
|
||||
"async": "^3.2.5",
|
||||
"axios": "^1.7.3",
|
||||
"@etherpad/express-session": "^1.18.4",
|
||||
"async": "^3.2.6",
|
||||
"axios": "^1.7.7",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cross-env": "^7.0.3",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"ejs": "^3.1.10",
|
||||
"esbuild": "^0.23.0",
|
||||
"esbuild": "^0.23.1",
|
||||
"express": "4.19.2",
|
||||
"express-rate-limit": "^7.4.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"find-root": "1.1.0",
|
||||
"formidable": "^3.5.1",
|
||||
"http-errors": "^2.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"jose": "^5.8.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsdom": "^24.1.1",
|
||||
"jsdom": "^25.0.0",
|
||||
"jsonminify": "0.4.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"languages4translatewiki": "0.1.3",
|
||||
|
@ -67,11 +67,10 @@
|
|||
"semver": "^7.6.3",
|
||||
"socket.io": "^4.7.5",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"superagent": "9.0.2",
|
||||
"threads": "^1.7.0",
|
||||
"superagent": "10.1.0",
|
||||
"tinycon": "0.6.8",
|
||||
"tsx": "4.17.0",
|
||||
"ueberdb2": "^4.2.92",
|
||||
"tsx": "4.19.0",
|
||||
"ueberdb2": "^4.2.100",
|
||||
"underscore": "1.13.7",
|
||||
"unorm": "1.6.0",
|
||||
"wtfnode": "^0.9.3"
|
||||
|
@ -81,26 +80,28 @@
|
|||
"etherpad-lite": "node/server.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.46.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@types/async": "^3.2.24",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/http-errors": "^2.0.4",
|
||||
"@types/jquery": "^3.5.30",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/oidc-provider": "^8.5.2",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/underscore": "^1.11.15",
|
||||
"chokidar": "^3.6.0",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-etherpad": "^4.0.4",
|
||||
"etherpad-cli-client": "^3.0.2",
|
||||
"mocha": "^10.7.0",
|
||||
"mocha": "^10.7.3",
|
||||
"mocha-froth": "^0.2.10",
|
||||
"nodeify": "^1.0.1",
|
||||
"openapi-schema-validation": "^0.4.2",
|
||||
|
@ -108,7 +109,9 @@
|
|||
"sinon": "^18.0.0",
|
||||
"split-grid": "^1.0.11",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.5.4",
|
||||
"vitest": "^2.0.5",
|
||||
"rusty-store-kv": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.2",
|
||||
|
@ -132,8 +135,9 @@
|
|||
"test-ui:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs --ui",
|
||||
"test-admin": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --workers 1",
|
||||
"test-admin:ui": "cross-env NODE_ENV=production npx playwright test tests/frontend-new/admin-spec --ui --workers 1",
|
||||
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts"
|
||||
"debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts",
|
||||
"test:vitest": "vitest"
|
||||
},
|
||||
"version": "2.2.2",
|
||||
"version": "2.2.3",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const attributes = require('./attributes');
|
||||
const underscore = require("underscore")
|
||||
// @ts-nocheck
|
||||
import AttributeMap from './AttributeMap';
|
||||
import {compose, deserializeOps, isIdentity} from './Changeset';
|
||||
import {Builder} from "./Builder";
|
||||
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils';
|
||||
import attributes from './attributes';
|
||||
import underscore from "underscore";
|
||||
|
||||
const lineMarkerAttribute = 'lmkr';
|
||||
|
||||
|
@ -51,7 +51,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
if (!this.applyChangesetCallback) return changeset;
|
||||
|
||||
const cs = changeset.toString();
|
||||
if (!Changeset.isIdentity(cs)) {
|
||||
if (!isIdentity(cs)) {
|
||||
this.applyChangesetCallback(cs);
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// as the range might not be continuous
|
||||
// due to the presence of line markers on the rows
|
||||
if (allChangesets) {
|
||||
allChangesets = Changeset.compose(
|
||||
allChangesets = compose(
|
||||
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||
} else {
|
||||
allChangesets = rowChangeset;
|
||||
|
@ -125,9 +125,9 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
* @param attribs an array of attributes
|
||||
*/
|
||||
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
||||
ChangesetUtils.buildKeepRange(
|
||||
const builder = new Builder(this.rep.lines.totalWidth());
|
||||
buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
||||
buildKeepRange(
|
||||
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
||||
return builder;
|
||||
},
|
||||
|
@ -150,7 +150,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// get `attributeName` attribute of first char of line
|
||||
const aline = this.rep.alines[lineNum];
|
||||
if (!aline) return '';
|
||||
const [op] = Changeset.deserializeOps(aline);
|
||||
const [op] = deserializeOps(aline);
|
||||
if (op == null) return '';
|
||||
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
|
||||
},
|
||||
|
@ -163,7 +163,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// get attributes of first char of line
|
||||
const aline = this.rep.alines[lineNum];
|
||||
if (!aline) return [];
|
||||
const [op] = Changeset.deserializeOps(aline);
|
||||
const [op] = deserializeOps(aline);
|
||||
if (op == null) return [];
|
||||
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
|
||||
},
|
||||
|
@ -221,7 +221,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
let hasAttrib = true;
|
||||
|
||||
let indexIntoLine = 0;
|
||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
||||
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||
const opStartInLine = indexIntoLine;
|
||||
const opEndInLine = opStartInLine + op.chars;
|
||||
if (!hasIt(op.attribs)) {
|
||||
|
@ -258,7 +258,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// we need to sum up how much characters each operations take until the wanted position
|
||||
let currentPointer = 0;
|
||||
|
||||
for (const currentOperation of Changeset.deserializeOps(aline)) {
|
||||
for (const currentOperation of deserializeOps(aline)) {
|
||||
currentPointer += currentOperation.chars;
|
||||
if (currentPointer <= column) continue;
|
||||
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
|
||||
|
@ -285,13 +285,13 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
*/
|
||||
setAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||
let loc = [0, 0];
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
const builder = new Builder(this.rep.lines.totalWidth());
|
||||
const hasMarker = this.lineHasMarker(lineNum);
|
||||
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||
|
||||
if (hasMarker) {
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
||||
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
||||
[attributeName, attributeValue],
|
||||
], this.rep.apool);
|
||||
} else {
|
||||
|
@ -314,7 +314,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
* @param attributeValue if given only attributes with equal value will be removed
|
||||
*/
|
||||
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
const builder = new Builder(this.rep.lines.totalWidth());
|
||||
const hasMarker = this.lineHasMarker(lineNum);
|
||||
let found = false;
|
||||
|
||||
|
@ -333,16 +333,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
return;
|
||||
}
|
||||
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||
buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||
|
||||
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||
|
||||
// if we have marker and any of attributes don't need to have marker. we need delete it
|
||||
if (hasMarker && !countAttribsWithMarker) {
|
||||
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||
buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||
} else {
|
||||
ChangesetUtils.buildKeepRange(
|
||||
buildKeepRange(
|
||||
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const attributes = require('./attributes');
|
||||
import AttributePool from "./AttributePool";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
import attributes from './attributes';
|
||||
|
||||
/**
|
||||
* A `[key, value]` pair of strings describing a text attribute.
|
||||
|
@ -21,6 +24,7 @@ const attributes = require('./attributes');
|
|||
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
|
||||
*/
|
||||
class AttributeMap extends Map {
|
||||
private readonly pool? : AttributePool|null
|
||||
/**
|
||||
* Converts an attribute string into an AttributeMap.
|
||||
*
|
||||
|
@ -28,14 +32,14 @@ class AttributeMap extends Map {
|
|||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @returns {AttributeMap}
|
||||
*/
|
||||
static fromString(str, pool) {
|
||||
public static fromString(str: string, pool?: AttributePool|null): AttributeMap {
|
||||
return new AttributeMap(pool).updateFromString(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
*/
|
||||
constructor(pool) {
|
||||
constructor(pool?: AttributePool|null) {
|
||||
super();
|
||||
/** @public */
|
||||
this.pool = pool;
|
||||
|
@ -46,15 +50,15 @@ class AttributeMap extends Map {
|
|||
* @param {string} v - Attribute value.
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
set(k, v) {
|
||||
set(k: string, v: string):this {
|
||||
k = k == null ? '' : String(k);
|
||||
v = v == null ? '' : String(v);
|
||||
this.pool.putAttrib([k, v]);
|
||||
this.pool!.putAttrib([k, v]);
|
||||
return super.set(k, v);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return attributes.attribsToString(attributes.sort([...this]), this.pool);
|
||||
return attributes.attribsToString(attributes.sort([...this]), this.pool!);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,7 +67,7 @@ class AttributeMap extends Map {
|
|||
* key is removed from this map (if present).
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
update(entries, emptyValueIsDelete = false) {
|
||||
update(entries: Iterable<Attribute>, emptyValueIsDelete: boolean = false): AttributeMap {
|
||||
for (let [k, v] of entries) {
|
||||
k = k == null ? '' : String(k);
|
||||
v = v == null ? '' : String(v);
|
||||
|
@ -83,9 +87,9 @@ class AttributeMap extends Map {
|
|||
* key is removed from this map (if present).
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
updateFromString(str, emptyValueIsDelete = false) {
|
||||
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
|
||||
updateFromString(str: string, emptyValueIsDelete: boolean = false): AttributeMap {
|
||||
return this.update(attributes.attribsFromString(str, this.pool!), emptyValueIsDelete);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttributeMap;
|
||||
export default AttributeMap
|
|
@ -44,6 +44,8 @@
|
|||
* @property {number} nextNum - The attribute ID to assign to the next new attribute.
|
||||
*/
|
||||
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
/**
|
||||
* Represents an attribute pool, which is a collection of attributes (pairs of key and value
|
||||
* strings) along with their identifiers (non-negative integers).
|
||||
|
@ -55,6 +57,14 @@
|
|||
* in the pad.
|
||||
*/
|
||||
class AttributePool {
|
||||
numToAttrib: {
|
||||
[key: number]: [string, string]
|
||||
}
|
||||
private attribToNum: {
|
||||
[key: number]: [string, string]
|
||||
}
|
||||
private nextNum: number
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* Maps an attribute identifier to the attribute's `[key, value]` string pair.
|
||||
|
@ -96,7 +106,10 @@ class AttributePool {
|
|||
*/
|
||||
clone() {
|
||||
const c = new AttributePool();
|
||||
for (const [n, a] of Object.entries(this.numToAttrib)) c.numToAttrib[n] = [a[0], a[1]];
|
||||
for (const [n, a] of Object.entries(this.numToAttrib)){
|
||||
// @ts-ignore
|
||||
c.numToAttrib[n] = [a[0], a[1]];
|
||||
}
|
||||
Object.assign(c.attribToNum, this.attribToNum);
|
||||
c.nextNum = this.nextNum;
|
||||
return c;
|
||||
|
@ -111,15 +124,17 @@ class AttributePool {
|
|||
* membership in the pool without mutating the pool.
|
||||
* @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool.
|
||||
*/
|
||||
putAttrib(attrib, dontAddIfAbsent = false) {
|
||||
putAttrib(attrib: Attribute, dontAddIfAbsent = false) {
|
||||
const str = String(attrib);
|
||||
if (str in this.attribToNum) {
|
||||
// @ts-ignore
|
||||
return this.attribToNum[str];
|
||||
}
|
||||
if (dontAddIfAbsent) {
|
||||
return -1;
|
||||
}
|
||||
const num = this.nextNum++;
|
||||
// @ts-ignore
|
||||
this.attribToNum[str] = num;
|
||||
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
|
||||
return num;
|
||||
|
@ -130,7 +145,7 @@ class AttributePool {
|
|||
* @returns {Attribute} The attribute with the given identifier, or nullish if there is no such
|
||||
* attribute.
|
||||
*/
|
||||
getAttrib(num) {
|
||||
getAttrib(num: number): Attribute {
|
||||
const pair = this.numToAttrib[num];
|
||||
if (!pair) {
|
||||
return pair;
|
||||
|
@ -143,7 +158,7 @@ class AttributePool {
|
|||
* @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty
|
||||
* string.
|
||||
*/
|
||||
getAttribKey(num) {
|
||||
getAttribKey(num: number): string {
|
||||
const pair = this.numToAttrib[num];
|
||||
if (!pair) return '';
|
||||
return pair[0];
|
||||
|
@ -154,7 +169,7 @@ class AttributePool {
|
|||
* @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty
|
||||
* string.
|
||||
*/
|
||||
getAttribValue(num) {
|
||||
getAttribValue(num: number) {
|
||||
const pair = this.numToAttrib[num];
|
||||
if (!pair) return '';
|
||||
return pair[1];
|
||||
|
@ -166,8 +181,8 @@ class AttributePool {
|
|||
* @param {Function} func - Callback to call with two arguments: key and value. Its return value
|
||||
* is ignored.
|
||||
*/
|
||||
eachAttrib(func) {
|
||||
for (const n of Object.keys(this.numToAttrib)) {
|
||||
eachAttrib(func: (k: string, v: string)=>void) {
|
||||
for (const n in this.numToAttrib) {
|
||||
const pair = this.numToAttrib[n];
|
||||
func(pair[0], pair[1]);
|
||||
}
|
||||
|
@ -196,11 +211,12 @@ class AttributePool {
|
|||
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
|
||||
* state will lead to pool corruption.
|
||||
*/
|
||||
fromJsonable(obj) {
|
||||
fromJsonable(obj: this) {
|
||||
this.numToAttrib = obj.numToAttrib;
|
||||
this.nextNum = obj.nextNum;
|
||||
this.attribToNum = {};
|
||||
for (const n of Object.keys(this.numToAttrib)) {
|
||||
// @ts-ignore
|
||||
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
|
||||
}
|
||||
return this;
|
||||
|
@ -213,6 +229,7 @@ class AttributePool {
|
|||
if (!Number.isInteger(this.nextNum)) throw new Error('nextNum property is not an integer');
|
||||
if (this.nextNum < 0) throw new Error('nextNum property is negative');
|
||||
for (const prop of ['numToAttrib', 'attribToNum']) {
|
||||
// @ts-ignore
|
||||
const obj = this[prop];
|
||||
if (obj == null) throw new Error(`${prop} property is null`);
|
||||
if (typeof obj !== 'object') throw new TypeError(`${prop} property is not an object`);
|
||||
|
@ -231,9 +248,10 @@ class AttributePool {
|
|||
if (v == null) throw new TypeError(`attrib ${i} value is null`);
|
||||
if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`);
|
||||
const attrStr = String(attr);
|
||||
// @ts-ignore
|
||||
if (this.attribToNum[attrStr] !== i) throw new Error(`attribToNum for ${attrStr} !== ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttributePool;
|
||||
export default AttributePool
|
108
src/static/js/Builder.ts
Normal file
108
src/static/js/Builder.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Incrementally builds a Changeset.
|
||||
*
|
||||
* @typedef {object} Builder
|
||||
* @property {Function} insert -
|
||||
* @property {Function} keep -
|
||||
* @property {Function} keepText -
|
||||
* @property {Function} remove -
|
||||
* @property {Function} toString -
|
||||
*/
|
||||
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||
import Op from "./Op";
|
||||
import {StringAssembler} from "./StringAssembler";
|
||||
import AttributeMap from "./AttributeMap";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
import AttributePool from "./AttributePool";
|
||||
import {opsFromText, pack} from "./Changeset";
|
||||
|
||||
/**
|
||||
* @param {number} oldLen - Old length
|
||||
* @returns {Builder}
|
||||
*/
|
||||
export class Builder {
|
||||
private readonly oldLen: number;
|
||||
private assem: SmartOpAssembler;
|
||||
private readonly o: Op;
|
||||
private charBank: StringAssembler;
|
||||
|
||||
constructor(oldLen: number) {
|
||||
this.oldLen = oldLen
|
||||
this.assem = new SmartOpAssembler()
|
||||
this.o = new Op()
|
||||
this.charBank = new StringAssembler()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} N - Number of characters to keep.
|
||||
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
|
||||
* character must be a newline.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
keep = (N: number, L?: number, attribs?: string|Attribute[], pool?: AttributePool): Builder => {
|
||||
this.o.opcode = '=';
|
||||
this.o.attribs = typeof attribs === 'string'
|
||||
? attribs : new AttributeMap(pool).update(attribs || []).toString();
|
||||
this.o.chars = N;
|
||||
this.o.lines = (L || 0);
|
||||
this.assem.append(this.o);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} text - Text to keep.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
keepText= (text: string, attribs?: string|Attribute[], pool?: AttributePool): Builder=> {
|
||||
for (const op of opsFromText('=', text, attribs, pool)) this.assem.append(op);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} text - Text to insert.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
insert= (text: string, attribs: string | Attribute[] | undefined, pool?: AttributePool | null | undefined): Builder => {
|
||||
for (const op of opsFromText('+', text, attribs, pool)) this.assem.append(op);
|
||||
this.charBank.append(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {number} N - Number of characters to remove.
|
||||
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
|
||||
* character must be a newline.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
remove= (N: number, L?: number): Builder => {
|
||||
this.o.opcode = '-';
|
||||
this.o.attribs = '';
|
||||
this.o.chars = N;
|
||||
this.o.lines = (L || 0);
|
||||
this.assem.append(this.o);
|
||||
return this;
|
||||
}
|
||||
|
||||
toString= () => {
|
||||
this.assem.endDocument();
|
||||
const newLen = this.oldLen + this.assem.getLengthChange();
|
||||
return pack(this.oldLen, newLen, this.assem.toString(), this.charBank.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,12 @@
|
|||
* based on a SkipList
|
||||
*/
|
||||
|
||||
import {RepModel} from "./types/RepModel";
|
||||
import {ChangeSetBuilder} from "./types/ChangeSetBuilder";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
import AttributePool from "./AttributePool";
|
||||
import {Builder} from "./Builder";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -20,7 +26,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
exports.buildRemoveRange = (rep, builder, start, end) => {
|
||||
export const buildRemoveRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number,number], end: [number, number]) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
|
@ -32,7 +38,7 @@ exports.buildRemoveRange = (rep, builder, start, end) => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
|
||||
export const buildKeepRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number], end:[number, number], attribs?: Attribute[], pool?: AttributePool) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
|
@ -44,9 +50,25 @@ exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.buildKeepToStartOfRange = (rep, builder, start) => {
|
||||
export const buildKeepToStartOfRange = (rep: RepModel, builder: Builder, start: [number, number]) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
|
||||
builder.keep(startLineOffset, start[0]);
|
||||
builder.keep(start[1]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a number from string base 36.
|
||||
*
|
||||
* @param {string} str - string of the number in base 36
|
||||
* @returns {number} number
|
||||
*/
|
||||
export const parseNum = (str: string): number => parseInt(str, 36);
|
||||
|
||||
/**
|
||||
* Writes a number in base 36 and puts it in a string.
|
||||
*
|
||||
* @param {number} num - number
|
||||
* @returns {string} string
|
||||
*/
|
||||
export const numToString = (num: number): string => num.toString(36).toLowerCase();
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const {padutils: {warnDeprecated}} = require('./pad_utils');
|
||||
import padUtils from './pad_utils'
|
||||
|
||||
/**
|
||||
* Represents a chat message stored in the database and transmitted among users. Plugins can extend
|
||||
|
@ -8,14 +8,25 @@ const {padutils: {warnDeprecated}} = require('./pad_utils');
|
|||
*
|
||||
* Supports serialization to JSON.
|
||||
*/
|
||||
class ChatMessage {
|
||||
static fromObject(obj) {
|
||||
export class ChatMessage {
|
||||
customMetadata: any
|
||||
text: string|null
|
||||
public authorId: string|null
|
||||
displayName: string|null
|
||||
time: number|null
|
||||
static fromObject(obj: ChatMessage) {
|
||||
// The userId property was renamed to authorId, and userName was renamed to displayName. Accept
|
||||
// the old names in case the db record was written by an older version of Etherpad.
|
||||
obj = Object.assign({}, obj); // Don't mutate the caller's object.
|
||||
if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId;
|
||||
if ('userId' in obj && !('authorId' in obj)) { // @ts-ignore
|
||||
obj.authorId = obj.userId;
|
||||
}
|
||||
// @ts-ignore
|
||||
delete obj.userId;
|
||||
if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName;
|
||||
if ('userName' in obj && !('displayName' in obj)) { // @ts-ignore
|
||||
obj.displayName = obj.userName;
|
||||
}
|
||||
// @ts-ignore
|
||||
delete obj.userName;
|
||||
return Object.assign(new ChatMessage(), obj);
|
||||
}
|
||||
|
@ -25,7 +36,7 @@ class ChatMessage {
|
|||
* @param {?string} [authorId] - Initial value of the `authorId` property.
|
||||
* @param {?number} [time] - Initial value of the `time` property.
|
||||
*/
|
||||
constructor(text = null, authorId = null, time = null) {
|
||||
constructor(text: string | null = null, authorId: string | null = null, time: number | null = null) {
|
||||
/**
|
||||
* The raw text of the user's chat message (before any rendering or processing).
|
||||
*
|
||||
|
@ -62,11 +73,11 @@ class ChatMessage {
|
|||
* @type {string}
|
||||
*/
|
||||
get userId() {
|
||||
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
return this.authorId;
|
||||
}
|
||||
set userId(val) {
|
||||
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||
this.authorId = val;
|
||||
}
|
||||
|
||||
|
@ -77,11 +88,11 @@ class ChatMessage {
|
|||
* @type {string}
|
||||
*/
|
||||
get userName() {
|
||||
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
return this.displayName;
|
||||
}
|
||||
set userName(val) {
|
||||
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||
this.displayName = val;
|
||||
}
|
||||
|
||||
|
@ -89,10 +100,12 @@ class ChatMessage {
|
|||
// doesn't support authorId and displayName.
|
||||
toJSON() {
|
||||
const {authorId, displayName, ...obj} = this;
|
||||
// @ts-ignore
|
||||
obj.userId = authorId;
|
||||
// @ts-ignore
|
||||
obj.userName = displayName;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatMessage;
|
||||
export default ChatMessage
|
73
src/static/js/MergingOpAssembler.ts
Normal file
73
src/static/js/MergingOpAssembler.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import {OpAssembler} from "./OpAssembler";
|
||||
import Op from "./Op";
|
||||
import {clearOp, copyOp} from "./Changeset";
|
||||
|
||||
export class MergingOpAssembler {
|
||||
private assem: OpAssembler;
|
||||
private readonly bufOp: Op;
|
||||
private bufOpAdditionalCharsAfterNewline: number;
|
||||
|
||||
constructor() {
|
||||
this.assem = new OpAssembler()
|
||||
this.bufOp = new Op()
|
||||
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
|
||||
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
|
||||
// This variable stores the length of yyy and any other newline-less
|
||||
// ops immediately after it.
|
||||
this.bufOpAdditionalCharsAfterNewline = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [isEndDocument]
|
||||
*/
|
||||
flush = (isEndDocument?: boolean) => {
|
||||
if (!this.bufOp.opcode) return;
|
||||
if (isEndDocument && this.bufOp.opcode === '=' && !this.bufOp.attribs) {
|
||||
// final merged keep, leave it implicit
|
||||
} else {
|
||||
this.assem.append(this.bufOp);
|
||||
if (this.bufOpAdditionalCharsAfterNewline) {
|
||||
this.bufOp.chars = this.bufOpAdditionalCharsAfterNewline;
|
||||
this.bufOp.lines = 0;
|
||||
this.assem.append(this.bufOp);
|
||||
this.bufOpAdditionalCharsAfterNewline = 0;
|
||||
}
|
||||
}
|
||||
this.bufOp.opcode = '';
|
||||
}
|
||||
|
||||
append = (op: Op) => {
|
||||
if (op.chars <= 0) return;
|
||||
if (this.bufOp.opcode === op.opcode && this.bufOp.attribs === op.attribs) {
|
||||
if (op.lines > 0) {
|
||||
// bufOp and additional chars are all mergeable into a multi-line op
|
||||
this.bufOp.chars += this.bufOpAdditionalCharsAfterNewline + op.chars;
|
||||
this.bufOp.lines += op.lines;
|
||||
this.bufOpAdditionalCharsAfterNewline = 0;
|
||||
} else if (this.bufOp.lines === 0) {
|
||||
// both bufOp and op are in-line
|
||||
this.bufOp.chars += op.chars;
|
||||
} else {
|
||||
// append in-line text to multi-line bufOp
|
||||
this.bufOpAdditionalCharsAfterNewline += op.chars;
|
||||
}
|
||||
} else {
|
||||
this.flush();
|
||||
copyOp(op, this.bufOp);
|
||||
}
|
||||
}
|
||||
|
||||
endDocument = () => {
|
||||
this.flush(true);
|
||||
};
|
||||
|
||||
toString = () => {
|
||||
this.flush();
|
||||
return this.assem.toString();
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.assem.clear();
|
||||
clearOp(this.bufOp);
|
||||
};
|
||||
}
|
78
src/static/js/Op.ts
Normal file
78
src/static/js/Op.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import {numToString} from "./ChangesetUtils";
|
||||
|
||||
export type OpCode = ''|'='|'+'|'-';
|
||||
|
||||
|
||||
/**
|
||||
* An operation to apply to a shared document.
|
||||
*/
|
||||
export default class Op {
|
||||
opcode: ''|'='|'+'|'-'
|
||||
chars: number
|
||||
lines: number
|
||||
attribs: string
|
||||
/**
|
||||
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
|
||||
*/
|
||||
constructor(opcode:''|'='|'+'|'-' = '') {
|
||||
/**
|
||||
* The operation's operator:
|
||||
* - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
|
||||
* document.
|
||||
* - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
|
||||
* document.
|
||||
* - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
|
||||
* the document. The inserted characters come from the changeset's character bank.
|
||||
* - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
|
||||
* operation.
|
||||
*
|
||||
* @type {(''|'='|'+'|'-')}
|
||||
* @public
|
||||
*/
|
||||
this.opcode = opcode;
|
||||
|
||||
/**
|
||||
* The number of characters to keep, insert, or delete.
|
||||
*
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
this.chars = 0;
|
||||
|
||||
/**
|
||||
* The number of characters among the `chars` characters that are newlines. If non-zero, the
|
||||
* last character must be a newline.
|
||||
*
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
this.lines = 0;
|
||||
|
||||
/**
|
||||
* Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
|
||||
* sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
|
||||
* '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
|
||||
* identifiers come from the document's attribute pool.
|
||||
*
|
||||
* For keep ('=') operations, the attributes are merged with the base text's existing
|
||||
* attributes:
|
||||
* - A keep op attribute with a non-empty value replaces an existing base text attribute that
|
||||
* has the same key.
|
||||
* - A keep op attribute with an empty value is interpreted as an instruction to remove an
|
||||
* existing base text attribute that has the same key, if one exists.
|
||||
*
|
||||
* This is the empty string for remove ('-') operations.
|
||||
*
|
||||
* @type {string}
|
||||
* @public
|
||||
*/
|
||||
this.attribs = '';
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (!this.opcode) throw new TypeError('null op');
|
||||
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
|
||||
const l = this.lines ? `|${numToString(this.lines)}` : '';
|
||||
return this.attribs + l + this.opcode + numToString(this.chars);
|
||||
}
|
||||
}
|
21
src/static/js/OpAssembler.ts
Normal file
21
src/static/js/OpAssembler.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Op from "./Op";
|
||||
import {assert} from './Changeset'
|
||||
|
||||
/**
|
||||
* @returns {OpAssembler}
|
||||
*/
|
||||
export class OpAssembler {
|
||||
private serialized: string;
|
||||
constructor() {
|
||||
this.serialized = ''
|
||||
|
||||
}
|
||||
append = (op: Op) => {
|
||||
assert(op instanceof Op, 'argument must be an instance of Op');
|
||||
this.serialized += op.toString();
|
||||
}
|
||||
toString = () => this.serialized
|
||||
clear = () => {
|
||||
this.serialized = '';
|
||||
}
|
||||
}
|
47
src/static/js/OpIter.ts
Normal file
47
src/static/js/OpIter.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import Op from "./Op";
|
||||
import {clearOp, copyOp, deserializeOps} from "./Changeset";
|
||||
|
||||
/**
|
||||
* Iterator over a changeset's operations.
|
||||
*
|
||||
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
|
||||
*
|
||||
* @deprecated Use `deserializeOps` instead.
|
||||
*/
|
||||
export class OpIter {
|
||||
private gen
|
||||
private _next: IteratorResult<Op, void>
|
||||
/**
|
||||
* @param {string} ops - String encoding the change operations to iterate over.
|
||||
*/
|
||||
constructor(ops: string) {
|
||||
this.gen = deserializeOps(ops);
|
||||
this._next = this.gen.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether there are any remaining operations.
|
||||
*/
|
||||
hasNext(): boolean {
|
||||
return !this._next.done;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next operation object and advances the iterator.
|
||||
*
|
||||
* Note: This does NOT implement the ECMAScript iterator protocol.
|
||||
*
|
||||
* @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value.
|
||||
* @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are
|
||||
* no more operations.
|
||||
*/
|
||||
next(opOut: Op = new Op()): Op {
|
||||
if (this.hasNext()) {
|
||||
copyOp(this._next.value!, opOut);
|
||||
this._next = this.gen.next();
|
||||
} else {
|
||||
clearOp(opOut);
|
||||
}
|
||||
return opOut;
|
||||
}
|
||||
}
|
115
src/static/js/SmartOpAssembler.ts
Normal file
115
src/static/js/SmartOpAssembler.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
import {MergingOpAssembler} from "./MergingOpAssembler";
|
||||
import {StringAssembler} from "./StringAssembler";
|
||||
import padutils from "./pad_utils";
|
||||
import Op from "./Op";
|
||||
import { Attribute } from "./types/Attribute";
|
||||
import AttributePool from "./AttributePool";
|
||||
import {opsFromText} from "./Changeset";
|
||||
|
||||
/**
|
||||
* Creates an object that allows you to append operations (type Op) and also compresses them if
|
||||
* possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser
|
||||
* input, at the cost of speed. Specifically:
|
||||
* - merges consecutive operations that can be merged
|
||||
* - strips final "="
|
||||
* - ignores 0-length changes
|
||||
* - reorders consecutive + and - (which MergingOpAssembler doesn't do)
|
||||
*
|
||||
* @typedef {object} SmartOpAssembler
|
||||
* @property {Function} append -
|
||||
* @property {Function} appendOpWithText -
|
||||
* @property {Function} clear -
|
||||
* @property {Function} endDocument -
|
||||
* @property {Function} getLengthChange -
|
||||
* @property {Function} toString -
|
||||
*/
|
||||
export class SmartOpAssembler {
|
||||
private minusAssem: MergingOpAssembler;
|
||||
private plusAssem: MergingOpAssembler;
|
||||
private keepAssem: MergingOpAssembler;
|
||||
private lastOpcode: string;
|
||||
private lengthChange: number;
|
||||
private assem: StringAssembler;
|
||||
|
||||
constructor() {
|
||||
this.minusAssem = new MergingOpAssembler()
|
||||
this.plusAssem = new MergingOpAssembler()
|
||||
this.keepAssem = new MergingOpAssembler()
|
||||
this.assem = new StringAssembler()
|
||||
this.lastOpcode = ''
|
||||
this.lengthChange = 0
|
||||
}
|
||||
|
||||
flushKeeps = () => {
|
||||
this.assem.append(this.keepAssem.toString());
|
||||
this.keepAssem.clear();
|
||||
};
|
||||
|
||||
flushPlusMinus = () => {
|
||||
this.assem.append(this.minusAssem.toString());
|
||||
this.minusAssem.clear();
|
||||
this.assem.append(this.plusAssem.toString());
|
||||
this.plusAssem.clear();
|
||||
};
|
||||
|
||||
append = (op: Op) => {
|
||||
if (!op.opcode) return;
|
||||
if (!op.chars) return;
|
||||
|
||||
if (op.opcode === '-') {
|
||||
if (this.lastOpcode === '=') {
|
||||
this.flushKeeps();
|
||||
}
|
||||
this.minusAssem.append(op);
|
||||
this.lengthChange -= op.chars;
|
||||
} else if (op.opcode === '+') {
|
||||
if (this.lastOpcode === '=') {
|
||||
this.flushKeeps();
|
||||
}
|
||||
this.plusAssem.append(op);
|
||||
this.lengthChange += op.chars;
|
||||
} else if (op.opcode === '=') {
|
||||
if (this.lastOpcode !== '=') {
|
||||
this.flushPlusMinus();
|
||||
}
|
||||
this.keepAssem.append(op);
|
||||
}
|
||||
this.lastOpcode = op.opcode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates operations from the given text and attributes.
|
||||
*
|
||||
* @deprecated Use `opsFromText` instead.
|
||||
* @param {('-'|'+'|'=')} opcode - The operator to use.
|
||||
* @param {string} text - The text to remove/add/keep.
|
||||
* @param {(string|Iterable<Attribute>)} attribs - The attributes to apply to the operations.
|
||||
* @param {?AttributePool.ts} pool - Attribute pool. Only required if `attribs` is an iterable of
|
||||
* attribute key, value pairs.
|
||||
*/
|
||||
appendOpWithText = (opcode: '-'|'+'|'=', text: string, attribs: Attribute[]|string, pool?: AttributePool) => {
|
||||
padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' +
|
||||
'use opsFromText() instead.');
|
||||
for (const op of opsFromText(opcode, text, attribs, pool)) this.append(op);
|
||||
};
|
||||
|
||||
toString = () => {
|
||||
this.flushPlusMinus();
|
||||
this.flushKeeps();
|
||||
return this.assem.toString();
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.minusAssem.clear();
|
||||
this.plusAssem.clear();
|
||||
this.keepAssem.clear();
|
||||
this.assem.clear();
|
||||
this.lengthChange = 0;
|
||||
};
|
||||
|
||||
endDocument = () => {
|
||||
this.keepAssem.endDocument();
|
||||
};
|
||||
|
||||
getLengthChange = () => this.lengthChange;
|
||||
}
|
18
src/static/js/StringAssembler.ts
Normal file
18
src/static/js/StringAssembler.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @returns {StringAssembler}
|
||||
*/
|
||||
export class StringAssembler {
|
||||
private str = ''
|
||||
clear = ()=> {
|
||||
this.str = '';
|
||||
}
|
||||
/**
|
||||
* @param {string} x -
|
||||
*/
|
||||
append(x: string) {
|
||||
this.str += String(x);
|
||||
}
|
||||
toString() {
|
||||
return this.str
|
||||
}
|
||||
}
|
54
src/static/js/StringIterator.ts
Normal file
54
src/static/js/StringIterator.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import {assert} from "./Changeset";
|
||||
|
||||
/**
|
||||
* A custom made String Iterator
|
||||
*
|
||||
* @typedef {object} StringIterator
|
||||
* @property {Function} newlines -
|
||||
* @property {Function} peek -
|
||||
* @property {Function} remaining -
|
||||
* @property {Function} skip -
|
||||
* @property {Function} take -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} str - String to iterate over
|
||||
* @returns {StringIterator}
|
||||
*/
|
||||
export class StringIterator {
|
||||
private curIndex: number;
|
||||
private newLines: number;
|
||||
private str: String
|
||||
|
||||
constructor(str: string) {
|
||||
this.curIndex = 0;
|
||||
this.str = str
|
||||
this.newLines = str.split('\n').length - 1;
|
||||
}
|
||||
remaining = () => this.str.length - this.curIndex;
|
||||
|
||||
getnewLines = () => this.newLines;
|
||||
|
||||
assertRemaining = (n: number) => {
|
||||
assert(n <= this.remaining(), `!(${n} <= ${this.remaining()})`);
|
||||
}
|
||||
|
||||
take = (n: number) => {
|
||||
this.assertRemaining(n);
|
||||
const s = this.str.substring(this.curIndex, this.curIndex+n);
|
||||
this.newLines -= s.split('\n').length - 1;
|
||||
this.curIndex += n;
|
||||
return s;
|
||||
}
|
||||
|
||||
peek = (n: number) => {
|
||||
this.assertRemaining(n);
|
||||
return this.str.substring(this.curIndex, this.curIndex+n);
|
||||
}
|
||||
|
||||
skip = (n: number) => {
|
||||
this.assertRemaining(n);
|
||||
this.curIndex += n;
|
||||
}
|
||||
|
||||
}
|
348
src/static/js/TextLinesMutator.ts
Normal file
348
src/static/js/TextLinesMutator.ts
Normal file
|
@ -0,0 +1,348 @@
|
|||
import {splitTextLines} from "./Changeset";
|
||||
|
||||
/**
|
||||
* Class to iterate and modify texts which have several lines. It is used for applying Changesets on
|
||||
* arrays of lines.
|
||||
*
|
||||
* Mutation operations have the same constraints as exports operations with respect to newlines, but
|
||||
* not the other additional constraints (i.e. ins/del ordering, forbidden no-ops, non-mergeability,
|
||||
* final newline). Can be used to mutate lists of strings where the last char of each string is not
|
||||
* actually a newline, but for the purposes of N and L values, the caller should pretend it is, and
|
||||
* for things to work right in that case, the input to the `insert` method should be a single line
|
||||
* with no newlines.
|
||||
*/
|
||||
class TextLinesMutator {
|
||||
private _lines: string[];
|
||||
private _curSplice: [number, number?];
|
||||
private _inSplice: boolean;
|
||||
private _curLine: number;
|
||||
private _curCol: number;
|
||||
/**
|
||||
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
|
||||
*/
|
||||
constructor(lines: string[]) {
|
||||
this._lines = lines;
|
||||
/**
|
||||
* this._curSplice holds values that will be passed as arguments to this._lines.splice() to
|
||||
* insert, delete, or change lines:
|
||||
* - this._curSplice[0] is an index into the this._lines array.
|
||||
* - this._curSplice[1] is the number of lines that will be removed from the this._lines array
|
||||
* starting at the index.
|
||||
* - The other elements represent mutated (changed by ops) lines or new lines (added by ops)
|
||||
* to insert at the index.
|
||||
*
|
||||
* @type {[number, number?, ...string[]?]}
|
||||
*/
|
||||
this._curSplice = [0, 0];
|
||||
this._inSplice = false;
|
||||
// position in lines after curSplice is applied:
|
||||
this._curLine = 0;
|
||||
this._curCol = 0;
|
||||
// invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) &&
|
||||
// curLine >= curSplice[0]
|
||||
// invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then
|
||||
// curCol == 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a line from `lines` at given index.
|
||||
*
|
||||
* @param {number} idx - an index
|
||||
* @returns {string}
|
||||
*/
|
||||
_linesGet(idx: number) {
|
||||
if ('get' in this._lines) {
|
||||
// @ts-ignore
|
||||
return this._lines.get(idx) as string;
|
||||
} else {
|
||||
return this._lines[idx];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a slice from `lines`.
|
||||
*
|
||||
* @param {number} start - the start index
|
||||
* @param {number} end - the end index
|
||||
* @returns {string[]}
|
||||
*/
|
||||
_linesSlice(start: number | undefined, end: number | undefined) {
|
||||
// can be unimplemented if removeLines's return value not needed
|
||||
if (this._lines.slice) {
|
||||
return this._lines.slice(start, end);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the length of `lines`.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
_linesLength() {
|
||||
if (typeof this._lines.length === 'number') {
|
||||
return this._lines.length;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return this._lines.length();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new splice.
|
||||
*/
|
||||
_enterSplice() {
|
||||
this._curSplice[0] = this._curLine;
|
||||
this._curSplice[1] = 0;
|
||||
// TODO(doc) when is this the case?
|
||||
// check all enterSplice calls and changes to curCol
|
||||
if (this._curCol > 0) this._putCurLineInSplice();
|
||||
this._inSplice = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the lines array according to the values in curSplice and resets curSplice. Called via
|
||||
* close or TODO(doc).
|
||||
*/
|
||||
_leaveSplice() {
|
||||
this._lines.splice(...this._curSplice);
|
||||
this._curSplice.length = 2;
|
||||
this._curSplice[0] = this._curSplice[1] = 0;
|
||||
this._inSplice = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if curLine is already in the splice. This is necessary because the last element in
|
||||
* curSplice is curLine when this line is currently worked on (e.g. when skipping or inserting).
|
||||
*
|
||||
* @returns {boolean} true if curLine is in splice
|
||||
*/
|
||||
_isCurLineInSplice() {
|
||||
// The value of `this._curSplice[1]` does not matter when determining the return value because
|
||||
// `this._curLine` refers to the line number *after* the splice is applied (so after those lines
|
||||
// are deleted).
|
||||
return this._curLine - this._curSplice[0] < this._curSplice.length - 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incorporates current line into the splice and marks its old position to be deleted.
|
||||
*
|
||||
* @returns {number} the index of the added line in curSplice
|
||||
*/
|
||||
_putCurLineInSplice() {
|
||||
if (!this._isCurLineInSplice()) {
|
||||
// @ts-ignore
|
||||
this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1]));
|
||||
// @ts-ignore
|
||||
this._curSplice[1]++;
|
||||
}
|
||||
// TODO should be the same as this._curSplice.length - 1
|
||||
return 2 + this._curLine - this._curSplice[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* It will skip some newlines by putting them into the splice.
|
||||
*
|
||||
* @param {number} L -
|
||||
* @param {boolean} includeInSplice - Indicates that attributes are present.
|
||||
*/
|
||||
skipLines(L: number, includeInSplice?: any) {
|
||||
if (!L) return;
|
||||
if (includeInSplice) {
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
// TODO(doc) should this count the number of characters that are skipped to check?
|
||||
for (let i = 0; i < L; i++) {
|
||||
this._curCol = 0;
|
||||
this._putCurLineInSplice();
|
||||
this._curLine++;
|
||||
}
|
||||
} else {
|
||||
if (this._inSplice) {
|
||||
if (L > 1) {
|
||||
// TODO(doc) figure out why single lines are incorporated into splice instead of ignored
|
||||
this._leaveSplice();
|
||||
} else {
|
||||
this._putCurLineInSplice();
|
||||
}
|
||||
}
|
||||
this._curLine += L;
|
||||
this._curCol = 0;
|
||||
}
|
||||
// tests case foo in remove(), which isn't otherwise covered in current impl
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip some characters. Can contain newlines.
|
||||
*
|
||||
* @param {number} N - number of characters to skip
|
||||
* @param {number} L - number of newlines to skip
|
||||
* @param {boolean} includeInSplice - indicates if attributes are present
|
||||
*/
|
||||
skip(N: number, L: number, includeInSplice?: any) {
|
||||
if (!N) return;
|
||||
if (L) {
|
||||
this.skipLines(L, includeInSplice);
|
||||
} else {
|
||||
if (includeInSplice && !this._inSplice) this._enterSplice();
|
||||
if (this._inSplice) {
|
||||
// although the line is put into splice curLine is not increased, because
|
||||
// only some chars are skipped, not the whole line
|
||||
this._putCurLineInSplice();
|
||||
}
|
||||
this._curCol += N;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove whole lines from lines array.
|
||||
*
|
||||
* @param {number} L - number of lines to remove
|
||||
* @returns {string}
|
||||
*/
|
||||
removeLines(L: number) {
|
||||
if (!L) return '';
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
|
||||
/**
|
||||
* Gets a string of joined lines after the end of the splice.
|
||||
*
|
||||
* @param {number} k - number of lines
|
||||
* @returns {string} joined lines
|
||||
*/
|
||||
const nextKLinesText = (k: number) => {
|
||||
// @ts-ignore
|
||||
const m = this._curSplice[0] + this._curSplice[1];
|
||||
return this._linesSlice(m, m + k).join('');
|
||||
};
|
||||
|
||||
let removed = '';
|
||||
if (this._isCurLineInSplice()) {
|
||||
if (this._curCol === 0) {
|
||||
// @ts-ignore
|
||||
removed = this._curSplice[this._curSplice.length - 1];
|
||||
this._curSplice.length--;
|
||||
removed += nextKLinesText(L - 1);
|
||||
// @ts-ignore
|
||||
this._curSplice[1] += L - 1;
|
||||
} else {
|
||||
removed = nextKLinesText(L - 1);
|
||||
// @ts-ignore
|
||||
this._curSplice[1] += L - 1;
|
||||
const sline = this._curSplice.length - 1;
|
||||
// @ts-ignore
|
||||
removed = this._curSplice[sline].substring(this._curCol) + removed;
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
|
||||
// @ts-ignore
|
||||
this._linesGet(this._curSplice[0] + this._curSplice[1]);
|
||||
// @ts-ignore
|
||||
this._curSplice[1] += 1;
|
||||
}
|
||||
} else {
|
||||
removed = nextKLinesText(L);
|
||||
this._curSplice[1]! += L;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove text from lines array.
|
||||
*
|
||||
* @param {number} N - characters to delete
|
||||
* @param {number} L - lines to delete
|
||||
* @returns {string}
|
||||
*/
|
||||
remove(N: number, L: any) {
|
||||
if (!N) return '';
|
||||
if (L) return this.removeLines(L);
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
// although the line is put into splice, curLine is not increased, because
|
||||
// only some chars are removed not the whole line
|
||||
const sline = this._putCurLineInSplice();
|
||||
// @ts-ignore
|
||||
const removed = this._curSplice[sline].substring(this._curCol, this._curCol + N);
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
|
||||
// @ts-ignore
|
||||
this._curSplice[sline].substring(this._curCol + N);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts text into lines array.
|
||||
*
|
||||
* @param {string} text - the text to insert
|
||||
* @param {number} L - number of newlines in text
|
||||
*/
|
||||
insert(text: string | any[], L: any) {
|
||||
if (!text) return;
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
if (L) {
|
||||
// @ts-ignore
|
||||
const newLines = splitTextLines(text);
|
||||
if (this._isCurLineInSplice()) {
|
||||
const sline = this._curSplice.length - 1;
|
||||
/** @type {string} */
|
||||
const theLine = this._curSplice[sline];
|
||||
const lineCol = this._curCol;
|
||||
// Insert the chars up to `curCol` and the first new line.
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
|
||||
this._curLine++;
|
||||
newLines!.splice(0, 1);
|
||||
// insert the remaining new lines
|
||||
// @ts-ignore
|
||||
this._curSplice.push(...newLines);
|
||||
this._curLine += newLines!.length;
|
||||
// insert the remaining chars from the "old" line (e.g. the line we were in
|
||||
// when we started to insert new lines)
|
||||
// @ts-ignore
|
||||
this._curSplice.push(theLine.substring(lineCol));
|
||||
this._curCol = 0; // TODO(doc) why is this not set to the length of last line?
|
||||
} else {
|
||||
this._curSplice.push(...newLines);
|
||||
this._curLine += newLines!.length;
|
||||
}
|
||||
} else {
|
||||
// There are no additional lines. Although the line is put into splice, curLine is not
|
||||
// increased because there may be more chars in the line (newline is not reached).
|
||||
const sline = this._putCurLineInSplice();
|
||||
if (!this._curSplice[sline]) {
|
||||
const err = new Error(
|
||||
'curSplice[sline] not populated, actual curSplice contents is ' +
|
||||
`${JSON.stringify(this._curSplice)}. Possibly related to ` +
|
||||
'https://github.com/ether/etherpad-lite/issues/2802');
|
||||
console.error(err.stack || err.toString());
|
||||
}
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + text +
|
||||
// @ts-ignore
|
||||
this._curSplice[sline].substring(this._curCol);
|
||||
this._curCol += text.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`.
|
||||
*
|
||||
* @returns {boolean} indicates if there are lines left
|
||||
*/
|
||||
hasMore() {
|
||||
let docLines = this._linesLength();
|
||||
if (this._inSplice) {
|
||||
// @ts-ignore
|
||||
docLines += this._curSplice.length - 2 - this._curSplice[1];
|
||||
}
|
||||
return this._curLine < docLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the splice
|
||||
*/
|
||||
close() {
|
||||
if (this._inSplice) this._leaveSplice();
|
||||
}
|
||||
}
|
||||
|
||||
export default TextLinesMutator
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
|
@ -6,6 +6,8 @@
|
|||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
import {MapArrayType} from "../../node/types/MapType";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -22,11 +24,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const isNodeText = (node) => (node.nodeType === 3);
|
||||
export const isNodeText = (node: {
|
||||
nodeType: number
|
||||
}) => (node.nodeType === 3);
|
||||
|
||||
const getAssoc = (obj, name) => obj[`_magicdom_${name}`];
|
||||
export const getAssoc = (obj: MapArrayType<any>, name: string) => obj[`_magicdom_${name}`];
|
||||
|
||||
const setAssoc = (obj, name, value) => {
|
||||
export const setAssoc = (obj: MapArrayType<any>, name: string, value: string) => {
|
||||
// note that in IE designMode, properties of a node can get
|
||||
// copied to new nodes that are spawned during editing; also,
|
||||
// properties representable in HTML text can survive copy-and-paste
|
||||
|
@ -38,7 +42,7 @@ const setAssoc = (obj, name, value) => {
|
|||
// between false and true, a number between 0 and numItems inclusive.
|
||||
|
||||
|
||||
const binarySearch = (numItems, func) => {
|
||||
export const binarySearch = (numItems: number, func: (num: number)=>boolean) => {
|
||||
if (numItems < 1) return 0;
|
||||
if (func(0)) return 0;
|
||||
if (!func(numItems - 1)) return numItems;
|
||||
|
@ -52,17 +56,10 @@ const binarySearch = (numItems, func) => {
|
|||
return high;
|
||||
};
|
||||
|
||||
const binarySearchInfinite = (expectedLength, func) => {
|
||||
export const binarySearchInfinite = (expectedLength: number, func: (num: number)=>boolean) => {
|
||||
let i = 0;
|
||||
while (!func(i)) i += expectedLength;
|
||||
return binarySearch(i, func);
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
exports.isNodeText = isNodeText;
|
||||
exports.getAssoc = getAssoc;
|
||||
exports.setAssoc = setAssoc;
|
||||
exports.binarySearch = binarySearch;
|
||||
exports.binarySearchInfinite = binarySearchInfinite;
|
||||
exports.noop = noop;
|
||||
export const noop = () => {};
|
|
@ -1,4 +1,5 @@
|
|||
'use strict';
|
||||
// @ts-nocheck
|
||||
import {Builder} from "./Builder";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
|
@ -18,30 +19,32 @@
|
|||
*/
|
||||
let documentAttributeManager;
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
import AttributeMap from './AttributeMap';
|
||||
const browser = require('./vendors/browser');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import padutils from './pad_utils'
|
||||
const Ace2Common = require('./ace2_common');
|
||||
const $ = require('./rjquery').$;
|
||||
import {characterRangeFollow, checkRep, cloneAText, compose, deserializeOps, filterAttribNumbers, inverse, isIdentity, makeAText, makeAttribution, mapAttribNumbers, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, opsFromAText, pack, splitAttributionLines} from './Changeset'
|
||||
|
||||
|
||||
const isNodeText = Ace2Common.isNodeText;
|
||||
const getAssoc = Ace2Common.getAssoc;
|
||||
const setAssoc = Ace2Common.setAssoc;
|
||||
const noop = Ace2Common.noop;
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
import SkipList from "./skiplist";
|
||||
import Scroll from './scroll'
|
||||
import AttribPool from './AttributePool'
|
||||
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||
import Op from "./Op";
|
||||
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils'
|
||||
|
||||
function Ace2Inner(editorInfo, cssManagers) {
|
||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
||||
const domline = require('./domline').domline;
|
||||
const AttribPool = require('./AttributePool');
|
||||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||
const SkipList = require('./skiplist');
|
||||
const undoModule = require('./undomodule').undoModule;
|
||||
const AttributeManager = require('./AttributeManager');
|
||||
const DEBUG = false;
|
||||
|
@ -174,9 +177,9 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// CCCCCCCCCCCCCCCCCCCC\n
|
||||
// CCCC\n
|
||||
// end[0]: <CCC end[1] CCC>-------\n
|
||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
||||
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
|
||||
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
|
||||
const builder = new Builder(rep.lines.totalWidth());
|
||||
buildKeepToStartOfRange(rep, builder, start);
|
||||
buildRemoveRange(rep, builder, start, end);
|
||||
builder.insert(newText, [
|
||||
['author', thisAuthor],
|
||||
], rep.apool);
|
||||
|
@ -495,10 +498,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const importAText = (atext, apoolJsonObj, undoable) => {
|
||||
atext = Changeset.cloneAText(atext);
|
||||
atext = cloneAText(atext);
|
||||
if (apoolJsonObj) {
|
||||
const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
|
||||
atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
|
||||
atext.attribs = moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
|
||||
}
|
||||
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||
setDocAText(atext);
|
||||
|
@ -527,18 +530,18 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const numLines = rep.lines.length();
|
||||
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
|
||||
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
|
||||
const assem = Changeset.smartOpAssembler();
|
||||
const o = new Changeset.Op('-');
|
||||
const assem = new SmartOpAssembler();
|
||||
const o = new Op('-');
|
||||
o.chars = upToLastLine;
|
||||
o.lines = numLines - 1;
|
||||
assem.append(o);
|
||||
o.chars = lastLineLength;
|
||||
o.lines = 0;
|
||||
assem.append(o);
|
||||
for (const op of Changeset.opsFromAText(atext)) assem.append(op);
|
||||
for (const op of opsFromAText(atext)) assem.append(op);
|
||||
const newLen = oldLen + assem.getLengthChange();
|
||||
const changeset = Changeset.checkRep(
|
||||
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
||||
const changeset = checkRep(
|
||||
pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
||||
performDocumentApplyChangeset(changeset);
|
||||
|
||||
performSelectionChange(
|
||||
|
@ -552,7 +555,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const setDocText = (text) => {
|
||||
setDocAText(Changeset.makeAText(text));
|
||||
setDocAText(makeAText(text));
|
||||
};
|
||||
|
||||
const getDocText = () => {
|
||||
|
@ -1271,7 +1274,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
||||
theIndent += THE_TAB;
|
||||
}
|
||||
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
|
||||
const cs = new Builder(rep.lines.totalWidth()).keep(
|
||||
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
|
||||
theIndent, [
|
||||
['author', thisAuthor],
|
||||
|
@ -1423,7 +1426,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
|
||||
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
|
||||
const result =
|
||||
Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
||||
characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
||||
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
|
||||
}
|
||||
|
||||
|
@ -1435,7 +1438,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
length: () => rep.lines.length(),
|
||||
};
|
||||
|
||||
Changeset.mutateTextLines(changes, linesMutatee);
|
||||
mutateTextLines(changes, linesMutatee);
|
||||
|
||||
if (requiredSelectionSetting) {
|
||||
performSelectionChange(
|
||||
|
@ -1446,10 +1449,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
|
||||
Changeset.checkRep(changes);
|
||||
checkRep(changes);
|
||||
|
||||
if (Changeset.oldLen(changes) !== rep.alltext.length) {
|
||||
const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`;
|
||||
if (oldLen(changes) !== rep.alltext.length) {
|
||||
const errMsg = `${oldLen(changes)}/${rep.alltext.length}`;
|
||||
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
|
||||
}
|
||||
|
||||
|
@ -1458,10 +1461,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (!editEvent.changeset) {
|
||||
editEvent.changeset = changes;
|
||||
} else {
|
||||
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
|
||||
editEvent.changeset = compose(editEvent.changeset, changes, rep.apool);
|
||||
}
|
||||
} else {
|
||||
const inverseChangeset = Changeset.inverse(changes, {
|
||||
const inverseChangeset = inverse(changes, {
|
||||
get: (i) => `${rep.lines.atIndex(i).text}\n`,
|
||||
length: () => rep.lines.length(),
|
||||
}, rep.alines, rep.apool);
|
||||
|
@ -1469,11 +1472,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (!editEvent.backset) {
|
||||
editEvent.backset = inverseChangeset;
|
||||
} else {
|
||||
editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
|
||||
editEvent.backset = compose(inverseChangeset, editEvent.backset, rep.apool);
|
||||
}
|
||||
}
|
||||
|
||||
Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);
|
||||
mutateAttributionLines(changes, rep.alines, rep.apool);
|
||||
|
||||
if (changesetTracker.isTracking()) {
|
||||
changesetTracker.composeUserChangeset(changes);
|
||||
|
@ -1582,7 +1585,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
let hasAttrib = true;
|
||||
|
||||
let indexIntoLine = 0;
|
||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
||||
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||
const opStartInLine = indexIntoLine;
|
||||
const opEndInLine = opStartInLine + op.chars;
|
||||
if (!hasIt(op.attribs)) {
|
||||
|
@ -1627,7 +1630,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (n === selEndLine) {
|
||||
selectionEndInLine = rep.selEnd[1];
|
||||
}
|
||||
for (const op of Changeset.deserializeOps(rep.alines[n])) {
|
||||
for (const op of deserializeOps(rep.alines[n])) {
|
||||
const opStartInLine = indexIntoLine;
|
||||
const opEndInLine = opStartInLine + op.chars;
|
||||
if (!hasIt(op.attribs)) {
|
||||
|
@ -1745,7 +1748,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
|
||||
|
||||
const startBuilder = () => {
|
||||
const builder = Changeset.builder(oldLen);
|
||||
const builder = new Builder(oldLen);
|
||||
builder.keep(spliceStartLineStart, spliceStartLine);
|
||||
builder.keep(spliceStart - spliceStartLineStart);
|
||||
return builder;
|
||||
|
@ -1755,7 +1758,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
let textIndex = 0;
|
||||
const newTextStart = commonStart;
|
||||
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
|
||||
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);
|
||||
|
@ -1773,7 +1776,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// changeset the applies the styles found in the DOM.
|
||||
// This allows us to incorporate, e.g., Safari's native "unbold".
|
||||
const incorpedAttribClearer = cachedStrFunc(
|
||||
(oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => {
|
||||
(oldAtts) => mapAttribNumbers(oldAtts, (n) => {
|
||||
const k = rep.apool.getAttribKey(n);
|
||||
if (isStyleAttribute(k)) {
|
||||
return rep.apool.putAttrib([k, '']);
|
||||
|
@ -1799,7 +1802,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
});
|
||||
const styler = builder2.toString();
|
||||
|
||||
theChangeset = Changeset.compose(clearer, styler, rep.apool);
|
||||
theChangeset = compose(clearer, styler, rep.apool);
|
||||
} else {
|
||||
const builder = startBuilder();
|
||||
|
||||
|
@ -1869,7 +1872,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const attribRuns = (attribs) => {
|
||||
const lengs = [];
|
||||
const atts = [];
|
||||
for (const op of Changeset.deserializeOps(attribs)) {
|
||||
for (const op of deserializeOps(attribs)) {
|
||||
lengs.push(op.chars);
|
||||
atts.push(op.attribs);
|
||||
}
|
||||
|
@ -1898,8 +1901,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const newLen = newText.length;
|
||||
const minLen = Math.min(oldLen, newLen);
|
||||
|
||||
const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
||||
const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
||||
const oldARuns = attribRuns(filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
||||
const newARuns = attribRuns(filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
||||
|
||||
let commonStart = 0;
|
||||
const oldStartIter = attribIterator(oldARuns, false);
|
||||
|
@ -2297,7 +2300,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
// 3-renumber every list item of the same level from the beginning, level 1
|
||||
// IMPORTANT: never skip a level because there imbrication may be arbitrary
|
||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
||||
const builder = new Builder(rep.lines.totalWidth());
|
||||
let loc = [0, 0];
|
||||
const applyNumberList = (line, level) => {
|
||||
// init
|
||||
|
@ -2312,8 +2315,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (isNaN(curLevel) || listType[0] === 'indent') {
|
||||
return line;
|
||||
} else if (curLevel === level) {
|
||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
||||
buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
||||
buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
||||
['start', position],
|
||||
], rep.apool);
|
||||
|
||||
|
@ -2330,7 +2333,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
applyNumberList(lineNum, 1);
|
||||
const cs = builder.toString();
|
||||
if (!Changeset.isIdentity(cs)) {
|
||||
if (!isIdentity(cs)) {
|
||||
performDocumentApplyChangeset(cs);
|
||||
}
|
||||
|
||||
|
@ -2618,7 +2621,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// TODO: There appears to be a race condition or so.
|
||||
const authorIds = new Set();
|
||||
if (alineAttrs) {
|
||||
for (const op of Changeset.deserializeOps(alineAttrs)) {
|
||||
for (const op of deserializeOps(alineAttrs)) {
|
||||
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
|
||||
if (authorId) authorIds.add(authorId);
|
||||
}
|
||||
|
@ -3513,8 +3516,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const oneEntry = createDomLineEntry('');
|
||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||
insertDomLines(null, [oneEntry.domInfo]);
|
||||
rep.alines = Changeset.splitAttributionLines(
|
||||
Changeset.makeAttribution('\n'), '\n');
|
||||
rep.alines = splitAttributionLines(
|
||||
makeAttribution('\n'), '\n');
|
||||
|
||||
bindTheEventHandlers();
|
||||
});
|
|
@ -17,6 +17,9 @@
|
|||
* @typedef {string} AttributeString
|
||||
*/
|
||||
|
||||
import AttributePool from "./AttributePool";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
|
||||
/**
|
||||
* Converts an attribute string into a sequence of attribute identifier numbers.
|
||||
*
|
||||
|
@ -28,7 +31,7 @@
|
|||
* appear in `str`.
|
||||
* @returns {Generator<number>}
|
||||
*/
|
||||
exports.decodeAttribString = function* (str) {
|
||||
export const decodeAttribString = function* (str: string): Generator<number> {
|
||||
const re = /\*([0-9a-z]+)|./gy;
|
||||
let match;
|
||||
while ((match = re.exec(str)) != null) {
|
||||
|
@ -38,7 +41,7 @@ exports.decodeAttribString = function* (str) {
|
|||
}
|
||||
};
|
||||
|
||||
const checkAttribNum = (n) => {
|
||||
const checkAttribNum = (n: number|object) => {
|
||||
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
|
||||
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
|
||||
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
|
||||
|
@ -50,7 +53,7 @@ const checkAttribNum = (n) => {
|
|||
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
|
||||
* @returns {AttributeString}
|
||||
*/
|
||||
exports.encodeAttribString = (attribNums) => {
|
||||
export const encodeAttribString = (attribNums: Iterable<number>): string => {
|
||||
let str = '';
|
||||
for (const n of attribNums) {
|
||||
checkAttribNum(n);
|
||||
|
@ -67,7 +70,7 @@ exports.encodeAttribString = (attribNums) => {
|
|||
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
|
||||
* @returns {Generator<Attribute>}
|
||||
*/
|
||||
exports.attribsFromNums = function* (attribNums, pool) {
|
||||
export const attribsFromNums = function* (attribNums: Iterable<number>, pool: AttributePool): Generator<Attribute> {
|
||||
for (const n of attribNums) {
|
||||
checkAttribNum(n);
|
||||
const attrib = pool.getAttrib(n);
|
||||
|
@ -87,7 +90,7 @@ exports.attribsFromNums = function* (attribNums, pool) {
|
|||
* @yields {number} The attribute number of each attribute in `attribs`, in order.
|
||||
* @returns {Generator<number>}
|
||||
*/
|
||||
exports.attribsToNums = function* (attribs, pool) {
|
||||
export const attribsToNums = function* (attribs: Iterable<Attribute>, pool: AttributePool) {
|
||||
for (const attrib of attribs) yield pool.putAttrib(attrib);
|
||||
};
|
||||
|
||||
|
@ -102,8 +105,8 @@ exports.attribsToNums = function* (attribs, pool) {
|
|||
* @yields {Attribute} The attributes identified in `str`, in order.
|
||||
* @returns {Generator<Attribute>}
|
||||
*/
|
||||
exports.attribsFromString = function* (str, pool) {
|
||||
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
|
||||
export const attribsFromString = function* (str: string, pool: AttributePool): Generator<Attribute> {
|
||||
yield* attribsFromNums(decodeAttribString(str), pool);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -116,8 +119,8 @@ exports.attribsFromString = function* (str, pool) {
|
|||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @returns {AttributeString}
|
||||
*/
|
||||
exports.attribsToString =
|
||||
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
|
||||
export const attribsToString =
|
||||
(attribs: Iterable<Attribute>, pool: AttributePool): string => encodeAttribString(attribsToNums(attribs, pool));
|
||||
|
||||
/**
|
||||
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
|
||||
|
@ -126,5 +129,14 @@ exports.attribsToString =
|
|||
* @param {Attribute[]} attribs - Attributes to sort in place.
|
||||
* @returns {Attribute[]} `attribs` (for chaining).
|
||||
*/
|
||||
exports.sort =
|
||||
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
|
||||
export const sort = (attribs: Attribute[]): Attribute[] => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
|
||||
|
||||
export default {
|
||||
decodeAttribString,
|
||||
encodeAttribString,
|
||||
attribsFromNums,
|
||||
attribsToNums,
|
||||
attribsFromString,
|
||||
attribsToString,
|
||||
sort,
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
|
||||
|
||||
/* Copyright 2021 Richard Hansen <rhansen@rhansen.org> */
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -24,8 +25,8 @@
|
|||
|
||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||
const domline = require('./domline').domline;
|
||||
const AttribPool = require('./AttributePool');
|
||||
const Changeset = require('./Changeset');
|
||||
import AttribPool from './AttributePool';
|
||||
import {compose, deserializeOps, inverse, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, splitAttributionLines, splitTextLines, unpack} from './Changeset';
|
||||
const attributes = require('./attributes');
|
||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
|
@ -53,11 +54,11 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
currentRevision: clientVars.collab_client_vars.rev,
|
||||
currentTime: clientVars.collab_client_vars.time,
|
||||
currentLines:
|
||||
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
||||
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),
|
||||
alines: Changeset.splitAttributionLines(
|
||||
alines: splitAttributionLines(
|
||||
clientVars.collab_client_vars.initialAttributedText.attribs,
|
||||
clientVars.collab_client_vars.initialAttributedText.text),
|
||||
|
||||
|
@ -120,7 +121,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
getActiveAuthors() {
|
||||
const authorIds = new Set();
|
||||
for (const aline of this.alines) {
|
||||
for (const op of Changeset.deserializeOps(aline)) {
|
||||
for (const op of deserializeOps(aline)) {
|
||||
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
|
||||
if (k !== 'author') continue;
|
||||
if (v) authorIds.add(v);
|
||||
|
@ -141,7 +142,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
const oldAlines = padContents.alines.slice();
|
||||
try {
|
||||
// must mutate attribution lines before text lines
|
||||
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
||||
mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
}
|
||||
|
@ -163,7 +164,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
// some chars are replaced (no attributes change and no length change)
|
||||
// test if there are keep ops at the start of the cs
|
||||
if (lineChanged === undefined) {
|
||||
const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops);
|
||||
const [op] = deserializeOps(unpack(changeset).ops);
|
||||
lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
|
||||
}
|
||||
|
||||
|
@ -183,7 +184,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
goToLineNumber(lineChanged);
|
||||
}
|
||||
|
||||
Changeset.mutateTextLines(changeset, padContents);
|
||||
mutateTextLines(changeset, padContents);
|
||||
padContents.currentRevision = revision;
|
||||
padContents.currentTime += timeDelta * 1000;
|
||||
|
||||
|
@ -272,7 +273,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
let changeset = cs[0];
|
||||
let timeDelta = path.times[0];
|
||||
for (let i = 1; i < cs.length; i++) {
|
||||
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
|
||||
changeset = compose(changeset, cs[i], padContents.apool);
|
||||
timeDelta += path.times[i];
|
||||
}
|
||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||
|
@ -290,7 +291,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
let changeset = cs[0];
|
||||
let timeDelta = path.times[0];
|
||||
for (let i = 1; i < cs.length; i++) {
|
||||
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
|
||||
changeset = compose(changeset, cs[i], padContents.apool);
|
||||
timeDelta += path.times[i];
|
||||
}
|
||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||
|
@ -396,9 +397,9 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
|
||||
// debugLog("adding changeset:", astart, aend);
|
||||
const forwardcs =
|
||||
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
||||
moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
||||
const backwardcs =
|
||||
Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
|
||||
moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
|
||||
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
|
||||
}
|
||||
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
|
||||
|
@ -408,13 +409,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
obj = obj.data;
|
||||
|
||||
if (obj.type === 'NEW_CHANGES') {
|
||||
const changeset = Changeset.moveOpsToNewPool(
|
||||
const changeset = moveOpsToNewPool(
|
||||
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
let changesetBack = Changeset.inverse(
|
||||
let changesetBack = inverse(
|
||||
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
||||
|
||||
changesetBack = Changeset.moveOpsToNewPool(
|
||||
changesetBack = moveOpsToNewPool(
|
||||
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -22,17 +23,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
const AttributePool = require('./AttributePool');
|
||||
const Changeset = require('./Changeset');
|
||||
import AttributeMap from './AttributeMap';
|
||||
import AttributePool from './AttributePool';
|
||||
import {applyToAText, checkRep, cloneAText, compose, deserializeOps, follow, identity, isIdentity, makeAText, moveOpsToNewPool, newLen, pack, prepareForWire, unpack} from './Changeset';
|
||||
import {MergingOpAssembler} from "./MergingOpAssembler";
|
||||
|
||||
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||
// latest official text from server
|
||||
let baseAText = Changeset.makeAText('\n');
|
||||
let baseAText = makeAText('\n');
|
||||
// changes applied to baseText that have been submitted
|
||||
let submittedChangeset = null;
|
||||
// changes applied to submittedChangeset since it was prepared
|
||||
let userChangeset = Changeset.identity(1);
|
||||
let userChangeset = identity(1);
|
||||
// is the changesetTracker enabled
|
||||
let tracking = false;
|
||||
// stack state flag so that when we change the rep we don't
|
||||
|
@ -66,18 +68,18 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
return self = {
|
||||
isTracking: () => tracking,
|
||||
setBaseText: (text) => {
|
||||
self.setBaseAttributedText(Changeset.makeAText(text), null);
|
||||
self.setBaseAttributedText(makeAText(text), null);
|
||||
},
|
||||
setBaseAttributedText: (atext, apoolJsonObj) => {
|
||||
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
|
||||
tracking = true;
|
||||
baseAText = Changeset.cloneAText(atext);
|
||||
baseAText = cloneAText(atext);
|
||||
if (apoolJsonObj) {
|
||||
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
||||
baseAText.attribs = moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
||||
}
|
||||
submittedChangeset = null;
|
||||
userChangeset = Changeset.identity(atext.text.length);
|
||||
userChangeset = identity(atext.text.length);
|
||||
applyingNonUserChanges = true;
|
||||
try {
|
||||
callbacks.setDocumentAttributedText(atext);
|
||||
|
@ -89,8 +91,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
composeUserChangeset: (c) => {
|
||||
if (!tracking) return;
|
||||
if (applyingNonUserChanges) return;
|
||||
if (Changeset.isIdentity(c)) return;
|
||||
userChangeset = Changeset.compose(userChangeset, c, apool);
|
||||
if (isIdentity(c)) return;
|
||||
userChangeset = compose(userChangeset, c, apool);
|
||||
|
||||
setChangeCallbackTimeout();
|
||||
},
|
||||
|
@ -100,23 +102,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
|
||||
if (apoolJsonObj) {
|
||||
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
|
||||
c = moveOpsToNewPool(c, wireApool, apool);
|
||||
}
|
||||
|
||||
baseAText = Changeset.applyToAText(c, baseAText, apool);
|
||||
baseAText = applyToAText(c, baseAText, apool);
|
||||
|
||||
let c2 = c;
|
||||
if (submittedChangeset) {
|
||||
const oldSubmittedChangeset = submittedChangeset;
|
||||
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
|
||||
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
|
||||
submittedChangeset = follow(c, oldSubmittedChangeset, false, apool);
|
||||
c2 = follow(oldSubmittedChangeset, c, true, apool);
|
||||
}
|
||||
|
||||
const preferInsertingAfterUserChanges = true;
|
||||
const oldUserChangeset = userChangeset;
|
||||
userChangeset = Changeset.follow(
|
||||
userChangeset = follow(
|
||||
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
|
||||
const postChange = Changeset.follow(
|
||||
const postChange = follow(
|
||||
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
|
||||
|
||||
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
|
||||
|
@ -135,17 +137,17 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
if (submittedChangeset) {
|
||||
// submission must have been canceled, prepare new changeset
|
||||
// that includes old submittedChangeset
|
||||
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
|
||||
toSubmit = compose(submittedChangeset, userChangeset, apool);
|
||||
} else {
|
||||
// Get my authorID
|
||||
const authorId = parent.parent.pad.myUserInfo.userId;
|
||||
|
||||
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
|
||||
// text was copied from another author.
|
||||
const cs = Changeset.unpack(userChangeset);
|
||||
const assem = Changeset.mergingOpAssembler();
|
||||
const cs = unpack(userChangeset);
|
||||
const assem = new MergingOpAssembler();
|
||||
|
||||
for (const op of Changeset.deserializeOps(cs.ops)) {
|
||||
for (const op of deserializeOps(cs.ops)) {
|
||||
if (op.opcode === '+') {
|
||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||
const oldAuthorId = attribs.get('author');
|
||||
|
@ -157,23 +159,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
assem.append(op);
|
||||
}
|
||||
assem.endDocument();
|
||||
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
||||
Changeset.checkRep(userChangeset);
|
||||
userChangeset = pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
||||
checkRep(userChangeset);
|
||||
|
||||
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
|
||||
if (isIdentity(userChangeset)) toSubmit = null;
|
||||
else toSubmit = userChangeset;
|
||||
}
|
||||
|
||||
let cs = null;
|
||||
if (toSubmit) {
|
||||
submittedChangeset = toSubmit;
|
||||
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
|
||||
userChangeset = identity(newLen(toSubmit));
|
||||
|
||||
cs = toSubmit;
|
||||
}
|
||||
let wireApool = null;
|
||||
if (cs) {
|
||||
const forWire = Changeset.prepareForWire(cs, apool);
|
||||
const forWire = prepareForWire(cs, apool);
|
||||
wireApool = forWire.pool.toJsonable();
|
||||
cs = forWire.translated;
|
||||
}
|
||||
|
@ -190,13 +192,13 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
|
||||
}
|
||||
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
|
||||
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
|
||||
baseAText = applyToAText(submittedChangeset, baseAText, apool);
|
||||
submittedChangeset = null;
|
||||
},
|
||||
setUserChangeNotificationCallback: (callback) => {
|
||||
changeCallback = callback;
|
||||
},
|
||||
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
|
||||
hasUncommittedChanges: () => !!(submittedChangeset || (!isIdentity(userChangeset))),
|
||||
};
|
||||
};
|
||||
|
5
src/static/js/chat.js → src/static/js/chat.ts
Executable file → Normal file
5
src/static/js/chat.js → src/static/js/chat.ts
Executable file → Normal file
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
/**
|
||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||
|
@ -15,8 +16,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const ChatMessage = require('./ChatMessage');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import ChatMessage from './ChatMessage';
|
||||
import padutils from './pad_utils'
|
||||
const padcookie = require('./pad_cookie').padcookie;
|
||||
const Tinycon = require('tinycon/tinycon');
|
||||
const hooks = require('./pluginfw/hooks');
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,5 @@
|
|||
// @ts-nocheck
|
||||
|
||||
'use strict';
|
||||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
|
@ -8,6 +10,8 @@
|
|||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
|
||||
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
||||
// %APPJET%: import("etherpad.admin.plugins");
|
||||
import Op from "./Op";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -26,9 +30,10 @@
|
|||
|
||||
const _MAX_LIST_LEVEL = 16;
|
||||
|
||||
const AttributeMap = require('./AttributeMap');
|
||||
const UNorm = require('unorm');
|
||||
const Changeset = require('./Changeset');
|
||||
import AttributeMap from './AttributeMap';
|
||||
import UNorm from 'unorm';
|
||||
import {subattribution} from './Changeset';
|
||||
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
||||
|
@ -83,14 +88,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
const textArray = [];
|
||||
const attribsArray = [];
|
||||
let attribsBuilder = null;
|
||||
const op = new Changeset.Op('+');
|
||||
const op = new Op('+');
|
||||
const self = {
|
||||
length: () => textArray.length,
|
||||
atColumnZero: () => textArray[textArray.length - 1] === '',
|
||||
startNew: () => {
|
||||
textArray.push('');
|
||||
self.flush(true);
|
||||
attribsBuilder = Changeset.smartOpAssembler();
|
||||
attribsBuilder = new SmartOpAssembler();
|
||||
},
|
||||
textOfLine: (i) => textArray[i],
|
||||
appendText: (txt, attrString = '') => {
|
||||
|
@ -653,8 +658,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
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);
|
||||
newAttribStrings.push(subattribution(oldAttribString, 0, lengthToTake));
|
||||
oldAttribString = subattribution(oldAttribString, lengthToTake);
|
||||
}
|
||||
if (oldString.length > 0) {
|
||||
newStrings.push(oldString);
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -30,12 +31,13 @@
|
|||
// requires: plugins
|
||||
// requires: undefined
|
||||
|
||||
const Changeset = require('./Changeset');
|
||||
const attributes = require('./attributes');
|
||||
import {deserializeOps} from './Changeset';
|
||||
import attributes from './attributes';
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const linestylefilter = {};
|
||||
const AttributeManager = require('./AttributeManager');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import padutils from './pad_utils'
|
||||
import Op from "./Op";
|
||||
|
||||
linestylefilter.ATTRIB_CLASSES = {
|
||||
bold: 'tag:b',
|
||||
|
@ -98,12 +100,12 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
|
|||
return classes.substring(1);
|
||||
};
|
||||
|
||||
const attrOps = Changeset.deserializeOps(aline);
|
||||
const attrOps = deserializeOps(aline);
|
||||
let attrOpsNext = attrOps.next();
|
||||
let nextOp, nextOpClasses;
|
||||
|
||||
const goNextOp = () => {
|
||||
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
|
||||
nextOp = attrOpsNext.done ? new Op() : attrOpsNext.value;
|
||||
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
|
||||
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -33,7 +34,8 @@ require('./vendors/gritter');
|
|||
|
||||
import html10n from './vendors/html10n'
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
import {Cookies} from "./pad_utils";
|
||||
|
||||
const chat = require('./chat').chat;
|
||||
const getCollabClient = require('./collab_client').getCollabClient;
|
||||
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
|
||||
|
@ -44,9 +46,9 @@ const padimpexp = require('./pad_impexp').padimpexp;
|
|||
const padmodals = require('./pad_modals').padmodals;
|
||||
const padsavedrevs = require('./pad_savedrevs');
|
||||
const paduserlist = require('./pad_userlist').paduserlist;
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import padutils from './pad_utils'
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
const randomString = require('./pad_utils').randomString;
|
||||
import {randomString} from "./pad_utils";
|
||||
const socketio = require('./socketio');
|
||||
|
||||
const hooks = require('./pluginfw/hooks');
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
import html10n from './vendors/html10n';
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -16,7 +17,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
import {Cookies} from "./pad_utils";
|
||||
|
||||
exports.padcookie = new class {
|
||||
constructor() {
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -24,7 +25,7 @@
|
|||
|
||||
const browser = require('./vendors/browser');
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import padutils from "./pad_utils";
|
||||
const padeditor = require('./pad_editor').padeditor;
|
||||
const padsavedrevs = require('./pad_savedrevs');
|
||||
const _ = require('underscore');
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
|
@ -21,9 +22,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Cookies = require('./pad_utils').Cookies;
|
||||
import padutils,{Cookies} from "./pad_utils";
|
||||
const padcookie = require('./pad_cookie').padcookie;
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
const Ace2Editor = require('./ace').Ace2Editor;
|
||||
import html10n from '../js/vendors/html10n'
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -16,7 +17,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const padutils = require('./pad_utils').padutils;
|
||||
import padutils from './pad_utils'
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
import html10n from './vendors/html10n';
|
||||
let myUserInfo = {};
|
|
@ -6,6 +6,8 @@
|
|||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||
*/
|
||||
|
||||
import {binarySearch} from "./ace2_common";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -22,13 +24,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Security = require('./security');
|
||||
const Security = require('security');
|
||||
import jsCookie, {CookiesStatic} from 'js-cookie'
|
||||
|
||||
/**
|
||||
* Generates a random String with the given length. Is needed to generate the Author, Group,
|
||||
* readonly, session Ids
|
||||
*/
|
||||
const randomString = (len) => {
|
||||
export const randomString = (len?: number) => {
|
||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
let randomstring = '';
|
||||
len = len || 20;
|
||||
|
@ -91,7 +94,35 @@ const urlRegex = (() => {
|
|||
// https://stackoverflow.com/a/68957976
|
||||
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
|
||||
|
||||
const padutils = {
|
||||
type PadEvent = {
|
||||
which: number
|
||||
}
|
||||
|
||||
type JQueryNode = JQuery<HTMLElement>
|
||||
|
||||
class PadUtils {
|
||||
public urlRegex: RegExp
|
||||
public wordCharRegex: RegExp
|
||||
public warnDeprecatedFlags: {
|
||||
disabledForTestingOnly: boolean,
|
||||
_rl?: {
|
||||
prevs: Map<string, number>,
|
||||
now: () => number,
|
||||
period: number
|
||||
}
|
||||
logger?: any
|
||||
}
|
||||
public globalExceptionHandler: null | any = null;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.warnDeprecatedFlags = {
|
||||
disabledForTestingOnly: false
|
||||
}
|
||||
this.wordCharRegex = wordCharRegex
|
||||
this.urlRegex = urlRegex
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
|
||||
* is using the deprecated function).
|
||||
|
@ -107,41 +138,41 @@ const padutils = {
|
|||
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
|
||||
* logger is set), with a stack trace appended if available.
|
||||
*/
|
||||
warnDeprecated: (...args) => {
|
||||
if (padutils.warnDeprecated.disabledForTestingOnly) return;
|
||||
warnDeprecated = (...args: any[]) => {
|
||||
if (this.warnDeprecatedFlags.disabledForTestingOnly) return;
|
||||
const err = new Error();
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
|
||||
if (Error.captureStackTrace) Error.captureStackTrace(err, this.warnDeprecated);
|
||||
err.name = '';
|
||||
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
|
||||
if (typeof err.stack === 'string') {
|
||||
if (padutils.warnDeprecated._rl == null) {
|
||||
padutils.warnDeprecated._rl =
|
||||
if (this.warnDeprecatedFlags._rl == null) {
|
||||
this.warnDeprecatedFlags._rl =
|
||||
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
|
||||
}
|
||||
const rl = padutils.warnDeprecated._rl;
|
||||
const rl = this.warnDeprecatedFlags._rl;
|
||||
const now = rl.now();
|
||||
const prev = rl.prevs.get(err.stack);
|
||||
if (prev != null && now - prev < rl.period) return;
|
||||
rl.prevs.set(err.stack, now);
|
||||
}
|
||||
if (err.stack) args.push(err.stack);
|
||||
(padutils.warnDeprecated.logger || console).warn(...args);
|
||||
},
|
||||
|
||||
escapeHtml: (x) => Security.escapeHTML(String(x)),
|
||||
uniqueId: () => {
|
||||
(this.warnDeprecatedFlags.logger || console).warn(...args);
|
||||
}
|
||||
escapeHtml = (x: string) => Security.escapeHTML(String(x))
|
||||
uniqueId = () => {
|
||||
const pad = require('./pad').pad; // Sidestep circular dependency
|
||||
// returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits
|
||||
const encodeNum =
|
||||
(n, width) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
|
||||
(n: number, width: number) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
|
||||
return [
|
||||
pad.getClientIp(),
|
||||
encodeNum(+new Date(), 7),
|
||||
encodeNum(Math.floor(Math.random() * 1e9), 4),
|
||||
].join('.');
|
||||
},
|
||||
}
|
||||
|
||||
// e.g. "Thu Jun 18 2009 13:09"
|
||||
simpleDateTime: (date) => {
|
||||
simpleDateTime = (date: string) => {
|
||||
const d = new Date(+date); // accept either number or date
|
||||
const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
|
||||
const month = ([
|
||||
|
@ -162,16 +193,14 @@ const padutils = {
|
|||
const year = d.getFullYear();
|
||||
const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`;
|
||||
return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`;
|
||||
},
|
||||
wordCharRegex,
|
||||
urlRegex,
|
||||
}
|
||||
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
|
||||
findURLs: (text) => {
|
||||
findURLs = (text: string) => {
|
||||
// Copy padutils.urlRegex so that the use of .exec() below (which mutates the RegExp object)
|
||||
// does not break other concurrent uses of padutils.urlRegex.
|
||||
const urlRegex = new RegExp(padutils.urlRegex, 'g');
|
||||
const urlRegex = new RegExp(this.urlRegex, 'g');
|
||||
urlRegex.lastIndex = 0;
|
||||
let urls = null;
|
||||
let urls: [number, string][] | null = null;
|
||||
let execResult;
|
||||
// TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped.
|
||||
while ((execResult = urlRegex.exec(text))) {
|
||||
|
@ -181,18 +210,19 @@ const padutils = {
|
|||
urls.push([startIndex, url]);
|
||||
}
|
||||
return urls;
|
||||
},
|
||||
escapeHtmlWithClickableLinks: (text, target) => {
|
||||
}
|
||||
escapeHtmlWithClickableLinks = (text: string, target: string) => {
|
||||
let idx = 0;
|
||||
const pieces = [];
|
||||
const urls = padutils.findURLs(text);
|
||||
const urls = this.findURLs(text);
|
||||
|
||||
const advanceTo = (i) => {
|
||||
const advanceTo = (i: number) => {
|
||||
if (i > idx) {
|
||||
pieces.push(Security.escapeHTML(text.substring(idx, i)));
|
||||
idx = i;
|
||||
}
|
||||
};
|
||||
}
|
||||
;
|
||||
if (urls) {
|
||||
for (let j = 0; j < urls.length; j++) {
|
||||
const startIndex = urls[j][0];
|
||||
|
@ -217,14 +247,14 @@ const padutils = {
|
|||
}
|
||||
advanceTo(text.length);
|
||||
return pieces.join('');
|
||||
},
|
||||
bindEnterAndEscape: (node, onEnter, onEscape) => {
|
||||
}
|
||||
bindEnterAndEscape = (node: JQueryNode, onEnter: Function, onEscape: Function) => {
|
||||
// Use keypress instead of keyup in bindEnterAndEscape. Keyup event is fired on enter in IME
|
||||
// (Input Method Editor), But keypress is not. So, I changed to use keypress instead of keyup.
|
||||
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
|
||||
// 3.6.10, Chrome 6.0.472, Safari 5.0).
|
||||
if (onEnter) {
|
||||
node.on('keypress', (evt) => {
|
||||
node.on('keypress', (evt: { which: number; }) => {
|
||||
if (evt.which === 13) {
|
||||
onEnter(evt);
|
||||
}
|
||||
|
@ -238,13 +268,15 @@ const padutils = {
|
|||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
timediff: (d) => {
|
||||
}
|
||||
|
||||
timediff = (d: number) => {
|
||||
const pad = require('./pad').pad; // Sidestep circular dependency
|
||||
const format = (n, word) => {
|
||||
const format = (n: number, word: string) => {
|
||||
n = Math.round(n);
|
||||
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
|
||||
};
|
||||
}
|
||||
;
|
||||
d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000);
|
||||
if (d < 60) {
|
||||
return format(d, 'second');
|
||||
|
@ -259,13 +291,14 @@ const padutils = {
|
|||
}
|
||||
d /= 24;
|
||||
return format(d, 'day');
|
||||
},
|
||||
makeAnimationScheduler: (funcToAnimateOneStep, stepTime, stepsAtOnce) => {
|
||||
}
|
||||
makeAnimationScheduler =
|
||||
(funcToAnimateOneStep: any, stepTime: number, stepsAtOnce?: number) => {
|
||||
if (stepsAtOnce === undefined) {
|
||||
stepsAtOnce = 1;
|
||||
}
|
||||
|
||||
let animationTimer = null;
|
||||
let animationTimer: any = null;
|
||||
|
||||
const scheduleAnimation = () => {
|
||||
if (!animationTimer) {
|
||||
|
@ -285,14 +318,18 @@ const padutils = {
|
|||
}
|
||||
};
|
||||
return {scheduleAnimation};
|
||||
},
|
||||
makeFieldLabeledWhenEmpty: (field, labelText) => {
|
||||
}
|
||||
|
||||
makeFieldLabeledWhenEmpty
|
||||
=
|
||||
(field: JQueryNode, labelText: string) => {
|
||||
field = $(field);
|
||||
|
||||
const clear = () => {
|
||||
field.addClass('editempty');
|
||||
field.val(labelText);
|
||||
};
|
||||
}
|
||||
;
|
||||
field.focus(() => {
|
||||
if (field.hasClass('editempty')) {
|
||||
field.val('');
|
||||
|
@ -307,30 +344,36 @@ const padutils = {
|
|||
return {
|
||||
clear,
|
||||
};
|
||||
},
|
||||
getCheckbox: (node) => $(node).is(':checked'),
|
||||
setCheckbox: (node, value) => {
|
||||
}
|
||||
getCheckbox = (node: string) => $(node).is(':checked')
|
||||
setCheckbox =
|
||||
(node: JQueryNode, value: boolean) => {
|
||||
if (value) {
|
||||
$(node).attr('checked', 'checked');
|
||||
} else {
|
||||
$(node).prop('checked', false);
|
||||
}
|
||||
},
|
||||
bindCheckboxChange: (node, func) => {
|
||||
$(node).on('change', func);
|
||||
},
|
||||
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => {
|
||||
}
|
||||
bindCheckboxChange =
|
||||
(node: JQueryNode, func: Function) => {
|
||||
// @ts-ignore
|
||||
$(node).on("change", func);
|
||||
}
|
||||
encodeUserId =
|
||||
(userId: string) => userId.replace(/[^a-y0-9]/g, (c) => {
|
||||
if (c === '.') return '-';
|
||||
return `z${c.charCodeAt(0)}z`;
|
||||
}),
|
||||
decodeUserId: (encodedUserId) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
|
||||
if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') {
|
||||
})
|
||||
decodeUserId =
|
||||
(encodedUserId: string) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
|
||||
if (cc === '-') {
|
||||
return '.';
|
||||
} else if (cc.charAt(0) === 'z') {
|
||||
return String.fromCharCode(Number(cc.slice(1, -1)));
|
||||
} else {
|
||||
return cc;
|
||||
}
|
||||
}),
|
||||
|
||||
})
|
||||
/**
|
||||
* Returns whether a string has the expected format to be used as a secret token identifying an
|
||||
* author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648
|
||||
|
@ -340,23 +383,21 @@ const padutils = {
|
|||
* conditional transformation of a token to a database key in a way that does not allow a
|
||||
* malicious user to impersonate another user).
|
||||
*/
|
||||
isValidAuthorToken: (t) => {
|
||||
isValidAuthorToken = (t: string | object) => {
|
||||
if (typeof t !== 'string' || !t.startsWith('t.')) return false;
|
||||
const v = t.slice(2);
|
||||
return v.length > 0 && base64url.test(v);
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
|
||||
* particular author.
|
||||
*/
|
||||
generateAuthorToken: () => `t.${randomString()}`,
|
||||
};
|
||||
|
||||
let globalExceptionHandler = null;
|
||||
padutils.setupGlobalExceptionHandler = () => {
|
||||
if (globalExceptionHandler == null) {
|
||||
globalExceptionHandler = (e) => {
|
||||
generateAuthorToken = () => `t.${randomString()}`
|
||||
setupGlobalExceptionHandler = () => {
|
||||
if (this.globalExceptionHandler == null) {
|
||||
this.globalExceptionHandler = (e: any) => {
|
||||
let type;
|
||||
let err;
|
||||
let msg, url, linenumber;
|
||||
|
@ -399,6 +440,7 @@ padutils.setupGlobalExceptionHandler = () => {
|
|||
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
|
||||
];
|
||||
|
||||
// @ts-ignore
|
||||
$.gritter.add({
|
||||
title: 'An error occurred',
|
||||
text: errorMsg,
|
||||
|
@ -423,26 +465,27 @@ padutils.setupGlobalExceptionHandler = () => {
|
|||
});
|
||||
};
|
||||
window.onerror = null; // Clear any pre-existing global error handler.
|
||||
window.addEventListener('error', globalExceptionHandler);
|
||||
window.addEventListener('unhandledrejection', globalExceptionHandler);
|
||||
window.addEventListener('error', this.globalExceptionHandler);
|
||||
window.addEventListener('unhandledrejection', this.globalExceptionHandler);
|
||||
}
|
||||
}
|
||||
binarySearch = binarySearch
|
||||
}
|
||||
};
|
||||
|
||||
padutils.binarySearch = require('./ace2_common').binarySearch;
|
||||
|
||||
// https://stackoverflow.com/a/42660748
|
||||
const inThirdPartyIframe = () => {
|
||||
try {
|
||||
return (!window.top.location.hostname);
|
||||
return (!window.top!.location.hostname);
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export let Cookies: CookiesStatic<string>
|
||||
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
||||
// window object.
|
||||
if (typeof window !== 'undefined') {
|
||||
exports.Cookies = require('js-cookie').withAttributes({
|
||||
Cookies = jsCookie.withAttributes({
|
||||
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
||||
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
||||
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
||||
|
@ -455,5 +498,5 @@ if (typeof window !== 'undefined') {
|
|||
secure: window.location.protocol === 'https:',
|
||||
});
|
||||
}
|
||||
exports.randomString = randomString;
|
||||
exports.padutils = padutils;
|
||||
|
||||
export default new PadUtils()
|
|
@ -44,6 +44,12 @@ export class LinkInstaller {
|
|||
await this.checkLinkedDependencies(installedPlugin)
|
||||
}
|
||||
|
||||
public async installFromGitHub(repository: string) {
|
||||
const installedPlugin = await this.livePluginManager.installFromGithub(repository)
|
||||
this.linkDependency(installedPlugin.name)
|
||||
await this.checkLinkedDependencies(installedPlugin)
|
||||
}
|
||||
|
||||
public async installPlugin(pluginName: string, version?: string) {
|
||||
if (version) {
|
||||
const installedPlugin = await this.livePluginManager.install(pluginName, version);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
const pluginUtils = require('./shared');
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
const pluginDefs = require('./plugin_defs');
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs').promises;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue