diff --git a/README.md b/README.md index 5ad3587c2..cc90800f1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # About Etherpad is a really-real time collaborative editor maintained by the Etherpad Community. -Etherpad is written in JavaScript(99.9%) on both the server and client so it's easy for developers to maintain and add new features. Because of this Etherpad has tons of customizations that you can leverage. +Etherpad is written in JavaScript (99.9%) on both the server and client so it's easy for developers to maintain and add new features. Because of this Etherpad has tons of customizations that you can leverage. Etherpad is designed to be easily embeddable and provides a [HTTP API](https://github.com/ether/etherpad-lite/wiki/HTTP-API) that allows your web application to manage pads, users and groups. It is recommended to use the [available client implementations](https://github.com/ether/etherpad-lite/wiki/HTTP-API-client-libraries) in order to interact with this API. diff --git a/bin/installDeps.sh b/bin/installDeps.sh index f2a3aafcf..df6ea9fb6 100755 --- a/bin/installDeps.sh +++ b/bin/installDeps.sh @@ -46,13 +46,14 @@ fi #check node version NODE_VERSION=$(node --version) NODE_V_MINOR=$(echo $NODE_VERSION | cut -d "." -f 1-2) +NODE_V_MAIN=$(echo $NODE_VERSION | cut -d "." -f 1) #iojs version checking added if hash iojs 2>/dev/null; then IOJS_VERSION=$(iojs --version) fi -if [ ! $NODE_V_MINOR = "v0.10" ] && [ ! $NODE_V_MINOR = "v0.11" ] && [ ! $NODE_V_MINOR = "v0.12" ]; then +if [ ! $NODE_V_MINOR = "v0.10" ] && [ ! $NODE_V_MINOR = "v0.11" ] && [ ! $NODE_V_MINOR = "v0.12" ] && [ ! $NODE_V_MAIN = "v4" ] && [ ! $NODE_V_MAIN = "v5" ]; then if [ ! $IOJS_VERSION ]; then - echo "You're running a wrong version of node, or io.js is not installed. You're using $NODE_VERSION, we need v0.10.x, v0.11.x or v0.12.x" >&2 + echo "You're running a wrong version of node, or io.js is not installed. You're using $NODE_VERSION, we need node v0.10.x or higher" >&2 exit 1 fi fi diff --git a/bin/installOnWindows.bat b/bin/installOnWindows.bat index 9b9a42e41..89fa335d8 100644 --- a/bin/installOnWindows.bat +++ b/bin/installOnWindows.bat @@ -6,11 +6,6 @@ cd /D "%~dp0\.." :: Is node installed? cmd /C node -e "" || ( echo "Please install node.js ( http://nodejs.org )" && exit /B 1 ) -echo _ -echo Checking node version... -set check_version="if(['10','11','12'].indexOf(process.version.split('.')[1]) === -1 && process.version.split('.')[0] !== '1') { console.log('You are running a wrong version of Node. Etherpad requires v0.10+'); process.exit(1) }" -cmd /C node -e %check_version% || exit /B 1 - echo _ echo Ensure that all dependencies are up to date... If this is the first time you have run Etherpad please be patient. cmd /C npm install src/ --loglevel warn || exit /B 1 diff --git a/bin/rebuildPad.js b/bin/rebuildPad.js index c83833420..60c5f4ed3 100644 --- a/bin/rebuildPad.js +++ b/bin/rebuildPad.js @@ -79,6 +79,9 @@ async.series([ newPad.pool.numToAttrib = oldPad.pool.numToAttrib; for(var curRevNum = 0; curRevNum <= newRevHead; curRevNum++) { db.db.get("pad:" + padId + ":revs:" + curRevNum, function(err, rev) { + if (rev.meta) { + throw "The specified revision number could not be found."; + } var newRevNum = ++newPad.head; var newRevId = "pad:" + newPad.id + ":revs:" + newRevNum; db.db.set(newRevId, rev); diff --git a/doc/api/hooks_client-side.md b/doc/api/hooks_client-side.md index fccdaf466..367e06884 100644 --- a/doc/api/hooks_client-side.md +++ b/doc/api/hooks_client-side.md @@ -339,3 +339,14 @@ Things in context: This hook is provided to allow author highlight style to be modified. Registered hooks should return 1 if the plugin handles highlighting. If no plugin returns 1, the core will use the default background-based highlighting. + +## aceSelectionChanged +Called from: src/static/js/ace2_inner.js + +Things in context: + +1. rep - information about where the user's cursor is +2. documentAttributeManager - information about attributes in the document + +This hook allows a plugin to react to a cursor or selection change, +perhaps to update a UI element based on the style at the cursor location. diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 79879b2fe..d9ec0db5f 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -81,7 +81,7 @@ Available blocks in `pad.html` are: * `modals` - Contains all connectivity messages * `embedPopup` - the embed dropdown * `scripts` - Add your script tags here, if you really have to (consider use client-side hooks instead) - + `timeslider.html` blocks: * `timesliderStyles` @@ -90,9 +90,9 @@ Available blocks in `pad.html` are: * `timesliderTop` * `timesliderEditbarRight` * `modals` - + `index.html` blocks: - + * `indexWrapper` - contains the form for creating new pads ## padInitToolbar @@ -334,7 +334,7 @@ exports.aceAttribClasses = function(hook_name, attr, cb){ ``` ## exportFileName -Called from src/node/handler/ExportHandler.js +Called from src/node/handler/ExportHandler.js Things in context: @@ -357,7 +357,7 @@ Things in context: 1. Pad object -This hook will allow a plug-in developer to include more properties and attributes to support during HTML Export. An Array should be returned. +This hook will allow a plug-in developer to include more properties and attributes to support during HTML Export. If tags are stored as `['color', 'red']` on the attribute pool, use `exportHtmlAdditionalTagsWithData` instead. An Array should be returned. Example: ``` @@ -368,6 +368,24 @@ exports.exportHtmlAdditionalTags = function(hook, pad, cb){ }; ``` +## exportHtmlAdditionalTagsWithData +Called from src/node/utils/ExportHtml.js + +Things in context: + +1. Pad object + +Identical to `exportHtmlAdditionalTags`, but for tags that are stored with an specific value (not simply `true`) on the attribute pool. For example `['color', 'red']`, instead of `['bold', true]`. This hook will allow a plug-in developer to include more properties and attributes to support during HTML Export. An Array of arrays should be returned. The exported HTML will contain tags like `` for the content where attributes are `['color', 'red']`. + +Example: +``` +// Add the props to be supported in export +exports.exportHtmlAdditionalTagsWithData = function(hook, pad, cb){ + var padId = pad.id; + cb([["color", "red"], ["color", "blue"]]); +}; +``` + ## userLeave Called from src/node/handler/PadMessageHandler.js @@ -384,3 +402,20 @@ exports.userLeave = function(hook, session, callback) { console.log('%s left pad %s', session.author, session.padId); }; ``` + +### clientReady +Called from src/node/handler/PadMessageHandler.js + +This in context: + +1. message + +This hook gets called when handling a CLIENT_READY which is the first message from the client to the server. + +Example: + +``` +exports.clientReady = function(hook, message) { + console.log('Client has entered the pad' + message.padId); +}; +``` diff --git a/settings.json.template b/settings.json.template index bfd0c7e66..f29bcbc0e 100644 --- a/settings.json.template +++ b/settings.json.template @@ -86,10 +86,14 @@ may cause problems during deployment. Set to 0 to disable caching */ "maxAge" : 21600, // 60 * 60 * 6 = 6 hours - /* This is the path to the Abiword executable. Setting it to null, disables abiword. + /* This is the absolute path to the Abiword executable. Setting it to null, disables abiword. Abiword is needed to advanced import/export features of pads*/ "abiword" : null, + /* This is the absolute path to the soffice executable. Setting it to null, disables LibreOffice exporting. + LibreOffice can be used in lieu of Abiword to export pads */ + "soffice" : null, + /* This is the path to the Tidy executable. Setting it to null, disables Tidy. Tidy is used to improve the quality of exported pads*/ "tidyHtml" : null, @@ -131,6 +135,11 @@ // Allow Load Testing tools to hit the Etherpad Instance. Warning this will disable security on the instance. "loadTest": false, + // Disable indentation on new line when previous line ends with some special chars (':', '[', '(', '{') + /* + "indentationOnNewLine": false, + */ + /* The toolbar buttons configuration. "toolbar": { "left": [ diff --git a/src/locales/ar.json b/src/locales/ar.json index 153faae82..33c6beb3f 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -6,7 +6,8 @@ "Alami", "Meno25", "Test Create account", - "محمد أحمد عبد الفتاح" + "محمد أحمد عبد الفتاح", + "Haytham morsy" ] }, "index.newPad": "باد جديد", @@ -97,6 +98,9 @@ "timeslider.exportCurrent": "تصدير النسخة الحالية ك:", "timeslider.version": "إصدار {{version}}", "timeslider.saved": "محفوظ {{month}} {{day}}, {{year}}", + "timeslider.playPause": "تشغيل / إيقاف مؤقت محتويات الباد", + "timeslider.backRevision": "عد إلى مراجعة في هذه الباد", + "timeslider.forwardRevision": "انطلق إلى مراجعة في هذه الباد", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "يناير", "timeslider.month.february": "فبراير", diff --git a/src/locales/es.json b/src/locales/es.json index c62cacc47..b26590630 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -132,7 +132,7 @@ "pad.impexp.confirmimport": "Al importar un archivo se borrará el contenido actual del pad. ¿Estás seguro de que quieres continuar?", "pad.impexp.convertFailed": "No pudimos importar este archivo. Inténtalo con un formato diferente o copia y pega manualmente.", "pad.impexp.padHasData": "No hemos podido importar este archivo porque este pad ya ha tenido cambios. Importa a un nuevo pad.", - "pad.impexp.uploadFailed": "El envío falló. Intentalo de nuevo.", + "pad.impexp.uploadFailed": "El envío falló. Inténtalo de nuevo.", "pad.impexp.importfailed": "Fallo al importar", "pad.impexp.copypaste": "Intenta copiar y pegar", "pad.impexp.exportdisabled": "La exportación al formato {{type}} está desactivada. Contacta a tu administrador de sistemas." diff --git a/src/locales/olo.json b/src/locales/olo.json index b65e8ebb2..e8db16f7f 100644 --- a/src/locales/olo.json +++ b/src/locales/olo.json @@ -2,7 +2,8 @@ "@metadata": { "authors": [ "Denö", - "Mashoi7" + "Mashoi7", + "Ilja.mos" ] }, "pad.toolbar.underline.title": "Alleviivua (Ctrl+U)", @@ -36,7 +37,7 @@ "timeslider.month.january": "pakkaskuudu", "timeslider.month.february": "tuhukuudu", "timeslider.month.march": "kevätkuudu", - "timeslider.month.april": "kevätkuudu", + "timeslider.month.april": "sulakuudu", "timeslider.month.may": "oraskuudu", "timeslider.month.june": "kezäkuudu", "timeslider.month.july": "heinykuudu", diff --git a/src/locales/pa.json b/src/locales/pa.json index 9e154e36d..531e4ac8e 100644 --- a/src/locales/pa.json +++ b/src/locales/pa.json @@ -2,7 +2,8 @@ "@metadata": { "authors": [ "Aalam", - "Babanwalia" + "Babanwalia", + "ਪ੍ਰਚਾਰਕ" ] }, "index.newPad": "ਨਵਾਂ ਪੈਡ", @@ -10,29 +11,31 @@ "pad.toolbar.bold.title": "ਗੂੜ੍ਹਾ (Ctrl-B)", "pad.toolbar.italic.title": "ਤਿਰਛਾ (Ctrl-I)", "pad.toolbar.underline.title": "ਹੇਠਾਂ-ਰੇਖਾ (Ctrl-U)", - "pad.toolbar.strikethrough.title": "ਵਿੰਨ੍ਹੋ ਵਿਨੋ", - "pad.toolbar.ol.title": "ਲੜੀਵਾਰ ਲਿਸਟ", + "pad.toolbar.strikethrough.title": "ਵਿੰਨ੍ਹੋ (Ctrl+5)", + "pad.toolbar.ol.title": "ਲੜੀਵਾਰ ਸੂਚੀ", "pad.toolbar.ul.title": "ਬਿਨ-ਲੜੀਬੱਧ ਸੂਚੀ", "pad.toolbar.indent.title": "ਹਾਸ਼ੀਏ ਤੋਂ ਪਰ੍ਹੇ (ਟੈਬ)", "pad.toolbar.unindent.title": "ਹਾਸ਼ੀਏ ਵੱਲ (ਸ਼ਿਫ਼ਟ+ਟੈਬ)", "pad.toolbar.undo.title": "ਵਾਪਸ (Ctrl-Z)", "pad.toolbar.redo.title": "ਪਰਤਾਓ (Ctrl-Y)", - "pad.toolbar.clearAuthorship.title": "ਪਰਮਾਣਕਿਤਾ ਰੰਗ ਸਾਫ਼ ਕਰੋ", + "pad.toolbar.clearAuthorship.title": "ਪਰਮਾਣਕਿਤਾ ਰੰਗ ਸਾਫ਼ ਕਰੋ (Ctrl+Shift+C)", "pad.toolbar.import_export.title": "ਵੱਖ-ਵੱਖ ਫਾਇਲ ਫਾਰਮੈਟ ਤੋਂ/ਵਿੱਚ ਇੰਪੋਰਟ/ਐਕਸਪੋਰਟ ਕਰੋ", "pad.toolbar.timeslider.title": "ਸਮਾਂ-ਲਕੀਰ", "pad.toolbar.savedRevision.title": "ਰੀਵਿਜ਼ਨ ਸੰਭਾਲੋ", "pad.toolbar.settings.title": "ਸੈਟਿੰਗ", "pad.toolbar.embed.title": "ਇਹ ਪੈਡ ਸਾਂਝਾ ਤੇ ਇੰਬੈੱਡ ਕਰੋ", "pad.toolbar.showusers.title": "ਇਹ ਪੈਡ ਉੱਤੇ ਯੂਜ਼ਰ ਵੇਖਾਓ", - "pad.colorpicker.save": "ਸਾਂਭੋ", + "pad.colorpicker.save": "ਸੰਭਾਲੋ", "pad.colorpicker.cancel": "ਰੱਦ ਕਰੋ", "pad.loading": "…ਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", + "pad.noCookie": "ਕੂਕੀਜ਼ ਨਹੀਂ ਲੱਭੀਅਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਬ੍ਰਾੳੂਜ਼ਰ ਵਿੱਚ ਕੂਕੀਜ਼ ਲਾਗੂ ਕਰੋ।", "pad.passwordRequired": "ਇਹ ਪੈਡ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ ਤੁਹਾਨੂੰ ਪਾਸਵਰਡ ਚਾਹੀਦਾ ਹੈ", "pad.permissionDenied": "ਇਹ ਪੈਡ ਵਰਤਨ ਲਈ ਤੁਹਾਨੂੰ ਅਧਿਕਾਰ ਨਹੀਂ ਹਨ", "pad.wrongPassword": "ਤੁਹਾਡਾ ਪਾਸਵਰਡ ਗਲਤੀ ਸੀ", "pad.settings.padSettings": "ਪੈਡ ਸੈਟਿੰਗ", "pad.settings.myView": "ਮੇਰੀ ਝਲਕ", "pad.settings.stickychat": "ਹਮੇਸ਼ਾ ਸਕਰੀਨ ਉੱਤੇ ਗੱਲ ਕਰੋ", + "pad.settings.chatandusers": "ਗੱਲ-ਬਾਤ ਅਤੇ ਵਰਤੋਂਕਾਰ ਦਿਖਾਵੋ", "pad.settings.colorcheck": "ਲੇਖਕੀ ਰੰਗ", "pad.settings.linenocheck": "ਲਾਈਨ ਨੰਬਰ", "pad.settings.rtlcheck": "ਸਮੱਗਰੀ ਸੱਜੇ ਤੋਂ ਖੱਬੇ ਪੜ੍ਹਨੀ ਹੈ?", @@ -45,10 +48,11 @@ "pad.importExport.import": "ਕੋਈ ਵੀ ਟੈਕਸਟ ਫਾਇਲ ਜਾਂ ਦਸਤਾਵੇਜ਼ ਅੱਪਲੋਡ ਕਰੋ", "pad.importExport.importSuccessful": "ਸਫ਼ਲ!", "pad.importExport.export": "ਮੌਜੂਦਾ ਪੈਡ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋ:", + "pad.importExport.exportetherpad": "ੲੈਥਰਪੈਡ", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "ਸਧਾਰਨ ਟੈਕਸਟ", "pad.importExport.exportword": "ਮਾਈਕਰੋਸਾਫਟ ਵਰਡ", - "pad.importExport.exportpdf": "ਪੀਡੀਐਫ", + "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (ਓਪਨ ਡੌਕੂਮੈਂਟ ਫਾਰਮੈਟ)", "pad.importExport.abiword.innerHTML": "ਤੁਸੀਂ ਸਿਰਫ਼ ਸਾਦੀਆਂ ਲਿਖਤੀ ਜਾਂ ਐੱਚ.ਟੀ.ਐੱਮ.ਐੱਲ. ਰੂਪ-ਰੇਖਾਵਾਂ ਤੋਂ ਦਰਾਮਦ ਕਰ ਸਕਦੇ ਹੋ। ਹੋਰ ਉੱਨਤ ਦਰਾਮਦੀ ਗੁਣਾਂ ਵਾਸਤੇ ਮਿਹਰਬਾਨੀ ਕਰਕੇ ਐਬੀਵਰਡ ਥਾਪੋ।", "pad.modals.connected": "ਕੁਨੈਕਟ ਹੈ।", @@ -88,8 +92,11 @@ "timeslider.toolbar.authorsList": "ਕੋਈ ਲੇਖਕ ਨਹੀਂ", "timeslider.toolbar.exportlink.title": "ਐਕਸਪੋਰਟ", "timeslider.exportCurrent": "ਮੌਜੂਦਾ ਵਰਜਨ ਇੰਝ ਐਕਸਪੋਰਟ ਕਰੋ:", - "timeslider.version": "ਵਰਜਨ {{version}}", + "timeslider.version": "ਵਰਜ਼ਨ {{version}}", "timeslider.saved": "{{day}} {{month}} {{year}} ਨੂੰ ਸੰਭਾਲਿਆ", + "timeslider.playPause": "ਪੈਡ ਸਮੱਗਰੀ ਚਲਾਓ / ਵਿਰਾਮ ਕਰੋ", + "timeslider.backRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਪਿਛਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ", + "timeslider.forwardRevision": "ਇਸ ਪੈਡ ਵਿੱਚ ਅਗਲੇ ਰੀਵਿਜ਼ਨ ਤੇ ਜਾਓ", "timeslider.dateformat": "{{day}}/{{month}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "ਜਨਵਰੀ", "timeslider.month.february": "ਫ਼ਰਵਰੀ", @@ -118,5 +125,5 @@ "pad.impexp.uploadFailed": "ਅੱਪਲੋਡ ਲਈ ਫੇਲ੍ਹ ਹੈ, ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ।", "pad.impexp.importfailed": "ਇੰਪੋਰਟ ਫੇਲ੍ਹ ਹੈ", "pad.impexp.copypaste": "ਕਾਪੀ ਕਰੋ ਚੇਪੋ ਜੀ", - "pad.impexp.exportdisabled": "{{type}} ਰੂਪ-ਰੇਖਾ ਵਜੋਂ ਬਰਾਮਦ ਕਰਨਾ ਬੰਦ ਹੈ। ਵੇਰਵੇ ਵਾਸਤੇ ਮਿਹਰਬਾਨੀ ਕਰਕੇ ਆਪਣੇ ਸਿਸਟਮ ਦੇ ਪ੍ਰਬੰਧਕ ਨਾਲ਼ ਰਾਬਤਾ ਬਣਾਉ।" + "pad.impexp.exportdisabled": "{{type}} ਫਾਰਮੈਟ ਵਜੋਂ ਬਰਾਮਦ ਕਰਨਾ ਬੰਦ ਹੈ। ਵੇਰਵੇ ਵਾਸਤੇ ਆਪਣੇ ਸਿਸਟਮ ਦੇ ਪਰਬੰਧਕ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।" } diff --git a/src/locales/ro.json b/src/locales/ro.json index ce38d3f9b..a73bfafc3 100644 --- a/src/locales/ro.json +++ b/src/locales/ro.json @@ -4,7 +4,8 @@ "Hedwig", "ImGelu", "Minisarm", - "Strainu" + "Strainu", + "Wintereu" ] }, "index.newPad": "Pad nou", @@ -21,6 +22,7 @@ "pad.toolbar.import_export.title": "Importă/Exportă din/în diferite formate", "pad.toolbar.savedRevision.title": "Salvează revizia", "pad.toolbar.settings.title": "Setări", + "pad.toolbar.showusers.title": "Arată utilizatorii de pe acest pad", "pad.colorpicker.save": "Salvează", "pad.colorpicker.cancel": "Anulează", "pad.loading": "Se încarcă...", @@ -46,10 +48,10 @@ "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", "pad.modals.connected": "Conectat.", - "pad.modals.reconnecting": "Se reconectează la pad-ul tău..", + "pad.modals.reconnecting": "Se reconectează la pad-ul dumneavoastră..", "pad.modals.forcereconnect": "Forțează reconectarea", "pad.modals.userdup": "Deschis în altă fereastră", - "pad.modals.userdup.advice": "Reconectează pentru a folosi această fereastră în schimb", + "pad.modals.userdup.advice": "Reconectați-vă dacă doriți să utilizați această fereastră.", "pad.modals.unauth": "Nu ești autorizat", "pad.modals.initsocketfail": "Serverul nu este disponibil.", "pad.modals.initsocketfail.explanation": "Nu s-a putut conecta la serverul de sincronizare.", @@ -61,12 +63,12 @@ "pad.share": "Distribuie acest pad", "pad.share.readonly": "Doar în citire", "pad.share.link": "Legătură", - "pad.share.emebdcode": "Încorporează URL-ul", + "pad.share.emebdcode": "Adresa URL încorporată", "pad.chat": "Chat", "pad.chat.title": "Deschide chat-ul pentru acest pad.", "pad.chat.loadmessages": "Încarcă mai multe mesaje", "timeslider.toolbar.returnbutton": "Înapoi la pad", - "timeslider.toolbar.authors": "Aurori:", + "timeslider.toolbar.authors": "Autori:", "timeslider.toolbar.authorsList": "Niciun autor", "timeslider.toolbar.exportlink.title": "Exportă", "timeslider.exportCurrent": "Exportă versiunea curentă ca:", @@ -85,7 +87,7 @@ "timeslider.month.october": "octombrie", "timeslider.month.november": "noiembrie", "timeslider.month.december": "decembrie", - "pad.userlist.entername": "Introdu numele tău", + "pad.userlist.entername": "Introduceți numele dumneavoastră", "pad.userlist.unnamed": "fără nume", "pad.userlist.guest": "Oaspete", "pad.userlist.deny": "Respinge", diff --git a/src/locales/sq.json b/src/locales/sq.json index a6b3d813f..a465cfaa2 100644 --- a/src/locales/sq.json +++ b/src/locales/sq.json @@ -5,7 +5,7 @@ "Kosovastar" ] }, - "index.newPad": "Bllok i Ri", + "index.newPad": "Bllok i ri", "index.createOpenPad": "ose krijoni/hapni një Bllok me emrin:", "pad.toolbar.bold.title": "Të trasha (Ctrl-B)", "pad.toolbar.italic.title": "Të pjerrëta (Ctrl-I)", @@ -13,7 +13,7 @@ "pad.toolbar.strikethrough.title": "Hequrvije (Ctrl+5)", "pad.toolbar.ol.title": "Listë e renditur (Ctrl+Shift+N)", "pad.toolbar.ul.title": "Listë e parenditur (Ctrl+Shift+L)", - "pad.toolbar.indent.title": "Brendazi (TAB)", + "pad.toolbar.indent.title": "E dhëmbëzuar (TAB)", "pad.toolbar.unindent.title": "Jashtazi (Shift+TAB)", "pad.toolbar.undo.title": "Zhbëje (Ctrl-Z)", "pad.toolbar.redo.title": "Ribëje (Ctrl-Y)", @@ -21,7 +21,7 @@ "pad.toolbar.import_export.title": "Importoni/Eksportoni nga/në formate të tjera kartelash", "pad.toolbar.timeslider.title": "Rrjedha kohore", "pad.toolbar.savedRevision.title": "Ruaje Rishikimin", - "pad.toolbar.settings.title": "Rregullime", + "pad.toolbar.settings.title": "Parametrat", "pad.toolbar.embed.title": "Ndajeni me të tjerët dhe Trupëzojeni këtë bllok", "pad.toolbar.showusers.title": "Shfaq përdoruesit në këtë bllok", "pad.colorpicker.save": "Ruaje", @@ -45,6 +45,7 @@ "pad.importExport.import": "Ngarkoni cilëndo kartelë teksti ose dokument", "pad.importExport.importSuccessful": "Me sukses!", "pad.importExport.export": "Eksportojeni bllokun e tanishëm si:", + "pad.importExport.exportetherpad": "Etherpad", "pad.importExport.exporthtml": "HTML", "pad.importExport.exportplain": "Tekst të thjeshtë", "pad.importExport.exportword": "Microsoft Word", @@ -58,7 +59,7 @@ "pad.modals.userdup.explanation": "Ky bllok duket se gjendet i hapur në më shumë se një dritare shfletuesi në këtë kompjuter.", "pad.modals.userdup.advice": "Rilidhuni që të përdoret kjo dritare.", "pad.modals.unauth": "I paautorizuar", - "pad.modals.unauth.explanation": "Ndërkohë që shihnit këtë dritare, lejet tuaja kanë ndryshuar. Provoni të rilidheni.", + "pad.modals.unauth.explanation": "Ndërkohë që sheh këtë dritare, lejet e tua kanë ndryshuar. Provo të rilidhesh.", "pad.modals.looping.explanation": "Ka probleme komunikimi me shërbyesin e njëkohësimit.", "pad.modals.looping.cause": "Ndoshta jeni lidhur përmes një firewall-i ose ndërmjetësi të papërputhshëm.", "pad.modals.initsocketfail": "Nuk kapet dot shërbyesi.", @@ -90,6 +91,7 @@ "timeslider.exportCurrent": "Eksportojeni versionin e tanishëm si:", "timeslider.version": "Versioni {{version}}", "timeslider.saved": "Ruajtur më {{month}} {{day}}, {{year}}", + "timeslider.playPause": "Luaj përmbajtjet e Pad / Pauzo", "timeslider.dateformat": "{{month}}/{{day}}/{{year}} {{hours}}:{{minutes}}:{{seconds}}", "timeslider.month.january": "Janar", "timeslider.month.february": "Shkurt", diff --git a/src/locales/zh-hans.json b/src/locales/zh-hans.json index 080fb62df..15f115912 100644 --- a/src/locales/zh-hans.json +++ b/src/locales/zh-hans.json @@ -82,7 +82,7 @@ "pad.modals.badChangeset.cause": "这可能是因为服务器配置的错误或者其他未预料到的行为。如果您认为这是错误,请联系服务管理员。要继续编辑,请尝试重新连接。", "pad.modals.corruptPad.explanation": "您试图连接的记事本已损坏。", "pad.modals.corruptPad.cause": "这可能是因为服务器配置的错误或者其他未预料到的行为。请联系服务管理员。", - "pad.modals.deleted": "已刪除。", + "pad.modals.deleted": "已删除。", "pad.modals.deleted.explanation": "此记事本已被移除。", "pad.modals.disconnected": "您已断开连接。", "pad.modals.disconnected.explanation": "到服务器的连接已丢失", diff --git a/src/node/db/API.js b/src/node/db/API.js index 87b6d7473..237bcb0a7 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -307,6 +307,38 @@ exports.setText = function(padID, text, callback) }); } +/** +appendText(padID, text) appends text to a pad + +Example returns: + +{code: 0, message:"ok", data: null} +{code: 1, message:"padID does not exist", data: null} +{code: 1, message:"text too long", data: null} +*/ +exports.appendText = function(padID, text, callback) +{ + //text is required + if(typeof text != "string") + { + callback(new customError("text is no string","apierror")); + return; + } + + //get the pad + getPadSafe(padID, true, function(err, pad) + { + if(ERR(err, callback)) return; + + pad.appendText(text); + + //update the clients on the pad + padMessageHandler.updatePadClients(pad, callback); + }); +}; + + + /** getHTML(padID, [rev]) returns the html of a pad diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js index e0f569efb..3e3b691a6 100644 --- a/src/node/db/AuthorManager.js +++ b/src/node/db/AuthorManager.js @@ -127,7 +127,7 @@ exports.createAuthor = function(name, callback) var author = "a." + randomString(16); //create the globalAuthors db entry - var authorObj = {"colorId" : Math.floor(Math.random()*32), "name": name, "timestamp": new Date().getTime()}; + var authorObj = {"colorId" : Math.floor(Math.random()*(exports.getColorPalette().length)), "name": name, "timestamp": new Date().getTime()}; //set the global author db entry db.set("globalAuthor:" + author, authorObj); diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js index eb6a3ed1b..83e15e6df 100644 --- a/src/node/db/Pad.js +++ b/src/node/db/Pad.js @@ -303,6 +303,19 @@ Pad.prototype.setText = function setText(newText) { this.appendRevision(changeset); }; +Pad.prototype.appendText = function appendText(newText) { + //clean the new text + newText = exports.cleanText(newText); + + var oldText = this.text(); + + //create the changeset + var changeset = Changeset.makeSplice(oldText, oldText.length, 0, newText); + + //append the changeset + this.appendRevision(changeset); +}; + Pad.prototype.appendChatMessage = function appendChatMessage(text, userId, time) { this.chatHead++; //save the chat entry in the database diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js index b4d242011..179c2b404 100644 --- a/src/node/handler/APIHandler.js +++ b/src/node/handler/APIHandler.js @@ -444,10 +444,61 @@ var version = , "getChatHead" : ["padID"] , "restoreRevision" : ["padID", "rev"] } +, "1.2.13": + { "createGroup" : [] + , "createGroupIfNotExistsFor" : ["groupMapper"] + , "deleteGroup" : ["groupID"] + , "listPads" : ["groupID"] + , "listAllPads" : [] + , "createDiffHTML" : ["padID", "startRev", "endRev"] + , "createPad" : ["padID", "text"] + , "createGroupPad" : ["groupID", "padName", "text"] + , "createAuthor" : ["name"] + , "createAuthorIfNotExistsFor": ["authorMapper" , "name"] + , "listPadsOfAuthor" : ["authorID"] + , "createSession" : ["groupID", "authorID", "validUntil"] + , "deleteSession" : ["sessionID"] + , "getSessionInfo" : ["sessionID"] + , "listSessionsOfGroup" : ["groupID"] + , "listSessionsOfAuthor" : ["authorID"] + , "getText" : ["padID", "rev"] + , "setText" : ["padID", "text"] + , "getHTML" : ["padID", "rev"] + , "setHTML" : ["padID", "html"] + , "getAttributePool" : ["padID"] + , "getRevisionsCount" : ["padID"] + , "getSavedRevisionsCount" : ["padID"] + , "listSavedRevisions" : ["padID"] + , "saveRevision" : ["padID", "rev"] + , "getRevisionChangeset" : ["padID", "rev"] + , "getLastEdited" : ["padID"] + , "deletePad" : ["padID"] + , "copyPad" : ["sourceID", "destinationID", "force"] + , "movePad" : ["sourceID", "destinationID", "force"] + , "getReadOnlyID" : ["padID"] + , "getPadID" : ["roID"] + , "setPublicStatus" : ["padID", "publicStatus"] + , "getPublicStatus" : ["padID"] + , "setPassword" : ["padID", "password"] + , "isPasswordProtected" : ["padID"] + , "listAuthorsOfPad" : ["padID"] + , "padUsersCount" : ["padID"] + , "getAuthorName" : ["authorID"] + , "padUsers" : ["padID"] + , "sendClientsMessage" : ["padID", "msg"] + , "listAllGroups" : [] + , "checkToken" : [] + , "appendChatMessage" : ["padID", "text", "authorID", "time"] + , "getChatHistory" : ["padID"] + , "getChatHistory" : ["padID", "start", "end"] + , "getChatHead" : ["padID"] + , "restoreRevision" : ["padID", "rev"] + , "appendText" : ["padID", "text"] + } }; // set the latest available API version here -exports.latestApiVersion = '1.2.12'; +exports.latestApiVersion = '1.2.13'; // exports the versions so it can be used by the new Swagger endpoint exports.version = version; diff --git a/src/node/handler/ExportHandler.js b/src/node/handler/ExportHandler.js index f861c82eb..0a8089775 100644 --- a/src/node/handler/ExportHandler.js +++ b/src/node/handler/ExportHandler.js @@ -30,9 +30,15 @@ var os = require('os'); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks"); var TidyHtml = require('../utils/TidyHtml'); +var convertor = null; + //load abiword only if its enabled if(settings.abiword != null) - var abiword = require("../utils/Abiword"); + convertor = require("../utils/Abiword"); + +// Use LibreOffice if an executable has been defined in the settings +if(settings.soffice != null) + convertor = require("../utils/LibreOffice"); var tempDirectory = "/tmp"; @@ -70,71 +76,11 @@ exports.doExport = function(req, res, padId, type) } else if(type == "txt") { - var txt; - var randNum; - var srcFile, destFile; - - async.series([ - //render the txt document - function(callback) - { - exporttxt.getPadTXTDocument(padId, req.params.rev, false, function(err, _txt) - { - if(ERR(err, callback)) return; - txt = _txt; - callback(); - }); - }, - //decide what to do with the txt export - function(callback) - { - //if this is a txt export, we can send this from here directly - res.send(txt); - callback("stop"); - }, - //send the convert job to abiword - function(callback) - { - //ensure html can be collected by the garbage collector - txt = null; - - destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type; - abiword.convertFile(srcFile, destFile, type, callback); - }, - //send the file - function(callback) - { - res.sendFile(destFile, null, callback); - }, - //clean up temporary files - function(callback) - { - async.parallel([ - function(callback) - { - fs.unlink(srcFile, callback); - }, - function(callback) - { - //100ms delay to accomidate for slow windows fs - if(os.type().indexOf("Windows") > -1) - { - setTimeout(function() - { - fs.unlink(destFile, callback); - }, 100); - } - else - { - fs.unlink(destFile, callback); - } - } - ], callback); - } - ], function(err) + exporttxt.getPadTXTDocument(padId, req.params.rev, false, function(err, txt) { - if(err && err != "stop") ERR(err); - }) + if(ERR(err)) return; + res.send(txt); + }); } else { @@ -183,11 +129,11 @@ exports.doExport = function(req, res, padId, type) TidyHtml.tidy(srcFile, callback); }, - //send the convert job to abiword + //send the convert job to the convertor (abiword, libreoffice, ..) function(callback) { destFile = tempDirectory + "/etherpad_export_" + randNum + "." + type; - abiword.convertFile(srcFile, destFile, type, callback); + convertor.convertFile(srcFile, destFile, type, callback); }, //send the file function(callback) diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 91fa37e43..9481889f3 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -630,8 +630,8 @@ function handleUserChanges(data, cb) messageLogger.warn("Dropped message, USER_CHANGES Message has no changeset!"); return cb(); } - //TODO: this might happen with other messages too => find one place to copy the session - //and always use the copy. atm a message will be ignored if the session is gone even + //TODO: this might happen with other messages too => find one place to copy the session + //and always use the copy. atm a message will be ignored if the session is gone even //if the session was valid when the message arrived in the first place if(!sessioninfos[client.id]) { @@ -960,7 +960,7 @@ function handleSwitchToPad(client, message) roomClients[i].leave(padId); } } - + // start up the new pad createSessionInfo(client, message); handleClientReady(client, message); @@ -1020,6 +1020,8 @@ function handleClientReady(client, message) var currentTime; var padIds; + hooks.callAll("clientReady", message); + async.series([ //Get ro/rw id:s function (callback) @@ -1229,6 +1231,7 @@ function handleClientReady(client, message) "plugins": plugins.plugins, "parts": plugins.parts, }, + "indentationOnNewLine": settings.indentationOnNewLine, "initialChangesets": [] // FIXME: REMOVE THIS SHIT } @@ -1365,6 +1368,12 @@ function handleChangesetRequest(client, message) messageLogger.warn("Dropped message, changeset request has no granularity!"); return; } + //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill + if(Math.floor(message.data.granularity) !== message.data.granularity) + { + messageLogger.warn("Dropped message, changeset request granularity is not an integer!"); + return; + } if(message.data.start == null) { messageLogger.warn("Dropped message, changeset request has no start!"); diff --git a/src/node/hooks/express/padurlsanitize.js b/src/node/hooks/express/padurlsanitize.js index 94cbe36a1..a9972220b 100644 --- a/src/node/hooks/express/padurlsanitize.js +++ b/src/node/hooks/express/padurlsanitize.js @@ -16,6 +16,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { if(sanitizedPadId != padId) { var real_url = sanitizedPadId; + real_url = encodeURIComponent(real_url); var query = url.parse(req.url).query; if ( query ) real_url += '?' + query; res.header('Location', real_url); diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 9e1ba1249..058127b39 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -19,6 +19,7 @@ var async = require("async"); var Changeset = require("ep_etherpad-lite/static/js/Changeset"); var padManager = require("../db/PadManager"); var ERR = require("async-stacktrace"); +var _ = require('underscore'); var Security = require('ep_etherpad-lite/static/js/security'); var hooks = require('ep_etherpad-lite/static/js/pluginfw/hooks'); var _analyzeLine = require('./ExportHelper')._analyzeLine; @@ -77,12 +78,21 @@ function getHTMLFromAtext(pad, atext, authorColors) var tags = ['h1', 'h2', 'strong', 'em', 'u', 's']; var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough']; + // prepare tags stored as ['tag', true] to be exported hooks.aCallAll("exportHtmlAdditionalTags", pad, function(err, newProps){ newProps.forEach(function (propName, i){ tags.push(propName); props.push(propName); }); }); + // prepare tags stored as ['tag', 'value'] to be exported. This will generate HTML + // with tags like + hooks.aCallAll("exportHtmlAdditionalTagsWithData", pad, function(err, newProps){ + newProps.forEach(function (propName, i){ + tags.push('span data-' + propName[0] + '="' + propName[1] + '"'); + props.push(propName); + }); + }); // holds a map of used styling attributes (*1, *2, etc) in the apool // and maps them to an index in props @@ -115,8 +125,8 @@ function getHTMLFromAtext(pad, atext, authorColors) var newLength = props.push(propName); anumMap[a] = newLength -1; - css+=".removed {text-decoration: line-through; " + - "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; "+ + css+=".removed {text-decoration: line-through; " + + "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)'; "+ "filter: alpha(opacity=80); "+ "opacity: 0.8; "+ "}\n"; @@ -130,7 +140,13 @@ function getHTMLFromAtext(pad, atext, authorColors) // this pad, and if yes puts its attrib id->props value into anumMap props.forEach(function (propName, i) { - var propTrueNum = apool.putAttrib([propName, true], true); + var attrib = [propName, true]; + if (_.isArray(propName)) { + // propName can be in the form of ['color', 'red'], + // see hook exportHtmlAdditionalTagsWithData + attrib = propName; + } + var propTrueNum = apool.putAttrib(attrib, true); if (propTrueNum >= 0) { anumMap[propTrueNum] = i; @@ -154,6 +170,12 @@ function getHTMLFromAtext(pad, atext, authorColors) var property = props[i]; + // we are not insterested on properties in the form of ['color', 'red'], + // see hook exportHtmlAdditionalTagsWithData + if (_.isArray(property)) { + return false; + } + if(property.substr(0,6) === "author"){ return stripDotFromAuthorID(property); } @@ -165,6 +187,13 @@ function getHTMLFromAtext(pad, atext, authorColors) return false; } + // tags added by exportHtmlAdditionalTagsWithData will be exported as with + // data attributes + function isSpanWithData(i){ + var property = props[i]; + return _.isArray(property); + } + function emitOpenTag(i) { openTags.unshift(i); @@ -186,8 +215,9 @@ function getHTMLFromAtext(pad, atext, authorColors) { openTags.shift(); var spanClass = getSpanClassFor(i); + var spanWithData = isSpanWithData(i); - if(spanClass){ + if(spanClass || spanWithData){ assem.append(''); } else { assem.append('
'); } }*/ - else//means we are getting closer to the lowest level of indentation or are at the same level + else//means we are getting closer to the lowest level of indentation or are at the same level { var toClose = lists.length > 0 ? listLevels[listLevels.length - 2] - line.listLevel : 0 if( toClose > 0){ @@ -431,7 +461,7 @@ function getHTMLFromAtext(pad, atext, authorColors) } } } - + for (var k = lists.length - 1; k >= 0; k--) { if(lists[k][1] == "number") @@ -460,14 +490,17 @@ exports.getPadHTMLDocument = function (padId, revNum, noDocType, callback) stylesForExportCSS += css; }); // Core inclusion of head etc. - var head = - (noDocType ? '' : '\n') + - '\n' + (noDocType ? '' : '\n' + + var head = + (noDocType ? '' : '\n') + + '\n' + (noDocType ? '' : '\n' + '' + Security.escapeHTML(padId) + '\n' + - '\n' + - '\n' + '\n') + + stylesForExportCSS + + '\n' + '\n') + ''; var foot = '\n\n'; diff --git a/src/node/utils/LibreOffice.js b/src/node/utils/LibreOffice.js new file mode 100644 index 000000000..415772456 --- /dev/null +++ b/src/node/utils/LibreOffice.js @@ -0,0 +1,93 @@ +/** + * Controls the communication with LibreOffice + */ + +/* + * 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. + */ + +var async = require("async"); +var fs = require("fs"); +var os = require("os"); +var path = require("path"); +var settings = require("./Settings"); +var spawn = require("child_process").spawn; + +// Conversion tasks will be queued up, so we don't overload the system +var queue = async.queue(doConvertTask, 1); + +/** + * Convert a file from one type to another + * + * @param {String} srcFile The path on disk to convert + * @param {String} destFile The path on disk where the converted file should be stored + * @param {String} type The type to convert into + * @param {Function} callback Standard callback function + */ +exports.convertFile = function(srcFile, destFile, type, callback) { + queue.push({"srcFile": srcFile, "destFile": destFile, "type": type, "callback": callback}); +}; + +function doConvertTask(task, callback) { + var tmpDir = os.tmpdir(); + + async.series([ + // Generate a PDF file with LibreOffice + function(callback) { + var soffice = spawn(settings.soffice, [ + '--headless', + '--invisible', + '--nologo', + '--nolockcheck', + '--convert-to', task.type, + task.srcFile, + '--outdir', tmpDir + ]); + + var stdoutBuffer = ''; + + // Delegate the processing of stdout to another function + soffice.stdout.on('data', function(data) { + stdoutBuffer += data.toString(); + }); + + // Append error messages to the buffer + soffice.stderr.on('data', function(data) { + stdoutBuffer += data.toString(); + }); + + // Throw an exception if libreoffice failed + soffice.on('exit', function(code) { + if (code != 0) { + return callback("LibreOffice died with exit code " + code + " and message: " + stdoutBuffer); + } + + callback(); + }) + }, + + // Move the PDF file to the correct place + function(callback) { + var filename = path.basename(task.srcFile); + var pdfFilename = filename.substr(0, filename.lastIndexOf('.')) + '.' + task.type; + var pdfPath = path.join(tmpDir, pdfFilename); + fs.rename(pdfPath, task.destFile, callback); + } + ], function(err) { + // Invoke the callback for the local queue + callback(); + + // Invoke the callback for the task + task.callback(err); + }); +} diff --git a/src/node/utils/Settings.js b/src/node/utils/Settings.js index 2c2f90bf8..f76cebdc2 100644 --- a/src/node/utils/Settings.js +++ b/src/node/utils/Settings.js @@ -152,6 +152,11 @@ exports.minify = true; */ exports.abiword = null; +/** + * The path of the libreoffice executable + */ +exports.soffice = null; + /** * The path of the tidy executable */ @@ -177,6 +182,11 @@ exports.disableIPlogging = false; */ exports.loadTest = false; +/** + * Enable indentation on new lines + */ +exports.indentationOnNewLine = true; + /* * log4js appender configuration */ @@ -218,8 +228,14 @@ exports.getGitCommit = function() { try { var rootPath = path.resolve(npm.dir, '..'); - var ref = fs.readFileSync(rootPath + "/.git/HEAD", "utf-8"); - var refPath = rootPath + "/.git/" + ref.substring(5, ref.indexOf("\n")); + if (fs.lstatSync(rootPath + '/.git').isFile()) { + rootPath = fs.readFileSync(rootPath + '/.git', "utf8"); + rootPath = rootPath.split(' ').pop().trim(); + } else { + rootPath += '/.git'; + } + var ref = fs.readFileSync(rootPath + "/HEAD", "utf-8"); + var refPath = rootPath + "/" + ref.substring(5, ref.indexOf("\n")); version = fs.readFileSync(refPath, "utf-8"); version = version.substring(0, 7); } diff --git a/src/static/js/ace.js b/src/static/js/ace.js index c446939a3..455bfaa36 100644 --- a/src/static/js/ace.js +++ b/src/static/js/ace.js @@ -265,7 +265,7 @@ plugins.ensure(function () {\n\ iframeHTML: iframeHTML }); - iframeHTML.push(' '); + iframeHTML.push(' '); // Expose myself to global for my child frame. var thisFunctionsName = "ChildAccessibleAce2Editor"; @@ -315,7 +315,7 @@ window.onload = function () {\n\ // bizarrely, in FF2, a file with no "external" dependencies won't finish loading properly // (throbs busy while typing) - outerHTML.push('', '', scriptTag(outerScript), '
x
'); + outerHTML.push('', '', scriptTag(outerScript), '
x
'); var outerFrame = document.createElement("IFRAME"); outerFrame.name = "ace_outer"; diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index d1657a7c4..220136afa 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1894,7 +1894,11 @@ function Ace2Inner(){ var prevLine = rep.lines.prev(thisLine); var prevLineText = prevLine.text; var theIndent = /^ *(?:)/.exec(prevLineText)[0]; - if (/[\[\(\:\{]\s*$/.exec(prevLineText)) theIndent += THE_TAB; + var shouldIndent = parent.parent.clientVars.indentationOnNewLine; + if (shouldIndent && /[\[\(\:\{]\s*$/.exec(prevLineText)) + { + theIndent += THE_TAB; + } var cs = Changeset.builder(rep.lines.totalWidth()).keep( rep.lines.offsetOfIndex(lineNum), lineNum).insert( theIndent, [ @@ -2336,7 +2340,7 @@ function Ace2Inner(){ function getAttributeOnSelection(attributeName){ if (!(rep.selStart && rep.selEnd)) return - + var withIt = Changeset.makeAttribsString('+', [ [attributeName, 'true'] ], rep.apool); @@ -2347,14 +2351,14 @@ function Ace2Inner(){ } return rangeHasAttrib(rep.selStart, rep.selEnd) - + function rangeHasAttrib(selStart, selEnd) { // if range is collapsed -> no attribs in range if(selStart[1] == selEnd[1] && selStart[0] == selEnd[0]) return false - + if(selStart[0] != selEnd[0]) { // -> More than one line selected var hasAttrib = true - + // from selStart to the end of the first line hasAttrib = hasAttrib && rangeHasAttrib(selStart, [selStart[0], rep.lines.atIndex(selStart[0]).text.length]) @@ -2365,22 +2369,22 @@ function Ace2Inner(){ // for the last, potentially partial, line hasAttrib = hasAttrib && rangeHasAttrib([selEnd[0], 0], [selEnd[0], selEnd[1]]) - + return hasAttrib } - + // Logic tells us we now have a range on a single line - + var lineNum = selStart[0] , start = selStart[1] , end = selEnd[1] , hasAttrib = true - + // Iterate over attribs on this line - + var opIter = Changeset.opIterator(rep.alines[lineNum]) , indexIntoLine = 0 - + while (opIter.hasNext()) { var op = opIter.next(); var opStartInLine = indexIntoLine; @@ -2394,11 +2398,11 @@ function Ace2Inner(){ } indexIntoLine = opEndInLine; } - + return hasAttrib } } - + editorInfo.ace_getAttributeOnSelection = getAttributeOnSelection; function toggleAttributeOnSelection(attributeName) @@ -2897,6 +2901,12 @@ function Ace2Inner(){ rep.selFocusAtStart = newSelFocusAtStart; currentCallStack.repChanged = true; + hooks.callAll('aceSelectionChanged', { + rep: rep, + callstack: currentCallStack, + documentAttributeManager: documentAttributeManager, + }); + return true; //console.log("selStart: %o, selEnd: %o, focusAtStart: %s", rep.selStart, rep.selEnd, //String(!!rep.selFocusAtStart)); @@ -3632,16 +3642,10 @@ function Ace2Inner(){ var altKey = evt.altKey; var shiftKey = evt.shiftKey; - // prevent ESC key - if (keyCode == 27) - { - evt.preventDefault(); - return; - } // Is caret potentially hidden by the chat button? var myselection = document.getSelection(); // get the current caret selection var caretOffsetTop = myselection.focusNode.parentNode.offsetTop | myselection.focusNode.offsetTop; // get the carets selection offset in px IE 214 - + if(myselection.focusNode.wholeText){ // Is there any content? If not lineHeight will report wrong.. var lineHeight = myselection.focusNode.parentNode.offsetHeight; // line height of populated links }else{ @@ -3706,20 +3710,25 @@ function Ace2Inner(){ documentAttributeManager: documentAttributeManager, evt:evt }); - specialHandled = (specialHandledInHook&&specialHandledInHook.length>0)?specialHandledInHook[0]:specialHandled; + + // if any hook returned true, set specialHandled with true + if (specialHandledInHook) { + specialHandled = _.contains(specialHandledInHook, true); + } + if ((!specialHandled) && altKey && isTypeForSpecialKey && keyCode == 120){ // Alt F9 focuses on the File Menu and/or editbar. // Note that while most editors use Alt F10 this is not desirable // As ubuntu cannot use Alt F10.... // Focus on the editbar. -- TODO: Move Focus back to previous state (we know it so we can use it) var firstEditbarElement = parent.parent.$('#editbar').children("ul").first().children().first().children().first().children().first(); - $(this).blur(); + $(this).blur(); firstEditbarElement.focus(); evt.preventDefault(); } if ((!specialHandled) && altKey && keyCode == 67 && type === "keydown"){ // Alt c focuses on the Chat window - $(this).blur(); + $(this).blur(); parent.parent.chat.show(); parent.parent.$("#chatinput").focus(); evt.preventDefault(); @@ -3830,6 +3839,15 @@ function Ace2Inner(){ }, 0); specialHandled = true; } + if ((!specialHandled) && isTypeForSpecialKey && keyCode == 27) + { + // prevent esc key; + // in mozilla versions 14-19 avoid reconnecting pad. + + fastIncorp(4); + evt.preventDefault(); + specialHandled = true; + } if ((!specialHandled) && isTypeForCmdKey && String.fromCharCode(which).toLowerCase() == "s" && (evt.metaKey || evt.ctrlKey) && !evt.altKey) /* Do a saved revision on ctrl S */ { evt.preventDefault(); @@ -4961,7 +4979,7 @@ function Ace2Inner(){ // Disabled: https://github.com/ether/etherpad-lite/issues/2546 // Will break OL re-numbering: https://github.com/ether/etherpad-lite/pull/2533 - // $(document).on("cut", handleCut); + // $(document).on("cut", handleCut); $(root).on("blur", handleBlur); if (browser.msie) @@ -4972,12 +4990,19 @@ function Ace2Inner(){ // Don't paste on middle click of links $(root).on("paste", function(e){ - // TODO: this breaks pasting strings into URLS when using + // TODO: this breaks pasting strings into URLS when using // Control C and Control V -- the Event is never available // here.. :( if(e.target.a || e.target.localName === "a"){ e.preventDefault(); } + + // Call paste hook + hooks.callAll('acePaste', { + editorInfo: editorInfo, + rep: rep, + documentAttributeManager: documentAttributeManager + }); }) // CompositionEvent is not implemented below IE version 8 @@ -5347,8 +5372,9 @@ function Ace2Inner(){ function initLineNumbers() { lineNumbersShown = 1; - sideDiv.innerHTML = '
1
'; + sideDiv.innerHTML = '
1
'; sideDivInner = outerWin.document.getElementById("sidedivinner"); + $(sideDiv).addClass("sidediv"); } function updateLineNumbers() diff --git a/src/static/js/admin/plugins.js b/src/static/js/admin/plugins.js index d337da03a..48d1ab70a 100644 --- a/src/static/js/admin/plugins.js +++ b/src/static/js/admin/plugins.js @@ -11,7 +11,7 @@ $(document).ready(function () { //connect var room = url + "pluginfw/installer"; - socket = io.connect(room, {resource : resource}); + socket = io.connect(room, {path: baseURL + "socket.io", resource : resource}); function search(searchTerm, limit) { if(search.searchTerm != searchTerm) { diff --git a/src/static/js/admin/settings.js b/src/static/js/admin/settings.js index 8a4473d6a..42b038d52 100644 --- a/src/static/js/admin/settings.js +++ b/src/static/js/admin/settings.js @@ -10,7 +10,7 @@ $(document).ready(function () { //connect var room = url + "settings"; - socket = io.connect(room, {resource : resource}); + socket = io.connect(room, {path: baseURL + "socket.io", resource : resource}); socket.on('settings', function (settings) { diff --git a/src/static/js/contentcollector.js b/src/static/js/contentcollector.js index 5c1e8efbf..6820da07c 100644 --- a/src/static/js/contentcollector.js +++ b/src/static/js/contentcollector.js @@ -100,7 +100,7 @@ function makeContentCollector(collectStyles, abrowser, apool, domInterface, clas function textify(str) { return sanitizeUnicode( - str.replace(/\n/g, '').replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ')); + str.replace(/(\n | \n)/g, ' ').replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ')); } function getAssoc(node, name) diff --git a/src/templates/pad.html b/src/templates/pad.html index 5311b3202..84f9fef85 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -100,7 +100,7 @@
-
+

You need a password to access this pad

diff --git a/tests/backend/specs/api/pad.js b/tests/backend/specs/api/pad.js index 14e990911..e04033c04 100644 --- a/tests/backend/specs/api/pad.js +++ b/tests/backend/specs/api/pad.js @@ -79,6 +79,8 @@ describe('Permission', function(){ -> movePad(newPadID, originalPadId) -- Should provide consistant pad data -> getText(originalPadId) -- Should be "hello world" -> getLastEdited(padID) -- Should not be 0 + -> appendText(padID, "hello") + -> getText(padID) -- Should be "hello worldhello" -> setHTML(padID) -- Should fail on invalid HTML -> setHTML(padID) *3 -- Should fail on invalid HTML -> getHTML(padID) -- Should return HTML close to posted HTML @@ -483,6 +485,30 @@ describe('getLastEdited', function(){ }); }) +describe('appendText', function(){ + it('Append text to a pad Id', function(done) { + api.get(endPoint('appendText', '1.2.13')+"&padID="+testPadId+"&text=hello") + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Pad Append Text failed"); + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); +}); + +describe('getText', function(){ + it('Gets text on a pad Id', function(done) { + api.get(endPoint('getText')+"&padID="+testPadId) + .expect(function(res){ + if(res.body.code !== 0) throw new Error("Pad Get Text failed"); + if(res.body.data.text !== text+"\nhello") throw new Error("Pad Text not set properly"); + }) + .expect('Content-Type', /json/) + .expect(200, done); + }); +}); + + describe('setHTML', function(){ it('Sets the HTML of a Pad attempting to pass ugly HTML', function(done) { var html = "
Hello HTML
"; @@ -542,8 +568,9 @@ describe('createPad', function(){ */ -var endPoint = function(point){ - return '/api/'+apiVersion+'/'+point+'?apikey='+apiKey; +var endPoint = function(point, version){ + version = version || apiVersion; + return '/api/'+version+'/'+point+'?apikey='+apiKey; } function makeid() diff --git a/tests/frontend/specs/indentation.js b/tests/frontend/specs/indentation.js index 8e851d873..de92cc8fe 100644 --- a/tests/frontend/specs/indentation.js +++ b/tests/frontend/specs/indentation.js @@ -15,7 +15,7 @@ describe("indentation button", function(){ //select this text element $firstTextElement.sendkeys('{selectall}'); - if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE var evtType = "keypress"; }else{ var evtType = "keydown"; @@ -31,7 +31,7 @@ describe("indentation button", function(){ }); it("indent text with button", function(done){ - var inner$ = helper.padInner$; + var inner$ = helper.padInner$; var chrome$ = helper.padChrome$; var $indentButton = chrome$(".buttonicon-indent"); @@ -43,7 +43,7 @@ describe("indentation button", function(){ }); it("keeps the indent on enter for the new line", function(done){ - var inner$ = helper.padInner$; + var inner$ = helper.padInner$; var chrome$ = helper.padChrome$; var $indentButton = chrome$(".buttonicon-indent"); @@ -51,9 +51,9 @@ describe("indentation button", function(){ //type a bit, make a line break and type again var $firstTextElement = inner$("div span").first(); - $firstTextElement.sendkeys('line 1'); - $firstTextElement.sendkeys('{enter}'); - $firstTextElement.sendkeys('line 2'); + $firstTextElement.sendkeys('line 1'); + $firstTextElement.sendkeys('{enter}'); + $firstTextElement.sendkeys('line 2'); $firstTextElement.sendkeys('{enter}'); helper.waitFor(function(){ @@ -68,13 +68,87 @@ describe("indentation button", function(){ }); }); + it("indents text with spaces on enter if previous line ends with ':', '[', '(', or '{'", function(done){ + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //type a bit, make a line break and type again + var $firstTextElement = inner$("div").first(); + $firstTextElement.sendkeys("line with ':'{enter}"); + $firstTextElement.sendkeys("line with '['{enter}"); + $firstTextElement.sendkeys("line with '('{enter}"); + $firstTextElement.sendkeys("line with '{{}'{enter}"); + + helper.waitFor(function(){ + // wait for Etherpad to split four lines into separated divs + var $fourthLine = inner$("div").first().next().next().next(); + return $fourthLine.text().indexOf("line with '{'") === 0; + }).done(function(){ + // we validate bottom to top for easier implementation + + // curly braces + var $lineWithCurlyBraces = inner$("div").first().next().next().next(); + $lineWithCurlyBraces.sendkeys('{{}'); + pressEnter(); // cannot use sendkeys('{enter}') here, browser does not read the command properly + var $lineAfterCurlyBraces = inner$("div").first().next().next().next().next(); + expect($lineAfterCurlyBraces.text()).to.match(/\s{4}/); // tab === 4 spaces + + // parenthesis + var $lineWithParenthesis = inner$("div").first().next().next(); + $lineWithParenthesis.sendkeys('('); + pressEnter(); + var $lineAfterParenthesis = inner$("div").first().next().next().next(); + expect($lineAfterParenthesis.text()).to.match(/\s{4}/); + + // bracket + var $lineWithBracket = inner$("div").first().next(); + $lineWithBracket.sendkeys('['); + pressEnter(); + var $lineAfterBracket = inner$("div").first().next().next(); + expect($lineAfterBracket.text()).to.match(/\s{4}/); + + // colon + var $lineWithColon = inner$("div").first(); + $lineWithColon.sendkeys(':'); + pressEnter(); + var $lineAfterColon = inner$("div").first().next(); + expect($lineAfterColon.text()).to.match(/\s{4}/); + + done(); + }); + }); + + it("appends indentation to the indent of previous line if previous line ends with ':', '[', '(', or '{'", function(done){ + var inner$ = helper.padInner$; + var chrome$ = helper.padChrome$; + + //type a bit, make a line break and type again + var $firstTextElement = inner$("div").first(); + $firstTextElement.sendkeys(" line with some indentation and ':'{enter}"); + $firstTextElement.sendkeys("line 2{enter}"); + + helper.waitFor(function(){ + // wait for Etherpad to split two lines into separated divs + var $secondLine = inner$("div").first().next(); + return $secondLine.text().indexOf("line 2") === 0; + }).done(function(){ + var $lineWithColon = inner$("div").first(); + $lineWithColon.sendkeys(':'); + pressEnter(); + var $lineAfterColon = inner$("div").first().next(); + expect($lineAfterColon.text()).to.match(/\s{6}/); // previous line indentation + regular tab (4 spaces) + + done(); + }); + }); + /* it("makes text indented and outdented", function() { //get the inner iframe var $inner = testHelper.$getPadInner(); - + //get the first text element out of the inner iframe var firstTextElement = $inner.find("div").first(); @@ -87,7 +161,7 @@ describe("indentation button", function(){ //ace creates a new dom element when you press a button, so just get the first text element again var newFirstTextElement = $inner.find("div").first(); - + // is there a list-indent class element now? var firstChild = newFirstTextElement.children(":first"); var isUL = firstChild.is('ul'); @@ -160,12 +234,12 @@ describe("indentation button", function(){ /* this test creates the below content, both should have double indentation line1 line2 - + firstTextElement.sendkeys('{rightarrow}'); // simulate a keypress of enter firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter firstTextElement.sendkeys('line 1'); // simulate writing the first line - firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter + firstTextElement.sendkeys('{enter}'); // simulate a keypress of enter firstTextElement.sendkeys('line 2'); // simulate writing the second line //get the second text element out of the inner iframe @@ -203,3 +277,15 @@ describe("indentation button", function(){ });*/ }); + +function pressEnter(){ + var inner$ = helper.padInner$; + if(inner$(window)[0].bowser.firefox || inner$(window)[0].bowser.modernIE){ // if it's a mozilla or IE + var evtType = "keypress"; + }else{ + var evtType = "keydown"; + } + var e = inner$.Event(evtType); + e.keyCode = 13; // enter :| + inner$("#innerdocbody").trigger(e); +}