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
|
name: Run the backend tests
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
- name: Run the new vitest tests
|
||||||
|
working-directory: src
|
||||||
|
run: pnpm run test:vitest
|
||||||
|
|
||||||
withpluginsLinux:
|
withpluginsLinux:
|
||||||
# run on pushes to any branch
|
# run on pushes to any branch
|
||||||
|
@ -142,6 +145,9 @@ jobs:
|
||||||
-
|
-
|
||||||
name: Run the backend tests
|
name: Run the backend tests
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
- name: Run the new vitest tests
|
||||||
|
working-directory: src
|
||||||
|
run: pnpm run test:vitest
|
||||||
|
|
||||||
withoutpluginsWindows:
|
withoutpluginsWindows:
|
||||||
# run on pushes to any branch
|
# 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"
|
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
|
||||||
-
|
-
|
||||||
name: Run the backend tests
|
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:
|
withpluginsWindows:
|
||||||
# run on pushes to any branch
|
# 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"
|
powershell -Command "(gc settings.json.holder) -replace '\"points\": 10', '\"points\": 1000' | Out-File -encoding ASCII settings.json"
|
||||||
-
|
-
|
||||||
name: Run the backend tests
|
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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pnpm-store-
|
${{ runner.os }}-pnpm-store-
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
|
6
.github/workflows/frontend-tests.yml
vendored
6
.github/workflows/frontend-tests.yml
vendored
|
@ -57,7 +57,7 @@ jobs:
|
||||||
name: Create settings.json
|
name: Create settings.json
|
||||||
run: cp ./src/tests/settings.json settings.json
|
run: cp ./src/tests/settings.json settings.json
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
@ -127,7 +127,7 @@ jobs:
|
||||||
- name: Create settings.json
|
- name: Create settings.json
|
||||||
run: cp ./src/tests/settings.json settings.json
|
run: cp ./src/tests/settings.json settings.json
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
|
@ -175,7 +175,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
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
|
# 2.2.2
|
||||||
|
|
||||||
### Notable enhancements and fixes
|
### Notable enhancements and fixes
|
||||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -49,6 +49,14 @@ ARG ETHERPAD_PLUGINS=
|
||||||
# ETHERPAD_LOCAL_PLUGINS="../ep_my_plugin ../ep_another_plugin"
|
# ETHERPAD_LOCAL_PLUGINS="../ep_my_plugin ../ep_another_plugin"
|
||||||
ARG ETHERPAD_LOCAL_PLUGINS=
|
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.
|
# Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats.
|
||||||
# By default, it is not installed.
|
# By default, it is not installed.
|
||||||
# If given any value, abiword will be 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
|
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/ templates/admin./src/templates/admin
|
||||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||||
|
|
||||||
RUN bin/installDeps.sh && \
|
RUN bin/installDeps.sh && \
|
||||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
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}}; \
|
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
|
||||||
fi
|
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
|
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 && \
|
RUN bin/installDeps.sh && rm -rf ~/.npm && rm -rf ~/.local && rm -rf ~/.cache && \
|
||||||
if [ ! -z "${ETHERPAD_PLUGINS}" ] || [ ! -z "${ETHERPAD_LOCAL_PLUGINS}" ]; then \
|
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}}; \
|
pnpm run plugins i ${ETHERPAD_PLUGINS} ${ETHERPAD_LOCAL_PLUGINS:+--path ${ETHERPAD_LOCAL_PLUGINS}} ${ETHERPAD_GITHUB_PLUGINS:+--github ${ETHERPAD_GITHUB_PLUGINS}}; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# Copy the configuration file.
|
# Copy the configuration file.
|
||||||
COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json
|
COPY --chown=etherpad:etherpad ${SETTINGS} "${EP_DIR}"/settings.json
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
@ -16,27 +16,27 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@types/react": "^18.3.2",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.4.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.5.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-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.9",
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
"i18next": "^23.12.2",
|
"i18next": "^23.14.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"lucide-react": "^0.426.0",
|
"lucide-react": "^0.438.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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-i18next": "^15.0.1",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.1",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.3",
|
||||||
"vite-plugin-static-copy": "^1.0.6",
|
"vite-plugin-static-copy": "^1.0.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"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);
|
background-color: var(--etherpad-color);
|
||||||
color: white
|
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 {SearchField} from "../components/SearchField.tsx";
|
||||||
import {Download, Trash} from "lucide-react";
|
import {Download, Trash} from "lucide-react";
|
||||||
import {IconButton} from "../components/IconButton.tsx";
|
import {IconButton} from "../components/IconButton.tsx";
|
||||||
|
import {determineSorting} from "../utils/sorting.ts";
|
||||||
|
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const pluginsSocket = useStore(state=>state.pluginsSocket)
|
const pluginsSocket = useStore(state=>state.pluginsSocket)
|
||||||
const [plugins,setPlugins] = useState<PluginDef[]>([])
|
const [plugins,setPlugins] = useState<PluginDef[]>([])
|
||||||
const [installedPlugins, setInstalledPlugins] = useState<InstalledPlugin[]>([])
|
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(()=>{
|
const sortedInstalledPlugins = useMemo(()=>{
|
||||||
return installedPlugins.sort((a, b)=>{
|
return installedPlugins.sort((a, b)=>{
|
||||||
|
|
||||||
if(a.name < b.name){
|
if(a.name < b.name){
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
@ -23,14 +60,8 @@ export const HomePage = () => {
|
||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
} ,[installedPlugins])
|
} ,[installedPlugins, searchParams])
|
||||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
|
||||||
offset: 0,
|
|
||||||
limit: 99999,
|
|
||||||
sortBy: 'name',
|
|
||||||
sortDir: 'asc',
|
|
||||||
searchTerm: ''
|
|
||||||
})
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
|
|
||||||
|
@ -165,16 +196,35 @@ export const HomePage = () => {
|
||||||
<table id="available-plugins">
|
<table id="available-plugins">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<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 style={{width: '30%'}}><Trans i18nKey="admin_plugins.description"/></th>
|
||||||
<th><Trans i18nKey="admin_plugins.version"/></th>
|
<th className={determineSorting(searchParams.sortBy, searchParams.sortDir == "asc", 'version')} onClick={()=>{
|
||||||
<th><Trans i18nKey="admin_plugins.last-update"/></th>
|
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>
|
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody style={{overflow: 'auto'}}>
|
<tbody style={{overflow: 'auto'}}>
|
||||||
{(plugins.length > 0) ?
|
{(filteredInstallablePlugins.length > 0) ?
|
||||||
plugins.map((plugin) => {
|
filteredInstallablePlugins.map((plugin) => {
|
||||||
return <tr key={plugin.name}>
|
return <tr key={plugin.name}>
|
||||||
<td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
|
<td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
|
||||||
<td>{plugin.description}</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')}/>
|
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr className="search-pads">
|
||||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
|
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
...searchParams,
|
...searchParams,
|
||||||
|
@ -136,7 +136,7 @@ export const PadPage = ()=>{
|
||||||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="search-pads-body">
|
||||||
{
|
{
|
||||||
pads?.results?.map((pad)=>{
|
pads?.results?.map((pad)=>{
|
||||||
return <tr key={pad.padName}>
|
return <tr key={pad.padName}>
|
||||||
|
|
|
@ -20,7 +20,7 @@ export type SearchParams = {
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
offset: number,
|
offset: number,
|
||||||
limit: number,
|
limit: number,
|
||||||
sortBy: 'name'|'version',
|
sortBy: 'name'|'version'|'last-updated',
|
||||||
sortDir: 'asc'|'desc'
|
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",
|
"name": "bin",
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "checkAllPads.js",
|
"main": "checkAllPads.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
"doc": "doc"
|
"doc": "doc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.7",
|
||||||
"ep_etherpad-lite": "workspace:../src",
|
"ep_etherpad-lite": "workspace:../src",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"tsx": "^4.17.0",
|
"tsx": "^4.19.0",
|
||||||
"ueberdb2": "^4.2.92"
|
"ueberdb2": "^4.2.100"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.1.0",
|
"@types/node": "^22.5.4",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"makeDocs": "node --import tsx make_docs.ts",
|
||||||
"checkPad": "node --import tsx checkPad.ts",
|
"checkPad": "node --import tsx checkPad.ts",
|
||||||
"checkAllPads": "node --import tsx checkAllPads.ts",
|
"checkAllPads": "node --import tsx checkAllPads.ts",
|
||||||
"createUserSession": "node --import tsx createUserSession.ts",
|
"createUserSession": "node --import tsx createUserSession.ts",
|
||||||
|
@ -33,7 +34,8 @@
|
||||||
"stalePlugins": "node --import tsx ./plugins/stalePlugins.ts",
|
"stalePlugins": "node --import tsx ./plugins/stalePlugins.ts",
|
||||||
"checkPlugin": "node --import tsx ./plugins/checkPlugin.ts",
|
"checkPlugin": "node --import tsx ./plugins/checkPlugin.ts",
|
||||||
"plugins": "node --import tsx ./plugins.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": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
|
|
@ -23,17 +23,13 @@ const possibleActions = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const install = ()=> {
|
const install = ()=> {
|
||||||
|
const argsAsString: string = args.join(" ");
|
||||||
let registryPlugins: string[] = [];
|
const regexRegistryPlugins = /(?<=i\s)(.*?)(?=--github|--path|$)/;
|
||||||
let localPlugins: string[] = [];
|
const regexLocalPlugins = /(?<=--path\s)(.*?)(?=--github|$)/;
|
||||||
|
const regexGithubPlugins = /(?<=--github\s)(.*?)(?=--path|$)/;
|
||||||
if (args.indexOf('--path') !== -1) {
|
const registryPlugins = argsAsString.match(regexRegistryPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||||
const indexToSplit = args.indexOf('--path');
|
const localPlugins = argsAsString.match(regexLocalPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||||
registryPlugins = args.slice(1, indexToSplit);
|
const githubPlugins = argsAsString.match(regexGithubPlugins)?.[0]?.split(" ")?.filter(s => s) || [];
|
||||||
localPlugins = args.slice(indexToSplit + 1);
|
|
||||||
} else {
|
|
||||||
registryPlugins = args;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
for (const plugin of registryPlugins) {
|
for (const plugin of registryPlugins) {
|
||||||
|
@ -53,6 +49,11 @@ const install = ()=> {
|
||||||
console.log(`Installing plugin from path: ${plugin}`);
|
console.log(`Installing plugin from path: ${plugin}`);
|
||||||
await linkInstaller.installFromPath(plugin);
|
await linkInstaller.installFromPath(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const plugin of githubPlugins) {
|
||||||
|
console.log(`Installing plugin from github: ${plugin}`);
|
||||||
|
await linkInstaller.installFromGitHub(plugin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
|
@ -197,7 +197,7 @@ try {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Building documentation...');
|
console.log('Building documentation...');
|
||||||
run('node ./make_docs.js');
|
run('pnpm run makeDocs');
|
||||||
console.log('Updating ether.github.com master branch...');
|
console.log('Updating ether.github.com master branch...');
|
||||||
run('git pull --ff-only', {cwd: '../ether.github.com/'});
|
run('git pull --ff-only', {cwd: '../ether.github.com/'});
|
||||||
console.log('Committing documentation...');
|
console.log('Committing documentation...');
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving 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. */
|
// "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. */
|
// "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. */
|
// "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": {
|
"devDependencies": {
|
||||||
"vitepress": "^1.3.2"
|
"vitepress": "^1.3.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs:dev": "vitepress dev",
|
"docs:dev": "vitepress dev",
|
||||||
|
|
|
@ -9,6 +9,7 @@ services:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
|
# Attention: installed plugins in the node_modules folder get overwritten during volume mount in dev
|
||||||
ETHERPAD_PLUGINS:
|
ETHERPAD_PLUGINS:
|
||||||
# change from development to production if needed
|
# change from development to production if needed
|
||||||
target: development
|
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",
|
"remove-plugins": "pnpm --filter bin run remove-plugins",
|
||||||
"list-plugins": "pnpm --filter bin run list-plugins",
|
"list-plugins": "pnpm --filter bin run list-plugins",
|
||||||
"build:etherpad": "pnpm --filter admin run build-copy && pnpm --filter ui run build-copy",
|
"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": {
|
"dependencies": {
|
||||||
"ep_etherpad-lite": "workspace:./src"
|
"ep_etherpad-lite": "workspace:./src"
|
||||||
|
@ -49,6 +50,6 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ether/etherpad-lite.git"
|
"url": "https://github.com/ether/etherpad-lite.git"
|
||||||
},
|
},
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"license": "Apache-2.0"
|
"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_plugins_info.version_number": "Нумар вэрсіі",
|
||||||
"admin_settings": "Налады",
|
"admin_settings": "Налады",
|
||||||
"admin_settings.current": "Цяперашняя канфігурацыя",
|
"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",
|
"admin_settings.page-title": "Налады — Etherpad",
|
||||||
"index.newPad": "Стварыць",
|
"index.newPad": "Стварыць",
|
||||||
"index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:",
|
"index.createOpenPad": "ці тварыць/адкрыць дакумэнт з назвай:",
|
||||||
|
@ -72,6 +76,7 @@
|
||||||
"pad.settings.fontType.normal": "Звычайны",
|
"pad.settings.fontType.normal": "Звычайны",
|
||||||
"pad.settings.language": "Мова:",
|
"pad.settings.language": "Мова:",
|
||||||
"pad.settings.about": "Пра",
|
"pad.settings.about": "Пра",
|
||||||
|
"pad.settings.poweredBy": "Працуе на",
|
||||||
"pad.importExport.import_export": "Імпарт/Экспарт",
|
"pad.importExport.import_export": "Імпарт/Экспарт",
|
||||||
"pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты",
|
"pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты",
|
||||||
"pad.importExport.importSuccessful": "Пасьпяхова!",
|
"pad.importExport.importSuccessful": "Пасьпяхова!",
|
||||||
|
@ -106,6 +111,9 @@
|
||||||
"pad.modals.corruptPad.cause": "Гэта можа быць выклікана няправільнай канфігурацыяй сэрвэру або іншымі нечаканымі дзеяньнямі. Калі ласка, скантактуйцеся з адміністратарам службы.",
|
"pad.modals.corruptPad.cause": "Гэта можа быць выклікана няправільнай канфігурацыяй сэрвэру або іншымі нечаканымі дзеяньнямі. Калі ласка, скантактуйцеся з адміністратарам службы.",
|
||||||
"pad.modals.deleted": "Выдалены.",
|
"pad.modals.deleted": "Выдалены.",
|
||||||
"pad.modals.deleted.explanation": "Гэты дакумэнт быў выдалены.",
|
"pad.modals.deleted.explanation": "Гэты дакумэнт быў выдалены.",
|
||||||
|
"pad.modals.rateLimited": "Хуткасьць абмежаваная.",
|
||||||
|
"pad.modals.rateLimited.explanation": "Вы адаслалі так шмат паведамленьняў, што гэты дакумэнт вас адключыў.",
|
||||||
|
"pad.modals.rejected.explanation": "Сэрвэр адхіліў паведамленьне, адасланае вашым броўзэрам.",
|
||||||
"pad.modals.disconnected": "Вы былі адключаныя.",
|
"pad.modals.disconnected": "Вы былі адключаныя.",
|
||||||
"pad.modals.disconnected.explanation": "Злучэньне з сэрвэрам было страчанае",
|
"pad.modals.disconnected.explanation": "Злучэньне з сэрвэрам было страчанае",
|
||||||
"pad.modals.disconnected.cause": "Магчыма, сэрвэр недаступны. Калі ласка, паведаміце адміністратару службы, калі праблема будзе паўтарацца.",
|
"pad.modals.disconnected.cause": "Магчыма, сэрвэр недаступны. Калі ласка, паведаміце адміністратару службы, калі праблема будзе паўтарацца.",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"authors": [
|
"authors": [
|
||||||
"Aefgh39622",
|
"Aefgh39622",
|
||||||
"Andibecker",
|
"Andibecker",
|
||||||
|
"Ekminarin",
|
||||||
"Patsagorn Y.",
|
"Patsagorn Y.",
|
||||||
"Trisorn Triboon"
|
"Trisorn Triboon"
|
||||||
]
|
]
|
||||||
|
@ -121,7 +122,7 @@
|
||||||
"pad.share.readonly": "อ่านเท่านั้น",
|
"pad.share.readonly": "อ่านเท่านั้น",
|
||||||
"pad.share.link": "ลิงก์",
|
"pad.share.link": "ลิงก์",
|
||||||
"pad.share.emebdcode": "URL แบบฝังตัว",
|
"pad.share.emebdcode": "URL แบบฝังตัว",
|
||||||
"pad.chat": "แชท",
|
"pad.chat": "แชต",
|
||||||
"pad.chat.title": "เปิดการแชทสำหรับแผ่นจดบันทึกนี้",
|
"pad.chat.title": "เปิดการแชทสำหรับแผ่นจดบันทึกนี้",
|
||||||
"pad.chat.loadmessages": "โหลดข้อความเพิ่มเติม",
|
"pad.chat.loadmessages": "โหลดข้อความเพิ่มเติม",
|
||||||
"pad.chat.stick.title": "ปักการสนทนาไว้บนหน้าจอ",
|
"pad.chat.stick.title": "ปักการสนทนาไว้บนหน้าจอ",
|
||||||
|
|
|
@ -19,8 +19,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps} from '../../static/js/Changeset';
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
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 CustomError = require('../utils/customError');
|
||||||
const padManager = require('./PadManager');
|
const padManager = require('./PadManager');
|
||||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||||
|
@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
const oldText = pad.text();
|
const oldText = pad.text();
|
||||||
atext.text += '\n';
|
atext.text += '\n';
|
||||||
|
|
||||||
const eachAttribRun = (attribs: string[], func:Function) => {
|
const eachAttribRun = (attribs: string, func:Function) => {
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = 0;
|
const newTextStart = 0;
|
||||||
const newTextEnd = atext.text.length;
|
const newTextEnd = atext.text.length;
|
||||||
for (const op of Changeset.deserializeOps(attribs)) {
|
for (const op of deserializeOps(attribs)) {
|
||||||
const nextIndex = textIndex + op.chars;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
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
|
// 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
|
// 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);
|
builder.insert(atext.text.substring(start, end), attribs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
|
|
||||||
const db = require('./DB');
|
const db = require('./DB');
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require('../utils/customError');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
const {randomString, padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
import padutils, {randomString} from "../../static/js/pad_utils";
|
||||||
|
|
||||||
exports.getColorPalette = () => [
|
exports.getColorPalette = () => [
|
||||||
'#ffc7c7',
|
'#ffc7c7',
|
||||||
|
@ -169,7 +169,7 @@ exports.getAuthorId = async (token: string, user: object) => {
|
||||||
* @param {String} token The token
|
* @param {String} token The token
|
||||||
*/
|
*/
|
||||||
exports.getAuthor4Token = async (token: string) => {
|
exports.getAuthor4Token = async (token: string) => {
|
||||||
warnDeprecated(
|
padutils.warnDeprecated(
|
||||||
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
'AuthorManager.getAuthor4Token() is deprecated; use AuthorManager.getAuthorId() instead');
|
||||||
return await getAuthor4Token(token);
|
return await getAuthor4Token(token);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
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 db = require('./DB');
|
||||||
const padManager = require('./PadManager');
|
const padManager = require('./PadManager');
|
||||||
const sessionManager = require('./SessionManager');
|
const sessionManager = require('./SessionManager');
|
||||||
|
|
|
@ -7,10 +7,10 @@ import {MapArrayType} from "../types/MapType";
|
||||||
* The pad object, defined with joose
|
* The pad object, defined with joose
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const AttributeMap = require('../../static/js/AttributeMap');
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {applyToAText, checkRep, copyAText, deserializeOps, makeAText, makeSplice, opsFromAText, pack, unpack} from '../../static/js/Changeset';
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
import ChatMessage from '../../static/js/ChatMessage';
|
||||||
const AttributePool = require('../../static/js/AttributePool');
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
const Stream = require('../utils/Stream');
|
const Stream = require('../utils/Stream');
|
||||||
const assert = require('assert').strict;
|
const assert = require('assert').strict;
|
||||||
const db = require('./DB');
|
const db = require('./DB');
|
||||||
|
@ -23,8 +23,10 @@ const CustomError = require('../utils/customError');
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
const readOnlyManager = require('./ReadOnlyManager');
|
||||||
const randomString = require('../utils/randomstring');
|
const randomString = require('../utils/randomstring');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
const {padutils: {warnDeprecated}} = require('../../static/js/pad_utils');
|
import pad_utils from "../../static/js/pad_utils";
|
||||||
const promises = require('../utils/promises');
|
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
|
* 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 {
|
class Pad {
|
||||||
private db: Database;
|
private db: Database;
|
||||||
private atext: AText;
|
private atext: AText;
|
||||||
private pool: APool;
|
private pool: AttributePool;
|
||||||
private head: number;
|
private head: number;
|
||||||
private chatHead: number;
|
private chatHead: number;
|
||||||
private publicStatus: boolean;
|
private publicStatus: boolean;
|
||||||
|
@ -56,7 +58,7 @@ class Pad {
|
||||||
*/
|
*/
|
||||||
constructor(id:string, database = db) {
|
constructor(id:string, database = db) {
|
||||||
this.db = database;
|
this.db = database;
|
||||||
this.atext = Changeset.makeAText('\n');
|
this.atext = makeAText('\n');
|
||||||
this.pool = new AttributePool();
|
this.pool = new AttributePool();
|
||||||
this.head = -1;
|
this.head = -1;
|
||||||
this.chatHead = -1;
|
this.chatHead = -1;
|
||||||
|
@ -93,13 +95,13 @@ class Pad {
|
||||||
* @param {String} authorId The id of the author
|
* @param {String} authorId The id of the author
|
||||||
* @return {Promise<number|string>}
|
* @return {Promise<number|string>}
|
||||||
*/
|
*/
|
||||||
async appendRevision(aChangeset:AChangeSet, authorId = '') {
|
async appendRevision(aChangeset:string, authorId = '') {
|
||||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
const newAText = applyToAText(aChangeset, this.atext, this.pool);
|
||||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
||||||
this.head !== -1) {
|
this.head !== -1) {
|
||||||
return this.head;
|
return this.head;
|
||||||
}
|
}
|
||||||
Changeset.copyAText(newAText, this.atext);
|
copyAText(newAText, this.atext);
|
||||||
|
|
||||||
const newRev = ++this.head;
|
const newRev = ++this.head;
|
||||||
|
|
||||||
|
@ -126,11 +128,11 @@ class Pad {
|
||||||
pad: this,
|
pad: this,
|
||||||
authorId,
|
authorId,
|
||||||
get author() {
|
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;
|
return this.authorId;
|
||||||
},
|
},
|
||||||
set author(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.authorId = authorId;
|
||||||
},
|
},
|
||||||
...this.head === 0 ? {} : {
|
...this.head === 0 ? {} : {
|
||||||
|
@ -215,7 +217,7 @@ class Pad {
|
||||||
]);
|
]);
|
||||||
const apool = this.apool();
|
const apool = this.apool();
|
||||||
let atext = keyAText;
|
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;
|
return atext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +295,7 @@ class Pad {
|
||||||
(!ins && start > 0 && orig[start - 1] === '\n');
|
(!ins && start > 0 && orig[start - 1] === '\n');
|
||||||
if (!willEndWithNewline) ins += '\n';
|
if (!willEndWithNewline) ins += '\n';
|
||||||
if (ndel === 0 && ins.length === 0) return;
|
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);
|
await this.appendRevision(changeset, authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,7 +332,7 @@ class Pad {
|
||||||
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
* @param {?number} [time] - Message timestamp (milliseconds since epoch). Deprecated; use
|
||||||
* `msgOrText.time` instead.
|
* `msgOrText.time` instead.
|
||||||
*/
|
*/
|
||||||
async appendChatMessage(msgOrText: string|typeof ChatMessage, authorId = null, time = null) {
|
async appendChatMessage(msgOrText: string| ChatMessage, authorId = null, time = null) {
|
||||||
const msg =
|
const msg =
|
||||||
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
msgOrText instanceof ChatMessage ? msgOrText : new ChatMessage(msgOrText, authorId, time);
|
||||||
this.chatHead++;
|
this.chatHead++;
|
||||||
|
@ -393,7 +395,7 @@ class Pad {
|
||||||
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
||||||
text = exports.cleanText(context.content);
|
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 this.appendRevision(firstChangeset, authorId);
|
||||||
}
|
}
|
||||||
await hooks.aCallAll('padLoad', {pad: this});
|
await hooks.aCallAll('padLoad', {pad: this});
|
||||||
|
@ -437,11 +439,11 @@ class Pad {
|
||||||
// let the plugins know the pad was copied
|
// let the plugins know the pad was copied
|
||||||
await hooks.aCallAll('padCopy', {
|
await hooks.aCallAll('padCopy', {
|
||||||
get originalPad() {
|
get originalPad() {
|
||||||
warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
pad_utils.warnDeprecated('padCopy originalPad context property is deprecated; use srcPad instead');
|
||||||
return this.srcPad;
|
return this.srcPad;
|
||||||
},
|
},
|
||||||
get destinationID() {
|
get destinationID() {
|
||||||
warnDeprecated(
|
pad_utils.warnDeprecated(
|
||||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||||
return this.dstPad.id;
|
return this.dstPad.id;
|
||||||
},
|
},
|
||||||
|
@ -520,8 +522,8 @@ class Pad {
|
||||||
const oldAText = this.atext;
|
const oldAText = this.atext;
|
||||||
|
|
||||||
// based on Changeset.makeSplice
|
// based on Changeset.makeSplice
|
||||||
const assem = Changeset.smartOpAssembler();
|
const assem = new SmartOpAssembler();
|
||||||
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
|
for (const op of opsFromAText(oldAText)) assem.append(op);
|
||||||
assem.endDocument();
|
assem.endDocument();
|
||||||
|
|
||||||
// although we have instantiated the dstPad with '\n', an additional '\n' is
|
// 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
|
// create a changeset that removes the previous text and add the newText with
|
||||||
// all atributes present on the source pad
|
// 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);
|
dstPad.appendRevision(changeset, authorId);
|
||||||
|
|
||||||
await hooks.aCallAll('padCopy', {
|
await hooks.aCallAll('padCopy', {
|
||||||
get originalPad() {
|
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;
|
return this.srcPad;
|
||||||
},
|
},
|
||||||
get destinationID() {
|
get destinationID() {
|
||||||
warnDeprecated(
|
pad_utils.warnDeprecated(
|
||||||
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
'padCopy destinationID context property is deprecated; use dstPad.id instead');
|
||||||
return this.dstPad.id;
|
return this.dstPad.id;
|
||||||
},
|
},
|
||||||
|
@ -585,12 +587,14 @@ class Pad {
|
||||||
p.push(db.remove(`pad2readonly:${padID}`));
|
p.push(db.remove(`pad2readonly:${padID}`));
|
||||||
|
|
||||||
// delete all chat messages
|
// 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);
|
await this.db.remove(`pad:${this.id}:chat:${i}`, null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// delete all revisions
|
// 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);
|
await this.db.remove(`pad:${this.id}:revs:${i}`, null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -603,7 +607,7 @@ class Pad {
|
||||||
p.push(padManager.removePad(padID));
|
p.push(padManager.removePad(padID));
|
||||||
p.push(hooks.aCallAll('padRemove', {
|
p.push(hooks.aCallAll('padRemove', {
|
||||||
get padID() {
|
get padID() {
|
||||||
warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
pad_utils.warnDeprecated('padRemove padID context property is deprecated; use pad.id instead');
|
||||||
return this.pad.id;
|
return this.pad.id;
|
||||||
},
|
},
|
||||||
pad: this,
|
pad: this,
|
||||||
|
@ -706,7 +710,7 @@ class Pad {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.batch(100).buffer(99);
|
.batch(100).buffer(99);
|
||||||
let atext = Changeset.makeAText('\n');
|
let atext = makeAText('\n');
|
||||||
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
||||||
try {
|
try {
|
||||||
assert(authorId != null);
|
assert(authorId != null);
|
||||||
|
@ -717,10 +721,10 @@ class Pad {
|
||||||
assert(timestamp > 0);
|
assert(timestamp > 0);
|
||||||
assert(changeset != null);
|
assert(changeset != null);
|
||||||
assert.equal(typeof changeset, 'string');
|
assert.equal(typeof changeset, 'string');
|
||||||
Changeset.checkRep(changeset);
|
checkRep(changeset);
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
let text = atext.text;
|
let text = atext.text;
|
||||||
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
for (const op of deserializeOps(unpacked.ops)) {
|
||||||
if (['=', '-'].includes(op.opcode)) {
|
if (['=', '-'].includes(op.opcode)) {
|
||||||
assert(text.length >= op.chars);
|
assert(text.length >= op.chars);
|
||||||
const consumed = text.slice(0, 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());
|
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);
|
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
||||||
} catch (err:any) {
|
} catch (err:any) {
|
||||||
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
import {UserSettingsObject} from "../types/UserSettingsObject";
|
import {UserSettingsObject} from "../types/UserSettingsObject";
|
||||||
|
|
||||||
const authorManager = require('./AuthorManager');
|
const authorManager = require('./AuthorManager');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
const padManager = require('./PadManager');
|
const padManager = require('./PadManager');
|
||||||
const readOnlyManager = require('./ReadOnlyManager');
|
const readOnlyManager = require('./ReadOnlyManager');
|
||||||
const sessionManager = require('./SessionManager');
|
const sessionManager = require('./SessionManager');
|
||||||
|
@ -30,7 +30,7 @@ const settings = require('../utils/Settings');
|
||||||
const webaccess = require('../hooks/express/webaccess');
|
const webaccess = require('../hooks/express/webaccess');
|
||||||
const log4js = require('log4js');
|
const log4js = require('log4js');
|
||||||
const authLogger = log4js.getLogger('auth');
|
const authLogger = log4js.getLogger('auth');
|
||||||
const {padutils} = require('../../static/js/pad_utils');
|
import padutils from '../../static/js/pad_utils'
|
||||||
|
|
||||||
const DENY = Object.freeze({accessStatus: 'deny'});
|
const DENY = Object.freeze({accessStatus: 'deny'});
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CustomError = require('../utils/customError');
|
const CustomError = require('../utils/customError');
|
||||||
const promises = require('../utils/promises');
|
import {firstSatisfies} from '../utils/promises';
|
||||||
const randomString = require('../utils/randomstring');
|
const randomString = require('../utils/randomstring');
|
||||||
const db = require('./DB');
|
const db = require('./DB');
|
||||||
const groupManager = require('./GroupManager');
|
const groupManager = require('./GroupManager');
|
||||||
|
@ -79,7 +79,7 @@ exports.findAuthorID = async (groupID:string, sessionCookie: string) => {
|
||||||
groupID: string;
|
groupID: string;
|
||||||
validUntil: number;
|
validUntil: number;
|
||||||
}|null) => (si != null && si.groupID === groupID && now < si.validUntil);
|
}|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;
|
if (sessionInfo == null) return undefined;
|
||||||
return sessionInfo.authorID;
|
return sessionInfo.authorID;
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
const ejs = require('ejs');
|
const ejs = require('ejs');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const resolve = require('resolve');
|
const resolve = require('resolve');
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
|
|
|
@ -31,7 +31,7 @@ import os from 'os';
|
||||||
const importHtml = require('../utils/ImportHtml');
|
const importHtml = require('../utils/ImportHtml');
|
||||||
const importEtherpad = require('../utils/ImportEtherpad');
|
const importEtherpad = require('../utils/ImportEtherpad');
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const hooks = require('../../static/js/pluginfw/hooks.js');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
|
|
||||||
const logger = log4js.getLogger('ImportHandler');
|
const logger = log4js.getLogger('ImportHandler');
|
||||||
|
|
||||||
|
|
|
@ -21,28 +21,30 @@
|
||||||
|
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
const AttributeMap = require('../../static/js/AttributeMap');
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require('../db/PadManager');
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {checkRep, cloneAText, compose, deserializeOps, follow, identity, inverse, makeAText, makeSplice, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, prepareForWire, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
|
||||||
const ChatMessage = require('../../static/js/ChatMessage');
|
import ChatMessage from '../../static/js/ChatMessage';
|
||||||
const AttributePool = require('../../static/js/AttributePool');
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
const AttributeManager = require('../../static/js/AttributeManager');
|
const AttributeManager = require('../../static/js/AttributeManager');
|
||||||
const authorManager = require('../db/AuthorManager');
|
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 readOnlyManager = require('../db/ReadOnlyManager');
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
const securityManager = require('../db/SecurityManager');
|
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';
|
import log4js from 'log4js';
|
||||||
const messageLogger = log4js.getLogger('message');
|
const messageLogger = log4js.getLogger('message');
|
||||||
const accessLogger = log4js.getLogger('access');
|
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 stats = require('../stats')
|
||||||
const assert = require('assert').strict;
|
const assert = require('assert').strict;
|
||||||
import {RateLimiterMemory} from 'rate-limiter-flexible';
|
import {RateLimiterMemory} from 'rate-limiter-flexible';
|
||||||
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
|
import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/SocketClientRequest";
|
||||||
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
|
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
|
||||||
import {ChangeSet} from "../types/ChangeSet";
|
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 webaccess = require('../hooks/express/webaccess');
|
||||||
const { checkValidRev } = require('../utils/checkValidRev');
|
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 socket the socket.io Socket object for the client
|
||||||
* @param message the message from 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';
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
if (env === 'production') {
|
if (env === 'production') {
|
||||||
|
@ -348,15 +350,15 @@ exports.handleMessage = async (socket:any, message:typeof ChatMessage) => {
|
||||||
stats.counter('pendingEdits').inc();
|
stats.counter('pendingEdits').inc();
|
||||||
await padChannels.enqueue(thisSession.padId, {socket, message});
|
await padChannels.enqueue(thisSession.padId, {socket, message});
|
||||||
break;
|
break;
|
||||||
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message); break;
|
case 'USERINFO_UPDATE': await handleUserInfoUpdate(socket, message as unknown as UserNewInfoMessage); break;
|
||||||
case 'CHAT_MESSAGE': await handleChatMessage(socket, message); break;
|
case 'CHAT_MESSAGE': await handleChatMessage(socket, message as unknown as ChatMessageMessage); break;
|
||||||
case 'GET_CHAT_MESSAGES': await handleGetChatMessages(socket, message); 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': {
|
case 'CLIENT_MESSAGE': {
|
||||||
const {type} = message.data.payload;
|
const {type} = message.data.payload;
|
||||||
try {
|
try {
|
||||||
switch (type) {
|
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');
|
default: throw new Error('unknown message type');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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 socket the socket.io Socket object for the client
|
||||||
* @param message the message from 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 {padId, author: authorId} = sessioninfos[socket.id];
|
||||||
const pad = await padManager.getPad(padId, null, authorId);
|
const pad = await padManager.getPad(padId, null, authorId);
|
||||||
await pad.addSavedRevision(pad.head, 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 msg {Object} the message we're sending
|
||||||
* @param sessionID {string} the socketIO session to which we're sending this message
|
* @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 (msg.data.type === 'CUSTOM') {
|
||||||
if (sessionID) {
|
if (sessionID) {
|
||||||
// a sessionID is targeted: directly to this 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 socket the socket.io Socket object for the client
|
||||||
* @param message the message from 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 chatMessage = ChatMessage.fromObject(message.data.message);
|
||||||
const {padId, author: authorId} = sessioninfos[socket.id];
|
const {padId, author: authorId} = sessioninfos[socket.id];
|
||||||
// Don't trust the user-supplied values.
|
// 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
|
* @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.
|
* 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);
|
const message = mt instanceof ChatMessage ? mt : new ChatMessage(text, puId, mt);
|
||||||
padId = mt instanceof ChatMessage ? puId : padId;
|
padId = mt instanceof ChatMessage ? puId : padId;
|
||||||
const pad = await padManager.getPad(padId, null, message.authorId);
|
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 socket the socket.io Socket object for the client
|
||||||
* @param message the message from 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;
|
const {newName, unnamedId} = message.data.payload;
|
||||||
if (newName == null) throw new Error('missing newName');
|
if (newName == null) throw new Error('missing newName');
|
||||||
if (unnamedId == null) throw new Error('missing unnamedId');
|
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 socket the socket.io Socket object for the client
|
||||||
* @param message the message from 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 (colorId == null) throw new Error('missing colorId');
|
||||||
if (!name) name = null;
|
if (!name) name = null;
|
||||||
const session = sessioninfos[socket.id];
|
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 socket the socket.io Socket object for the client
|
||||||
* @param message the message from 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
|
// This one's no longer pending, as we're gonna process it now
|
||||||
stats.counter('pendingEdits').dec();
|
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);
|
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
|
||||||
|
|
||||||
// Verify that the changeset has valid syntax and is in canonical form
|
// 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
|
// 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 add text with attribs
|
||||||
// = can change or add attribs
|
// = can change or add attribs
|
||||||
// - can have attribs, but they are discarded and don't show up in the 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
|
// ex. adoptChangesetAttribs
|
||||||
|
|
||||||
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
|
// 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
|
// ex. applyUserChanges
|
||||||
let r = baseRev;
|
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);
|
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
|
||||||
if (changeset === c && thisSession.author === authorId) {
|
if (changeset === c && thisSession.author === authorId) {
|
||||||
// Assume this is a retransmission of an already applied changeset.
|
// 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
|
// At this point, both "c" (from the pad) and "changeset" (from the
|
||||||
// client) are relative to revision r - 1. The follow function
|
// client) are relative to revision r - 1. The follow function
|
||||||
// rebases "changeset" so that it is relative to revision r
|
// rebases "changeset" so that it is relative to revision r
|
||||||
// and can be applied after "c".
|
// 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();
|
const prevText = pad.text();
|
||||||
|
|
||||||
if (Changeset.oldLen(rebasedChangeset) !== prevText.length) {
|
if (oldLen(rebasedChangeset) !== prevText.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
|
`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);
|
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.
|
// Make sure the pad always ends with an empty line.
|
||||||
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
|
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);
|
await pad.appendRevision(nlChangeset, thisSession.author);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -710,7 +714,7 @@ exports.updatePadClients = async (pad: PadType) => {
|
||||||
const revChangeset = revision.changeset;
|
const revChangeset = revision.changeset;
|
||||||
const currentTime = revision.meta.timestamp;
|
const currentTime = revision.meta.timestamp;
|
||||||
|
|
||||||
const forWire = Changeset.prepareForWire(revChangeset, pad.pool);
|
const forWire = prepareForWire(revChangeset, pad.pool);
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'COLLABROOM',
|
type: 'COLLABROOM',
|
||||||
data: {
|
data: {
|
||||||
|
@ -738,14 +742,14 @@ exports.updatePadClients = async (pad: PadType) => {
|
||||||
/**
|
/**
|
||||||
* Copied from the Etherpad Source Code. Don't know what this method does excatly...
|
* Copied from the Etherpad Source Code. Don't know what this method does excatly...
|
||||||
*/
|
*/
|
||||||
const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||||
const text = atext.text;
|
const text = atext.text;
|
||||||
|
|
||||||
// collect char positions of line markers (e.g. bullets) in new atext
|
// collect char positions of line markers (e.g. bullets) in new atext
|
||||||
// that aren't at the start of a line
|
// that aren't at the start of a line
|
||||||
const badMarkers = [];
|
const badMarkers = [];
|
||||||
let offset = 0;
|
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 attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
|
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
|
||||||
if (hasMarker) {
|
if (hasMarker) {
|
||||||
|
@ -767,7 +771,7 @@ const _correctMarkersInPad = (atext: AText, apool: APool) => {
|
||||||
// create changeset that removes these bad markers
|
// create changeset that removes these bad markers
|
||||||
offset = 0;
|
offset = 0;
|
||||||
|
|
||||||
const builder = Changeset.builder(text.length);
|
const builder = new Builder(text.length);
|
||||||
|
|
||||||
badMarkers.forEach((pos) => {
|
badMarkers.forEach((pos) => {
|
||||||
builder.keepText(text.substring(offset, 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 socket the socket.io Socket object for the client
|
||||||
* @param message the message from 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];
|
const sessionInfo = sessioninfos[socket.id];
|
||||||
if (sessionInfo == null) throw new Error('client disconnected');
|
if (sessionInfo == null) throw new Error('client disconnected');
|
||||||
assert(sessionInfo.author);
|
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.
|
await hooks.aCallAll('clientReady', message); // Deprecated due to awkward context.
|
||||||
|
|
||||||
let {colorId: authorColorId, name: authorName} = message.userInfo || {};
|
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}`);
|
messageLogger.warn(`Ignoring invalid colorId in CLIENT_READY message: ${authorColorId}`);
|
||||||
|
// @ts-ignore
|
||||||
authorColorId = null;
|
authorColorId = null;
|
||||||
}
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -872,7 +877,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
const revisionsNeeded = [];
|
const revisionsNeeded = [];
|
||||||
const changesets:MapArrayType<any> = {};
|
const changesets:MapArrayType<any> = {};
|
||||||
|
|
||||||
let startNum = message.client_rev + 1;
|
let startNum = message.client_rev! + 1;
|
||||||
let endNum = pad.getHeadRevisionNumber() + 1;
|
let endNum = pad.getHeadRevisionNumber() + 1;
|
||||||
|
|
||||||
const headNum = pad.getHeadRevisionNumber();
|
const headNum = pad.getHeadRevisionNumber();
|
||||||
|
@ -901,7 +906,7 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
|
|
||||||
// return pending changesets
|
// return pending changesets
|
||||||
for (const r of revisionsNeeded) {
|
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',
|
const wireMsg = {type: 'COLLABROOM',
|
||||||
data: {type: 'CLIENT_RECONNECT',
|
data: {type: 'CLIENT_RECONNECT',
|
||||||
headRev: pad.getHeadRevisionNumber(),
|
headRev: pad.getHeadRevisionNumber(),
|
||||||
|
@ -926,8 +931,8 @@ const handleClientReady = async (socket:any, message: typeof ChatMessage) => {
|
||||||
let apool;
|
let apool;
|
||||||
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
|
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
|
||||||
try {
|
try {
|
||||||
atext = Changeset.cloneAText(pad.atext);
|
atext = cloneAText(pad.atext);
|
||||||
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
const attribsForWire = prepareForWire(atext.attribs, pad.pool);
|
||||||
apool = attribsForWire.pool.toJsonable();
|
apool = attribsForWire.pool.toJsonable();
|
||||||
atext.attribs = attribsForWire.translated;
|
atext.attribs = attribsForWire.translated;
|
||||||
} catch (e:any) {
|
} catch (e:any) {
|
||||||
|
@ -1163,13 +1168,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
||||||
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
|
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
|
||||||
|
|
||||||
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
|
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());
|
mutateAttributionLines(forwards, lines.alines, pad.apool());
|
||||||
Changeset.mutateTextLines(forwards, lines.textlines);
|
mutateTextLines(forwards, lines.textlines);
|
||||||
|
|
||||||
const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
|
const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool);
|
||||||
const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
|
const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool);
|
||||||
|
|
||||||
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
|
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
|
||||||
const t2 = revisionDate[compositeEnd - 1];
|
const t2 = revisionDate[compositeEnd - 1];
|
||||||
|
@ -1195,12 +1200,12 @@ const getPadLines = async (pad: PadType, revNum: number) => {
|
||||||
if (revNum >= 0) {
|
if (revNum >= 0) {
|
||||||
atext = await pad.getInternalRevisionAText(revNum);
|
atext = await pad.getInternalRevisionAText(revNum);
|
||||||
} else {
|
} else {
|
||||||
atext = Changeset.makeAText('\n');
|
atext = makeAText('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
textlines: Changeset.splitTextLines(atext.text),
|
textlines: splitTextLines(atext.text),
|
||||||
alines: Changeset.splitAttributionLines(atext.attribs, 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++) {
|
for (r = startNum + 1; r < endNum; r++) {
|
||||||
const cs = changesets[r];
|
const cs = changesets[r];
|
||||||
changeset = Changeset.compose(changeset, cs, pool);
|
changeset = compose(changeset as string, cs as string, pool);
|
||||||
}
|
}
|
||||||
return changeset;
|
return changeset;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -324,7 +324,7 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
|
||||||
|
|
||||||
// serve index.html under /
|
// serve index.html under /
|
||||||
args.app.get('/', (req: any, res: any) => {
|
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,
|
req,
|
||||||
toolbar,
|
toolbar,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
entrypoint: "/"+fileNamePad
|
entrypoint: "../"+fileNamePad
|
||||||
})
|
})
|
||||||
res.send(content);
|
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', {
|
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
|
||||||
req,
|
req,
|
||||||
toolbar,
|
toolbar,
|
||||||
entrypoint: "/"+fileNameTimeSlider
|
entrypoint: "../../"+fileNameTimeSlider
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,11 +4,10 @@ import {MapArrayType} from "../../types/MapType";
|
||||||
import {PartType} from "../../types/PartType";
|
import {PartType} from "../../types/PartType";
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const minify = require('../../utils/Minify');
|
import {minify} from '../../utils/Minify';
|
||||||
const path = require('path');
|
import path from 'node:path';
|
||||||
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
const plugins = require('../../../static/js/pluginfw/plugin_defs');
|
||||||
const settings = require('../../utils/Settings');
|
const settings = require('../../utils/Settings');
|
||||||
import CachingMiddleware from '../../utils/caching_middleware';
|
|
||||||
|
|
||||||
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
// Rewrite tar to include modules with no extensions and proper rooted paths.
|
||||||
const getTar = async () => {
|
const getTar = async () => {
|
||||||
|
@ -32,15 +31,10 @@ const getTar = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.expressPreSession = async (hookName:string, {app}:any) => {
|
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
|
// Minify will serve static files compressed (minify enabled). It also has
|
||||||
// file-specific hacks for ace/require-kernel/etc.
|
// file-specific hacks for ace/require-kernel/etc.
|
||||||
app.all('/static/:filename(*)', minify.minify);
|
app.all('/static/:filename(*)', minify);
|
||||||
|
|
||||||
// serve plugin definitions
|
// serve plugin definitions
|
||||||
// not very static, but served here so that client can do
|
// 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');
|
res.status(401).send('Authentication Required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (ctx.username === '__proto__' || ctx.username === 'constructor' || ctx.username === 'prototype') {
|
||||||
|
res.end(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
settings.users[ctx.username].username = ctx.username;
|
settings.users[ctx.username].username = ctx.username;
|
||||||
// Make a shallow copy so that the password property can be deleted (to prevent it from
|
// 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.
|
// 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 fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
||||||
const pluginDefs = require('../../static/js/pluginfw/plugin_defs.js');
|
const pluginDefs = require('../../static/js/pluginfw/plugin_defs');
|
||||||
const existsSync = require('../utils/path_exists');
|
import existsSync from '../utils/path_exists';
|
||||||
const settings = require('../utils/Settings');
|
const settings = require('../utils/Settings');
|
||||||
|
|
||||||
// returns all existing messages merged together and grouped by langcode
|
// 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 hooks = require('../static/js/pluginfw/hooks');
|
||||||
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
const pluginDefs = require('../static/js/pluginfw/plugin_defs');
|
||||||
const plugins = require('../static/js/pluginfw/plugins');
|
const plugins = require('../static/js/pluginfw/plugins');
|
||||||
const {Gate} = require('./utils/promises');
|
import {Gate} from './utils/promises';
|
||||||
const stats = require('./stats')
|
const stats = require('./stats')
|
||||||
|
|
||||||
const logger = log4js.getLogger('server');
|
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 () => {
|
exports.start = async () => {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case State.INITIAL:
|
case State.INITIAL:
|
||||||
|
@ -181,12 +181,14 @@ exports.start = async () => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error occurred while starting Etherpad');
|
logger.error('Error occurred while starting Etherpad');
|
||||||
state = State.STATE_TRANSITION_FAILED;
|
state = State.STATE_TRANSITION_FAILED;
|
||||||
|
// @ts-ignore
|
||||||
startDoneGate.resolve();
|
startDoneGate.resolve();
|
||||||
return await exports.exit(err);
|
return await exports.exit(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Etherpad is running');
|
logger.info('Etherpad is running');
|
||||||
state = State.RUNNING;
|
state = State.RUNNING;
|
||||||
|
// @ts-ignore
|
||||||
startDoneGate.resolve();
|
startDoneGate.resolve();
|
||||||
|
|
||||||
// Return the HTTP server to make it easier to write tests.
|
// Return the HTTP server to make it easier to write tests.
|
||||||
|
@ -228,11 +230,13 @@ exports.stop = async () => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error occurred while stopping Etherpad');
|
logger.error('Error occurred while stopping Etherpad');
|
||||||
state = State.STATE_TRANSITION_FAILED;
|
state = State.STATE_TRANSITION_FAILED;
|
||||||
|
// @ts-ignore
|
||||||
stopDoneGate.resolve();
|
stopDoneGate.resolve();
|
||||||
return await exports.exit(err);
|
return await exports.exit(err);
|
||||||
}
|
}
|
||||||
logger.info('Etherpad stopped');
|
logger.info('Etherpad stopped');
|
||||||
state = State.STOPPED;
|
state = State.STOPPED;
|
||||||
|
// @ts-ignore
|
||||||
stopDoneGate.resolve();
|
stopDoneGate.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {MapArrayType} from "./MapType";
|
import {MapArrayType} from "./MapType";
|
||||||
|
import AttributePool from "../../static/js/AttributePool";
|
||||||
|
|
||||||
export type PadType = {
|
export type PadType = {
|
||||||
id: string,
|
id: string,
|
||||||
apool: ()=>APool,
|
apool: ()=>AttributePool,
|
||||||
atext: AText,
|
atext: AText,
|
||||||
pool: APool,
|
pool: AttributePool,
|
||||||
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
|
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
|
||||||
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
|
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
|
||||||
getRevisionAuthor: (rev: number)=>Promise<string>,
|
getRevisionAuthor: (rev: number)=>Promise<string>,
|
||||||
|
@ -35,6 +36,7 @@ export type APool = {
|
||||||
clone: ()=>APool,
|
clone: ()=>APool,
|
||||||
check: ()=>Promise<void>,
|
check: ()=>Promise<void>,
|
||||||
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
|
eachAttrib: (callback: (key: string, value: any)=>void)=>void,
|
||||||
|
getAttrib: (key: number)=>any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,9 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const AttributeMap = require('../../static/js/AttributeMap');
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import AttributePool from "../../static/js/AttributePool";
|
||||||
|
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||||
const { checkValidRev } = require('./checkValidRev');
|
const { checkValidRev } = require('./checkValidRev');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -30,7 +31,7 @@ exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any;
|
||||||
const _analyzeLine = exports._analyzeLine;
|
const _analyzeLine = exports._analyzeLine;
|
||||||
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
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 apool = pad.pool;
|
||||||
|
|
||||||
const pieces = [];
|
const pieces = [];
|
||||||
|
@ -51,14 +52,14 @@ type LineModel = {
|
||||||
[id:string]:string|number|LineModel
|
[id:string]:string|number|LineModel
|
||||||
}
|
}
|
||||||
|
|
||||||
exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
exports._analyzeLine = (text:string, aline: string, apool: AttributePool) => {
|
||||||
const line: LineModel = {};
|
const line: LineModel = {};
|
||||||
|
|
||||||
// identify list
|
// identify list
|
||||||
let lineMarker = 0;
|
let lineMarker = 0;
|
||||||
line.listLevel = 0;
|
line.listLevel = 0;
|
||||||
if (aline) {
|
if (aline) {
|
||||||
const [op] = Changeset.deserializeOps(aline);
|
const [op] = deserializeOps(aline);
|
||||||
if (op != null) {
|
if (op != null) {
|
||||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
let listType = attribs.get('list');
|
let listType = attribs.get('list');
|
||||||
|
@ -78,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: Function) => {
|
||||||
}
|
}
|
||||||
if (lineMarker) {
|
if (lineMarker) {
|
||||||
line.text = text.substring(1);
|
line.text = text.substring(1);
|
||||||
line.aline = Changeset.subattribution(aline, 1);
|
line.aline = subattribution(aline, 1);
|
||||||
} else {
|
} else {
|
||||||
line.text = text;
|
line.text = text;
|
||||||
line.aline = aline;
|
line.aline = aline;
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {MapArrayType} from "../types/MapType";
|
||||||
* limitations under the License.
|
* 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 attributes = require('../../static/js/attributes');
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require('../db/PadManager');
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
||||||
|
@ -27,7 +27,9 @@ const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
const eejs = require('../eejs');
|
const eejs = require('../eejs');
|
||||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||||
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
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) => {
|
const getPadHTML = async (pad: PadType, revNum: string) => {
|
||||||
let atext = pad.atext;
|
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 getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string[]) => {
|
||||||
const apool = pad.apool();
|
const apool = pad.apool();
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
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 tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
|
||||||
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||||
|
@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
css += '<style>\n';
|
css += '<style>\n';
|
||||||
|
|
||||||
for (const a of Object.keys(apool.numToAttrib)) {
|
for (const a of Object.keys(apool.numToAttrib)) {
|
||||||
|
// @ts-ignore
|
||||||
const attr = apool.numToAttrib[a];
|
const attr = apool.numToAttrib[a];
|
||||||
|
|
||||||
// skip non author attributes
|
// skip non author attributes
|
||||||
|
@ -115,6 +118,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
// see hook exportHtmlAdditionalTagsWithData
|
// see hook exportHtmlAdditionalTagsWithData
|
||||||
attrib = propName;
|
attrib = propName;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
const propTrueNum = apool.putAttrib(attrib, true);
|
const propTrueNum = apool.putAttrib(attrib, true);
|
||||||
if (propTrueNum >= 0) {
|
if (propTrueNum >= 0) {
|
||||||
anumMap[propTrueNum] = i;
|
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>
|
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
// becomes
|
// becomes
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
const taker = Changeset.stringIterator(text);
|
const taker = new StringIterator(text);
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
const openTags:string[] = [];
|
const openTags:string[] = [];
|
||||||
|
|
||||||
const getSpanClassFor = (i: string) => {
|
const getSpanClassFor = (i: string) => {
|
||||||
|
@ -204,7 +208,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
// @ts-ignore
|
||||||
|
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||||
idx += numChars;
|
idx += numChars;
|
||||||
|
|
||||||
// this iterates over every op string and decides which tags to open or to close
|
// 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 {AText, PadType} from "../types/PadType";
|
||||||
import {MapType} from "../types/MapType";
|
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 attributes = require('../../static/js/attributes');
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require('../db/PadManager');
|
||||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
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 getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
const apool = pad.apool();
|
const apool = pad.apool();
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
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 props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||||
const anumMap: MapType = {};
|
const anumMap: MapType = {};
|
||||||
const css = '';
|
const css = '';
|
||||||
|
|
||||||
props.forEach((propName, i) => {
|
props.forEach((propName, i) => {
|
||||||
|
// @ts-ignore
|
||||||
const propTrueNum = apool.putAttrib([propName, true], true);
|
const propTrueNum = apool.putAttrib([propName, true], true);
|
||||||
if (propTrueNum >= 0) {
|
if (propTrueNum >= 0) {
|
||||||
anumMap[propTrueNum] = i;
|
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>
|
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
// becomes
|
// becomes
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
const taker = Changeset.stringIterator(text);
|
const taker = new StringIterator(text);
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
|
@ -79,7 +82,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||||
idx += numChars;
|
idx += numChars;
|
||||||
|
|
||||||
for (const o of ops) {
|
for (const o of ops) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {APool} from "../types/PadType";
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const AttributePool = require('../../static/js/AttributePool');
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
const {Pad} = require('../db/Pad');
|
const {Pad} = require('../db/Pad');
|
||||||
const Stream = require('./Stream');
|
const Stream = require('./Stream');
|
||||||
const authorManager = require('../db/AuthorManager');
|
const authorManager = require('../db/AuthorManager');
|
||||||
|
@ -61,7 +61,7 @@ exports.setPadRaw = async (padId: string, r: string, authorId = '') => {
|
||||||
try {
|
try {
|
||||||
const processRecord = async (key:string, value: null|{
|
const processRecord = async (key:string, value: null|{
|
||||||
padIDs: string|Record<string, unknown>,
|
padIDs: string|Record<string, unknown>,
|
||||||
pool: APool
|
pool: AttributePool
|
||||||
}) => {
|
}) => {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const keyParts = key.split(':');
|
const keyParts = key.split(':');
|
||||||
|
|
|
@ -16,10 +16,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps} from '../../static/js/Changeset';
|
||||||
const contentcollector = require('../../static/js/contentcollector');
|
const contentcollector = require('../../static/js/contentcollector');
|
||||||
import jsdom from 'jsdom';
|
import jsdom from 'jsdom';
|
||||||
import {PadType} from "../types/PadType";
|
import {PadType} from "../types/PadType";
|
||||||
|
import {Builder} from "../../static/js/Builder";
|
||||||
|
|
||||||
const apiLogger = log4js.getLogger('ImportHtml');
|
const apiLogger = log4js.getLogger('ImportHtml');
|
||||||
let processor:any;
|
let processor:any;
|
||||||
|
@ -69,13 +70,13 @@ exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
|
||||||
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
|
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
|
||||||
|
|
||||||
// create a new changeset with a helper builder object
|
// 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
|
// assemble each line into the builder
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = 0;
|
const newTextStart = 0;
|
||||||
const newTextEnd = newText.length;
|
const newTextEnd = newText.length;
|
||||||
for (const op of Changeset.deserializeOps(newAttribs)) {
|
for (const op of deserializeOps(newAttribs)) {
|
||||||
const nextIndex = textIndex + op.chars;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
const start = Math.max(newTextStart, textIndex);
|
const start = Math.max(newTextStart, textIndex);
|
||||||
|
|
|
@ -21,20 +21,20 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const settings = require('./Settings');
|
import {TransformResult} from "esbuild";
|
||||||
const fs = require('fs').promises;
|
import mime from 'mime-types';
|
||||||
const path = require('path');
|
import log4js from 'log4js';
|
||||||
const plugins = require('../../static/js/pluginfw/plugin_defs');
|
import {compressCSS, compressJS} from './MinifyWorker'
|
||||||
const mime = require('mime-types');
|
|
||||||
const Threads = require('threads');
|
|
||||||
const log4js = require('log4js');
|
|
||||||
const sanitizePathname = require('./sanitizePathname');
|
|
||||||
|
|
||||||
|
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 logger = log4js.getLogger('Minify');
|
||||||
|
|
||||||
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
const ROOT_DIR = path.join(settings.root, 'src/static/');
|
||||||
|
|
||||||
const threadsPool = new Threads.Pool(() => Threads.spawn(new Threads.Worker('./MinifyWorker')), 2);
|
|
||||||
|
|
||||||
const LIBRARY_WHITELIST = [
|
const LIBRARY_WHITELIST = [
|
||||||
'async',
|
'async',
|
||||||
|
@ -48,10 +48,10 @@ const LIBRARY_WHITELIST = [
|
||||||
|
|
||||||
// What follows is a terrible hack to avoid loop-back within the server.
|
// 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.
|
// 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);
|
const parsedUrl = new URL(url);
|
||||||
let status = 500;
|
let status = 500;
|
||||||
const content = [];
|
const content: any[] = [];
|
||||||
const mockRequest = {
|
const mockRequest = {
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
|
@ -61,7 +61,7 @@ const requestURI = async (url, method, headers) => {
|
||||||
let mockResponse;
|
let mockResponse;
|
||||||
const p = new Promise((resolve) => {
|
const p = new Promise((resolve) => {
|
||||||
mockResponse = {
|
mockResponse = {
|
||||||
writeHead: (_status, _headers) => {
|
writeHead: (_status: number, _headers: { [x: string]: any; }) => {
|
||||||
status = _status;
|
status = _status;
|
||||||
for (const header in _headers) {
|
for (const header in _headers) {
|
||||||
if (Object.prototype.hasOwnProperty.call(_headers, header)) {
|
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();
|
headers[header.toLowerCase()] = value.toString();
|
||||||
},
|
},
|
||||||
header: (header, value) => {
|
header: (header: string, value: { toString: () => any; }) => {
|
||||||
headers[header.toLowerCase()] = value.toString();
|
headers[header.toLowerCase()] = value.toString();
|
||||||
},
|
},
|
||||||
write: (_content) => {
|
write: (_content: any) => {
|
||||||
_content && content.push(_content);
|
_content && content.push(_content);
|
||||||
},
|
},
|
||||||
end: (_content) => {
|
end: (_content: any) => {
|
||||||
_content && content.push(_content);
|
_content && content.push(_content);
|
||||||
resolve([status, headers, content.join('')]);
|
resolve([status, headers, content.join('')]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await minify(mockRequest, mockResponse);
|
await _minify(mockRequest, mockResponse);
|
||||||
return await p;
|
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) => {
|
Promise.all(locations.map(async (loc) => {
|
||||||
try {
|
try {
|
||||||
return await requestURI(loc, method, headers);
|
return await requestURI(loc, method, headers);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
|
logger.debug(`requestURI(${JSON.stringify(loc)}, ${JSON.stringify(method)}, ` +
|
||||||
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
|
// @ts-ignore
|
||||||
|
`${JSON.stringify(headers)}) failed: ${err.stack || err}`);
|
||||||
return [500, headers, ''];
|
return [500, headers, ''];
|
||||||
}
|
}
|
||||||
})).then((responses) => {
|
})).then((responses) => {
|
||||||
|
// @ts-ignore
|
||||||
const statuss = responses.map((x) => x[0]);
|
const statuss = responses.map((x) => x[0]);
|
||||||
|
// @ts-ignore
|
||||||
const headerss = responses.map((x) => x[1]);
|
const headerss = responses.map((x) => x[1]);
|
||||||
|
// @ts-ignore
|
||||||
const contentss = responses.map((x) => x[2]);
|
const contentss = responses.map((x) => x[2]);
|
||||||
callback(statuss, headerss, contentss);
|
callback(statuss, headerss, contentss);
|
||||||
});
|
});
|
||||||
|
@ -119,11 +145,12 @@ const compatPaths = {
|
||||||
* @param req the Express request
|
* @param req the Express request
|
||||||
* @param res the Express response
|
* @param res the Express response
|
||||||
*/
|
*/
|
||||||
const minify = async (req, res) => {
|
const _minify = async (req:any, res:any) => {
|
||||||
let filename = req.params.filename;
|
let filename = req.params.filename;
|
||||||
try {
|
try {
|
||||||
filename = sanitizePathname(filename);
|
filename = sanitizePathname(filename);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// @ts-ignore
|
||||||
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
|
logger.error(`sanitization of pathname "${filename}" failed: ${err.stack || err}`);
|
||||||
res.writeHead(404, {});
|
res.writeHead(404, {});
|
||||||
res.end();
|
res.end();
|
||||||
|
@ -131,6 +158,7 @@ const minify = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility for plugins that require() files from old paths.
|
// Backward compatibility for plugins that require() files from old paths.
|
||||||
|
// @ts-ignore
|
||||||
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
|
const newLocation = compatPaths[filename.replace(/^plugins\/ep_etherpad-lite\/static\//, '')];
|
||||||
if (newLocation != null) {
|
if (newLocation != null) {
|
||||||
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
|
logger.warn(`request for deprecated path "${filename}", replacing with "${newLocation}"`);
|
||||||
|
@ -193,7 +221,7 @@ const minify = async (req, res) => {
|
||||||
res.writeHead(200, {});
|
res.writeHead(200, {});
|
||||||
res.end();
|
res.end();
|
||||||
} else if (req.method === 'GET') {
|
} 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.header('Content-Type', contentType);
|
||||||
res.writeHead(200, {});
|
res.writeHead(200, {});
|
||||||
res.write(content);
|
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.
|
// 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
|
* The only external call to this function provides an explicit value for
|
||||||
* dirStatLimit: this check could be removed.
|
* dirStatLimit: this check could be removed.
|
||||||
|
@ -221,6 +249,7 @@ const statFile = async (filename, dirStatLimit) => {
|
||||||
try {
|
try {
|
||||||
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
|
stats = await fs.stat(path.resolve(ROOT_DIR, filename));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// @ts-ignore
|
||||||
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
|
if (['ENOENT', 'ENOTDIR'].includes(err.code)) {
|
||||||
// Stat the directory instead.
|
// Stat the directory instead.
|
||||||
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
|
const [date] = await statFile(path.dirname(filename), dirStatLimit - 1);
|
||||||
|
@ -234,69 +263,64 @@ const statFile = async (filename, dirStatLimit) => {
|
||||||
|
|
||||||
let contentCache = new Map();
|
let contentCache = new Map();
|
||||||
|
|
||||||
const getFileCompressed = async (filename, contentType) => {
|
const getFileCompressed = async (filename: any, contentType: string) => {
|
||||||
if (contentCache.has(filename)) {
|
if (contentCache.has(filename)) {
|
||||||
return contentCache.get(filename);
|
return contentCache.get(filename);
|
||||||
}
|
}
|
||||||
let content = await getFile(filename);
|
let content: Buffer|string = await getFile(filename);
|
||||||
if (!content || !settings.minify) {
|
if (!content || !settings.minify) {
|
||||||
return content;
|
return content;
|
||||||
} else if (contentType === 'application/javascript') {
|
} else if (contentType === 'application/javascript') {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise(async (resolve) => {
|
||||||
threadsPool.queue(async ({compressJS}) => {
|
try {
|
||||||
|
logger.info('Compress JS file %s.', filename);
|
||||||
|
|
||||||
|
content = content.toString();
|
||||||
try {
|
try {
|
||||||
logger.info('Compress JS file %s.', filename);
|
let compressResult: TransformResult<{ minify: boolean }>
|
||||||
|
compressResult = await compressJS(content);
|
||||||
content = content.toString();
|
content = compressResult.code.toString(); // Convert content obj code to string
|
||||||
const compressResult = await compressJS(content);
|
|
||||||
|
|
||||||
if (compressResult.error) {
|
|
||||||
console.error(`Error compressing JS (${filename}) using terser`, compressResult.error);
|
|
||||||
} else {
|
|
||||||
content = compressResult.code.toString(); // Convert content obj code to string
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('getFile() returned an error in ' +
|
console.error(`Error compressing JS (${filename}) using esbuild`, error);
|
||||||
`getFileCompressed(${filename}, ${contentType}): ${error}`);
|
|
||||||
}
|
}
|
||||||
contentCache.set(filename, content);
|
} catch (error) {
|
||||||
resolve(content);
|
console.error('getFile() returned an error in ' +
|
||||||
});
|
`getFileCompressed(${filename}, ${contentType}): ${error}`);
|
||||||
|
}
|
||||||
|
contentCache.set(filename, content);
|
||||||
|
resolve(content);
|
||||||
});
|
});
|
||||||
} else if (contentType === 'text/css') {
|
} else if (contentType === 'text/css') {
|
||||||
return await new Promise((resolve) => {
|
return await new Promise(async (resolve) => {
|
||||||
threadsPool.queue(async ({compressCSS}) => {
|
try {
|
||||||
|
logger.info('Compress CSS file %s.', filename);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info('Compress CSS file %s.', filename);
|
content = await compressCSS(path.resolve(ROOT_DIR, 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
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
console.error(`CleanCSS.minify() returned an error on ${filename}: ${error}`);
|
||||||
}
|
}
|
||||||
contentCache.set(filename, content);
|
contentCache.set(filename, content);
|
||||||
resolve(content);
|
resolve(content);
|
||||||
});
|
} catch (e) {
|
||||||
});
|
console.error('getFile() returned an error in ' +
|
||||||
|
`getFileCompressed(${filename}, ${contentType}): ${e}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
contentCache.set(filename, content);
|
contentCache.set(filename, content);
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFile = async (filename) => {
|
const getFile = async (filename: any) => {
|
||||||
return await fs.readFile(path.resolve(ROOT_DIR, filename));
|
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) => {
|
export const shutdown = async (hookName: string, context:any) => {
|
||||||
await threadsPool.terminate();
|
contentCache = new Map();
|
||||||
};
|
};
|
|
@ -3,14 +3,13 @@
|
||||||
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
* Worker thread to minify JS & CSS files out of the main NodeJS thread
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {expose} from 'threads'
|
|
||||||
import {build, transform} from 'esbuild';
|
import {build, transform} from 'esbuild';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Minify JS content
|
* Minify JS content
|
||||||
* @param {string} content - JS content to minify
|
* @param {string} content - JS content to minify
|
||||||
*/
|
*/
|
||||||
const compressJS = async (content) => {
|
export const compressJS = async (content: string) => {
|
||||||
return await transform(content, {minify: true});
|
return await transform(content, {minify: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +18,7 @@ const compressJS = async (content) => {
|
||||||
* @param {string} filename - name of the file
|
* @param {string} filename - name of the file
|
||||||
* @param {string} ROOT_DIR - the root dir of Etherpad
|
* @param {string} ROOT_DIR - the root dir of Etherpad
|
||||||
*/
|
*/
|
||||||
const compressCSS = async (content) => {
|
export const compressCSS = async (content: string) => {
|
||||||
const transformedCSS = await build(
|
const transformedCSS = await build(
|
||||||
{
|
{
|
||||||
entryPoints: [content],
|
entryPoints: [content],
|
||||||
|
@ -41,8 +40,3 @@ const compressCSS = async (content) => {
|
||||||
)
|
)
|
||||||
return transformedCSS.outputFiles[0].text
|
return transformedCSS.outputFiles[0].text
|
||||||
};
|
};
|
||||||
|
|
||||||
expose({
|
|
||||||
compressJS: compressJS,
|
|
||||||
compressCSS,
|
|
||||||
});
|
|
|
@ -169,11 +169,11 @@ exports.authenticationMethod = 'sso'
|
||||||
/*
|
/*
|
||||||
* The Type of the database
|
* The Type of the database
|
||||||
*/
|
*/
|
||||||
exports.dbType = 'dirty';
|
exports.dbType = 'rustydb';
|
||||||
/**
|
/**
|
||||||
* This setting is passed with dbType to ueberDB to set up the database
|
* 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
|
* The default Text of a new pad
|
||||||
|
@ -837,7 +837,7 @@ exports.reloadSettings = () => {
|
||||||
exports.skinName = 'colibris';
|
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'].");
|
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'];
|
exports.socketTransportProtocols = ['websocket', 'polling'];
|
||||||
}
|
}
|
||||||
|
@ -941,6 +941,11 @@ exports.reloadSettings = () => {
|
||||||
logger.warn(`${dirtyWarning} File location: ${exports.dbSettings.filename}`);
|
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 === '') {
|
if (exports.ip === '') {
|
||||||
// using Unix socket for connectivity
|
// using Unix socket for connectivity
|
||||||
logger.warn('The settings file contains an empty string ("") for the "ip" parameter. The ' +
|
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 {PadAuthor, PadType} from "../types/PadType";
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
const AttributeMap = require('../../static/js/AttributeMap');
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
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 attributes = require('../../static/js/attributes');
|
||||||
const exportHtml = require('./ExportHtml');
|
const exportHtml = require('./ExportHtml');
|
||||||
|
|
||||||
|
@ -33,7 +38,7 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
_isClearAuthorship(changeset: any){
|
_isClearAuthorship(changeset: any){
|
||||||
// unpack
|
// unpack
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
|
|
||||||
// check if there is nothing in the charBank
|
// check if there is nothing in the charBank
|
||||||
if (unpacked.charBank !== '') {
|
if (unpacked.charBank !== '') {
|
||||||
|
@ -45,7 +50,7 @@ class PadDiff {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
|
const [clearOperator, anotherOp] = deserializeOps(unpacked.ops);
|
||||||
|
|
||||||
// check if there is only one operator
|
// check if there is only one operator
|
||||||
if (anotherOp != null) return false;
|
if (anotherOp != null) return false;
|
||||||
|
@ -78,7 +83,7 @@ class PadDiff {
|
||||||
const atext = await this._pad.getInternalRevisionAText(rev);
|
const atext = await this._pad.getInternalRevisionAText(rev);
|
||||||
|
|
||||||
// build clearAuthorship changeset
|
// 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);
|
builder.keepText(atext.text, [['author', '']], this._pad.pool);
|
||||||
const changeset = builder.toString();
|
const changeset = builder.toString();
|
||||||
|
|
||||||
|
@ -93,7 +98,7 @@ class PadDiff {
|
||||||
const changeset = await this._createClearAuthorship(rev);
|
const changeset = await this._createClearAuthorship(rev);
|
||||||
|
|
||||||
// apply the clearAuthorship changeset
|
// apply the clearAuthorship changeset
|
||||||
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
|
const newAText = applyToAText(changeset, atext, this._pad.pool);
|
||||||
|
|
||||||
return newAText;
|
return newAText;
|
||||||
}
|
}
|
||||||
|
@ -157,7 +162,7 @@ class PadDiff {
|
||||||
if (superChangeset == null) {
|
if (superChangeset == null) {
|
||||||
superChangeset = changeset;
|
superChangeset = changeset;
|
||||||
} else {
|
} 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);
|
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
|
||||||
|
|
||||||
// apply the superChangeset, which includes all addings
|
// 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
|
// apply the deletionChangeset, which adds a deletions
|
||||||
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
|
atext = applyToAText(deletionChangeset, atext, this._pad.pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
return atext;
|
return atext;
|
||||||
|
@ -209,22 +214,22 @@ class PadDiff {
|
||||||
|
|
||||||
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
|
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
|
||||||
// unpack
|
// unpack
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
|
|
||||||
const assem = Changeset.opAssembler();
|
const assem = new OpAssembler();
|
||||||
|
|
||||||
// create deleted attribs
|
// create deleted attribs
|
||||||
const authorAttrib = apool.putAttrib(['author', author || '']);
|
const authorAttrib = apool.putAttrib(['author', author || '']);
|
||||||
const deletedAttrib = apool.putAttrib(['removed', true]);
|
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 === '-') {
|
if (operator.opcode === '-') {
|
||||||
// this is a delete operator, extend it with the author
|
// this is a delete operator, extend it with the author
|
||||||
operator.attribs = attribs;
|
operator.attribs = attribs;
|
||||||
} else if (operator.opcode === '=' && operator.attribs) {
|
} else if (operator.opcode === '=' && operator.attribs) {
|
||||||
// this is operator changes only attributes, let's mark which author did that
|
// 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
|
// append the new operator to our assembler
|
||||||
|
@ -232,26 +237,31 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the modified changeset
|
// 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){
|
_createDeletionChangeset(cs: any, startAText: any, apool: any){
|
||||||
const lines = Changeset.splitTextLines(startAText.text);
|
const lines = splitTextLines(startAText.text);
|
||||||
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
|
const alines = splitAttributionLines(startAText.attribs, startAText.text);
|
||||||
|
|
||||||
// lines and alines are what the exports is meant to apply to.
|
// 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 may be arrays or objects with .get(i) and .length methods.
|
||||||
// They include final newlines on lines.
|
// They include final newlines on lines.
|
||||||
|
|
||||||
const linesGet = (idx: number) => {
|
const linesGet = (idx: number) => {
|
||||||
|
// @ts-ignore
|
||||||
if (lines.get) {
|
if (lines.get) {
|
||||||
|
// @ts-ignore
|
||||||
return lines.get(idx);
|
return lines.get(idx);
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
return lines[idx];
|
return lines[idx];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const aLinesGet = (idx: number) => {
|
const aLinesGet = (idx: number) => {
|
||||||
|
// @ts-ignore
|
||||||
if (alines.get) {
|
if (alines.get) {
|
||||||
|
// @ts-ignore
|
||||||
return alines.get(idx);
|
return alines.get(idx);
|
||||||
} else {
|
} else {
|
||||||
return alines[idx];
|
return alines[idx];
|
||||||
|
@ -263,14 +273,14 @@ class PadDiff {
|
||||||
let curLineOps: { next: () => any; } | null = null;
|
let curLineOps: { next: () => any; } | null = null;
|
||||||
let curLineOpsNext: { done: any; value: any; } | null = null;
|
let curLineOpsNext: { done: any; value: any; } | null = null;
|
||||||
let curLineOpsLine: number;
|
let curLineOpsLine: number;
|
||||||
let curLineNextOp = new Changeset.Op('+');
|
let curLineNextOp = new Op('+');
|
||||||
|
|
||||||
const unpacked = Changeset.unpack(cs);
|
const unpacked = unpack(cs);
|
||||||
const builder = Changeset.builder(unpacked.newLen);
|
const builder = new Builder(unpacked.newLen);
|
||||||
|
|
||||||
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
|
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
|
||||||
if (!curLineOps || curLineOpsLine !== curLine) {
|
if (!curLineOps || curLineOpsLine !== curLine) {
|
||||||
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
|
curLineOps = deserializeOps(aLinesGet(curLine));
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
curLineOpsLine = curLine;
|
curLineOpsLine = curLine;
|
||||||
let indexIntoLine = 0;
|
let indexIntoLine = 0;
|
||||||
|
@ -291,13 +301,13 @@ class PadDiff {
|
||||||
curChar = 0;
|
curChar = 0;
|
||||||
curLineOpsLine = curLine;
|
curLineOpsLine = curLine;
|
||||||
curLineNextOp.chars = 0;
|
curLineNextOp.chars = 0;
|
||||||
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
|
curLineOps = deserializeOps(aLinesGet(curLine));
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!curLineNextOp.chars) {
|
if (!curLineNextOp.chars) {
|
||||||
if (curLineOpsNext!.done) {
|
if (curLineOpsNext!.done) {
|
||||||
curLineNextOp = new Changeset.Op();
|
curLineNextOp = new Op();
|
||||||
} else {
|
} else {
|
||||||
curLineNextOp = curLineOpsNext!.value;
|
curLineNextOp = curLineOpsNext!.value;
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
|
@ -332,7 +342,7 @@ class PadDiff {
|
||||||
|
|
||||||
const nextText = (numChars: number) => {
|
const nextText = (numChars: number) => {
|
||||||
let len = 0;
|
let len = 0;
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
const firstString = linesGet(curLine).substring(curChar);
|
const firstString = linesGet(curLine).substring(curChar);
|
||||||
len += firstString.length;
|
len += firstString.length;
|
||||||
assem.append(firstString);
|
assem.append(firstString);
|
||||||
|
@ -360,7 +370,7 @@ class PadDiff {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
|
for (const csOp of deserializeOps(unpacked.ops)) {
|
||||||
if (csOp.opcode === '=') {
|
if (csOp.opcode === '=') {
|
||||||
const textBank = nextText(csOp.chars);
|
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,
|
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
|
||||||
// it adds deletions and attribute changes to the atext.
|
// it adds deletions and attribute changes to the atext.
|
||||||
|
// @ts-ignore
|
||||||
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
const fs = require('fs');
|
import fs from 'node:fs';
|
||||||
|
|
||||||
const check = (path:string) => {
|
const check = (path:string) => {
|
||||||
const existsSync = fs.statSync || fs.existsSync;
|
const existsSync = fs.statSync || fs.existsSync;
|
||||||
|
@ -13,4 +13,4 @@ const check = (path:string) => {
|
||||||
return result;
|
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
|
// `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
|
// `promises` is empty. If `predicate` is nullish, the truthiness of the resolved value is used as
|
||||||
// the predicate.
|
// the predicate.
|
||||||
exports.firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
|
export const firstSatisfies = <T>(promises: Promise<T>[], predicate: null|Function) => {
|
||||||
if (predicate == null) {
|
if (predicate == null) {
|
||||||
predicate = (x: any) => x;
|
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,
|
// `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
|
// and each remaining Promise will be created once one of the earlier Promises resolves.) This async
|
||||||
// function resolves once all `total` Promises have resolved.
|
// 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');
|
if (total > 0 && concurrency <= 0) throw new RangeError('concurrency must be positive');
|
||||||
let next = 0;
|
let next = 0;
|
||||||
const addAnother = () => promiseCreator(next++).finally(() => {
|
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
|
* An ordinary Promise except the `resolve` and `reject` executor functions are exposed as
|
||||||
* properties.
|
* 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
|
// Coax `.then()` into returning an ordinary Promise, not a Gate. See
|
||||||
// https://stackoverflow.com/a/65669070 for the rationale.
|
// https://stackoverflow.com/a/65669070 for the rationale.
|
||||||
static get [Symbol.species]() { return Promise; }
|
static get [Symbol.species]() { return Promise; }
|
||||||
|
@ -75,4 +75,3 @@ class Gate<T> extends Promise<T> {
|
||||||
Object.assign(this, props);
|
Object.assign(this, props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.Gate = Gate;
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
'use strict';
|
import path from 'path';
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Normalizes p and ensures that it is a relative path that does not reach outside. See
|
// 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.
|
// 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
|
// 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
|
// "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.,
|
// 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, '/');
|
if (pathApi.sep === '\\') p = p.replace(/\\/g, '/');
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default sanitizeRoot
|
||||||
|
|
|
@ -30,23 +30,23 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@etherpad/express-session": "^1.18.2",
|
"@etherpad/express-session": "^1.18.4",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.6",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.7",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"esbuild": "^0.23.0",
|
"esbuild": "^0.23.1",
|
||||||
"express": "4.19.2",
|
"express": "4.19.2",
|
||||||
"express-rate-limit": "^7.4.0",
|
"express-rate-limit": "^7.4.0",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"find-root": "1.1.0",
|
"find-root": "1.1.0",
|
||||||
"formidable": "^3.5.1",
|
"formidable": "^3.5.1",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.8.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jsdom": "^24.1.1",
|
"jsdom": "^25.0.0",
|
||||||
"jsonminify": "0.4.2",
|
"jsonminify": "0.4.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"languages4translatewiki": "0.1.3",
|
"languages4translatewiki": "0.1.3",
|
||||||
|
@ -67,11 +67,10 @@
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"socket.io": "^4.7.5",
|
"socket.io": "^4.7.5",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
"superagent": "9.0.2",
|
"superagent": "10.1.0",
|
||||||
"threads": "^1.7.0",
|
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"tsx": "4.17.0",
|
"tsx": "4.19.0",
|
||||||
"ueberdb2": "^4.2.92",
|
"ueberdb2": "^4.2.100",
|
||||||
"underscore": "1.13.7",
|
"underscore": "1.13.7",
|
||||||
"unorm": "1.6.0",
|
"unorm": "1.6.0",
|
||||||
"wtfnode": "^0.9.3"
|
"wtfnode": "^0.9.3"
|
||||||
|
@ -81,26 +80,28 @@
|
||||||
"etherpad-lite": "node/server.ts"
|
"etherpad-lite": "node/server.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.46.0",
|
"@playwright/test": "^1.46.1",
|
||||||
"@types/async": "^3.2.24",
|
"@types/async": "^3.2.24",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/formidable": "^3.4.5",
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/http-errors": "^2.0.4",
|
"@types/http-errors": "^2.0.4",
|
||||||
"@types/jquery": "^3.5.30",
|
"@types/jquery": "^3.5.30",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/mocha": "^10.0.7",
|
"@types/mocha": "^10.0.7",
|
||||||
"@types/node": "^22.1.0",
|
"@types/node": "^22.5.4",
|
||||||
"@types/oidc-provider": "^8.5.1",
|
"@types/oidc-provider": "^8.5.2",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/sinon": "^17.0.3",
|
"@types/sinon": "^17.0.3",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/underscore": "^1.11.15",
|
"@types/underscore": "^1.11.15",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
"eslint": "^9.8.0",
|
"eslint": "^9.9.1",
|
||||||
"eslint-config-etherpad": "^4.0.4",
|
"eslint-config-etherpad": "^4.0.4",
|
||||||
"etherpad-cli-client": "^3.0.2",
|
"etherpad-cli-client": "^3.0.2",
|
||||||
"mocha": "^10.7.0",
|
"mocha": "^10.7.3",
|
||||||
"mocha-froth": "^0.2.10",
|
"mocha-froth": "^0.2.10",
|
||||||
"nodeify": "^1.0.1",
|
"nodeify": "^1.0.1",
|
||||||
"openapi-schema-validation": "^0.4.2",
|
"openapi-schema-validation": "^0.4.2",
|
||||||
|
@ -108,7 +109,9 @@
|
||||||
"sinon": "^18.0.0",
|
"sinon": "^18.0.0",
|
||||||
"split-grid": "^1.0.11",
|
"split-grid": "^1.0.11",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.5.4"
|
"typescript": "^5.5.4",
|
||||||
|
"vitest": "^2.0.5",
|
||||||
|
"rusty-store-kv": "^1.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.18.2",
|
"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-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": "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",
|
"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"
|
"license": "Apache-2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
'use strict';
|
// @ts-nocheck
|
||||||
|
import AttributeMap from './AttributeMap';
|
||||||
const AttributeMap = require('./AttributeMap');
|
import {compose, deserializeOps, isIdentity} from './Changeset';
|
||||||
const Changeset = require('./Changeset');
|
import {Builder} from "./Builder";
|
||||||
const ChangesetUtils = require('./ChangesetUtils');
|
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils';
|
||||||
const attributes = require('./attributes');
|
import attributes from './attributes';
|
||||||
const underscore = require("underscore")
|
import underscore from "underscore";
|
||||||
|
|
||||||
const lineMarkerAttribute = 'lmkr';
|
const lineMarkerAttribute = 'lmkr';
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
if (!this.applyChangesetCallback) return changeset;
|
if (!this.applyChangesetCallback) return changeset;
|
||||||
|
|
||||||
const cs = changeset.toString();
|
const cs = changeset.toString();
|
||||||
if (!Changeset.isIdentity(cs)) {
|
if (!isIdentity(cs)) {
|
||||||
this.applyChangesetCallback(cs);
|
this.applyChangesetCallback(cs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
// as the range might not be continuous
|
// as the range might not be continuous
|
||||||
// due to the presence of line markers on the rows
|
// due to the presence of line markers on the rows
|
||||||
if (allChangesets) {
|
if (allChangesets) {
|
||||||
allChangesets = Changeset.compose(
|
allChangesets = compose(
|
||||||
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||||
} else {
|
} else {
|
||||||
allChangesets = rowChangeset;
|
allChangesets = rowChangeset;
|
||||||
|
@ -125,9 +125,9 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
* @param attribs an array of attributes
|
* @param attribs an array of attributes
|
||||||
*/
|
*/
|
||||||
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
||||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
const builder = new Builder(this.rep.lines.totalWidth());
|
||||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
||||||
ChangesetUtils.buildKeepRange(
|
buildKeepRange(
|
||||||
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
||||||
return builder;
|
return builder;
|
||||||
},
|
},
|
||||||
|
@ -150,7 +150,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
// get `attributeName` attribute of first char of line
|
// get `attributeName` attribute of first char of line
|
||||||
const aline = this.rep.alines[lineNum];
|
const aline = this.rep.alines[lineNum];
|
||||||
if (!aline) return '';
|
if (!aline) return '';
|
||||||
const [op] = Changeset.deserializeOps(aline);
|
const [op] = deserializeOps(aline);
|
||||||
if (op == null) return '';
|
if (op == null) return '';
|
||||||
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
|
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
|
||||||
},
|
},
|
||||||
|
@ -163,7 +163,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
// get attributes of first char of line
|
// get attributes of first char of line
|
||||||
const aline = this.rep.alines[lineNum];
|
const aline = this.rep.alines[lineNum];
|
||||||
if (!aline) return [];
|
if (!aline) return [];
|
||||||
const [op] = Changeset.deserializeOps(aline);
|
const [op] = deserializeOps(aline);
|
||||||
if (op == null) return [];
|
if (op == null) return [];
|
||||||
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
|
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
|
||||||
},
|
},
|
||||||
|
@ -221,7 +221,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
let hasAttrib = true;
|
let hasAttrib = true;
|
||||||
|
|
||||||
let indexIntoLine = 0;
|
let indexIntoLine = 0;
|
||||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||||
const opStartInLine = indexIntoLine;
|
const opStartInLine = indexIntoLine;
|
||||||
const opEndInLine = opStartInLine + op.chars;
|
const opEndInLine = opStartInLine + op.chars;
|
||||||
if (!hasIt(op.attribs)) {
|
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
|
// we need to sum up how much characters each operations take until the wanted position
|
||||||
let currentPointer = 0;
|
let currentPointer = 0;
|
||||||
|
|
||||||
for (const currentOperation of Changeset.deserializeOps(aline)) {
|
for (const currentOperation of deserializeOps(aline)) {
|
||||||
currentPointer += currentOperation.chars;
|
currentPointer += currentOperation.chars;
|
||||||
if (currentPointer <= column) continue;
|
if (currentPointer <= column) continue;
|
||||||
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
|
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
|
||||||
|
@ -285,13 +285,13 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
*/
|
*/
|
||||||
setAttributeOnLine(lineNum, attributeName, attributeValue) {
|
setAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||||
let loc = [0, 0];
|
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);
|
const hasMarker = this.lineHasMarker(lineNum);
|
||||||
|
|
||||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||||
|
|
||||||
if (hasMarker) {
|
if (hasMarker) {
|
||||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
||||||
[attributeName, attributeValue],
|
[attributeName, attributeValue],
|
||||||
], this.rep.apool);
|
], this.rep.apool);
|
||||||
} else {
|
} else {
|
||||||
|
@ -314,7 +314,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
* @param attributeValue if given only attributes with equal value will be removed
|
* @param attributeValue if given only attributes with equal value will be removed
|
||||||
*/
|
*/
|
||||||
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
|
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
const builder = new Builder(this.rep.lines.totalWidth());
|
||||||
const hasMarker = this.lineHasMarker(lineNum);
|
const hasMarker = this.lineHasMarker(lineNum);
|
||||||
let found = false;
|
let found = false;
|
||||||
|
|
||||||
|
@ -333,16 +333,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||||
|
|
||||||
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||||
|
|
||||||
// if we have marker and any of attributes don't need to have marker. we need delete it
|
// if we have marker and any of attributes don't need to have marker. we need delete it
|
||||||
if (hasMarker && !countAttribsWithMarker) {
|
if (hasMarker && !countAttribsWithMarker) {
|
||||||
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||||
} else {
|
} else {
|
||||||
ChangesetUtils.buildKeepRange(
|
buildKeepRange(
|
||||||
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
|
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
'use strict';
|
'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.
|
* 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.
|
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
|
||||||
*/
|
*/
|
||||||
class AttributeMap extends Map {
|
class AttributeMap extends Map {
|
||||||
|
private readonly pool? : AttributePool|null
|
||||||
/**
|
/**
|
||||||
* Converts an attribute string into an AttributeMap.
|
* Converts an attribute string into an AttributeMap.
|
||||||
*
|
*
|
||||||
|
@ -28,14 +32,14 @@ class AttributeMap extends Map {
|
||||||
* @param {AttributePool} pool - Attribute pool.
|
* @param {AttributePool} pool - Attribute pool.
|
||||||
* @returns {AttributeMap}
|
* @returns {AttributeMap}
|
||||||
*/
|
*/
|
||||||
static fromString(str, pool) {
|
public static fromString(str: string, pool?: AttributePool|null): AttributeMap {
|
||||||
return new AttributeMap(pool).updateFromString(str);
|
return new AttributeMap(pool).updateFromString(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AttributePool} pool - Attribute pool.
|
* @param {AttributePool} pool - Attribute pool.
|
||||||
*/
|
*/
|
||||||
constructor(pool) {
|
constructor(pool?: AttributePool|null) {
|
||||||
super();
|
super();
|
||||||
/** @public */
|
/** @public */
|
||||||
this.pool = pool;
|
this.pool = pool;
|
||||||
|
@ -46,15 +50,15 @@ class AttributeMap extends Map {
|
||||||
* @param {string} v - Attribute value.
|
* @param {string} v - Attribute value.
|
||||||
* @returns {AttributeMap} `this` (for chaining).
|
* @returns {AttributeMap} `this` (for chaining).
|
||||||
*/
|
*/
|
||||||
set(k, v) {
|
set(k: string, v: string):this {
|
||||||
k = k == null ? '' : String(k);
|
k = k == null ? '' : String(k);
|
||||||
v = v == null ? '' : String(v);
|
v = v == null ? '' : String(v);
|
||||||
this.pool.putAttrib([k, v]);
|
this.pool!.putAttrib([k, v]);
|
||||||
return super.set(k, v);
|
return super.set(k, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
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).
|
* key is removed from this map (if present).
|
||||||
* @returns {AttributeMap} `this` (for chaining).
|
* @returns {AttributeMap} `this` (for chaining).
|
||||||
*/
|
*/
|
||||||
update(entries, emptyValueIsDelete = false) {
|
update(entries: Iterable<Attribute>, emptyValueIsDelete: boolean = false): AttributeMap {
|
||||||
for (let [k, v] of entries) {
|
for (let [k, v] of entries) {
|
||||||
k = k == null ? '' : String(k);
|
k = k == null ? '' : String(k);
|
||||||
v = v == null ? '' : String(v);
|
v = v == null ? '' : String(v);
|
||||||
|
@ -83,9 +87,9 @@ class AttributeMap extends Map {
|
||||||
* key is removed from this map (if present).
|
* key is removed from this map (if present).
|
||||||
* @returns {AttributeMap} `this` (for chaining).
|
* @returns {AttributeMap} `this` (for chaining).
|
||||||
*/
|
*/
|
||||||
updateFromString(str, emptyValueIsDelete = false) {
|
updateFromString(str: string, emptyValueIsDelete: boolean = false): AttributeMap {
|
||||||
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
|
return this.update(attributes.attribsFromString(str, this.pool!), emptyValueIsDelete);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AttributeMap;
|
export default AttributeMap
|
|
@ -44,6 +44,8 @@
|
||||||
* @property {number} nextNum - The attribute ID to assign to the next new attribute.
|
* @property {number} nextNum - The attribute ID to assign to the next new attribute.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {Attribute} from "./types/Attribute";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an attribute pool, which is a collection of attributes (pairs of key and value
|
* Represents an attribute pool, which is a collection of attributes (pairs of key and value
|
||||||
* strings) along with their identifiers (non-negative integers).
|
* strings) along with their identifiers (non-negative integers).
|
||||||
|
@ -55,6 +57,14 @@
|
||||||
* in the pad.
|
* in the pad.
|
||||||
*/
|
*/
|
||||||
class AttributePool {
|
class AttributePool {
|
||||||
|
numToAttrib: {
|
||||||
|
[key: number]: [string, string]
|
||||||
|
}
|
||||||
|
private attribToNum: {
|
||||||
|
[key: number]: [string, string]
|
||||||
|
}
|
||||||
|
private nextNum: number
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
/**
|
/**
|
||||||
* Maps an attribute identifier to the attribute's `[key, value]` string pair.
|
* Maps an attribute identifier to the attribute's `[key, value]` string pair.
|
||||||
|
@ -96,7 +106,10 @@ class AttributePool {
|
||||||
*/
|
*/
|
||||||
clone() {
|
clone() {
|
||||||
const c = new AttributePool();
|
const c = new AttributePool();
|
||||||
for (const [n, a] of Object.entries(this.numToAttrib)) c.numToAttrib[n] = [a[0], a[1]];
|
for (const [n, a] of Object.entries(this.numToAttrib)){
|
||||||
|
// @ts-ignore
|
||||||
|
c.numToAttrib[n] = [a[0], a[1]];
|
||||||
|
}
|
||||||
Object.assign(c.attribToNum, this.attribToNum);
|
Object.assign(c.attribToNum, this.attribToNum);
|
||||||
c.nextNum = this.nextNum;
|
c.nextNum = this.nextNum;
|
||||||
return c;
|
return c;
|
||||||
|
@ -111,15 +124,17 @@ class AttributePool {
|
||||||
* membership in the pool without mutating the pool.
|
* membership in the pool without mutating the pool.
|
||||||
* @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool.
|
* @returns {number} The attribute's identifier, or -1 if the attribute is not in the pool.
|
||||||
*/
|
*/
|
||||||
putAttrib(attrib, dontAddIfAbsent = false) {
|
putAttrib(attrib: Attribute, dontAddIfAbsent = false) {
|
||||||
const str = String(attrib);
|
const str = String(attrib);
|
||||||
if (str in this.attribToNum) {
|
if (str in this.attribToNum) {
|
||||||
|
// @ts-ignore
|
||||||
return this.attribToNum[str];
|
return this.attribToNum[str];
|
||||||
}
|
}
|
||||||
if (dontAddIfAbsent) {
|
if (dontAddIfAbsent) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
const num = this.nextNum++;
|
const num = this.nextNum++;
|
||||||
|
// @ts-ignore
|
||||||
this.attribToNum[str] = num;
|
this.attribToNum[str] = num;
|
||||||
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
|
this.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')];
|
||||||
return num;
|
return num;
|
||||||
|
@ -130,7 +145,7 @@ class AttributePool {
|
||||||
* @returns {Attribute} The attribute with the given identifier, or nullish if there is no such
|
* @returns {Attribute} The attribute with the given identifier, or nullish if there is no such
|
||||||
* attribute.
|
* attribute.
|
||||||
*/
|
*/
|
||||||
getAttrib(num) {
|
getAttrib(num: number): Attribute {
|
||||||
const pair = this.numToAttrib[num];
|
const pair = this.numToAttrib[num];
|
||||||
if (!pair) {
|
if (!pair) {
|
||||||
return pair;
|
return pair;
|
||||||
|
@ -143,7 +158,7 @@ class AttributePool {
|
||||||
* @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty
|
* @returns {string} Eqivalent to `getAttrib(num)[0]` if the attribute exists, otherwise the empty
|
||||||
* string.
|
* string.
|
||||||
*/
|
*/
|
||||||
getAttribKey(num) {
|
getAttribKey(num: number): string {
|
||||||
const pair = this.numToAttrib[num];
|
const pair = this.numToAttrib[num];
|
||||||
if (!pair) return '';
|
if (!pair) return '';
|
||||||
return pair[0];
|
return pair[0];
|
||||||
|
@ -154,7 +169,7 @@ class AttributePool {
|
||||||
* @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty
|
* @returns {string} Eqivalent to `getAttrib(num)[1]` if the attribute exists, otherwise the empty
|
||||||
* string.
|
* string.
|
||||||
*/
|
*/
|
||||||
getAttribValue(num) {
|
getAttribValue(num: number) {
|
||||||
const pair = this.numToAttrib[num];
|
const pair = this.numToAttrib[num];
|
||||||
if (!pair) return '';
|
if (!pair) return '';
|
||||||
return pair[1];
|
return pair[1];
|
||||||
|
@ -166,8 +181,8 @@ class AttributePool {
|
||||||
* @param {Function} func - Callback to call with two arguments: key and value. Its return value
|
* @param {Function} func - Callback to call with two arguments: key and value. Its return value
|
||||||
* is ignored.
|
* is ignored.
|
||||||
*/
|
*/
|
||||||
eachAttrib(func) {
|
eachAttrib(func: (k: string, v: string)=>void) {
|
||||||
for (const n of Object.keys(this.numToAttrib)) {
|
for (const n in this.numToAttrib) {
|
||||||
const pair = this.numToAttrib[n];
|
const pair = this.numToAttrib[n];
|
||||||
func(pair[0], pair[1]);
|
func(pair[0], pair[1]);
|
||||||
}
|
}
|
||||||
|
@ -196,11 +211,12 @@ class AttributePool {
|
||||||
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
|
* `new AttributePool().fromJsonable(pool.toJsonable())` to copy because the resulting shared
|
||||||
* state will lead to pool corruption.
|
* state will lead to pool corruption.
|
||||||
*/
|
*/
|
||||||
fromJsonable(obj) {
|
fromJsonable(obj: this) {
|
||||||
this.numToAttrib = obj.numToAttrib;
|
this.numToAttrib = obj.numToAttrib;
|
||||||
this.nextNum = obj.nextNum;
|
this.nextNum = obj.nextNum;
|
||||||
this.attribToNum = {};
|
this.attribToNum = {};
|
||||||
for (const n of Object.keys(this.numToAttrib)) {
|
for (const n of Object.keys(this.numToAttrib)) {
|
||||||
|
// @ts-ignore
|
||||||
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
|
this.attribToNum[String(this.numToAttrib[n])] = Number(n);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
@ -213,6 +229,7 @@ class AttributePool {
|
||||||
if (!Number.isInteger(this.nextNum)) throw new Error('nextNum property is not an integer');
|
if (!Number.isInteger(this.nextNum)) throw new Error('nextNum property is not an integer');
|
||||||
if (this.nextNum < 0) throw new Error('nextNum property is negative');
|
if (this.nextNum < 0) throw new Error('nextNum property is negative');
|
||||||
for (const prop of ['numToAttrib', 'attribToNum']) {
|
for (const prop of ['numToAttrib', 'attribToNum']) {
|
||||||
|
// @ts-ignore
|
||||||
const obj = this[prop];
|
const obj = this[prop];
|
||||||
if (obj == null) throw new Error(`${prop} property is null`);
|
if (obj == null) throw new Error(`${prop} property is null`);
|
||||||
if (typeof obj !== 'object') throw new TypeError(`${prop} property is not an object`);
|
if (typeof obj !== 'object') throw new TypeError(`${prop} property is not an object`);
|
||||||
|
@ -231,9 +248,10 @@ class AttributePool {
|
||||||
if (v == null) throw new TypeError(`attrib ${i} value is null`);
|
if (v == null) throw new TypeError(`attrib ${i} value is null`);
|
||||||
if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`);
|
if (typeof v !== 'string') throw new TypeError(`attrib ${i} value is not a string`);
|
||||||
const attrStr = String(attr);
|
const attrStr = String(attr);
|
||||||
|
// @ts-ignore
|
||||||
if (this.attribToNum[attrStr] !== i) throw new Error(`attribToNum for ${attrStr} !== ${i}`);
|
if (this.attribToNum[attrStr] !== i) throw new Error(`attribToNum for ${attrStr} !== ${i}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = AttributePool;
|
export default AttributePool
|
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
|
* 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.
|
* Copyright 2009 Google Inc.
|
||||||
*
|
*
|
||||||
|
@ -20,7 +26,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
exports.buildRemoveRange = (rep, builder, start, end) => {
|
export const buildRemoveRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number,number], end: [number, number]) => {
|
||||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||||
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||||
|
|
||||||
|
@ -32,7 +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 startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||||
const endLineOffset = rep.lines.offsetOfIndex(end[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]);
|
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||||
|
|
||||||
builder.keep(startLineOffset, start[0]);
|
builder.keep(startLineOffset, start[0]);
|
||||||
builder.keep(start[1]);
|
builder.keep(start[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a number from string base 36.
|
||||||
|
*
|
||||||
|
* @param {string} str - string of the number in base 36
|
||||||
|
* @returns {number} number
|
||||||
|
*/
|
||||||
|
export const parseNum = (str: string): 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';
|
'use strict';
|
||||||
|
|
||||||
const {padutils: {warnDeprecated}} = require('./pad_utils');
|
import padUtils from './pad_utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a chat message stored in the database and transmitted among users. Plugins can extend
|
* Represents a chat message stored in the database and transmitted among users. Plugins can extend
|
||||||
|
@ -8,14 +8,25 @@ const {padutils: {warnDeprecated}} = require('./pad_utils');
|
||||||
*
|
*
|
||||||
* Supports serialization to JSON.
|
* Supports serialization to JSON.
|
||||||
*/
|
*/
|
||||||
class ChatMessage {
|
export class ChatMessage {
|
||||||
static fromObject(obj) {
|
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 userId property was renamed to authorId, and userName was renamed to displayName. Accept
|
||||||
// the old names in case the db record was written by an older version of Etherpad.
|
// the old names in case the db record was written by an older version of Etherpad.
|
||||||
obj = Object.assign({}, obj); // Don't mutate the caller's object.
|
obj = Object.assign({}, obj); // Don't mutate the caller's object.
|
||||||
if ('userId' in obj && !('authorId' in obj)) obj.authorId = obj.userId;
|
if ('userId' in obj && !('authorId' in obj)) { // @ts-ignore
|
||||||
|
obj.authorId = obj.userId;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
delete obj.userId;
|
delete obj.userId;
|
||||||
if ('userName' in obj && !('displayName' in obj)) obj.displayName = obj.userName;
|
if ('userName' in obj && !('displayName' in obj)) { // @ts-ignore
|
||||||
|
obj.displayName = obj.userName;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
delete obj.userName;
|
delete obj.userName;
|
||||||
return Object.assign(new ChatMessage(), obj);
|
return Object.assign(new ChatMessage(), obj);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +36,7 @@ class ChatMessage {
|
||||||
* @param {?string} [authorId] - Initial value of the `authorId` property.
|
* @param {?string} [authorId] - Initial value of the `authorId` property.
|
||||||
* @param {?number} [time] - Initial value of the `time` property.
|
* @param {?number} [time] - Initial value of the `time` property.
|
||||||
*/
|
*/
|
||||||
constructor(text = null, authorId = null, time = null) {
|
constructor(text: string | null = null, authorId: string | null = null, time: number | null = null) {
|
||||||
/**
|
/**
|
||||||
* The raw text of the user's chat message (before any rendering or processing).
|
* The raw text of the user's chat message (before any rendering or processing).
|
||||||
*
|
*
|
||||||
|
@ -62,11 +73,11 @@ class ChatMessage {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get userId() {
|
get userId() {
|
||||||
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||||
return this.authorId;
|
return this.authorId;
|
||||||
}
|
}
|
||||||
set userId(val) {
|
set userId(val) {
|
||||||
warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
padUtils.warnDeprecated('ChatMessage.userId property is deprecated; use .authorId instead');
|
||||||
this.authorId = val;
|
this.authorId = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,11 +88,11 @@ class ChatMessage {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
get userName() {
|
get userName() {
|
||||||
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||||
return this.displayName;
|
return this.displayName;
|
||||||
}
|
}
|
||||||
set userName(val) {
|
set userName(val) {
|
||||||
warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
padUtils.warnDeprecated('ChatMessage.userName property is deprecated; use .displayName instead');
|
||||||
this.displayName = val;
|
this.displayName = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,10 +100,12 @@ class ChatMessage {
|
||||||
// doesn't support authorId and displayName.
|
// doesn't support authorId and displayName.
|
||||||
toJSON() {
|
toJSON() {
|
||||||
const {authorId, displayName, ...obj} = this;
|
const {authorId, displayName, ...obj} = this;
|
||||||
|
// @ts-ignore
|
||||||
obj.userId = authorId;
|
obj.userId = authorId;
|
||||||
|
// @ts-ignore
|
||||||
obj.userName = displayName;
|
obj.userName = displayName;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
* 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
|
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {MapArrayType} from "../../node/types/MapType";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc.
|
* Copyright 2009 Google Inc.
|
||||||
*
|
*
|
||||||
|
@ -22,11 +24,13 @@
|
||||||
* limitations under the License.
|
* 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
|
// note that in IE designMode, properties of a node can get
|
||||||
// copied to new nodes that are spawned during editing; also,
|
// copied to new nodes that are spawned during editing; also,
|
||||||
// properties representable in HTML text can survive copy-and-paste
|
// 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.
|
// 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 (numItems < 1) return 0;
|
||||||
if (func(0)) return 0;
|
if (func(0)) return 0;
|
||||||
if (!func(numItems - 1)) return numItems;
|
if (!func(numItems - 1)) return numItems;
|
||||||
|
@ -52,17 +56,10 @@ const binarySearch = (numItems, func) => {
|
||||||
return high;
|
return high;
|
||||||
};
|
};
|
||||||
|
|
||||||
const binarySearchInfinite = (expectedLength, func) => {
|
export const binarySearchInfinite = (expectedLength: number, func: (num: number)=>boolean) => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (!func(i)) i += expectedLength;
|
while (!func(i)) i += expectedLength;
|
||||||
return binarySearch(i, func);
|
return binarySearch(i, func);
|
||||||
};
|
};
|
||||||
|
|
||||||
const noop = () => {};
|
export const noop = () => {};
|
||||||
|
|
||||||
exports.isNodeText = isNodeText;
|
|
||||||
exports.getAssoc = getAssoc;
|
|
||||||
exports.setAssoc = setAssoc;
|
|
||||||
exports.binarySearch = binarySearch;
|
|
||||||
exports.binarySearchInfinite = binarySearchInfinite;
|
|
||||||
exports.noop = noop;
|
|
|
@ -1,4 +1,5 @@
|
||||||
'use strict';
|
// @ts-nocheck
|
||||||
|
import {Builder} from "./Builder";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc.
|
* Copyright 2009 Google Inc.
|
||||||
|
@ -18,30 +19,32 @@
|
||||||
*/
|
*/
|
||||||
let documentAttributeManager;
|
let documentAttributeManager;
|
||||||
|
|
||||||
const AttributeMap = require('./AttributeMap');
|
import AttributeMap from './AttributeMap';
|
||||||
const browser = require('./vendors/browser');
|
const browser = require('./vendors/browser');
|
||||||
const padutils = require('./pad_utils').padutils;
|
import padutils from './pad_utils'
|
||||||
const Ace2Common = require('./ace2_common');
|
const Ace2Common = require('./ace2_common');
|
||||||
const $ = require('./rjquery').$;
|
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 isNodeText = Ace2Common.isNodeText;
|
||||||
const getAssoc = Ace2Common.getAssoc;
|
const getAssoc = Ace2Common.getAssoc;
|
||||||
const setAssoc = Ace2Common.setAssoc;
|
const setAssoc = Ace2Common.setAssoc;
|
||||||
const noop = Ace2Common.noop;
|
const noop = Ace2Common.noop;
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
import SkipList from "./skiplist";
|
||||||
import Scroll from './scroll'
|
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) {
|
function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
||||||
const domline = require('./domline').domline;
|
const domline = require('./domline').domline;
|
||||||
const AttribPool = require('./AttributePool');
|
|
||||||
const Changeset = require('./Changeset');
|
|
||||||
const ChangesetUtils = require('./ChangesetUtils');
|
|
||||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||||
const SkipList = require('./skiplist');
|
|
||||||
const undoModule = require('./undomodule').undoModule;
|
const undoModule = require('./undomodule').undoModule;
|
||||||
const AttributeManager = require('./AttributeManager');
|
const AttributeManager = require('./AttributeManager');
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
@ -174,9 +177,9 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// CCCCCCCCCCCCCCCCCCCC\n
|
// CCCCCCCCCCCCCCCCCCCC\n
|
||||||
// CCCC\n
|
// CCCC\n
|
||||||
// end[0]: <CCC end[1] CCC>-------\n
|
// end[0]: <CCC end[1] CCC>-------\n
|
||||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
const builder = new Builder(rep.lines.totalWidth());
|
||||||
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
|
buildKeepToStartOfRange(rep, builder, start);
|
||||||
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
|
buildRemoveRange(rep, builder, start, end);
|
||||||
builder.insert(newText, [
|
builder.insert(newText, [
|
||||||
['author', thisAuthor],
|
['author', thisAuthor],
|
||||||
], rep.apool);
|
], rep.apool);
|
||||||
|
@ -495,10 +498,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const importAText = (atext, apoolJsonObj, undoable) => {
|
const importAText = (atext, apoolJsonObj, undoable) => {
|
||||||
atext = Changeset.cloneAText(atext);
|
atext = cloneAText(atext);
|
||||||
if (apoolJsonObj) {
|
if (apoolJsonObj) {
|
||||||
const wireApool = (new AttribPool()).fromJsonable(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' : ''}`, () => {
|
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||||
setDocAText(atext);
|
setDocAText(atext);
|
||||||
|
@ -527,18 +530,18 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const numLines = rep.lines.length();
|
const numLines = rep.lines.length();
|
||||||
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
|
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
|
||||||
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
|
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
|
||||||
const assem = Changeset.smartOpAssembler();
|
const assem = new SmartOpAssembler();
|
||||||
const o = new Changeset.Op('-');
|
const o = new Op('-');
|
||||||
o.chars = upToLastLine;
|
o.chars = upToLastLine;
|
||||||
o.lines = numLines - 1;
|
o.lines = numLines - 1;
|
||||||
assem.append(o);
|
assem.append(o);
|
||||||
o.chars = lastLineLength;
|
o.chars = lastLineLength;
|
||||||
o.lines = 0;
|
o.lines = 0;
|
||||||
assem.append(o);
|
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 newLen = oldLen + assem.getLengthChange();
|
||||||
const changeset = Changeset.checkRep(
|
const changeset = checkRep(
|
||||||
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
||||||
performDocumentApplyChangeset(changeset);
|
performDocumentApplyChangeset(changeset);
|
||||||
|
|
||||||
performSelectionChange(
|
performSelectionChange(
|
||||||
|
@ -552,7 +555,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setDocText = (text) => {
|
const setDocText = (text) => {
|
||||||
setDocAText(Changeset.makeAText(text));
|
setDocAText(makeAText(text));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDocText = () => {
|
const getDocText = () => {
|
||||||
|
@ -1271,7 +1274,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
||||||
theIndent += THE_TAB;
|
theIndent += THE_TAB;
|
||||||
}
|
}
|
||||||
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
|
const cs = new Builder(rep.lines.totalWidth()).keep(
|
||||||
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
|
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
|
||||||
theIndent, [
|
theIndent, [
|
||||||
['author', thisAuthor],
|
['author', thisAuthor],
|
||||||
|
@ -1423,7 +1426,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
|
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
|
||||||
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
|
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
|
||||||
const result =
|
const result =
|
||||||
Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
||||||
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
|
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1435,7 +1438,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
length: () => rep.lines.length(),
|
length: () => rep.lines.length(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Changeset.mutateTextLines(changes, linesMutatee);
|
mutateTextLines(changes, linesMutatee);
|
||||||
|
|
||||||
if (requiredSelectionSetting) {
|
if (requiredSelectionSetting) {
|
||||||
performSelectionChange(
|
performSelectionChange(
|
||||||
|
@ -1446,10 +1449,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
|
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
|
||||||
Changeset.checkRep(changes);
|
checkRep(changes);
|
||||||
|
|
||||||
if (Changeset.oldLen(changes) !== rep.alltext.length) {
|
if (oldLen(changes) !== rep.alltext.length) {
|
||||||
const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`;
|
const errMsg = `${oldLen(changes)}/${rep.alltext.length}`;
|
||||||
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
|
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1458,10 +1461,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (!editEvent.changeset) {
|
if (!editEvent.changeset) {
|
||||||
editEvent.changeset = changes;
|
editEvent.changeset = changes;
|
||||||
} else {
|
} else {
|
||||||
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
|
editEvent.changeset = compose(editEvent.changeset, changes, rep.apool);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const inverseChangeset = Changeset.inverse(changes, {
|
const inverseChangeset = inverse(changes, {
|
||||||
get: (i) => `${rep.lines.atIndex(i).text}\n`,
|
get: (i) => `${rep.lines.atIndex(i).text}\n`,
|
||||||
length: () => rep.lines.length(),
|
length: () => rep.lines.length(),
|
||||||
}, rep.alines, rep.apool);
|
}, rep.alines, rep.apool);
|
||||||
|
@ -1469,11 +1472,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (!editEvent.backset) {
|
if (!editEvent.backset) {
|
||||||
editEvent.backset = inverseChangeset;
|
editEvent.backset = inverseChangeset;
|
||||||
} else {
|
} 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()) {
|
if (changesetTracker.isTracking()) {
|
||||||
changesetTracker.composeUserChangeset(changes);
|
changesetTracker.composeUserChangeset(changes);
|
||||||
|
@ -1582,7 +1585,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
let hasAttrib = true;
|
let hasAttrib = true;
|
||||||
|
|
||||||
let indexIntoLine = 0;
|
let indexIntoLine = 0;
|
||||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||||
const opStartInLine = indexIntoLine;
|
const opStartInLine = indexIntoLine;
|
||||||
const opEndInLine = opStartInLine + op.chars;
|
const opEndInLine = opStartInLine + op.chars;
|
||||||
if (!hasIt(op.attribs)) {
|
if (!hasIt(op.attribs)) {
|
||||||
|
@ -1627,7 +1630,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (n === selEndLine) {
|
if (n === selEndLine) {
|
||||||
selectionEndInLine = rep.selEnd[1];
|
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 opStartInLine = indexIntoLine;
|
||||||
const opEndInLine = opStartInLine + op.chars;
|
const opEndInLine = opStartInLine + op.chars;
|
||||||
if (!hasIt(op.attribs)) {
|
if (!hasIt(op.attribs)) {
|
||||||
|
@ -1745,7 +1748,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
|
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
|
||||||
|
|
||||||
const startBuilder = () => {
|
const startBuilder = () => {
|
||||||
const builder = Changeset.builder(oldLen);
|
const builder = new Builder(oldLen);
|
||||||
builder.keep(spliceStartLineStart, spliceStartLine);
|
builder.keep(spliceStartLineStart, spliceStartLine);
|
||||||
builder.keep(spliceStart - spliceStartLineStart);
|
builder.keep(spliceStart - spliceStartLineStart);
|
||||||
return builder;
|
return builder;
|
||||||
|
@ -1755,7 +1758,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = commonStart;
|
const newTextStart = commonStart;
|
||||||
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
|
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;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
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.
|
// changeset the applies the styles found in the DOM.
|
||||||
// This allows us to incorporate, e.g., Safari's native "unbold".
|
// This allows us to incorporate, e.g., Safari's native "unbold".
|
||||||
const incorpedAttribClearer = cachedStrFunc(
|
const incorpedAttribClearer = cachedStrFunc(
|
||||||
(oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => {
|
(oldAtts) => mapAttribNumbers(oldAtts, (n) => {
|
||||||
const k = rep.apool.getAttribKey(n);
|
const k = rep.apool.getAttribKey(n);
|
||||||
if (isStyleAttribute(k)) {
|
if (isStyleAttribute(k)) {
|
||||||
return rep.apool.putAttrib([k, '']);
|
return rep.apool.putAttrib([k, '']);
|
||||||
|
@ -1799,7 +1802,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
});
|
});
|
||||||
const styler = builder2.toString();
|
const styler = builder2.toString();
|
||||||
|
|
||||||
theChangeset = Changeset.compose(clearer, styler, rep.apool);
|
theChangeset = compose(clearer, styler, rep.apool);
|
||||||
} else {
|
} else {
|
||||||
const builder = startBuilder();
|
const builder = startBuilder();
|
||||||
|
|
||||||
|
@ -1869,7 +1872,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const attribRuns = (attribs) => {
|
const attribRuns = (attribs) => {
|
||||||
const lengs = [];
|
const lengs = [];
|
||||||
const atts = [];
|
const atts = [];
|
||||||
for (const op of Changeset.deserializeOps(attribs)) {
|
for (const op of deserializeOps(attribs)) {
|
||||||
lengs.push(op.chars);
|
lengs.push(op.chars);
|
||||||
atts.push(op.attribs);
|
atts.push(op.attribs);
|
||||||
}
|
}
|
||||||
|
@ -1898,8 +1901,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const newLen = newText.length;
|
const newLen = newText.length;
|
||||||
const minLen = Math.min(oldLen, newLen);
|
const minLen = Math.min(oldLen, newLen);
|
||||||
|
|
||||||
const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
const oldARuns = attribRuns(filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
||||||
const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
const newARuns = attribRuns(filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
||||||
|
|
||||||
let commonStart = 0;
|
let commonStart = 0;
|
||||||
const oldStartIter = attribIterator(oldARuns, false);
|
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
|
// 3-renumber every list item of the same level from the beginning, level 1
|
||||||
// IMPORTANT: never skip a level because there imbrication may be arbitrary
|
// IMPORTANT: never skip a level because there imbrication may be arbitrary
|
||||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
const builder = new Builder(rep.lines.totalWidth());
|
||||||
let loc = [0, 0];
|
let loc = [0, 0];
|
||||||
const applyNumberList = (line, level) => {
|
const applyNumberList = (line, level) => {
|
||||||
// init
|
// init
|
||||||
|
@ -2312,8 +2315,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (isNaN(curLevel) || listType[0] === 'indent') {
|
if (isNaN(curLevel) || listType[0] === 'indent') {
|
||||||
return line;
|
return line;
|
||||||
} else if (curLevel === level) {
|
} else if (curLevel === level) {
|
||||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
||||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
||||||
['start', position],
|
['start', position],
|
||||||
], rep.apool);
|
], rep.apool);
|
||||||
|
|
||||||
|
@ -2330,7 +2333,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
|
|
||||||
applyNumberList(lineNum, 1);
|
applyNumberList(lineNum, 1);
|
||||||
const cs = builder.toString();
|
const cs = builder.toString();
|
||||||
if (!Changeset.isIdentity(cs)) {
|
if (!isIdentity(cs)) {
|
||||||
performDocumentApplyChangeset(cs);
|
performDocumentApplyChangeset(cs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2618,7 +2621,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// TODO: There appears to be a race condition or so.
|
// TODO: There appears to be a race condition or so.
|
||||||
const authorIds = new Set();
|
const authorIds = new Set();
|
||||||
if (alineAttrs) {
|
if (alineAttrs) {
|
||||||
for (const op of Changeset.deserializeOps(alineAttrs)) {
|
for (const op of deserializeOps(alineAttrs)) {
|
||||||
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
|
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
|
||||||
if (authorId) authorIds.add(authorId);
|
if (authorId) authorIds.add(authorId);
|
||||||
}
|
}
|
||||||
|
@ -3513,8 +3516,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const oneEntry = createDomLineEntry('');
|
const oneEntry = createDomLineEntry('');
|
||||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||||
insertDomLines(null, [oneEntry.domInfo]);
|
insertDomLines(null, [oneEntry.domInfo]);
|
||||||
rep.alines = Changeset.splitAttributionLines(
|
rep.alines = splitAttributionLines(
|
||||||
Changeset.makeAttribution('\n'), '\n');
|
makeAttribution('\n'), '\n');
|
||||||
|
|
||||||
bindTheEventHandlers();
|
bindTheEventHandlers();
|
||||||
});
|
});
|
|
@ -17,6 +17,9 @@
|
||||||
* @typedef {string} AttributeString
|
* @typedef {string} AttributeString
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import AttributePool from "./AttributePool";
|
||||||
|
import {Attribute} from "./types/Attribute";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an attribute string into a sequence of attribute identifier numbers.
|
* Converts an attribute string into a sequence of attribute identifier numbers.
|
||||||
*
|
*
|
||||||
|
@ -28,7 +31,7 @@
|
||||||
* appear in `str`.
|
* appear in `str`.
|
||||||
* @returns {Generator<number>}
|
* @returns {Generator<number>}
|
||||||
*/
|
*/
|
||||||
exports.decodeAttribString = function* (str) {
|
export const decodeAttribString = function* (str: string): Generator<number> {
|
||||||
const re = /\*([0-9a-z]+)|./gy;
|
const re = /\*([0-9a-z]+)|./gy;
|
||||||
let match;
|
let match;
|
||||||
while ((match = re.exec(str)) != null) {
|
while ((match = re.exec(str)) != null) {
|
||||||
|
@ -38,7 +41,7 @@ exports.decodeAttribString = function* (str) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkAttribNum = (n) => {
|
const checkAttribNum = (n: number|object) => {
|
||||||
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
|
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
|
||||||
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
|
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
|
||||||
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
|
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
|
||||||
|
@ -50,7 +53,7 @@ const checkAttribNum = (n) => {
|
||||||
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
|
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
|
||||||
* @returns {AttributeString}
|
* @returns {AttributeString}
|
||||||
*/
|
*/
|
||||||
exports.encodeAttribString = (attribNums) => {
|
export const encodeAttribString = (attribNums: Iterable<number>): string => {
|
||||||
let str = '';
|
let str = '';
|
||||||
for (const n of attribNums) {
|
for (const n of attribNums) {
|
||||||
checkAttribNum(n);
|
checkAttribNum(n);
|
||||||
|
@ -67,7 +70,7 @@ exports.encodeAttribString = (attribNums) => {
|
||||||
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
|
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
|
||||||
* @returns {Generator<Attribute>}
|
* @returns {Generator<Attribute>}
|
||||||
*/
|
*/
|
||||||
exports.attribsFromNums = function* (attribNums, pool) {
|
export const attribsFromNums = function* (attribNums: Iterable<number>, pool: AttributePool): Generator<Attribute> {
|
||||||
for (const n of attribNums) {
|
for (const n of attribNums) {
|
||||||
checkAttribNum(n);
|
checkAttribNum(n);
|
||||||
const attrib = pool.getAttrib(n);
|
const attrib = pool.getAttrib(n);
|
||||||
|
@ -87,7 +90,7 @@ exports.attribsFromNums = function* (attribNums, pool) {
|
||||||
* @yields {number} The attribute number of each attribute in `attribs`, in order.
|
* @yields {number} The attribute number of each attribute in `attribs`, in order.
|
||||||
* @returns {Generator<number>}
|
* @returns {Generator<number>}
|
||||||
*/
|
*/
|
||||||
exports.attribsToNums = function* (attribs, pool) {
|
export const attribsToNums = function* (attribs: Iterable<Attribute>, pool: AttributePool) {
|
||||||
for (const attrib of attribs) yield pool.putAttrib(attrib);
|
for (const attrib of attribs) yield pool.putAttrib(attrib);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,8 +105,8 @@ exports.attribsToNums = function* (attribs, pool) {
|
||||||
* @yields {Attribute} The attributes identified in `str`, in order.
|
* @yields {Attribute} The attributes identified in `str`, in order.
|
||||||
* @returns {Generator<Attribute>}
|
* @returns {Generator<Attribute>}
|
||||||
*/
|
*/
|
||||||
exports.attribsFromString = function* (str, pool) {
|
export const attribsFromString = function* (str: string, pool: AttributePool): Generator<Attribute> {
|
||||||
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
|
yield* attribsFromNums(decodeAttribString(str), pool);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,8 +119,8 @@ exports.attribsFromString = function* (str, pool) {
|
||||||
* @param {AttributePool} pool - Attribute pool.
|
* @param {AttributePool} pool - Attribute pool.
|
||||||
* @returns {AttributeString}
|
* @returns {AttributeString}
|
||||||
*/
|
*/
|
||||||
exports.attribsToString =
|
export const attribsToString =
|
||||||
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
|
(attribs: Iterable<Attribute>, pool: AttributePool): string => encodeAttribString(attribsToNums(attribs, pool));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
|
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
|
||||||
|
@ -126,5 +129,14 @@ exports.attribsToString =
|
||||||
* @param {Attribute[]} attribs - Attributes to sort in place.
|
* @param {Attribute[]} attribs - Attributes to sort in place.
|
||||||
* @returns {Attribute[]} `attribs` (for chaining).
|
* @returns {Attribute[]} `attribs` (for chaining).
|
||||||
*/
|
*/
|
||||||
exports.sort =
|
export const sort = (attribs: Attribute[]): Attribute[] => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
|
||||||
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
|
|
||||||
|
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
|
// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0
|
||||||
|
|
||||||
/* Copyright 2021 Richard Hansen <rhansen@rhansen.org> */
|
/* Copyright 2021 Richard Hansen <rhansen@rhansen.org> */
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,8 +25,8 @@
|
||||||
|
|
||||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||||
const domline = require('./domline').domline;
|
const domline = require('./domline').domline;
|
||||||
const AttribPool = require('./AttributePool');
|
import AttribPool from './AttributePool';
|
||||||
const Changeset = require('./Changeset');
|
import {compose, deserializeOps, inverse, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, splitAttributionLines, splitTextLines, unpack} from './Changeset';
|
||||||
const attributes = require('./attributes');
|
const attributes = require('./attributes');
|
||||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
|
@ -53,11 +54,11 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
currentRevision: clientVars.collab_client_vars.rev,
|
currentRevision: clientVars.collab_client_vars.rev,
|
||||||
currentTime: clientVars.collab_client_vars.time,
|
currentTime: clientVars.collab_client_vars.time,
|
||||||
currentLines:
|
currentLines:
|
||||||
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
||||||
currentDivs: null,
|
currentDivs: null,
|
||||||
// to be filled in once the dom loads
|
// to be filled in once the dom loads
|
||||||
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
|
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
|
||||||
alines: Changeset.splitAttributionLines(
|
alines: splitAttributionLines(
|
||||||
clientVars.collab_client_vars.initialAttributedText.attribs,
|
clientVars.collab_client_vars.initialAttributedText.attribs,
|
||||||
clientVars.collab_client_vars.initialAttributedText.text),
|
clientVars.collab_client_vars.initialAttributedText.text),
|
||||||
|
|
||||||
|
@ -120,7 +121,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
getActiveAuthors() {
|
getActiveAuthors() {
|
||||||
const authorIds = new Set();
|
const authorIds = new Set();
|
||||||
for (const aline of this.alines) {
|
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)) {
|
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
|
||||||
if (k !== 'author') continue;
|
if (k !== 'author') continue;
|
||||||
if (v) authorIds.add(v);
|
if (v) authorIds.add(v);
|
||||||
|
@ -141,7 +142,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
const oldAlines = padContents.alines.slice();
|
const oldAlines = padContents.alines.slice();
|
||||||
try {
|
try {
|
||||||
// must mutate attribution lines before text lines
|
// must mutate attribution lines before text lines
|
||||||
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog(e);
|
debugLog(e);
|
||||||
}
|
}
|
||||||
|
@ -163,7 +164,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
// some chars are replaced (no attributes change and no length change)
|
// some chars are replaced (no attributes change and no length change)
|
||||||
// test if there are keep ops at the start of the cs
|
// test if there are keep ops at the start of the cs
|
||||||
if (lineChanged === undefined) {
|
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;
|
lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +184,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
goToLineNumber(lineChanged);
|
goToLineNumber(lineChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
Changeset.mutateTextLines(changeset, padContents);
|
mutateTextLines(changeset, padContents);
|
||||||
padContents.currentRevision = revision;
|
padContents.currentRevision = revision;
|
||||||
padContents.currentTime += timeDelta * 1000;
|
padContents.currentTime += timeDelta * 1000;
|
||||||
|
|
||||||
|
@ -272,7 +273,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
let changeset = cs[0];
|
let changeset = cs[0];
|
||||||
let timeDelta = path.times[0];
|
let timeDelta = path.times[0];
|
||||||
for (let i = 1; i < cs.length; i++) {
|
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];
|
timeDelta += path.times[i];
|
||||||
}
|
}
|
||||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||||
|
@ -290,7 +291,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
let changeset = cs[0];
|
let changeset = cs[0];
|
||||||
let timeDelta = path.times[0];
|
let timeDelta = path.times[0];
|
||||||
for (let i = 1; i < cs.length; i++) {
|
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];
|
timeDelta += path.times[i];
|
||||||
}
|
}
|
||||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
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;
|
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
|
||||||
// debugLog("adding changeset:", astart, aend);
|
// debugLog("adding changeset:", astart, aend);
|
||||||
const forwardcs =
|
const forwardcs =
|
||||||
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
||||||
const backwardcs =
|
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]);
|
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
|
||||||
}
|
}
|
||||||
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
|
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
|
||||||
|
@ -408,13 +409,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
obj = obj.data;
|
obj = obj.data;
|
||||||
|
|
||||||
if (obj.type === 'NEW_CHANGES') {
|
if (obj.type === 'NEW_CHANGES') {
|
||||||
const changeset = Changeset.moveOpsToNewPool(
|
const changeset = moveOpsToNewPool(
|
||||||
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||||
|
|
||||||
let changesetBack = Changeset.inverse(
|
let changesetBack = inverse(
|
||||||
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
||||||
|
|
||||||
changesetBack = Changeset.moveOpsToNewPool(
|
changesetBack = moveOpsToNewPool(
|
||||||
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||||
|
|
||||||
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,17 +23,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const AttributeMap = require('./AttributeMap');
|
import AttributeMap from './AttributeMap';
|
||||||
const AttributePool = require('./AttributePool');
|
import AttributePool from './AttributePool';
|
||||||
const Changeset = require('./Changeset');
|
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) => {
|
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
// latest official text from server
|
// latest official text from server
|
||||||
let baseAText = Changeset.makeAText('\n');
|
let baseAText = makeAText('\n');
|
||||||
// changes applied to baseText that have been submitted
|
// changes applied to baseText that have been submitted
|
||||||
let submittedChangeset = null;
|
let submittedChangeset = null;
|
||||||
// changes applied to submittedChangeset since it was prepared
|
// changes applied to submittedChangeset since it was prepared
|
||||||
let userChangeset = Changeset.identity(1);
|
let userChangeset = identity(1);
|
||||||
// is the changesetTracker enabled
|
// is the changesetTracker enabled
|
||||||
let tracking = false;
|
let tracking = false;
|
||||||
// stack state flag so that when we change the rep we don't
|
// stack state flag so that when we change the rep we don't
|
||||||
|
@ -66,18 +68,18 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
return self = {
|
return self = {
|
||||||
isTracking: () => tracking,
|
isTracking: () => tracking,
|
||||||
setBaseText: (text) => {
|
setBaseText: (text) => {
|
||||||
self.setBaseAttributedText(Changeset.makeAText(text), null);
|
self.setBaseAttributedText(makeAText(text), null);
|
||||||
},
|
},
|
||||||
setBaseAttributedText: (atext, apoolJsonObj) => {
|
setBaseAttributedText: (atext, apoolJsonObj) => {
|
||||||
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
|
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
|
||||||
tracking = true;
|
tracking = true;
|
||||||
baseAText = Changeset.cloneAText(atext);
|
baseAText = cloneAText(atext);
|
||||||
if (apoolJsonObj) {
|
if (apoolJsonObj) {
|
||||||
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||||
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
baseAText.attribs = moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
||||||
}
|
}
|
||||||
submittedChangeset = null;
|
submittedChangeset = null;
|
||||||
userChangeset = Changeset.identity(atext.text.length);
|
userChangeset = identity(atext.text.length);
|
||||||
applyingNonUserChanges = true;
|
applyingNonUserChanges = true;
|
||||||
try {
|
try {
|
||||||
callbacks.setDocumentAttributedText(atext);
|
callbacks.setDocumentAttributedText(atext);
|
||||||
|
@ -89,8 +91,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
composeUserChangeset: (c) => {
|
composeUserChangeset: (c) => {
|
||||||
if (!tracking) return;
|
if (!tracking) return;
|
||||||
if (applyingNonUserChanges) return;
|
if (applyingNonUserChanges) return;
|
||||||
if (Changeset.isIdentity(c)) return;
|
if (isIdentity(c)) return;
|
||||||
userChangeset = Changeset.compose(userChangeset, c, apool);
|
userChangeset = compose(userChangeset, c, apool);
|
||||||
|
|
||||||
setChangeCallbackTimeout();
|
setChangeCallbackTimeout();
|
||||||
},
|
},
|
||||||
|
@ -100,23 +102,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
|
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
|
||||||
if (apoolJsonObj) {
|
if (apoolJsonObj) {
|
||||||
const wireApool = (new AttributePool()).fromJsonable(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;
|
let c2 = c;
|
||||||
if (submittedChangeset) {
|
if (submittedChangeset) {
|
||||||
const oldSubmittedChangeset = submittedChangeset;
|
const oldSubmittedChangeset = submittedChangeset;
|
||||||
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
|
submittedChangeset = follow(c, oldSubmittedChangeset, false, apool);
|
||||||
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
|
c2 = follow(oldSubmittedChangeset, c, true, apool);
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferInsertingAfterUserChanges = true;
|
const preferInsertingAfterUserChanges = true;
|
||||||
const oldUserChangeset = userChangeset;
|
const oldUserChangeset = userChangeset;
|
||||||
userChangeset = Changeset.follow(
|
userChangeset = follow(
|
||||||
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
|
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
|
||||||
const postChange = Changeset.follow(
|
const postChange = follow(
|
||||||
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
|
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
|
||||||
|
|
||||||
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
|
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
|
||||||
|
@ -135,17 +137,17 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
if (submittedChangeset) {
|
if (submittedChangeset) {
|
||||||
// submission must have been canceled, prepare new changeset
|
// submission must have been canceled, prepare new changeset
|
||||||
// that includes old submittedChangeset
|
// that includes old submittedChangeset
|
||||||
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
|
toSubmit = compose(submittedChangeset, userChangeset, apool);
|
||||||
} else {
|
} else {
|
||||||
// Get my authorID
|
// Get my authorID
|
||||||
const authorId = parent.parent.pad.myUserInfo.userId;
|
const authorId = parent.parent.pad.myUserInfo.userId;
|
||||||
|
|
||||||
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
|
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
|
||||||
// text was copied from another author.
|
// text was copied from another author.
|
||||||
const cs = Changeset.unpack(userChangeset);
|
const cs = unpack(userChangeset);
|
||||||
const assem = Changeset.mergingOpAssembler();
|
const assem = new MergingOpAssembler();
|
||||||
|
|
||||||
for (const op of Changeset.deserializeOps(cs.ops)) {
|
for (const op of deserializeOps(cs.ops)) {
|
||||||
if (op.opcode === '+') {
|
if (op.opcode === '+') {
|
||||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
const oldAuthorId = attribs.get('author');
|
const oldAuthorId = attribs.get('author');
|
||||||
|
@ -157,23 +159,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
assem.append(op);
|
assem.append(op);
|
||||||
}
|
}
|
||||||
assem.endDocument();
|
assem.endDocument();
|
||||||
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
userChangeset = pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
||||||
Changeset.checkRep(userChangeset);
|
checkRep(userChangeset);
|
||||||
|
|
||||||
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
|
if (isIdentity(userChangeset)) toSubmit = null;
|
||||||
else toSubmit = userChangeset;
|
else toSubmit = userChangeset;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cs = null;
|
let cs = null;
|
||||||
if (toSubmit) {
|
if (toSubmit) {
|
||||||
submittedChangeset = toSubmit;
|
submittedChangeset = toSubmit;
|
||||||
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
|
userChangeset = identity(newLen(toSubmit));
|
||||||
|
|
||||||
cs = toSubmit;
|
cs = toSubmit;
|
||||||
}
|
}
|
||||||
let wireApool = null;
|
let wireApool = null;
|
||||||
if (cs) {
|
if (cs) {
|
||||||
const forWire = Changeset.prepareForWire(cs, apool);
|
const forWire = prepareForWire(cs, apool);
|
||||||
wireApool = forWire.pool.toJsonable();
|
wireApool = forWire.pool.toJsonable();
|
||||||
cs = forWire.translated;
|
cs = forWire.translated;
|
||||||
}
|
}
|
||||||
|
@ -190,13 +192,13 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
|
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
|
||||||
}
|
}
|
||||||
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
|
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
|
||||||
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
|
baseAText = applyToAText(submittedChangeset, baseAText, apool);
|
||||||
submittedChangeset = null;
|
submittedChangeset = null;
|
||||||
},
|
},
|
||||||
setUserChangeNotificationCallback: (callback) => {
|
setUserChangeNotificationCallback: (callback) => {
|
||||||
changeCallback = 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';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
||||||
|
@ -15,8 +16,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ChatMessage = require('./ChatMessage');
|
import ChatMessage from './ChatMessage';
|
||||||
const padutils = require('./pad_utils').padutils;
|
import padutils from './pad_utils'
|
||||||
const padcookie = require('./pad_cookie').padcookie;
|
const padcookie = require('./pad_cookie').padcookie;
|
||||||
const Tinycon = require('tinycon/tinycon');
|
const Tinycon = require('tinycon/tinycon');
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,5 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
* 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
|
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
|
||||||
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
||||||
// %APPJET%: import("etherpad.admin.plugins");
|
// %APPJET%: import("etherpad.admin.plugins");
|
||||||
|
import Op from "./Op";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc.
|
* Copyright 2009 Google Inc.
|
||||||
*
|
*
|
||||||
|
@ -26,9 +30,10 @@
|
||||||
|
|
||||||
const _MAX_LIST_LEVEL = 16;
|
const _MAX_LIST_LEVEL = 16;
|
||||||
|
|
||||||
const AttributeMap = require('./AttributeMap');
|
import AttributeMap from './AttributeMap';
|
||||||
const UNorm = require('unorm');
|
import UNorm from 'unorm';
|
||||||
const Changeset = require('./Changeset');
|
import {subattribution} from './Changeset';
|
||||||
|
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
|
||||||
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
||||||
|
@ -83,14 +88,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
||||||
const textArray = [];
|
const textArray = [];
|
||||||
const attribsArray = [];
|
const attribsArray = [];
|
||||||
let attribsBuilder = null;
|
let attribsBuilder = null;
|
||||||
const op = new Changeset.Op('+');
|
const op = new Op('+');
|
||||||
const self = {
|
const self = {
|
||||||
length: () => textArray.length,
|
length: () => textArray.length,
|
||||||
atColumnZero: () => textArray[textArray.length - 1] === '',
|
atColumnZero: () => textArray[textArray.length - 1] === '',
|
||||||
startNew: () => {
|
startNew: () => {
|
||||||
textArray.push('');
|
textArray.push('');
|
||||||
self.flush(true);
|
self.flush(true);
|
||||||
attribsBuilder = Changeset.smartOpAssembler();
|
attribsBuilder = new SmartOpAssembler();
|
||||||
},
|
},
|
||||||
textOfLine: (i) => textArray[i],
|
textOfLine: (i) => textArray[i],
|
||||||
appendText: (txt, attrString = '') => {
|
appendText: (txt, attrString = '') => {
|
||||||
|
@ -653,8 +658,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
||||||
const lengthToTake = lineLimit;
|
const lengthToTake = lineLimit;
|
||||||
newStrings.push(oldString.substring(0, lengthToTake));
|
newStrings.push(oldString.substring(0, lengthToTake));
|
||||||
oldString = oldString.substring(lengthToTake);
|
oldString = oldString.substring(lengthToTake);
|
||||||
newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake));
|
newAttribStrings.push(subattribution(oldAttribString, 0, lengthToTake));
|
||||||
oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake);
|
oldAttribString = subattribution(oldAttribString, lengthToTake);
|
||||||
}
|
}
|
||||||
if (oldString.length > 0) {
|
if (oldString.length > 0) {
|
||||||
newStrings.push(oldString);
|
newStrings.push(oldString);
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
|
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.domline
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,12 +31,13 @@
|
||||||
// requires: plugins
|
// requires: plugins
|
||||||
// requires: undefined
|
// requires: undefined
|
||||||
|
|
||||||
const Changeset = require('./Changeset');
|
import {deserializeOps} from './Changeset';
|
||||||
const attributes = require('./attributes');
|
import attributes from './attributes';
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
const linestylefilter = {};
|
const linestylefilter = {};
|
||||||
const AttributeManager = require('./AttributeManager');
|
const AttributeManager = require('./AttributeManager');
|
||||||
const padutils = require('./pad_utils').padutils;
|
import padutils from './pad_utils'
|
||||||
|
import Op from "./Op";
|
||||||
|
|
||||||
linestylefilter.ATTRIB_CLASSES = {
|
linestylefilter.ATTRIB_CLASSES = {
|
||||||
bold: 'tag:b',
|
bold: 'tag:b',
|
||||||
|
@ -98,12 +100,12 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
|
||||||
return classes.substring(1);
|
return classes.substring(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const attrOps = Changeset.deserializeOps(aline);
|
const attrOps = deserializeOps(aline);
|
||||||
let attrOpsNext = attrOps.next();
|
let attrOpsNext = attrOps.next();
|
||||||
let nextOp, nextOpClasses;
|
let nextOp, nextOpClasses;
|
||||||
|
|
||||||
const goNextOp = () => {
|
const goNextOp = () => {
|
||||||
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
|
nextOp = attrOpsNext.done ? new Op() : attrOpsNext.value;
|
||||||
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
|
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
|
||||||
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
|
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
|
||||||
};
|
};
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +34,8 @@ require('./vendors/gritter');
|
||||||
|
|
||||||
import html10n from './vendors/html10n'
|
import html10n from './vendors/html10n'
|
||||||
|
|
||||||
const Cookies = require('./pad_utils').Cookies;
|
import {Cookies} from "./pad_utils";
|
||||||
|
|
||||||
const chat = require('./chat').chat;
|
const chat = require('./chat').chat;
|
||||||
const getCollabClient = require('./collab_client').getCollabClient;
|
const getCollabClient = require('./collab_client').getCollabClient;
|
||||||
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
|
const padconnectionstatus = require('./pad_connectionstatus').padconnectionstatus;
|
||||||
|
@ -44,9 +46,9 @@ const padimpexp = require('./pad_impexp').padimpexp;
|
||||||
const padmodals = require('./pad_modals').padmodals;
|
const padmodals = require('./pad_modals').padmodals;
|
||||||
const padsavedrevs = require('./pad_savedrevs');
|
const padsavedrevs = require('./pad_savedrevs');
|
||||||
const paduserlist = require('./pad_userlist').paduserlist;
|
const paduserlist = require('./pad_userlist').paduserlist;
|
||||||
const padutils = require('./pad_utils').padutils;
|
import padutils from './pad_utils'
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
const randomString = require('./pad_utils').randomString;
|
import {randomString} from "./pad_utils";
|
||||||
const socketio = require('./socketio');
|
const socketio = require('./socketio');
|
||||||
|
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
import html10n from './vendors/html10n';
|
import html10n from './vendors/html10n';
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +17,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Cookies = require('./pad_utils').Cookies;
|
import {Cookies} from "./pad_utils";
|
||||||
|
|
||||||
exports.padcookie = new class {
|
exports.padcookie = new class {
|
||||||
constructor() {
|
constructor() {
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +25,7 @@
|
||||||
|
|
||||||
const browser = require('./vendors/browser');
|
const browser = require('./vendors/browser');
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
const padutils = require('./pad_utils').padutils;
|
import padutils from "./pad_utils";
|
||||||
const padeditor = require('./pad_editor').padeditor;
|
const padeditor = require('./pad_editor').padeditor;
|
||||||
const padsavedrevs = require('./pad_savedrevs');
|
const padsavedrevs = require('./pad_savedrevs');
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||||
|
@ -21,9 +22,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Cookies = require('./pad_utils').Cookies;
|
import padutils,{Cookies} from "./pad_utils";
|
||||||
const padcookie = require('./pad_cookie').padcookie;
|
const padcookie = require('./pad_cookie').padcookie;
|
||||||
const padutils = require('./pad_utils').padutils;
|
|
||||||
const Ace2Editor = require('./ace').Ace2Editor;
|
const Ace2Editor = require('./ace').Ace2Editor;
|
||||||
import html10n from '../js/vendors/html10n'
|
import html10n from '../js/vendors/html10n'
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,7 +17,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const padutils = require('./pad_utils').padutils;
|
import padutils from './pad_utils'
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
import html10n from './vendors/html10n';
|
import html10n from './vendors/html10n';
|
||||||
let myUserInfo = {};
|
let myUserInfo = {};
|
|
@ -6,6 +6,8 @@
|
||||||
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {binarySearch} from "./ace2_common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc.
|
* Copyright 2009 Google Inc.
|
||||||
*
|
*
|
||||||
|
@ -22,13 +24,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Security = require('./security');
|
const Security = require('security');
|
||||||
|
import jsCookie, {CookiesStatic} from 'js-cookie'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a random String with the given length. Is needed to generate the Author, Group,
|
* Generates a random String with the given length. Is needed to generate the Author, Group,
|
||||||
* readonly, session Ids
|
* readonly, session Ids
|
||||||
*/
|
*/
|
||||||
const randomString = (len) => {
|
export const randomString = (len?: number) => {
|
||||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||||
let randomstring = '';
|
let randomstring = '';
|
||||||
len = len || 20;
|
len = len || 20;
|
||||||
|
@ -85,13 +88,41 @@ const urlRegex = (() => {
|
||||||
'tel',
|
'tel',
|
||||||
].join('|')}):`;
|
].join('|')}):`;
|
||||||
return new RegExp(
|
return new RegExp(
|
||||||
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
|
`(?:${withAuth}|${withoutAuth}|www\\.)${urlChar}*(?!${postUrlPunct})${urlChar}`, 'g');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// https://stackoverflow.com/a/68957976
|
// https://stackoverflow.com/a/68957976
|
||||||
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
|
const base64url = /^(?=(?:.{4})*$)[A-Za-z0-9_-]*(?:[AQgw]==|[AEIMQUYcgkosw048]=)?$/;
|
||||||
|
|
||||||
const padutils = {
|
type PadEvent = {
|
||||||
|
which: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type JQueryNode = JQuery<HTMLElement>
|
||||||
|
|
||||||
|
class PadUtils {
|
||||||
|
public urlRegex: RegExp
|
||||||
|
public wordCharRegex: RegExp
|
||||||
|
public warnDeprecatedFlags: {
|
||||||
|
disabledForTestingOnly: boolean,
|
||||||
|
_rl?: {
|
||||||
|
prevs: Map<string, number>,
|
||||||
|
now: () => number,
|
||||||
|
period: number
|
||||||
|
}
|
||||||
|
logger?: any
|
||||||
|
}
|
||||||
|
public globalExceptionHandler: null | any = null;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.warnDeprecatedFlags = {
|
||||||
|
disabledForTestingOnly: false
|
||||||
|
}
|
||||||
|
this.wordCharRegex = wordCharRegex
|
||||||
|
this.urlRegex = urlRegex
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
|
* Prints a warning message followed by a stack trace (to make it easier to figure out what code
|
||||||
* is using the deprecated function).
|
* is using the deprecated function).
|
||||||
|
@ -107,41 +138,41 @@ const padutils = {
|
||||||
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
|
* @param {...*} args - Passed to `padutils.warnDeprecated.logger.warn` (or `console.warn` if no
|
||||||
* logger is set), with a stack trace appended if available.
|
* logger is set), with a stack trace appended if available.
|
||||||
*/
|
*/
|
||||||
warnDeprecated: (...args) => {
|
warnDeprecated = (...args: any[]) => {
|
||||||
if (padutils.warnDeprecated.disabledForTestingOnly) return;
|
if (this.warnDeprecatedFlags.disabledForTestingOnly) return;
|
||||||
const err = new Error();
|
const err = new Error();
|
||||||
if (Error.captureStackTrace) Error.captureStackTrace(err, padutils.warnDeprecated);
|
if (Error.captureStackTrace) Error.captureStackTrace(err, this.warnDeprecated);
|
||||||
err.name = '';
|
err.name = '';
|
||||||
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
|
// Rate limit identical deprecation warnings (as determined by the stack) to avoid log spam.
|
||||||
if (typeof err.stack === 'string') {
|
if (typeof err.stack === 'string') {
|
||||||
if (padutils.warnDeprecated._rl == null) {
|
if (this.warnDeprecatedFlags._rl == null) {
|
||||||
padutils.warnDeprecated._rl =
|
this.warnDeprecatedFlags._rl =
|
||||||
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
|
{prevs: new Map(), now: () => Date.now(), period: 10 * 60 * 1000};
|
||||||
}
|
}
|
||||||
const rl = padutils.warnDeprecated._rl;
|
const rl = this.warnDeprecatedFlags._rl;
|
||||||
const now = rl.now();
|
const now = rl.now();
|
||||||
const prev = rl.prevs.get(err.stack);
|
const prev = rl.prevs.get(err.stack);
|
||||||
if (prev != null && now - prev < rl.period) return;
|
if (prev != null && now - prev < rl.period) return;
|
||||||
rl.prevs.set(err.stack, now);
|
rl.prevs.set(err.stack, now);
|
||||||
}
|
}
|
||||||
if (err.stack) args.push(err.stack);
|
if (err.stack) args.push(err.stack);
|
||||||
(padutils.warnDeprecated.logger || console).warn(...args);
|
(this.warnDeprecatedFlags.logger || console).warn(...args);
|
||||||
},
|
}
|
||||||
|
escapeHtml = (x: string) => Security.escapeHTML(String(x))
|
||||||
escapeHtml: (x) => Security.escapeHTML(String(x)),
|
uniqueId = () => {
|
||||||
uniqueId: () => {
|
|
||||||
const pad = require('./pad').pad; // Sidestep circular dependency
|
const pad = require('./pad').pad; // Sidestep circular dependency
|
||||||
// returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits
|
// returns string that is exactly 'width' chars, padding with zeros and taking rightmost digits
|
||||||
const encodeNum =
|
const encodeNum =
|
||||||
(n, width) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
|
(n: number, width: number) => (Array(width + 1).join('0') + Number(n).toString(35)).slice(-width);
|
||||||
return [
|
return [
|
||||||
pad.getClientIp(),
|
pad.getClientIp(),
|
||||||
encodeNum(+new Date(), 7),
|
encodeNum(+new Date(), 7),
|
||||||
encodeNum(Math.floor(Math.random() * 1e9), 4),
|
encodeNum(Math.floor(Math.random() * 1e9), 4),
|
||||||
].join('.');
|
].join('.');
|
||||||
},
|
}
|
||||||
|
|
||||||
// e.g. "Thu Jun 18 2009 13:09"
|
// e.g. "Thu Jun 18 2009 13:09"
|
||||||
simpleDateTime: (date) => {
|
simpleDateTime = (date: string) => {
|
||||||
const d = new Date(+date); // accept either number or date
|
const d = new Date(+date); // accept either number or date
|
||||||
const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
|
const dayOfWeek = (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])[d.getDay()];
|
||||||
const month = ([
|
const month = ([
|
||||||
|
@ -162,16 +193,14 @@ const padutils = {
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`;
|
const hourmin = `${d.getHours()}:${(`0${d.getMinutes()}`).slice(-2)}`;
|
||||||
return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`;
|
return `${dayOfWeek} ${month} ${dayOfMonth} ${year} ${hourmin}`;
|
||||||
},
|
}
|
||||||
wordCharRegex,
|
|
||||||
urlRegex,
|
|
||||||
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
|
// returns null if no URLs, or [[startIndex1, url1], [startIndex2, url2], ...]
|
||||||
findURLs: (text) => {
|
findURLs = (text: string) => {
|
||||||
// Copy padutils.urlRegex so that the use of .exec() below (which mutates the RegExp object)
|
// Copy padutils.urlRegex so that the use of .exec() below (which mutates the RegExp object)
|
||||||
// does not break other concurrent uses of padutils.urlRegex.
|
// does not break other concurrent uses of padutils.urlRegex.
|
||||||
const urlRegex = new RegExp(padutils.urlRegex, 'g');
|
const urlRegex = new RegExp(this.urlRegex, 'g');
|
||||||
urlRegex.lastIndex = 0;
|
urlRegex.lastIndex = 0;
|
||||||
let urls = null;
|
let urls: [number, string][] | null = null;
|
||||||
let execResult;
|
let execResult;
|
||||||
// TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped.
|
// TODO: Switch to String.prototype.matchAll() after support for Node.js < 12.0.0 is dropped.
|
||||||
while ((execResult = urlRegex.exec(text))) {
|
while ((execResult = urlRegex.exec(text))) {
|
||||||
|
@ -181,18 +210,19 @@ const padutils = {
|
||||||
urls.push([startIndex, url]);
|
urls.push([startIndex, url]);
|
||||||
}
|
}
|
||||||
return urls;
|
return urls;
|
||||||
},
|
}
|
||||||
escapeHtmlWithClickableLinks: (text, target) => {
|
escapeHtmlWithClickableLinks = (text: string, target: string) => {
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
const pieces = [];
|
const pieces = [];
|
||||||
const urls = padutils.findURLs(text);
|
const urls = this.findURLs(text);
|
||||||
|
|
||||||
const advanceTo = (i) => {
|
const advanceTo = (i: number) => {
|
||||||
if (i > idx) {
|
if (i > idx) {
|
||||||
pieces.push(Security.escapeHTML(text.substring(idx, i)));
|
pieces.push(Security.escapeHTML(text.substring(idx, i)));
|
||||||
idx = i;
|
idx = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
;
|
||||||
if (urls) {
|
if (urls) {
|
||||||
for (let j = 0; j < urls.length; j++) {
|
for (let j = 0; j < urls.length; j++) {
|
||||||
const startIndex = urls[j][0];
|
const startIndex = urls[j][0];
|
||||||
|
@ -206,25 +236,25 @@ const padutils = {
|
||||||
// https://mathiasbynens.github.io/rel-noopener/
|
// https://mathiasbynens.github.io/rel-noopener/
|
||||||
// https://github.com/ether/etherpad-lite/pull/3636
|
// https://github.com/ether/etherpad-lite/pull/3636
|
||||||
pieces.push(
|
pieces.push(
|
||||||
'<a ',
|
'<a ',
|
||||||
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
|
(target ? `target="${Security.escapeHTMLAttribute(target)}" ` : ''),
|
||||||
'href="',
|
'href="',
|
||||||
Security.escapeHTMLAttribute(href),
|
Security.escapeHTMLAttribute(href),
|
||||||
'" rel="noreferrer noopener">');
|
'" rel="noreferrer noopener">');
|
||||||
advanceTo(startIndex + href.length);
|
advanceTo(startIndex + href.length);
|
||||||
pieces.push('</a>');
|
pieces.push('</a>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
advanceTo(text.length);
|
advanceTo(text.length);
|
||||||
return pieces.join('');
|
return pieces.join('');
|
||||||
},
|
}
|
||||||
bindEnterAndEscape: (node, onEnter, onEscape) => {
|
bindEnterAndEscape = (node: JQueryNode, onEnter: Function, onEscape: Function) => {
|
||||||
// Use keypress instead of keyup in bindEnterAndEscape. Keyup event is fired on enter in IME
|
// Use keypress instead of keyup in bindEnterAndEscape. Keyup event is fired on enter in IME
|
||||||
// (Input Method Editor), But keypress is not. So, I changed to use keypress instead of keyup.
|
// (Input Method Editor), But keypress is not. So, I changed to use keypress instead of keyup.
|
||||||
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
|
// It is work on Windows (IE8, Chrome 6.0.472), CentOs (Firefox 3.0) and Mac OSX (Firefox
|
||||||
// 3.6.10, Chrome 6.0.472, Safari 5.0).
|
// 3.6.10, Chrome 6.0.472, Safari 5.0).
|
||||||
if (onEnter) {
|
if (onEnter) {
|
||||||
node.on('keypress', (evt) => {
|
node.on('keypress', (evt: { which: number; }) => {
|
||||||
if (evt.which === 13) {
|
if (evt.which === 13) {
|
||||||
onEnter(evt);
|
onEnter(evt);
|
||||||
}
|
}
|
||||||
|
@ -238,13 +268,15 @@ const padutils = {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
timediff: (d) => {
|
|
||||||
|
timediff = (d: number) => {
|
||||||
const pad = require('./pad').pad; // Sidestep circular dependency
|
const pad = require('./pad').pad; // Sidestep circular dependency
|
||||||
const format = (n, word) => {
|
const format = (n: number, word: string) => {
|
||||||
n = Math.round(n);
|
n = Math.round(n);
|
||||||
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
|
return (`${n} ${word}${n !== 1 ? 's' : ''} ago`);
|
||||||
};
|
}
|
||||||
|
;
|
||||||
d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000);
|
d = Math.max(0, (+(new Date()) - (+d) - pad.clientTimeOffset) / 1000);
|
||||||
if (d < 60) {
|
if (d < 60) {
|
||||||
return format(d, 'second');
|
return format(d, 'second');
|
||||||
|
@ -259,78 +291,89 @@ const padutils = {
|
||||||
}
|
}
|
||||||
d /= 24;
|
d /= 24;
|
||||||
return format(d, 'day');
|
return format(d, 'day');
|
||||||
},
|
}
|
||||||
makeAnimationScheduler: (funcToAnimateOneStep, stepTime, stepsAtOnce) => {
|
makeAnimationScheduler =
|
||||||
if (stepsAtOnce === undefined) {
|
(funcToAnimateOneStep: any, stepTime: number, stepsAtOnce?: number) => {
|
||||||
stepsAtOnce = 1;
|
if (stepsAtOnce === undefined) {
|
||||||
|
stepsAtOnce = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let animationTimer: any = null;
|
||||||
|
|
||||||
|
const scheduleAnimation = () => {
|
||||||
|
if (!animationTimer) {
|
||||||
|
animationTimer = window.setTimeout(() => {
|
||||||
|
animationTimer = null;
|
||||||
|
let n = stepsAtOnce;
|
||||||
|
let moreToDo = true;
|
||||||
|
while (moreToDo && n > 0) {
|
||||||
|
moreToDo = funcToAnimateOneStep();
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
if (moreToDo) {
|
||||||
|
// more to do
|
||||||
|
scheduleAnimation();
|
||||||
|
}
|
||||||
|
}, stepTime * stepsAtOnce);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {scheduleAnimation};
|
||||||
}
|
}
|
||||||
|
|
||||||
let animationTimer = null;
|
makeFieldLabeledWhenEmpty
|
||||||
|
=
|
||||||
|
(field: JQueryNode, labelText: string) => {
|
||||||
|
field = $(field);
|
||||||
|
|
||||||
const scheduleAnimation = () => {
|
const clear = () => {
|
||||||
if (!animationTimer) {
|
field.addClass('editempty');
|
||||||
animationTimer = window.setTimeout(() => {
|
field.val(labelText);
|
||||||
animationTimer = null;
|
}
|
||||||
let n = stepsAtOnce;
|
;
|
||||||
let moreToDo = true;
|
field.focus(() => {
|
||||||
while (moreToDo && n > 0) {
|
if (field.hasClass('editempty')) {
|
||||||
moreToDo = funcToAnimateOneStep();
|
field.val('');
|
||||||
n--;
|
}
|
||||||
}
|
field.removeClass('editempty');
|
||||||
if (moreToDo) {
|
});
|
||||||
// more to do
|
field.on('blur', () => {
|
||||||
scheduleAnimation();
|
if (!field.val()) {
|
||||||
}
|
clear();
|
||||||
}, stepTime * stepsAtOnce);
|
}
|
||||||
}
|
});
|
||||||
};
|
return {
|
||||||
return {scheduleAnimation};
|
clear,
|
||||||
},
|
};
|
||||||
makeFieldLabeledWhenEmpty: (field, labelText) => {
|
|
||||||
field = $(field);
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
field.addClass('editempty');
|
|
||||||
field.val(labelText);
|
|
||||||
};
|
|
||||||
field.focus(() => {
|
|
||||||
if (field.hasClass('editempty')) {
|
|
||||||
field.val('');
|
|
||||||
}
|
|
||||||
field.removeClass('editempty');
|
|
||||||
});
|
|
||||||
field.on('blur', () => {
|
|
||||||
if (!field.val()) {
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
clear,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getCheckbox: (node) => $(node).is(':checked'),
|
|
||||||
setCheckbox: (node, value) => {
|
|
||||||
if (value) {
|
|
||||||
$(node).attr('checked', 'checked');
|
|
||||||
} else {
|
|
||||||
$(node).prop('checked', false);
|
|
||||||
}
|
}
|
||||||
},
|
getCheckbox = (node: string) => $(node).is(':checked')
|
||||||
bindCheckboxChange: (node, func) => {
|
setCheckbox =
|
||||||
$(node).on('change', func);
|
(node: JQueryNode, value: boolean) => {
|
||||||
},
|
if (value) {
|
||||||
encodeUserId: (userId) => userId.replace(/[^a-y0-9]/g, (c) => {
|
$(node).attr('checked', 'checked');
|
||||||
if (c === '.') return '-';
|
} else {
|
||||||
return `z${c.charCodeAt(0)}z`;
|
$(node).prop('checked', false);
|
||||||
}),
|
}
|
||||||
decodeUserId: (encodedUserId) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
|
|
||||||
if (cc === '-') { return '.'; } else if (cc.charAt(0) === 'z') {
|
|
||||||
return String.fromCharCode(Number(cc.slice(1, -1)));
|
|
||||||
} else {
|
|
||||||
return cc;
|
|
||||||
}
|
}
|
||||||
}),
|
bindCheckboxChange =
|
||||||
|
(node: JQueryNode, func: Function) => {
|
||||||
|
// @ts-ignore
|
||||||
|
$(node).on("change", func);
|
||||||
|
}
|
||||||
|
encodeUserId =
|
||||||
|
(userId: string) => userId.replace(/[^a-y0-9]/g, (c) => {
|
||||||
|
if (c === '.') return '-';
|
||||||
|
return `z${c.charCodeAt(0)}z`;
|
||||||
|
})
|
||||||
|
decodeUserId =
|
||||||
|
(encodedUserId: string) => encodedUserId.replace(/[a-y0-9]+|-|z.+?z/g, (cc) => {
|
||||||
|
if (cc === '-') {
|
||||||
|
return '.';
|
||||||
|
} else if (cc.charAt(0) === 'z') {
|
||||||
|
return String.fromCharCode(Number(cc.slice(1, -1)));
|
||||||
|
} else {
|
||||||
|
return cc;
|
||||||
|
}
|
||||||
|
})
|
||||||
/**
|
/**
|
||||||
* Returns whether a string has the expected format to be used as a secret token identifying an
|
* Returns whether a string has the expected format to be used as a secret token identifying an
|
||||||
* author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648
|
* author. The format is defined as: 't.' followed by a non-empty base64url string (RFC 4648
|
||||||
|
@ -340,109 +383,109 @@ const padutils = {
|
||||||
* conditional transformation of a token to a database key in a way that does not allow a
|
* conditional transformation of a token to a database key in a way that does not allow a
|
||||||
* malicious user to impersonate another user).
|
* malicious user to impersonate another user).
|
||||||
*/
|
*/
|
||||||
isValidAuthorToken: (t) => {
|
isValidAuthorToken = (t: string | object) => {
|
||||||
if (typeof t !== 'string' || !t.startsWith('t.')) return false;
|
if (typeof t !== 'string' || !t.startsWith('t.')) return false;
|
||||||
const v = t.slice(2);
|
const v = t.slice(2);
|
||||||
return v.length > 0 && base64url.test(v);
|
return v.length > 0 && base64url.test(v);
|
||||||
},
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
|
* Returns a string that can be used in the `token` cookie as a secret that authenticates a
|
||||||
* particular author.
|
* particular author.
|
||||||
*/
|
*/
|
||||||
generateAuthorToken: () => `t.${randomString()}`,
|
generateAuthorToken = () => `t.${randomString()}`
|
||||||
};
|
setupGlobalExceptionHandler = () => {
|
||||||
|
if (this.globalExceptionHandler == null) {
|
||||||
let globalExceptionHandler = null;
|
this.globalExceptionHandler = (e: any) => {
|
||||||
padutils.setupGlobalExceptionHandler = () => {
|
let type;
|
||||||
if (globalExceptionHandler == null) {
|
let err;
|
||||||
globalExceptionHandler = (e) => {
|
let msg, url, linenumber;
|
||||||
let type;
|
if (e instanceof ErrorEvent) {
|
||||||
let err;
|
type = 'Uncaught exception';
|
||||||
let msg, url, linenumber;
|
err = e.error || {};
|
||||||
if (e instanceof ErrorEvent) {
|
({message: msg, filename: url, lineno: linenumber} = e);
|
||||||
type = 'Uncaught exception';
|
} else if (e instanceof PromiseRejectionEvent) {
|
||||||
err = e.error || {};
|
type = 'Unhandled Promise rejection';
|
||||||
({message: msg, filename: url, lineno: linenumber} = e);
|
err = e.reason || {};
|
||||||
} else if (e instanceof PromiseRejectionEvent) {
|
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
|
||||||
type = 'Unhandled Promise rejection';
|
} else {
|
||||||
err = e.reason || {};
|
throw new Error(`unknown event: ${e.toString()}`);
|
||||||
({message: msg = 'unknown', fileName: url = 'unknown', lineNumber: linenumber = -1} = err);
|
|
||||||
} else {
|
|
||||||
throw new Error(`unknown event: ${e.toString()}`);
|
|
||||||
}
|
|
||||||
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
|
|
||||||
msg = `${err.name}: ${msg}`;
|
|
||||||
}
|
|
||||||
const errorId = randomString(20);
|
|
||||||
|
|
||||||
let msgAlreadyVisible = false;
|
|
||||||
$('.gritter-item .error-msg').each(function () {
|
|
||||||
if ($(this).text() === msg) {
|
|
||||||
msgAlreadyVisible = true;
|
|
||||||
}
|
}
|
||||||
});
|
if (err.name != null && msg !== err.name && !msg.startsWith(`${err.name}: `)) {
|
||||||
|
msg = `${err.name}: ${msg}`;
|
||||||
|
}
|
||||||
|
const errorId = randomString(20);
|
||||||
|
|
||||||
if (!msgAlreadyVisible) {
|
let msgAlreadyVisible = false;
|
||||||
const txt = document.createTextNode.bind(document); // Convenience shorthand.
|
$('.gritter-item .error-msg').each(function () {
|
||||||
const errorMsg = [
|
if ($(this).text() === msg) {
|
||||||
$('<p>')
|
msgAlreadyVisible = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!msgAlreadyVisible) {
|
||||||
|
const txt = document.createTextNode.bind(document); // Convenience shorthand.
|
||||||
|
const errorMsg = [
|
||||||
|
$('<p>')
|
||||||
.append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')),
|
.append($('<b>').text('Please press and hold Ctrl and press F5 to reload this page')),
|
||||||
$('<p>')
|
$('<p>')
|
||||||
.text('If the problem persists, please send this error message to your webmaster:'),
|
.text('If the problem persists, please send this error message to your webmaster:'),
|
||||||
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
|
$('<div>').css('text-align', 'left').css('font-size', '.8em').css('margin-top', '1em')
|
||||||
.append($('<b>').addClass('error-msg').text(msg)).append($('<br>'))
|
.append($('<b>').addClass('error-msg').text(msg)).append($('<br>'))
|
||||||
.append(txt(`at ${url} at line ${linenumber}`)).append($('<br>'))
|
.append(txt(`at ${url} at line ${linenumber}`)).append($('<br>'))
|
||||||
.append(txt(`ErrorId: ${errorId}`)).append($('<br>'))
|
.append(txt(`ErrorId: ${errorId}`)).append($('<br>'))
|
||||||
.append(txt(type)).append($('<br>'))
|
.append(txt(type)).append($('<br>'))
|
||||||
.append(txt(`URL: ${window.location.href}`)).append($('<br>'))
|
.append(txt(`URL: ${window.location.href}`)).append($('<br>'))
|
||||||
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
|
.append(txt(`UserAgent: ${navigator.userAgent}`)).append($('<br>')),
|
||||||
];
|
];
|
||||||
|
|
||||||
$.gritter.add({
|
// @ts-ignore
|
||||||
title: 'An error occurred',
|
$.gritter.add({
|
||||||
text: errorMsg,
|
title: 'An error occurred',
|
||||||
class_name: 'error',
|
text: errorMsg,
|
||||||
position: 'bottom',
|
class_name: 'error',
|
||||||
sticky: true,
|
position: 'bottom',
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// send javascript errors to the server
|
||||||
|
$.post('../jserror', {
|
||||||
|
errorInfo: JSON.stringify({
|
||||||
|
errorId,
|
||||||
|
type,
|
||||||
|
msg,
|
||||||
|
url: window.location.href,
|
||||||
|
source: url,
|
||||||
|
linenumber,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
stack: err.stack,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
window.onerror = null; // Clear any pre-existing global error handler.
|
||||||
// send javascript errors to the server
|
window.addEventListener('error', this.globalExceptionHandler);
|
||||||
$.post('../jserror', {
|
window.addEventListener('unhandledrejection', this.globalExceptionHandler);
|
||||||
errorInfo: JSON.stringify({
|
}
|
||||||
errorId,
|
|
||||||
type,
|
|
||||||
msg,
|
|
||||||
url: window.location.href,
|
|
||||||
source: url,
|
|
||||||
linenumber,
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
stack: err.stack,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
window.onerror = null; // Clear any pre-existing global error handler.
|
|
||||||
window.addEventListener('error', globalExceptionHandler);
|
|
||||||
window.addEventListener('unhandledrejection', globalExceptionHandler);
|
|
||||||
}
|
}
|
||||||
};
|
binarySearch = binarySearch
|
||||||
|
}
|
||||||
padutils.binarySearch = require('./ace2_common').binarySearch;
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/42660748
|
// https://stackoverflow.com/a/42660748
|
||||||
const inThirdPartyIframe = () => {
|
const inThirdPartyIframe = () => {
|
||||||
try {
|
try {
|
||||||
return (!window.top.location.hostname);
|
return (!window.top!.location.hostname);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export let Cookies: CookiesStatic<string>
|
||||||
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
// This file is included from Node so that it can reuse randomString, but Node doesn't have a global
|
||||||
// window object.
|
// window object.
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
exports.Cookies = require('js-cookie').withAttributes({
|
Cookies = jsCookie.withAttributes({
|
||||||
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
// Use `SameSite=Lax`, unless Etherpad is embedded in an iframe from another site in which case
|
||||||
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
// use `SameSite=None`. For iframes from another site, only `None` has a chance of working
|
||||||
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
// because the cookies are third-party (not same-site). Many browsers/users block third-party
|
||||||
|
@ -455,5 +498,5 @@ if (typeof window !== 'undefined') {
|
||||||
secure: window.location.protocol === 'https:',
|
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)
|
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) {
|
public async installPlugin(pluginName: string, version?: string) {
|
||||||
if (version) {
|
if (version) {
|
||||||
const installedPlugin = await this.livePluginManager.install(pluginName, version);
|
const installedPlugin = await this.livePluginManager.install(pluginName, version);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const pluginUtils = require('./shared');
|
const pluginUtils = require('./shared');
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const pluginDefs = require('./plugin_defs');
|
const pluginDefs = require('./plugin_defs');
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @ts-nocheck
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs').promises;
|
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