Non working hmr

This commit is contained in:
SamTV12345 2024-07-16 22:00:50 +02:00
parent 70092c1cb7
commit d522de17e6
20 changed files with 2621 additions and 1185 deletions

685
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -10,10 +10,8 @@ const settings = require('../../utils/Settings');
import util from 'node:util';
const webaccess = require('./webaccess');
const plugins = require('../../../static/js/pluginfw/plugin_defs');
import {hash, createHash} from 'node:crypto'
import {buildSync} from 'esbuild'
import {build, buildSync} from 'esbuild'
exports.expressPreSession = async (hookName:string, {app}:any) => {
// This endpoint is intended to conform to:
// https://www.ietf.org/archive/id/draft-inadarei-api-health-check-06.html
@ -77,6 +75,54 @@ exports.expressPreSession = async (hookName:string, {app}:any) => {
});
};
const convertTypescript = (content: string) => {
const outputRaw = buildSync({
stdin: {
contents: content,
resolveDir: path.join(settings.root, 'var','js'),
loader: 'js'
},
bundle: true, // Bundle the files together
minify: process.env.NODE_ENV === "production", // Minify the output
sourcemap: !(process.env.NODE_ENV === "production"), // Generate source maps
sourceRoot: settings.root+"/src/static/js/",
target: ['es2020'], // Target ECMAScript version
metafile: true,
write: false, // Do not write to file system,
})
const output = outputRaw.outputFiles[0].text
return {
output,
hash: outputRaw.outputFiles[0].hash.replaceAll('/','2')
}
}
const convertTypescriptWatched = (content: string, cb: (output:string, hash: string)=>void) => {
build({
stdin: {
contents: content,
resolveDir: path.join(settings.root, 'var','js'),
loader: 'js'
},
bundle: true, // Bundle the files together
minify: process.env.NODE_ENV === "production", // Minify the output
sourcemap: !(process.env.NODE_ENV === "production"), // Generate source maps
sourceRoot: settings.root+"/src/static/js/",
target: ['es2020'], // Target ECMAScript version
metafile: true,
write: false, // Do not write to file system,
}).then((outputRaw) => {
cb(
outputRaw.outputFiles[0].text,
outputRaw.outputFiles[0].hash.replaceAll('/','2')
)
})
}
exports.expressCreateServer = async (hookName: string, args: any, cb: Function) => {
// serve index.html under /
args.app.get('/', (req: any, res: any) => {
@ -98,7 +144,6 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
settings,
})
const timeSliderString = eejs.require('ep_etherpad-lite/templates/timeSliderBootstrap.js', {
pluginModules: (() => {
const pluginModules = new Set();
@ -117,97 +162,128 @@ exports.expressCreateServer = async (hookName: string, args: any, cb: Function)
const outdir = path.join(settings.root, 'var','js')
const padWriteResult = buildSync({
stdin: {
contents: padString,
resolveDir: path.join(settings.root, 'var','js'),
loader: 'js'
}, // Entry file(s)
bundle: true, // Bundle the files together
minify: process.env.NODE_ENV === "production", // Minify the output
sourcemap: !(process.env.NODE_ENV === "production"), // Generate source maps
sourceRoot: settings.root+"/src/static/js/",
target: ['es2020'], // Target ECMAScript version
metafile: true,
write: false, // Do not write to file system,
})
let fileNamePad: string
let fileNameTimeSlider: string
if(process.env.NODE_ENV === "production"){
const padSliderWrite = convertTypescript(padString)
const timeSliderWrite = convertTypescript(timeSliderString)
const outputPadJS = padWriteResult.outputFiles[0].text
fileNamePad = `padbootstrap-${padSliderWrite.hash}.min.js`
fileNameTimeSlider = `timeSliderBootstrap-${timeSliderWrite.hash}.min.js`
const pathNamePad = path.join(outdir, fileNamePad)
const pathNameTimeSlider = path.join(outdir, fileNameTimeSlider)
const timeSliderWrite = buildSync({
stdin: {
contents: timeSliderString,
resolveDir: path.join(settings.root, 'var','js'),
loader: 'js'
},
bundle: true, // Bundle the files together
minify: process.env.NODE_ENV === "production", // Minify the output
sourcemap: !(process.env.NODE_ENV === "production"), // Generate source maps
sourceRoot: settings.root+"/src/static/js/",
target: ['es2020'], // Target ECMAScript version
metafile: true,
write: false, // Do not write to file system,
})
if (!fs.existsSync(pathNamePad)) {
fs.writeFileSync(pathNamePad, padSliderWrite.output);
}
const outputTimeslider = timeSliderWrite.outputFiles[0].text
if (!fs.existsSync(pathNameTimeSlider)) {
fs.writeFileSync(pathNameTimeSlider,timeSliderWrite.output)
}
const hash = padWriteResult.outputFiles[0].hash
const hashTimeSlider = timeSliderWrite.outputFiles[0].hash
args.app.get("/"+fileNamePad, (req: any, res: any) => {
res.sendFile(pathNamePad)
})
const fileNamePad = `padbootstrap-${hash}.min.js`
const fileNameTimeSlider = `timeSliderBootstrap-${hashTimeSlider}.min.js`
const pathNamePad = path.join(outdir, fileNamePad)
const pathNameTimeSlider = path.join(outdir, fileNameTimeSlider)
if (!fs.existsSync(pathNamePad)) {
fs.writeFileSync(pathNamePad, outputPadJS);
}
if (!fs.existsSync(pathNameTimeSlider)) {
fs.writeFileSync(pathNameTimeSlider,outputTimeslider)
}
args.app.get("/"+fileNamePad, (req: any, res: any) => {
res.sendFile(pathNamePad)
})
args.app.get("/"+fileNameTimeSlider, (req: any, res: any) => {
res.sendFile(pathNameTimeSlider)
})
args.app.get("/"+fileNameTimeSlider, (req: any, res: any) => {
res.sendFile(pathNameTimeSlider)
})
// serve pad.html under /p
args.app.get('/p/:pad', (req: any, res: any, next: Function) => {
// The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
// serve pad.html under /p
args.app.get('/p/:pad', (req: any, res: any, next: Function) => {
// The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', {
toolbar,
isReadOnly
hooks.callAll('padInitToolbar', {
toolbar,
isReadOnly
});
// can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\'');
const content = eejs.require('ep_etherpad-lite/templates/pad.html', {
req,
toolbar,
isReadOnly,
entrypoint: "/"+fileNamePad
})
res.send(content);
});
// can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\'');
res.send(eejs.require('ep_etherpad-lite/templates/pad.html', {
req,
toolbar,
isReadOnly,
entrypoint: "/"+fileNamePad
}));
});
// serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => {
hooks.callAll('padInitToolbar', {
toolbar,
});
// serve timeslider.html under /p/$padname/timeslider
args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => {
hooks.callAll('padInitToolbar', {
toolbar,
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
req,
toolbar,
entrypoint: "/"+fileNameTimeSlider
}));
});
} else {
const chokidar = await import('chokidar')
const map = new Map<string, string>()
const watcher = chokidar.watch(path.join(settings.root, 'src','static','js'));
watcher.on('change', path => {
console.log(`File ${path} has been changed`);
convertTypescriptWatched(padString, (output, hash)=>{
console.log("New hash is", hash)
map.set('output', output)
map.set('fileNamePad', `padbootstrap-${hash}.js`)
});
convertTypescriptWatched(timeSliderString, (output, hash)=>{
// serve timeslider.html under /p/$padname/timeslider
console.log("New hash is", hash)
args.app.get('/watch/timeslider', (req: any, res: any) => {
res.header('Content-Type', 'application/javascript');
res.send(output)
})
});
});
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
req,
toolbar,
entrypoint: "/"+fileNameTimeSlider
}));
});
args.app.get('/watch/pad', (req: any, res: any) => {
res.header('Content-Type', 'application/javascript');
res.send(map.get('output'))
})
// serve pad.html under /p
args.app.get('/p/:pad', (req: any, res: any, next: Function) => {
console.log("Reloading pad")
// The below might break for pads being rewritten
const isReadOnly = !webaccess.userCanModify(req.params.pad, req);
hooks.callAll('padInitToolbar', {
toolbar,
isReadOnly
});
// can be removed when require-kernel is dropped
res.header('Feature-Policy', 'sync-xhr \'self\'');
const content = eejs.require('ep_etherpad-lite/templates/pad.html', {
req,
toolbar,
isReadOnly,
entrypoint: map.get('fileNamePad')
})
res.send(content);
});
args.app.get('/p/:pad/timeslider', (req: any, res: any, next: Function) => {
hooks.callAll('padInitToolbar', {
toolbar,
});
let timeSliderFileName = "/watch/timeslider?hash="+map.get('fileNamePad')
res.send(eejs.require('ep_etherpad-lite/templates/timeslider.html', {
req,
toolbar,
entrypoint: timeSliderFileName
}));
});
}
// The client occasionally polls this endpoint to get an updated expiration for the express_sid
// cookie. This handler must be installed after the express-session middleware.

View file

@ -109,7 +109,8 @@
"sinon": "^18.0.0",
"split-grid": "^1.0.11",
"supertest": "^7.0.0",
"typescript": "^5.5.3"
"typescript": "^5.5.3",
"chokidar": "^3.6.0"
},
"engines": {
"node": ">=18.18.2",

View file

@ -32,6 +32,9 @@ const colorutils = require('./colorutils').colorutils;
const _ = require('./underscore');
const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
// These parameters were global, now they are injected. A reference to the
// Timeslider controller would probably be more appropriate.
const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, BroadcastSlider) => {

View file

@ -26,6 +26,7 @@
const _ = require('./underscore');
const padmodals = require('./pad_modals').padmodals;
const colorutils = require('./colorutils').colorutils;
import html10n from './vendors/html10n';
const loadBroadcastSliderJS = (fireWhenAllScriptsAreLoaded) => {
let BroadcastSlider;

View file

@ -21,10 +21,12 @@ const padcookie = require('./pad_cookie').padcookie;
const Tinycon = require('tinycon/tinycon');
const hooks = require('./pluginfw/hooks');
const padeditor = require('./pad_editor').padeditor;
import html10n from './vendors/html10n';
// Removes diacritics and lower-cases letters. https://stackoverflow.com/a/37511463
const normalize = (s) => s.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
exports.chat = (() => {
let isStuck = false;
let userAndChat = false;

View file

@ -1,17 +0,0 @@
import html10n from '../js/vendors/html10n';
((document) => {
// Set language for l10n
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) language = language[1];
html10n.bind('indexed', () => {
html10n.localize([language, navigator.language, navigator.userLanguage, 'en']);
});
html10n.bind('localized', () => {
document.documentElement.lang = html10n.getLanguage();
document.documentElement.dir = html10n.getDirection();
});
})(document);

18
src/static/js/l10n.ts Normal file
View file

@ -0,0 +1,18 @@
import html10n from '../js/vendors/html10n';
((document) => {
// Set language for l10n
let regexpLang: string | undefined;
let language = document.cookie.match(/language=((\w{2,3})(-\w+)?)/);
if (language) regexpLang = language[1];
html10n.mt.bind('indexed', () => {
html10n.localize([regexpLang, navigator.language, 'en']);
});
html10n.mt.bind('localized', () => {
document.documentElement.lang = html10n.getLanguage()!;
document.documentElement.dir = html10n.getDirection()!;
});
})(document);

View file

@ -30,6 +30,8 @@ require('./vendors/jquery');
require('./vendors/farbtastic');
require('./vendors/gritter');
import html10n from './vendors/html10n'
const Cookies = require('./pad_utils').Cookies;
const chat = require('./chat').chat;
const getCollabClient = require('./collab_client').getCollabClient;
@ -136,7 +138,7 @@ const getParameters = [
name: 'lang',
checkVal: null,
callback: (val) => {
window.html10n.localize([val, 'en']);
html10n.localize([val, 'en']);
Cookies.set('language', val);
},
},

View file

@ -1,4 +1,5 @@
'use strict';
import html10n from './vendors/html10n';
exports.showCountDownTimerToReconnectOnModal = ($modal, pad) => {
if (clientVars.automaticReconnectionTimeout && $modal.is('.with_reconnect_timer')) {

View file

@ -25,6 +25,7 @@ const Cookies = require('./pad_utils').Cookies;
const padcookie = require('./pad_cookie').padcookie;
const padutils = require('./pad_utils').padutils;
const Ace2Editor = require('./ace').Ace2Editor;
import html10n from '../js/vendors/html10n'
const padeditor = (() => {
let pad = undefined;
@ -98,7 +99,7 @@ const padeditor = (() => {
$('#languagemenu').val(html10n.getLanguage());
$('#languagemenu').on('change', () => {
Cookies.set('language', $('#languagemenu').val());
window.html10n.localize([$('#languagemenu').val(), 'en']);
html10n.localize([$('#languagemenu').val(), 'en']);
if ($('select').niceSelect) {
$('select').niceSelect('update');
}

View file

@ -22,6 +22,9 @@
* limitations under the License.
*/
import html10n from './vendors/html10n';
const padimpexp = (() => {
let pad;

View file

@ -18,7 +18,7 @@
const padutils = require('./pad_utils').padutils;
const hooks = require('./pluginfw/hooks');
import html10n from './vendors/html10n';
let myUserInfo = {};
let colorPickerOpen = false;

View file

@ -7,6 +7,7 @@
// Licensed under the terms of the GNU General Public License v2.0:
// https://github.com/mattfarina/farbtastic/blob/71ca15f4a09c8e5a08a1b0d1cf37ef028adf22f0/LICENSE.txt
// edited by Sebastian Castro <sebastian.castro@protonmail.com> on 2020-04-06
(function ($) {
var __debug = false;

File diff suppressed because it is too large Load diff

992
src/static/js/vendors/html10n.ts vendored Normal file
View file

@ -0,0 +1,992 @@
import {Func} from "mocha";
type PluralFunc = (n: number) => string
export class Html10n {
public language?: string
private rtl: string[]
private _pluralRules?: PluralFunc
public mt: MicroEvent
private loader: Loader
private translations: Map<string, any>
private macros: Map<string, Function>
constructor() {
this.language = undefined
this.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"]
this.mt = new MicroEvent()
this.loader = new Loader([])
this.translations = new Map()
this.macros = new Map()
this.macros.set('plural', (_key: string, param:string, opts: any)=>{
let str
, n = parseFloat(param);
if (isNaN(n))
return;
// initialize _pluralRules
if (this._pluralRules === undefined) {
this._pluralRules = this.getPluralRules(this.language!);
}
let index = this._pluralRules!(n);
// try to find a [zero|one|two] key if it's defined
if (n === 0 && ('zero') in opts) {
str = opts['zero'];
} else if (n == 1 && ('one') in opts) {
str = opts['one'];
} else if (n == 2 && ('two') in opts) {
str = opts['two'];
} else if (index in opts) {
str = opts[index];
}
return str;
})
document.addEventListener('DOMContentLoaded', ()=> {
this.index()
}, false)
}
bind(event: string, fct: Func) {
this.mt.bind(event, fct)
}
/**
* Get rules for plural forms (shared with JetPack), see:
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
* https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
*
* @param {string} lang
* locale (language) used.
*
* @return {PluralFunc}
* returns a function that gives the plural form name for a given integer:
* var fun = getPluralRules('en');
* fun(1) -> 'one'
* fun(0) -> 'other'
* fun(1000) -> 'other'.
*/
getPluralRules(lang: string): PluralFunc {
const locales2rules = new Map([
['af', 3],
['ak', 4],
['am', 4],
['ar', 1],
['asa', 3],
['az', 0],
['be', 11],
['bem', 3],
['bez', 3],
['bg', 3],
['bh', 4],
['bm', 0],
['bn', 3],
['bo', 0],
['br', 20],
['brx', 3],
['bs', 11],
['ca', 3],
['cgg', 3],
['chr', 3],
['cs', 12],
['cy', 17],
['da', 3],
['de', 3],
['dv', 3],
['dz', 0],
['ee', 3],
['el', 3],
['en', 3],
['eo', 3],
['es', 3],
['et', 3],
['eu', 3],
['fa', 0],
['ff', 5],
['fi', 3],
['fil', 4],
['fo', 3],
['fr', 5],
['fur', 3],
['fy', 3],
['ga', 8],
['gd', 24],
['gl', 3],
['gsw', 3],
['gu', 3],
['guw', 4],
['gv', 23],
['ha', 3],
['haw', 3],
['he', 2],
['hi', 4],
['hr', 11],
['hu', 0],
['id', 0],
['ig', 0],
['ii', 0],
['is', 3],
['it', 3],
['iu', 7],
['ja', 0],
['jmc', 3],
['jv', 0],
['ka', 0],
['kab', 5],
['kaj', 3],
['kcg', 3],
['kde', 0],
['kea', 0],
['kk', 3],
['kl', 3],
['km', 0],
['kn', 0],
['ko', 0],
['ksb', 3],
['ksh', 21],
['ku', 3],
['kw', 7],
['lag', 18],
['lb', 3],
['lg', 3],
['ln', 4],
['lo', 0],
['lt', 10],
['lv', 6],
['mas', 3],
['mg', 4],
['mk', 16],
['ml', 3],
['mn', 3],
['mo', 9],
['mr', 3],
['ms', 0],
['mt', 15],
['my', 0],
['nah', 3],
['naq', 7],
['nb', 3],
['nd', 3],
['ne', 3],
['nl', 3],
['nn', 3],
['no', 3],
['nr', 3],
['nso', 4],
['ny', 3],
['nyn', 3],
['om', 3],
['or', 3],
['pa', 3],
['pap', 3],
['pl', 13],
['ps', 3],
['pt', 3],
['rm', 3],
['ro', 9],
['rof', 3],
['ru', 11],
['rwk', 3],
['sah', 0],
['saq', 3],
['se', 7],
['seh', 3],
['ses', 0],
['sg', 0],
['sh', 11],
['shi', 19],
['sk', 12],
['sl', 14],
['sma', 7],
['smi', 7],
['smj', 7],
['smn', 7],
['sms', 7],
['sn', 3],
['so', 3],
['sq', 3],
['sr', 11],
['ss', 3],
['ssy', 3],
['st', 3],
['sv', 3],
['sw', 3],
['syr', 3],
['ta', 3],
['te', 3],
['teo', 3],
['th', 0],
['ti', 4],
['tig', 3],
['tk', 3],
['tl', 4],
['tn', 3],
['to', 0],
['tr', 0],
['ts', 3],
['tzm', 22],
['uk', 11],
['ur', 3],
['ve', 3],
['vi', 0],
['vun', 3],
['wa', 4],
['wae', 3],
['wo', 0],
['xh', 3],
['xog', 3],
['yo', 0],
['zh', 0],
['zu', 3]
])
function isIn(n: number, list: number[]) {
return list.indexOf(n) !== -1;
}
function isBetween(n: number, start: number, end: number) {
return start <= n && n <= end;
}
type PluralFunc = (n: number) => string
const pluralRules: {
[key: string]: PluralFunc
} = {
'0': function() {
return 'other';
},
'1': function(n: number) {
if ((isBetween((n % 100), 3, 10)))
return 'few';
if (n === 0)
return 'zero';
if ((isBetween((n % 100), 11, 99)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'2': function(n: number) {
if (n !== 0 && (n % 10) === 0)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'3': function(n: number) {
if (n == 1)
return 'one';
return 'other';
},
'4': function(n: number) {
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'5': function(n: number) {
if ((isBetween(n, 0, 2)) && n != 2)
return 'one';
return 'other';
},
'6': function(n: number) {
if (n === 0)
return 'zero';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'7': function(n: number) {
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'8': function(n: number) {
if ((isBetween(n, 3, 6)))
return 'few';
if ((isBetween(n, 7, 10)))
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'9': function(n: number) {
if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'10': function(n: number) {
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
return 'few';
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
return 'one';
return 'other';
},
'11': function(n: number) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if ((n % 10) === 0 ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 11, 14)))
return 'many';
if ((n % 10) == 1 && (n % 100) != 11)
return 'one';
return 'other';
},
'12': function(n: number) {
if ((isBetween(n, 2, 4)))
return 'few';
if (n == 1)
return 'one';
return 'other';
},
'13': function(n: number) {
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
return 'few';
if (n != 1 && (isBetween((n % 10), 0, 1)) ||
(isBetween((n % 10), 5, 9)) ||
(isBetween((n % 100), 12, 14)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'14': function(n: number) {
if ((isBetween((n % 100), 3, 4)))
return 'few';
if ((n % 100) == 2)
return 'two';
if ((n % 100) == 1)
return 'one';
return 'other';
},
'15': function(n: number) {
if (n === 0 || (isBetween((n % 100), 2, 10)))
return 'few';
if ((isBetween((n % 100), 11, 19)))
return 'many';
if (n == 1)
return 'one';
return 'other';
},
'16': function(n: number) {
if ((n % 10) == 1 && n != 11)
return 'one';
return 'other';
},
'17': function(n: number) {
if (n == 3)
return 'few';
if (n === 0)
return 'zero';
if (n == 6)
return 'many';
if (n == 2)
return 'two';
if (n == 1)
return 'one';
return 'other';
},
'18': function(n: number) {
if (n === 0)
return 'zero';
if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
return 'one';
return 'other';
},
'19': function(n: number) {
if ((isBetween(n, 2, 10)))
return 'few';
if ((isBetween(n, 0, 1)))
return 'one';
return 'other';
},
'20': function(n: number) {
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
isBetween((n % 100), 10, 19) ||
isBetween((n % 100), 70, 79) ||
isBetween((n % 100), 90, 99)
))
return 'few';
if ((n % 1000000) === 0 && n !== 0)
return 'many';
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
return 'two';
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
return 'one';
return 'other';
},
'21': function(n: number) {
if (n === 0)
return 'zero';
if (n == 1)
return 'one';
return 'other';
},
'22': function(n: number) {
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
return 'one';
return 'other';
},
'23': function(n: number) {
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
return 'one';
return 'other';
},
'24': function(n: number) {
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
return 'few';
if (isIn(n, [2, 12]))
return 'two';
if (isIn(n, [1, 11]))
return 'one';
return 'other';
}
};
const index = locales2rules.get(lang.replace(/-.*$/, ''));
// @ts-ignore
if (!(index in pluralRules)) {
console.warn('plural form unknown for [' + lang + ']');
return function() { return 'other'; };
}
// @ts-ignore
return pluralRules[index];
}
getTranslatableChildren(element: HTMLElement) {
if(!document.querySelectorAll) {
if (!element) return []
const nodes = element.getElementsByTagName('*')
, l10nElements = []
for (let i=0, n=nodes.length; i < n; i++) {
if (nodes[i].getAttribute('data-l10n-id'))
l10nElements.push(nodes[i]);
}
return l10nElements
}
return element.querySelectorAll('*[data-l10n-id]')
}
localize(langs: (string|undefined)[]|string) {
if ('string' === typeof langs) {
langs = [langs];
}
let i = 0
langs.forEach((lang) => {
if(!lang) return;
langs[i++] = lang;
if(~lang.indexOf('-')) langs[i++] = lang.substring(0, lang.indexOf('-'));
})
this.build(langs, (er: null, translations: Map<string, any>) =>{
this.translations = translations
this.translateElement(translations)
this.mt.trigger('localized')
})
}
/**
* Triggers the translation process
* for an element
* @param translations A hash of all translation strings
* @param element A DOM element, if omitted, the document element will be used
*/
translateElement(translations: Map<string, any>, element?: HTMLElement) {
element = element || document.documentElement
const children = element ? this.getTranslatableChildren(element): document.childNodes
for (let child of children) {
this.translateNode(translations, child as HTMLElement)
}
// translate element itself if necessary
this.translateNode(translations, element)
}
asyncForEach(list: (string|undefined)[], iterator: any, cb: Function) {
let i = 0
, n = list.length
iterator(list[i], i, function each(err?: string) {
if(err) console.error(err)
i++
if (i < n) return iterator(list[i],i, each);
cb()
})
}
/**
* Builds a translation object from a list of langs (loads the necessary translations)
* @param langs Array - a list of langs sorted by priority (default langs should go last)
* @param cb Function - a callback that will be called once all langs have been loaded
*/
build(langs: (string|undefined)[], cb: Function) {
const that = this;
const build = new Map<string, any>()
this.asyncForEach(langs, function (lang: string, _i: number, next:LoaderFunc) {
if(!lang) return next();
that.loader.load(lang, next)
}, () =>{
let lang;
langs.reverse()
// loop through the priority array...
for (let i=0, n=langs.length; i < n; i++) {
lang = langs[i]
if(!lang) continue;
if(!(lang in that.loader.langs)) {// uh, we don't have this lang availbable..
// then check for related langs
if(~lang.indexOf('-')) lang = lang.split('-')[0];
let l
for(l in that.loader.langs) {
if(lang != l && l.indexOf(lang) === 0) {
lang = l
break;
}
}
if(lang != l) continue;
}
// ... and apply all strings of the current lang in the list
// to our build object
for (let string in that.loader.langs.get(lang)) {
build.set(string,that.loader.langs.get(lang).get(string))
}
// the last applied lang will be exposed as the
// lang the page was translated to
that.language = lang
}
cb(null, build)
})
}
/**
* Returns the language that was last applied to the translations hash
* thus overriding most of the formerly applied langs
*/
getLanguage() {
return this.language
}
/**
* Returns the direction of the language returned be html10n#getLanguage
*/
getDirection() {
if(!this.language) return
const langCode = this.language.indexOf('-') == -1? this.language : this.language.substring(0, this.language.indexOf('-'))
return this.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl'
}
/**
* Index all <link>s
*/
index() {
// Find all <link>s
const links = document.getElementsByTagName('link')
, resources = []
for (let i=0, n=links.length; i < n; i++) {
if (links[i].type != 'application/l10n+json')
continue;
resources.push(links[i].href)
}
this.loader = new Loader(resources)
this.mt.trigger('indexed')
}
translateNode(translations: Map<string, any>, node: HTMLElement) {
const str: {
id?: string,
args?: any,
str?: string
} = {}
// get id
str.id = node.getAttribute('data-l10n-id') as string
if (!str.id) return
if(!translations.get(str.id)) return console.warn('Couldn\'t find translation key '+str.id)
// get args
if(window.JSON) {
str.args = JSON.parse(node.getAttribute('data-l10n-args') as string)
}else{
try{
str.args = eval(node.getAttribute('data-l10n-args') as string)
}catch(e) {
console.warn('Couldn\'t parse args for '+str.id)
}
}
str.str = this.get(str.id, str.args)
// get attribute name to apply str to
let prop
, index = str.id.lastIndexOf('.')
, attrList = // allowed attributes
{ "title": 1
, "innerHTML": 1
, "alt": 1
, "textContent": 1
, "value": 1
, "placeholder": 1
}
if (index > 0 && str.id.substring(index + 1) in attrList) {
// an attribute has been specified (example: "my_translation_key.placeholder")
prop = str.id.substring(index + 1)
} else { // no attribute: assuming text content by default
prop = document.body.textContent ? 'textContent' : 'innerText'
}
// Apply translation
if (node.children.length === 0 || prop != 'textContent') {
// @ts-ignore
node[prop] = str.str!
node.setAttribute("aria-label", str.str!); // Sets the aria-label
// The idea of the above is that we always have an aria value
// This might be a bit of an abrupt solution but let's see how it goes
} else {
let children = node.childNodes,
found = false
let i = 0, n = children.length;
for (; i < n; i++) {
if (children[i].nodeType === 3 && /\S/.test(children[i].textContent!)) {
if (!found) {
children[i].nodeValue = str.str!
found = true
} else {
children[i].nodeValue = ''
}
}
}
if (!found) {
console.warn('Unexpected error: could not translate element content for key '+str.id, node)
}
}
}
get(id: string, args?:any) {
let translations = this.translations
if(!translations) return console.warn('No translations available (yet)')
if(!translations.get(id)) return console.warn('Could not find string '+id)
// apply macros
let str = translations.get(id)
str = this.substMacros(id, str, args)
// apply args
str = this.substArguments(str, args)
return str
}
substMacros(key: string, str:string, args:any) {
let regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}')
, match
while(match = regex.exec(str)) {
// a macro has been found
// Note: at the moment, only one parameter is supported
let macroName = match[1]
, paramName = match[2]
, optv = match[3]
, opts: {[key:string]:any} = {}
if (!(this.macros.has(macroName))) continue
if(optv) {
optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g)!.forEach(function(arg) {
const parts = arg.split(':')
, name = parts[0];
opts[name] = parts[1].trim()
})
}
let param
if (args && paramName in args) {
param = args[paramName]
} else if (paramName in this.translations) {
param = this.translations.get(paramName)
}
// there's no macro parser: it has to be defined in html10n.macros
let macro = this.macros.get(macroName)!
str = str.substring(0, match.index) + macro(key, param, opts) + str.substring(match.index+match[0].length)
}
return str
}
substArguments(str: string, args:any) {
let reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/
, match
let translations = this.translations;
while (match = reArgs.exec(str)) {
if (!match || match.length < 2)
return str // argument key not found
let arg = match[1]
, sub = ''
if (args && arg in args) {
sub = args[arg]
} else if (translations && arg in translations) {
sub = translations.get(arg)
} else {
console.warn('Could not find argument {{' + arg + '}}')
return str
}
str = str.substring(0, match.index) + sub + str.substring(match.index + match[0].length)
}
return str
}
}
class MicroEvent {
private events: Map<string, Function[]>
constructor() {
this.events = new Map();
}
bind(event: string, fct: Func) {
if (this.events.get(event) === undefined) {
this.events.set(event, []);
}
this.events.get(event)!.push(fct);
}
unbind(event: string, fct: Func) {
if (this.events.get(event) === undefined) {
return;
}
const index = this.events.get(event)!.indexOf(fct);
if (index !== -1) {
this.events.get(event)!.splice(index, 1);
}
}
trigger(event: string, ...args: any[]) {
if (this.events.get(event) === undefined) {
return;
}
for (const fct of this.events.get(event)!) {
fct(...args);
}
}
mixin(destObject: any) {
const props = ['bind', 'unbind', 'trigger'];
if (destObject !== undefined) {
for (const prop of props) {
// @ts-ignore
destObject[prop] = this[prop];
}
}
}
}
type LoaderFunc = () => void
type ErrorFunc = (data?:any)=>void
class Loader {
private resources: any
private cache: Map<string, any>
langs: Map<string, any>
constructor(resources: any) {
this.resources = resources;
this.cache = new Map();
this.langs = new Map();
}
load(lang: string, callback: LoaderFunc) {
if (this.langs.get(lang) !== undefined) {
callback();
return;
}
if (this.resources.length > 0) {
let reqs = 0
for (const resource of this.resources) {
this.fetch(resource, lang, (e)=> {
reqs++;
if (e) console.warn(e)
if (reqs < this.resources.length) return;// Call back once all reqs are completed
callback && callback()
})
}
}
}
fetch(href: string, lang: string, callback: ErrorFunc) {
const that = this;
if (this.cache.get(href)) {
this.parse(lang, href, this.cache.get(href), callback)
return;
}
const xhr = new XMLHttpRequest();
xhr.open('GET', href, /*async: */true)
if (xhr.overrideMimeType) {
xhr.overrideMimeType('application/json; charset=utf-8');
}
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) {
const data = JSON.parse(xhr.responseText);
that.cache.set(href, data)
// Pass on the contents for parsing
that.parse(lang, href, data, callback)
} else {
callback(new Error('Failed to load '+href))
}
}
};
xhr.send(null);
}
parse(lang: string, href: string, data: {
[key: string]: string
}, callback: ErrorFunc) {
if ('object' !== typeof data) {
callback(new Error('A file couldn\'t be parsed as json.'))
return
}
function getBcp47LangCode(browserLang: string) {
const bcp47Lang = browserLang.toLowerCase();
// Browser => BCP 47
const langCodeMap = new Map([
['zh-cn', 'zh-hans-cn'],
['zh-hk', 'zh-hant-hk'],
['zh-mo', 'zh-hant-mo'],
['zh-my', 'zh-hans-my'],
['zh-sg', 'zh-hans-sg'],
['zh-tw', 'zh-hant-tw'],
])
return langCodeMap.get(bcp47Lang) ?? bcp47Lang;
}
// Issue #6129: Fix exceptions
// NOTE: translatewiki.net use all lowercase form by default ('en-gb' insted of 'en-GB')
function getJsonLangCode(bcp47Lang: string) {
const jsonLang = bcp47Lang.toLowerCase();
// BCP 47 => JSON
const langCodeMap = new Map([
['sr-ec', 'sr-cyrl'],
['sr-el', 'sr-latn'],
['zh-hk', 'zh-hant-hk'],
])
return langCodeMap.get(jsonLang) ?? jsonLang;
}
let bcp47LangCode = getBcp47LangCode(lang);
let jsonLangCode = getJsonLangCode(bcp47LangCode);
if (!data[jsonLangCode]) {
// lang not found
// This may be due to formatting (expected 'ru' but browser sent 'ru-RU')
// Set err msg before mutating lang (we may need this later)
const msg = 'Couldn\'t find translations for ' + lang +
'(lowercase BCP 47 lang tag ' + bcp47LangCode +
', JSON lang code ' + jsonLangCode + ')';
// Check for '-' (BCP 47 'ROOT-SCRIPT-REGION-VARIANT') and fallback until found data or ROOT
// - 'ROOT-SCRIPT-REGION': 'zh-Hans-CN'
// - 'ROOT-SCRIPT': 'zh-Hans'
// - 'ROOT-REGION': 'en-GB'
// - 'ROOT-VARIANT': 'be-tarask'
while (!data[jsonLangCode] && bcp47LangCode.lastIndexOf('-') > -1) {
// ROOT-SCRIPT-REGION-VARIANT formatting detected
bcp47LangCode = bcp47LangCode.substring(0, bcp47LangCode.lastIndexOf('-')); // set lang to ROOT lang
jsonLangCode = getJsonLangCode(bcp47LangCode);
}
if (!data[jsonLangCode]) {
// ROOT lang not found. (e.g 'zh')
// Loop through langs data. Maybe we have a variant? e.g (zh-hans)
let l; // langs item. Declare outside of loop
for (l in data) {
// Is not ROOT?
// And is variant of ROOT?
// (NOTE: index of ROOT equals 0 would cause unexpected ISO 639-1 vs. 639-3 issues,
// so append dash into query string)
// And is known lang?
if (bcp47LangCode != l && l.indexOf(lang + '-') === 0 && data[l]) {
bcp47LangCode = l; // set lang to ROOT-SCRIPT (e.g 'zh-hans')
jsonLangCode = getJsonLangCode(bcp47LangCode);
break;
}
}
// Did we find a variant? If not, return err.
if (bcp47LangCode != l) {
return callback(new Error(msg));
}
}
}
lang = jsonLangCode
if('string' === typeof data[lang]) {
// Import rule
// absolute path
let importUrl = data[lang];
// relative path
if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) {
importUrl = href+"/../"+data[lang]
}
this.fetch(importUrl, lang, callback)
return
}
if ('object' != typeof data[lang]) {
callback(new Error('Translations should be specified as JSON objects!'))
return
}
this.langs.set(lang,data[lang])
// TODO: Also store accompanying langs
callback()
}
}
export default new Html10n()

View file

@ -0,0 +1,41 @@
window.$ = window.jQuery = await import('../../src/static/js/rjquery').jQuery;
await import('../../src/static/js/l10n')
window.clientVars = {
// This is needed to fetch /pluginfw/plugin-definitions.json, which happens before the server
// sends the CLIENT_VARS message.
randomVersionString: "7a7bdbad",
};
(async () => {
// Allow other frames to access this frame's modules.
//window.require.resolveTmp = require.resolve('ep_etherpad-lite/static/js/pad_cookie');
const basePath = new URL('..', window.location.href).pathname;
window.browser = require('../../src/static/js/vendors/browser');
const pad = require('../../src/static/js/pad');
pad.baseURL = basePath;
window.plugins = require('../../src/static/js/pluginfw/client_plugins');
const hooks = require('../../src/static/js/pluginfw/hooks');
// TODO: These globals shouldn't exist.
window.pad = pad.pad;
window.chat = require('../../src/static/js/chat').chat;
window.padeditbar = require('../../src/static/js/pad_editbar').padeditbar;
window.padimpexp = require('../../src/static/js/pad_impexp').padimpexp;
require('../../src/static/js/skin_variants');
require('../../src/static/js/basic_error_handler')
window.plugins.baseURL = basePath;
await window.plugins.update(new Map([
]));
// Mechanism for tests to register hook functions (install fake plugins).
window._postPluginUpdateForTestingDone = false;
if (window._postPluginUpdateForTesting != null) window._postPluginUpdateForTesting();
window._postPluginUpdateForTestingDone = true;
window.pluginDefs = require('../../src/static/js/pluginfw/plugin_defs');
pad.init();
await new Promise((resolve) => $(resolve));
await hooks.aCallAll('documentReady');
})();

View file

@ -9,7 +9,12 @@
"preview": "vite preview"
},
"devDependencies": {
"ep_etherpad-lite": "workspace:../src",
"typescript": "^5.5.3",
"vite": "^5.3.3"
"vite": "^5.3.3",
"vite-plugin-require": "^1.2.14"
},
"dependencies": {
"@originjs/vite-plugin-commonjs": "^1.0.3"
}
}

686
ui/pad.html Normal file
View file

@ -0,0 +1,686 @@
<!doctype html>
<html translate="no" class="pad super-light-toolbar super-light-editor light-background">
<head>
<title>Etherpad</title>
<link rel="manifest" href="/manifest.json" />
<script>
/*
|@licstart The following is the entire license notice for the
JavaScript code in this page.|
Copyright 2011 Peter Martischka, Primary Technology.
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.
|@licend The above is the entire license notice
for the JavaScript code in this page.|
*/
</script>
<meta charset="utf-8">
<meta name="robots" content="noindex, nofollow">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<link rel="shortcut icon" href="../favicon.ico">
<link href="../static/css/pad.css?v=5ba315cd" rel="stylesheet">
<link href="../static/skins/colibris/pad.css?v=5ba315cd" rel="stylesheet">
<style title="dynamicsyntax"></style>
<link rel="localizations" type="application/l10n+json" href="../locales.json" />
</head>
<body>
<!----------------------------->
<!--------- TOOLBAR ----------->
<!----------------------------->
<div id="editbar" class="toolbar">
<div id="toolbar-overlay"></div>
<ul class="menu_left" role="toolbar">
<li data-type="button" data-key="bold"><a class="grouped-left" data-l10n-id="pad.toolbar.bold.title"><button class=" buttonicon buttonicon-bold" data-l10n-id="pad.toolbar.bold.title"></button></a></li>
<li data-type="button" data-key="italic"><a class="grouped-middle" data-l10n-id="pad.toolbar.italic.title"><button class=" buttonicon buttonicon-italic" data-l10n-id="pad.toolbar.italic.title"></button></a></li>
<li data-type="button" data-key="underline"><a class="grouped-middle" data-l10n-id="pad.toolbar.underline.title"><button class=" buttonicon buttonicon-underline" data-l10n-id="pad.toolbar.underline.title"></button></a></li>
<li data-type="button" data-key="strikethrough"><a class="grouped-right" data-l10n-id="pad.toolbar.strikethrough.title"><button class=" buttonicon buttonicon-strikethrough" data-l10n-id="pad.toolbar.strikethrough.title"></button
></a></li><li class="separator"></li><li data-type="button" data-key="insertorderedlist"><a class="grouped-left" data-l10n-id="pad.toolbar.ol.title"><button class=" buttonicon buttonicon-insertorderedlist" data-l10n-id="pad.toolbar.ol.title"></button></a></li>
<li data-type="button" data-key="insertunorderedlist"><a class="grouped-middle" data-l10n-id="pad.toolbar.ul.title"><button class=" buttonicon buttonicon-insertunorderedlist" data-l10n-id="pad.toolbar.ul.title"></button></a></li>
<li data-type="button" data-key="indent"><a class="grouped-middle" data-l10n-id="pad.toolbar.indent.title"><button class=" buttonicon buttonicon-indent" data-l10n-id="pad.toolbar.indent.title"></button></a></li>
<li data-type="button" data-key="outdent"><a class="grouped-right" data-l10n-id="pad.toolbar.unindent.title"><button class=" buttonicon buttonicon-outdent" data-l10n-id="pad.toolbar.unindent.title"></button></a></li><li class="separator"></li><li data-type="button" data-key="undo"><a class="grouped-left" data-l10n-id="pad.toolbar.undo.title"><button class=" buttonicon buttonicon-undo" data-l10n-id="pad.toolbar.undo.title"></button></a></li>
<li data-type="button" data-key="redo"><a class="grouped-right" data-l10n-id="pad.toolbar.redo.title"><button class=" buttonicon buttonicon-redo" data-l10n-id="pad.toolbar.redo.title"></button></a></li><li class="separator"></li
><li data-type="button" data-key="clearauthorship"><a class="" data-l10n-id="pad.toolbar.clearAuthorship.title"><button class=" buttonicon buttonicon-clearauthorship" data-l10n-id="pad.toolbar.clearAuthorship.title"></button></a></li>
</ul>
<ul class="menu_right" role="toolbar">
<li data-type="button" data-key="import_export"><a class="grouped-left" data-l10n-id="pad.toolbar.import_export.title"><button class=" buttonicon buttonicon-import_export" data-l10n-id="pad.toolbar.import_export.title"></button></a></li>
<li data-type="button" data-key="showTimeSlider"><a class="grouped-middle" data-l10n-id="pad.toolbar.timeslider.title"><button class=" buttonicon buttonicon-history" data-l10n-id="pad.toolbar.timeslider.title"></button></a></li>
<li data-type="button" data-key="savedRevision"><a class="grouped-right" data-l10n-id="pad.toolbar.savedRevision.title"><button class=" buttonicon buttonicon-savedRevision" data-l10n-id="pad.toolbar.savedRevision.title"></button
></a></li><li class="separator"></li><li data-type="button" data-key="settings"><a class="grouped-left" data-l10n-id="pad.toolbar.settings.title"><button class=" buttonicon buttonicon-settings" data-l10n-id="pad.toolbar.settings.title"></button></a></li>
<li data-type="button" data-key="embed"><a class="grouped-right" data-l10n-id="pad.toolbar.embed.title"><button class=" buttonicon buttonicon-embed" data-l10n-id="pad.toolbar.embed.title"></button></a></li><li class="separator"></li><li data-type="button" data-key="showusers"><a class="" data-l10n-id="pad.toolbar.showusers.title"><button class=" buttonicon buttonicon-showusers" data-l10n-id="pad.toolbar.showusers.title"></button></a></li>
</ul>
<span class="show-more-icon-btn"></span> <!-- use on small screen to display hidden toolbar buttons -->
</div>
<div id="editorcontainerbox" class="flex-layout">
<!----------------------------->
<!--- PAD EDITOR (in iframe) -->
<!----------------------------->
<div id="editorcontainer" class="editorcontainer"></div>
<div id="editorloadingbox">
<div id="permissionDenied">
<p data-l10n-id="pad.permissionDenied" class="editorloadingbox-message">
You do not have permission to access this pad
</p>
</div>
<p data-l10n-id="pad.loading" id="loading" class="editorloadingbox-message">
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
Loading...
</p>
<noscript>
<p class="editorloadingbox-message">
<strong>
Sorry, you have to enable Javascript in order to use this.
</strong>
</p>
</noscript>
</div>
<!------------------------------------------------------------->
<!-- SETTINGS POPUP (change font, language, chat parameters) -->
<!------------------------------------------------------------->
<div id="settings" class="popup"><div class="popup-content">
<h1 data-l10n-id="pad.settings.padSettings"></h1>
<h2 data-l10n-id="pad.settings.myView"></h2>
<p class="hide-for-mobile">
<input type="checkbox" id="options-stickychat">
<label for="options-stickychat" data-l10n-id="pad.settings.stickychat"></label>
</p>
<p class="hide-for-mobile">
<input type="checkbox" id="options-chatandusers" onClick="chat.chatAndUsers();">
<label for="options-chatandusers" data-l10n-id="pad.settings.chatandusers"></label>
</p>
<p>
<input type="checkbox" id="options-colorscheck">
<label for="options-colorscheck" data-l10n-id="pad.settings.colorcheck"></label>
</p>
<p>
<input type="checkbox" id="options-linenoscheck" checked>
<label for="options-linenoscheck" data-l10n-id="pad.settings.linenocheck"></label>
</p>
<p>
<input type="checkbox" id="options-rtlcheck">
<label for="options-rtlcheck" data-l10n-id="pad.settings.rtlcheck"></label>
</p>
<div class="dropdowns-container">
<p class="dropdown-line">
<label for="viewfontmenu" data-l10n-id="pad.settings.fontType">Font type:</label>
<select id="viewfontmenu">
<option value="" data-l10n-id="pad.settings.fontType.normal">Normal</option>
Quicksand,Roboto,Alegreya,PlayfairDisplay,Montserrat,OpenDyslexic,RobotoMono
<option value="Quicksand">Quicksand</option>
<option value="Roboto">Roboto</option>
<option value="Alegreya">Alegreya</option>
<option value="PlayfairDisplay">PlayfairDisplay</option>
<option value="Montserrat">Montserrat</option>
<option value="OpenDyslexic">OpenDyslexic</option>
<option value="RobotoMono">RobotoMono</option>
</select>
</p>
<p class="dropdown-line">
<label for="languagemenu" data-l10n-id="pad.settings.language">Language:</label>
<select id="languagemenu">
<option value="af">Afrikaans</option>
<option value="ar">العربية</option>
<option value="ast">asturianu</option>
<option value="az">azərbaycanca</option>
<option value="azb">تورکجه</option>
<option value="bcc">بلوچی مکرانی</option>
<option value="be-tarask">беларуская (тарашкевіца)</option>
<option value="bg">български</option>
<option value="bn">বাংলা</option>
<option value="br">brezhoneg</option>
<option value="bs">bosanski</option>
<option value="ca">català</option>
<option value="cs">česky</option>
<option value="da">dansk</option>
<option value="de">Deutsch</option>
<option value="diq">Zazaki</option>
<option value="dsb">dolnoserbski</option>
<option value="el">Ελληνικά</option>
<option value="en-gb">British English</option>
<option value="en">English</option>
<option value="eo">Esperanto</option>
<option value="es">español</option>
<option value="et">eesti</option>
<option value="eu">euskara</option>
<option value="fa">فارسی</option>
<option value="ff">Fulfulde</option>
<option value="fi">suomi</option>
<option value="fo">føroyskt</option>
<option value="fr">français</option>
<option value="fy">Frysk</option>
<option value="gl">galego</option>
<option value="gu">ગુજરાતી</option>
<option value="he">עברית</option>
<option value="hi">हिन्दी</option>
<option value="hr">hrvatski</option>
<option value="hsb">hornjoserbsce</option>
<option value="hu">magyar</option>
<option value="hy">Հայերեն</option>
<option value="ia">interlingua</option>
<option value="id">Bahasa Indonesia</option>
<option value="is">íslenska</option>
<option value="it">italiano</option>
<option value="ja">日本語</option>
<option value="kab">Taqbaylit</option>
<option value="km">ភាសាខ្មែរ</option>
<option value="kn">ಕನ್ನಡ</option>
<option value="ko">한국어</option>
<option value="krc">къарачай-малкъар</option>
<option value="ksh">Ripoarisch</option>
<option value="ku-latn">Kurdî (latînî)</option>
<option value="lb">Lëtzebuergesch</option>
<option value="lt">lietuvių</option>
<option value="lv">latviešu</option>
<option value="map-bms">Basa Banyumasan</option>
<option value="mg">Malagasy</option>
<option value="mk">македонски</option>
<option value="ml">മലയാളം</option>
<option value="mn">монгол</option>
<option value="mnw">ဘာသာ မန်</option>
<option value="mr">मराठी</option>
<option value="ms">Bahasa Melayu</option>
<option value="my">မြန်မာဘာသာ</option>
<option value="nah">Nāhuatl</option>
<option value="nap">Nnapulitano</option>
<option value="nb">norsk (bokmål)</option>
<option value="nds">Plattdüütsch</option>
<option value="ne">नेपाली</option>
<option value="nl">Nederlands</option>
<option value="nn">norsk (nynorsk)</option>
<option value="oc">occitan</option>
<option value="os">Ирон</option>
<option value="pa">ਪੰਜਾਬੀ</option>
<option value="pl">polski</option>
<option value="pms">Piemontèis</option>
<option value="ps">پښتو</option>
<option value="pt-br">português do Brasil</option>
<option value="pt">português</option>
<option value="qqq">Message documentation</option>
<option value="ro">română</option>
<option value="ru">русский</option>
<option value="sc">sardu</option>
<option value="sco">Scots</option>
<option value="sd">سنڌي</option>
<option value="sh">srpskohrvatski / српскохрватски</option>
<option value="shn">လိၵ်ႈတႆး</option>
<option value="sk">slovenčina</option>
<option value="sl">slovenščina</option>
<option value="sq">shqip</option>
<option value="sr-ec">српски (ћирилица)</option>
<option value="sr-el">srpski (latinica)</option>
<option value="sv">svenska</option>
<option value="sw">Kiswahili</option>
<option value="ta">தமிழ்</option>
<option value="tcy">ತುಳು</option>
<option value="te">తెలుగు</option>
<option value="th">ไทย</option>
<option value="tr">Türkçe</option>
<option value="uk">українська</option>
<option value="vec">vèneto</option>
<option value="vi">Tiếng Việt</option>
<option value="zh-hans">中文(简体)‎</option>
<option value="zh-hant">中文(繁體)‎</option>
</select>
</p>
</div>
<h2 data-l10n-id="pad.settings.about">About</h2>
<span data-l10n-id="pad.settings.poweredBy">Powered by</span>
<a href="https://etherpad.org">Etherpad</a>
</div></div>
<!------------------------->
<!-- IMPORT EXPORT POPUP -->
<!------------------------->
<div id="import_export" class="popup"><div class="popup-content">
<h1 data-l10n-id="pad.importExport.import_export"></h1>
<div class="acl-write">
<h2 data-l10n-id="pad.importExport.import"></h2>
<div class="importmessage" id="importmessageabiword" data-l10n-id="pad.importExport.abiword.innerHTML"></div><br>
<form id="importform" method="post" action="" target="importiframe" enctype="multipart/form-data">
<div class="importformdiv" id="importformfilediv">
<input type="file" name="file" size="10" id="importfileinput">
<div class="importmessage" id="importmessagefail"></div>
</div>
<div id="import"></div>
<div class="importmessage" id="importmessagesuccess" data-l10n-id="pad.importExport.importSuccessful"></div>
<div class="importformdiv" id="importformsubmitdiv">
<span class="nowrap">
<input type="submit" class="btn btn-primary" name="submit" value="Import Now" disabled="disabled" id="importsubmitinput">
<div alt="" id="importstatusball" class="loadingAnimation" align="top"></div>
</span>
</div>
</form>
</div>
<div id="exportColumn">
<h2 data-l10n-id="pad.importExport.export"></h2>
<a id="exportetherpada" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-powerpoint" id="exportetherpad" data-l10n-id="pad.importExport.exportetherpad"></span>
</a>
<a id="exporthtmla" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-code" id="exporthtml" data-l10n-id="pad.importExport.exporthtml"></span>
</a>
<a id="exportplaina" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file" id="exportplain" data-l10n-id="pad.importExport.exportplain"></span>
</a>
<a id="exportworda" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-word" id="exportword" data-l10n-id="pad.importExport.exportword"></span>
</a>
<a id="exportpdfa" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-pdf" id="exportpdf" data-l10n-id="pad.importExport.exportpdf"></span>
</a>
<a id="exportopena" target="_blank" class="exportlink">
<span class="exporttype buttonicon buttonicon-file-alt" id="exportopen" data-l10n-id="pad.importExport.exportopen"></span>
</a>
</div>
</div></div>
<!---------------------------------------------------->
<!-- CONNECTIVITY POPUP (when you get disconnected) -->
<!---------------------------------------------------->
<div id="connectivity" class="popup"><div class="popup-content">
<div class="connected visible">
<h2 data-l10n-id="pad.modals.connected"></h2>
</div>
<div class="reconnecting">
<h1 data-l10n-id="pad.modals.reconnecting"></h1>
<i class='buttonicon buttonicon-spin5 icon-spin'>
<img src='../static/img/brand.svg' class='etherpadBrand'><br/>
</i>
</div>
<div class="userdup">
<h1 data-l10n-id="pad.modals.userdup"></h1>
<h2 data-l10n-id="pad.modals.userdup.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.userdup.advice"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="unauth">
<h1 data-l10n-id="pad.modals.unauth"></h1>
<p id="defaulttext" data-l10n-id="pad.modals.unauth.explanation"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="looping">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.looping.explanation"></h2>
<p data-l10n-id="pad.modals.looping.cause"></p>
</div>
<div class="initsocketfail">
<h1 data-l10n-id="pad.modals.initsocketfail"></h1>
<h2 data-l10n-id="pad.modals.initsocketfail.explanation"></h2>
<p data-l10n-id="pad.modals.initsocketfail.cause"></p>
</div>
<div class="slowcommit with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.slowcommit.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.slowcommit.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="badChangeset with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.badChangeset.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.badChangeset.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<div class="corruptPad">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.corruptPad.explanation"></h2>
<p data-l10n-id="pad.modals.corruptPad.cause"></p>
</div>
<div class="deleted">
<h1 data-l10n-id="pad.modals.deleted"></h1>
<p data-l10n-id="pad.modals.deleted.explanation"></p>
</div>
<div class="rateLimited">
<h1 data-l10n-id="pad.modals.rateLimited"></h1>
<p data-l10n-id="pad.modals.rateLimited.explanation"></p>
</div>
<div class="rejected">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.rejected.explanation"></h2>
<p data-l10n-id="pad.modals.rejected.cause"></p>
</div>
<div class="disconnected with_reconnect_timer">
<h1 data-l10n-id="pad.modals.disconnected"></h1>
<h2 data-l10n-id="pad.modals.disconnected.explanation"></h2>
<p id="defaulttext" data-l10n-id="pad.modals.disconnected.cause"></p>
<button id="forcereconnect" class="btn btn-primary" data-l10n-id="pad.modals.forcereconnect"></button>
</div>
<form id="reconnectform" method="post" action="/ep/pad/reconnect" accept-charset="UTF-8" style="display: none;">
<input type="hidden" class="padId" name="padId">
<input type="hidden" class="diagnosticInfo" name="diagnosticInfo">
<input type="hidden" class="missedChanges" name="missedChanges">
</form>
</div></div>
<!-------------------------------->
<!-- EMBED POPUP (Share, embed) -->
<!-------------------------------->
<div id="embed" class="popup"><div class="popup-content">
<h1 data-l10n-id="pad.share"></h1>
<div id="embedreadonly" class="acl-write">
<input type="checkbox" id="readonlyinput">
<label for="readonlyinput" data-l10n-id="pad.share.readonly"></label>
</div>
<div id="linkcode">
<h2 data-l10n-id="pad.share.link"></h2>
<input id="linkinput" type="text" value="" onclick="this.select()">
</div>
<div id="embedcode">
<h2 data-l10n-id="pad.share.emebdcode"></h2>
<input id="embedinput" type="text" value="" onclick="this.select()">
</div>
</div></div>
<div class="sticky-container">
<!---------------------------------------------------------------------->
<!-- USERS POPUP (set username, color, see other users names & color) -->
<!---------------------------------------------------------------------->
<div id="users" class="popup"><div class="popup-content">
<div id="connectionstatus"></div>
<div id="myuser">
<div id="mycolorpicker" class="popup"><div class="popup-content">
<div id="colorpicker"></div>
<div class="btn-container">
<button id="mycolorpickersave" data-l10n-id="pad.colorpicker.save" class="btn btn-primary"></button>
<button id="mycolorpickercancel" data-l10n-id="pad.colorpicker.cancel" class="btn btn-default"></button>
<span id="mycolorpickerpreview" class="myswatchboxhoverable"></span>
</div>
</div></div>
<div id="myswatchbox"><div id="myswatch"></div></div>
<div id="myusernameform">
<input type="text" id="myusernameedit" disabled="disabled" data-l10n-id="pad.userlist.entername">
</div>
</div>
<div id="otherusers" aria-role="document">
<table id="otheruserstable" cellspacing="0" cellpadding="0" border="0">
<tr><td></td></tr>
</table>
</div>
<div id="userlistbuttonarea"></div>
</div></div>
<!----------------------------->
<!----------- CHAT ------------>
<!----------------------------->
<div id="chaticon" class="visible" onclick="chat.show();return false;" title="Chat (Alt C)">
<span id="chatlabel" data-l10n-id="pad.chat"></span>
<span class="buttonicon buttonicon-chat"></span>
<span id="chatcounter">0</span>
</div>
<div id="chatbox">
<div class="chat-content">
<div id="titlebar">
<h1 id ="titlelabel" data-l10n-id="pad.chat"></h1>
<a id="titlecross" class="hide-reduce-btn" onClick="chat.hide();return false;">-&nbsp;</a>
<a id="titlesticky" class="stick-to-screen-btn" onClick="chat.stickToScreen(true);return false;" data-l10n-id="pad.chat.stick.title">&nbsp;&nbsp;</a>
</div>
<div id="chattext" class="thin-scrollbar" aria-live="polite" aria-relevant="additions removals text" role="log" aria-atomic="false">
<div alt="loading.." id="chatloadmessagesball" class="chatloadmessages loadingAnimation" align="top"></div>
<button id="chatloadmessagesbutton" class="chatloadmessages" data-l10n-id="pad.chat.loadmessages"></button>
</div>
<div id="chatinputbox">
<form>
<textarea id="chatinput" maxlength="999" data-l10n-id="pad.chat.writeMessage.placeholder"></textarea>
</form>
</div>
</div>
</div>
</div>
<!------------------------------------------------------------------>
<!-- SKIN VARIANTS BUILDER (Customize rendering, only for admins) -->
<!------------------------------------------------------------------>
<div id="skin-variants" class="popup"><div class="popup-content">
<h1>Skin Builder</h1>
<div class="dropdowns-container">
<p class="dropdown-line">
<label class="skin-variant-container">toolbar</label>
<select class="skin-variant skin-variant-color" data-container="toolbar">
<option value="super-light">Super Light</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="super-dark">Super Dark</option>
</select>
</p>
<p class="dropdown-line">
<label class="skin-variant-container">background</label>
<select class="skin-variant skin-variant-color" data-container="background">
<option value="super-light">Super Light</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="super-dark">Super Dark</option>
</select>
</p>
<p class="dropdown-line">
<label class="skin-variant-container">editor</label>
<select class="skin-variant skin-variant-color" data-container="editor">
<option value="super-light">Super Light</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="super-dark">Super Dark</option>
</select>
</p>
</div>
<p>
<input type="checkbox" id="skin-variant-full-width" class="skin-variant"/>
<label for="skin-variant-full-width">Full Width Editor</label>
</p>
<p>
<label>Result to copy in settings.json</label>
<input id="skin-variants-result" type="text" readonly class="disabled" />
</p>
</div></div>
</div> <!-- End of #editorcontainerbox -->
<!----------------------------->
<!-------- JAVASCRIPT --------->
<!----------------------------->
<script type="text/javascript" src="../static/skins/colibris/pad.js?v=5ba315cd"></script>
<div style="display:none"><a href="/javascript" data-jslicense="1">JavaScript license information</a></div>
<script type="module" src="./node_modules/ep_etherpad-lite/templates/padViteBootstrap.js"></script>
</body>
</html>

View file

@ -1,10 +1,18 @@
// vite.config.js
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vitePluginRequire from 'vite-plugin-require';
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
export default defineConfig({
base: '/views/',
base: '/views/',
plugins: [
viteCommonjs(),
],
build: {
commonjsOptions:{
transformMixedEsModules: true,
},
outDir: resolve(__dirname, '../src/static/oidc'),
rollupOptions: {
input: {
@ -14,4 +22,31 @@ export default defineConfig({
},
emptyOutDir: true,
},
server:{
proxy:{
'/static':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
},
'/views/manifest.json':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/views/, ''),
},
'/locales.json':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/views/, ''),
},
'/locales':{
target: 'http://localhost:9001',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/views/, ''),
},
}
}
})