diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml
index ab949181..fbc64019 100644
--- a/.github/workflows/django.yml
+++ b/.github/workflows/django.yml
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Cache dependencies
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: |
~/.cache/pip
diff --git a/dashboard.py b/dashboard.py
index 8c18d2a3..4b2b2e3b 100644
--- a/dashboard.py
+++ b/dashboard.py
@@ -122,6 +122,24 @@ def init_with_context(self, context):
"external": True,
"header": True,
},
+ {
+ "title": _("How to add MediaCentral & Flickr links"),
+ "url": "https://github.com/UCL-ARC/museum_of_dreams/wiki/Adding-links-from-mediacentral-&-flickr",
+ "external": True,
+ "header": True,
+ },
+ {
+ "title": _("Adding Images"),
+ "url": "https://github.com/UCL-ARC/museum_of_dreams/wiki/Adding-Images",
+ "external": True,
+ "header": True,
+ },
+ {
+ "title": _("Importing from Word"),
+ "url": "https://github.com/UCL-ARC/museum_of_dreams/wiki/Importing-from-Word",
+ "external": True,
+ "header": True,
+ },
],
)
)
diff --git a/docs/addingCKEPlugins.md b/docs/addingCKEPlugins.md
new file mode 100644
index 00000000..273edcad
--- /dev/null
+++ b/docs/addingCKEPlugins.md
@@ -0,0 +1,7 @@
+# Adding Plugins to CK Editor
+
+- Download the plugin from CKE 4
+- unpack the zip file
+- in the codebase, create a new folder with the plugin name under `/mod_app/static/ckeditor/plugins/`
+- from the unpacked zip, you mainly need the plugin.js file but some other files can/should be included, especially if you need icons
+- in `/museum_of_dreams_project/settings/base.py` update `CKEDITOR_CONFIGS.default.extraPlugins` to include the name of the plugin and if it should have a button, add it in the toolbar section as well
diff --git a/mod_app/admin/note_admin.py b/mod_app/admin/note_admin.py
index 86bf17b3..bdacdcef 100644
--- a/mod_app/admin/note_admin.py
+++ b/mod_app/admin/note_admin.py
@@ -66,7 +66,7 @@ class WritInline(s3BrowserButtonMixin, admin.TabularInline):
@admin.register(VisualInfluences)
-class VisualInfluencesAdmin(s3BrowserButtonMixin, admin.ModelAdmin):
+class VisualInfluencesAdmin(admin.ModelAdmin):
class Media:
js = ("admin/js/mentionsPluginConfig.js",)
@@ -85,7 +85,7 @@ def safe_content(self, obj):
@admin.register(WrittenInfluences)
-class WrittenInfluencesAdmin(s3BrowserButtonMixin, admin.ModelAdmin):
+class WrittenInfluencesAdmin(admin.ModelAdmin):
class Media:
js = ("admin/js/mentionsPluginConfig.js",)
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/footnotes/README.md b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/README.md
new file mode 100644
index 00000000..aae5ba3d
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/README.md
@@ -0,0 +1,56 @@
+CKEditorFootnotes
+==================
+
+Maintainers Required
+--------------------
+
+Unfortunately I don't have the time to give this project the attention it deserves. I'm happy to hand this over to someone or add contributors to help keep this ticking over.
+If you're interested, please get in touch.
+
+---
+
+Footnotes plugin for CKEditor.
+
+Demo: http://demo.gridlight-design.co.uk/ckeditor-footnotes.html
+
+CKEditor Addon: http://ckeditor.com/addon/footnotes
+
+Configuring multiple instances
+------------------------------
+
+As of 1.0.5 the plugin accepts a configuration option to allow you to prefix all your footnotes when the editor is instantiated.
+
+E.g.
+
+~~~
+CKEDITOR.replace( 'editor1', {
+ footnotesPrefix: 'a'
+} );
+~~~
+
+This could be set dynamically to allow you to ensure that all chunks of text can contain unique ID's, allowing you to include multiple chunks of text on any given page with ID clashes.
+
+For example, it should be possible to use a server-side script to set this variable to the id of a database row.
+
+
+Other configuration
+-------------------
+
+In master, it's now possible to to set configuration for the Footnotes title and the titles elements:
+
+E.g.
+
+~~~
+CKEDITOR.replace( 'editor1', {
+ footnotesDisableHeader: true, // Defaults to false
+ footnotesHeaderEls: ['
', '
'], // Defaults to ['
', '
']
+ footnotesTitle: 'References', // Defaults to 'Footnotes'
+ footnotesDialogEditorExtraConfig: { height: 150 } // Will be merged with the default options for the footnote editor
+} );
+~~~
+
+Paste From Word
+---------------
+
+A complimentary plugin that allows automatic conversion from content pasted from word is now available:
+[CKEditorFootnotes-PasteFromWord](https://github.com/andykirk/CKEditorFootnotes-PasteFromWord)
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/footnotes/dialogs/footnotes.js b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/dialogs/footnotes.js
new file mode 100644
index 00000000..b1c47dfb
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/dialogs/footnotes.js
@@ -0,0 +1,217 @@
+/**
+ * The footnotes dialog definition.
+ *
+ * Version 1.0.9
+ * https://github.com/andykirk/CKEditorFootnotes
+ *
+ */
+
+(function() {
+ "use strict";
+
+ // Dialog definition.
+ CKEDITOR.dialog.add( 'footnotesDialog', function( editor ) {
+
+ return {
+ editor_name: false,
+ footnotes_editor: false,
+ dialog_dom_id: false,
+ // Basic properties of the dialog window: title, minimum size.
+ title: 'Manage Footnotes',
+ minWidth: 400,
+ minHeight: 200,
+
+ // Dialog window contents definition.
+ contents: [
+ {
+ // Definition of the Basic Settings dialog tab (page).
+ id: 'tab-basic',
+ label: 'Basic Settings',
+
+ // The tab contents.
+ elements: [
+ {
+ // Text input field for the footnotes text.
+ type: 'textarea',
+ id: 'new_footnote',
+ 'class': 'footnote_text',
+ label: 'New footnote:',
+ inputStyle: 'height: 100px',
+ },
+ {
+ // Text input field for the footnotes title (explanation).
+ type: 'text',
+ id: 'footnote_id',
+ name: 'footnote_id',
+ label: 'No existing footnotes',
+
+ // Called by the main setupContent call on dialog initialization.
+ setup: function( element ) {
+
+ var dialog = this.getDialog(),
+ editor = dialog.getParentEditor(),
+ el = dialog.getElement().findOne('#' + this.domId),
+ footnotes = editor.editable().findOne('.footnotes ol');
+
+ dialog.dialog_dom_id = this.domId;
+
+ if (footnotes !== null) {
+
+ if (el.findOne('p') === null) {
+ el.appendHtml('
';
+ });
+
+ el.find('label,div').toArray().forEach(function(item){
+ item.setStyle('display', 'none');
+ });
+ el.findOne('ol').appendHtml(radios);
+
+ el.find('input[type="radio"]').toArray().forEach(function(item){
+ item.on('change', function(){
+
+ // Set the hidden input with the radio ident for the
+ // footnote links to use:
+ el.findOne('input[type="text"]').setValue(item.getValue());
+
+ // Also clear the editor to avoid any confusion:
+ dialog.footnotes_editor.setData('');
+ });
+ });
+
+ } else {
+ el.find('div').toArray().forEach(function(item){
+ item.setStyle('display', 'none');
+ });
+ }
+ }
+ }
+ ]
+ },
+ ],
+
+ // Invoked when the dialog is loaded.
+ onShow: function() {
+ this.setupContent();
+
+ var dialog = this;
+ CKEDITOR.on( 'instanceLoaded', function( evt ) {
+ dialog.editor_name = evt.editor.name;
+ dialog.footnotes_editor = evt.editor;
+ } );
+
+ // Allow page to scroll with dialog to allow for many/long footnotes
+ // (https://github.com/andykirk/CKEditorFootnotes/issues/12)
+ /*this.getElement().findOne('.cke_dialog').setStyles({
+ 'position': 'absolute',
+ 'top': '2%'
+ });*/
+ // Note that it seems core CKEditor Dialog CSS now solves this for me so I don't
+ // need the above code. I'll keep it here for reference for now though.
+
+ var current_editor_id = dialog.getParentEditor().id;
+
+ CKEDITOR.replaceAll( function( textarea, config ) {
+ // Make sure the textarea has the correct class:
+ if (!textarea.className.match(/footnote_text/)) {
+ return false;
+ }
+
+ // Make sure we only instantiate the relevant editor:
+ var el = textarea;
+ while ((el = el.parentElement) && !el.classList.contains(current_editor_id));
+ if (!el) {
+ return false;
+ }
+
+ config.toolbarGroups = [
+ { name: 'editing', groups: [ 'undo', 'find', 'selection', 'spellchecker' ] },
+ { name: 'clipboard', groups: [ 'clipboard' ] },
+ { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
+ ]
+ config.allowedContent = 'br em strong; a[!href]';
+ config.enterMode = CKEDITOR.ENTER_BR;
+ config.autoParagraph = false;
+ config.height = 80;
+ config.resize_enabled = false;
+ config.autoGrow_minHeight = 80;
+ config.removePlugins = 'footnotes';
+
+ var extra_config = editor.config.footnotesDialogEditorExtraConfig;
+ if (extra_config) {
+ for (var attribute in extra_config) {
+ config[attribute] = extra_config[attribute];
+ }
+ }
+
+ // If we focus on the dialog editor we should clear the radios to avoid any
+ // confusion. Similarly, if we focus on a radio, we should clear the editor
+ // (see setup above for radio change event handler for that)
+ config.on = {
+ focus: function( evt ){
+ var form_row = evt.editor.element.getAscendant('tr').getNext();
+ form_row.find('input[type="radio"]').toArray().forEach(function(item){
+ item.$.checked = false;
+ });
+ form_row.findOne('input[type="text"]').setValue('');
+ }
+ };
+ return true;
+ });
+
+ },
+
+ // This method is invoked once a user clicks the OK button, confirming the dialog.
+ onOk: function() {
+ var dialog = this;
+ var footnote_editor = CKEDITOR.instances[dialog.editor_name];
+ var footnote_id = dialog.getValueOf('tab-basic', 'footnote_id');
+ var footnote_data = footnote_editor.getData();
+
+
+ if (footnote_id == '') {
+ // No existing id selected, check for new footnote:
+ if (footnote_data == '') {
+ // Nothing entered, so quit:
+ return;
+ } else {
+ // Insert new footnote:
+ editor.plugins.footnotes.build(footnote_data, true, editor);
+ }
+ } else {
+ // Insert existing footnote:
+ editor.plugins.footnotes.build(footnote_id, false, editor);
+ }
+ // Destroy the editor so it's rebuilt properly next time:
+ footnote_editor.destroy();
+ // Destroy the list of footnotes so it's rebuilt properly next time:
+ var list = dialog.getElement().findOne('#' + dialog.dialog_dom_id).findOne('ol');
+ if (list) {
+ list.getChildren().toArray().forEach(function(item){
+ item.remove();
+ });
+ }
+ return;
+ },
+
+ onCancel: function() {
+ var dialog = this;
+ var footnote_editor = CKEDITOR.instances[dialog.editor_name];
+ footnote_editor.destroy();
+ }
+ };
+ });
+}());
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/footnotes/icons/findFootnotes.png b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/icons/findFootnotes.png
new file mode 100644
index 00000000..0f50e80d
Binary files /dev/null and b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/icons/findFootnotes.png differ
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/footnotes/icons/footnotes.png b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/icons/footnotes.png
new file mode 100644
index 00000000..77157440
Binary files /dev/null and b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/icons/footnotes.png differ
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/footnotes/plugin.js b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/plugin.js
new file mode 100644
index 00000000..be1d01b9
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/footnotes/plugin.js
@@ -0,0 +1,467 @@
+/**
+ * Basic sample plugin inserting footnotes elements into CKEditor editing area.
+ *
+ * Version 1.0.9
+ * https://github.com/andykirk/CKEditorFootnotes
+ *
+ */
+// Register the plugin within the editor.
+(function () {
+ "use strict";
+
+ CKEDITOR.plugins.add("footnotes", {
+ footnote_ids: [],
+ requires: "widget",
+ icons: "footnotes,findFootnotes",
+
+ // The plugin initialization logic goes inside this method.
+ init: function (editor) {
+ // Allow `cite` to be editable:
+ CKEDITOR.dtd.$editable["cite"] = 1;
+
+ // Add some CSS tweaks:
+ var css =
+ ".footnotes{background:#eee; padding:1px 15px;} .footnotes cite{font-style: normal;}";
+ CKEDITOR.addCss(css);
+
+ var $this = this;
+
+ /*editor.on('saveSnapshot', function(evt) {
+ console.log('saveSnapshot');
+ });*/
+
+ // Force a reorder on startup to make sure all vars are set: (e.g. footnotes store):
+ editor.on("instanceReady", function (evt) {
+ $this.reorderMarkers(editor);
+ });
+
+ // Add the reorder change event:
+ editor.on("change", function (evt) {
+ // Copy the footnotes_store as we may be doing a cut:
+ if (!evt.editor.footnotes_tmp) {
+ evt.editor.footnotes_tmp = evt.editor.footnotes_store;
+ }
+
+ // Prevent no selection errors:
+ if (!evt.editor.getSelection().getStartElement()) {
+ return;
+ }
+ // Don't reorder the markers if editing a cite:
+ var footnote_section = evt.editor
+ .getSelection()
+ .getStartElement()
+ .getAscendant("section");
+ if (
+ footnote_section &&
+ footnote_section.$.className.indexOf("footnotes") != -1
+ ) {
+ return;
+ }
+ // SetTimeout seems to be necessary (it's used in the core but can't be 100% sure why)
+ setTimeout(function () {
+ $this.reorderMarkers(editor);
+ }, 0);
+ });
+
+ // Build the initial footnotes widget editables definition:
+ var prefix = editor.config.footnotesPrefix
+ ? "-" + editor.config.footnotesPrefix
+ : "";
+ var def = {};
+
+ if (!editor.config.footnotesDisableHeader) {
+ def.header = {
+ selector: "header > *",
+ //allowedContent: ''
+ allowedContent: "strong em span sub sup;",
+ };
+ }
+
+ // Get the number of existing footnotes. Note that the editor document isn't populated
+ // yet so we need to use vanilla JS:
+ var div = document.createElement("div");
+ div.innerHTML = editor.element.$.textContent.trim();
+
+ var l = div.querySelectorAll(".footnotes li").length,
+ i = 1;
+
+ for (i; i <= l; i++) {
+ def["footnote_" + i] = {
+ selector: "#footnote" + prefix + "-" + i + " cite",
+ allowedContent: "a[*]; cite[*](*); strong em span br",
+ };
+ }
+
+ // Register the footnotes widget.
+ editor.widgets.add("footnotes", {
+ // Minimum HTML which is required by this widget to work.
+ requiredContent: "section(footnotes)",
+
+ // Check the elements that need to be converted to widgets.
+ upcast: function (element) {
+ return element.name == "section" && element.hasClass("footnotes");
+ },
+
+ editables: def,
+ });
+
+ // Register the footnotemarker widget.
+ editor.widgets.add("footnotemarker", {
+ // Minimum HTML which is required by this widget to work.
+ requiredContent: "sup[data-footnote-id]",
+
+ // Check the elements that need to be converted to widgets.
+ upcast: function (element) {
+ return (
+ element.name == "sup" &&
+ typeof element.attributes["data-footnote-id"] != "undefined"
+ );
+ },
+ });
+
+ // Define an editor command that opens our dialog.
+ editor.addCommand(
+ "footnotes",
+ new CKEDITOR.dialogCommand("footnotesDialog", {
+ // @TODO: This needs work:
+ allowedContent:
+ "section[*](*);header[*](*);li[*];a[*];cite(*)[*];sup[*]",
+ requiredContent:
+ "section(footnotes);header;li[id,data-footnote-id];a[href,id,rel];cite;sup[data-footnote-id]",
+ })
+ );
+
+ editor.addCommand("findFootnotesCommand", {
+ exec: function (editor) {
+ var content = editor.editable();
+ var noteRefs = content.find('a[name^="_ftnref"]');
+
+ noteRefs.toArray().forEach((link) => {
+ // could maybe do this by href instead
+ var refId = link.getAttribute("name").substring(7); // Removes '_ftnref' prefix
+
+ // Find the companion content link
+ var contentRef = content.findOne(`a[name="_ftn${refId}"]`);
+
+ if (contentRef) {
+ var note = contentRef.getParent();
+ var footnoteContent = note.getHtml();
+ // removing the content link so it doesn't appear in the new note content, []
+ var contentRefTextToRemove = contentRef.getOuterHtml();
+ footnoteContent = footnoteContent
+ .replace(contentRefTextToRemove, "")
+ .trim();
+
+ editor.fire("lockSnapshot");
+ editor.plugins.footnotes.build(
+ footnoteContent,
+ true,
+ editor,
+ link
+ );
+ editor.fire("unlockSnapshot");
+ note.remove();
+ }
+ });
+ },
+ });
+
+ editor.ui.addButton("FindFootnotes", {
+ label: "Find Footnotes",
+ command: "findFootnotesCommand",
+ toolbar: "insert",
+ icon: "findFootnotes",
+ });
+
+ // Create a toolbar button that executes the above command.
+ editor.ui.addButton("Footnotes", {
+ // The text part of the button (if available) and tooptip.
+ label: "Insert Footnotes",
+
+ // The command to execute on click.
+ command: "footnotes",
+
+ // The button placement in the toolbar (toolbar group name).
+ toolbar: "insert",
+
+ icon: "footnotes", // Use an existing icon from the plugin, or add a new one
+ });
+
+ // Register our dialog file. this.path is the plugin folder path.
+ CKEDITOR.dialog.add(
+ "footnotesDialog",
+ this.path + "dialogs/footnotes.js"
+ );
+ },
+
+ build: function (footnote, is_new, editor, is_replacement = false) {
+ var footnote_id;
+ if (is_new) {
+ // Generate new id:
+ footnote_id = this.generateFootnoteId();
+ } else {
+ // Existing footnote id passed:
+ footnote_id = footnote;
+ }
+
+ // Insert the marker:
+ var footnote_marker =
+ 'X';
+
+ if (is_replacement) {
+ var markerElem = new CKEDITOR.dom.element.createFromHtml(
+ footnote_marker,
+ editor.document
+ );
+ markerElem.replace(is_replacement);
+ } else {
+ editor.insertHtml(footnote_marker);
+ }
+
+ if (is_new) {
+ editor.fire("lockSnapshot");
+ this.addFootnote(
+ this.buildFootnote(footnote_id, footnote, false, editor),
+ editor
+ );
+ editor.fire("unlockSnapshot");
+ }
+ this.reorderMarkers(editor);
+ },
+
+ buildFootnote: function (footnote_id, footnote_text, data, editor) {
+ var links = "",
+ footnote,
+ letters = "abcdefghijklmnopqrstuvwxyz",
+ order = data ? data.order.indexOf(footnote_id) + 1 : 1,
+ prefix = editor.config.footnotesPrefix
+ ? "-" + editor.config.footnotesPrefix
+ : "";
+
+ if (data && data.occurrences[footnote_id] == 1) {
+ links =
+ '^ ';
+ } else if (data && data.occurrences[footnote_id] > 1) {
+ var i = 0,
+ l = data.occurrences[footnote_id],
+ n = l;
+ for (i; i < l; i++) {
+ links +=
+ '' +
+ letters.charAt(i) +
+ "";
+ if (i < l - 1) {
+ links += ", ";
+ } else {
+ links += " ";
+ }
+ }
+ }
+ footnote =
+ '
' +
+ links +
+ "" +
+ footnote_text +
+ "
";
+ return footnote;
+ },
+
+ addFootnote: function (footnote, editor) {
+ // function to add the section for the notes
+ var contents = editor.editable();
+ var footnotes = contents.findOne(".footnotes");
+
+ if (footnotes === null) {
+ var container = '';
+
+ // Add header
+ if (!editor.config.footnotesDisableHeader) {
+ var header_title = editor.config.footnotesTitle
+ ? editor.config.footnotesTitle
+ : "Footnotes";
+ var header_els = ["
", "
"]; //editor.config.editor.config.footnotesHeaderEls
+ if (editor.config.footnotesHeaderEls) {
+ header_els = editor.config.footnotesHeaderEls;
+ }
+ container +=
+ "" +
+ header_els[0] +
+ header_title +
+ header_els[1] +
+ "";
+ }
+
+ // Add footnote
+ container += "" + footnote + "";
+
+ // End section
+ container += "";
+
+ // Move cursor to end of content:
+ var range = editor.createRange();
+ range.moveToElementEditEnd(range.root);
+ editor.getSelection().selectRanges([range]);
+
+ // Insert the container:
+ editor.insertHtml(container);
+ } else {
+ footnotes.findOne("ol").appendHtml(footnote);
+ }
+ },
+
+ generateFootnoteId: function () {
+ var id = Math.random().toString(36).substr(2, 5);
+ while (String.prototype.indexOf(id, this.footnote_ids) != -1) {
+ id = String(this.generateFootnoteId());
+ }
+ this.footnote_ids.push(id);
+ return id;
+ },
+
+ reorderMarkers: function (editor) {
+ editor.fire("lockSnapshot");
+ var prefix = editor.config.footnotesPrefix
+ ? "-" + editor.config.footnotesPrefix
+ : "";
+
+ var contents = editor.editable();
+ var data = {
+ order: [],
+ occurrences: {},
+ };
+
+ // Check that there's a footnotes section. If it's been deleted the markers are useless:
+ if (contents.find(".footnotes").toArray().length == 0) {
+ contents
+ .find("sup[data-footnote-id]")
+ .toArray()
+ .forEach(function (item) {
+ item.remove();
+ });
+ editor.fire("unlockSnapshot");
+ return;
+ }
+
+ // If a header was previously added but is now disabled, remove it
+ var header_element = contents.findOne(".footnotes > header");
+ if (editor.config.footnotesDisableHeader && header_element) {
+ header_element.remove();
+ }
+
+ // Find all the markers in the document:
+ var markers = contents.find("sup[data-footnote-id]").toArray();
+
+ // If there aren't any, remove the Footnotes container:
+ if (markers.length == 0) {
+ contents.findOne(".footnotes").getParent().remove();
+ editor.fire("unlockSnapshot");
+ return;
+ }
+
+ // Otherwise reorder the markers:
+ markers.forEach(function (item) {
+ var footnote_id = item.getAttribute("data-footnote-id"),
+ marker_ref,
+ n = data.order.indexOf(footnote_id);
+
+ // If this is the markers first occurrence:
+ if (n == -1) {
+ // Store the id:
+ data.order.push(footnote_id);
+ n = data.order.length;
+ data.occurrences[footnote_id] = 1;
+ marker_ref = n + "-1";
+ } else {
+ // Otherwise increment the number of occurrences:
+ // (increment n due to zero-index array)
+ n++;
+ data.occurrences[footnote_id]++;
+ marker_ref = n + "-" + data.occurrences[footnote_id];
+ }
+ // Replace the marker contents:
+ var marker =
+ '[' +
+ n +
+ "]";
+
+ item.setHtml(marker);
+ });
+
+ // Prepare the footnotes_store object:
+ editor.footnotes_store = {};
+
+ // Then rebuild the Footnotes content to match marker order:
+ var footnotes = "",
+ footnote_text = "",
+ footnote_id,
+ i = 0,
+ l = data.order.length;
+ for (i; i < l; i++) {
+ footnote_id = data.order[i];
+ footnote_text = contents
+ .findOne('.footnotes [data-footnote-id="' + footnote_id + '"] cite')
+ .getHtml();
+ // If the footnotes text can't be found in the editor, it may be in the tmp store
+ // following a cut:
+ if (!footnote_text) {
+ footnote_text = editor.footnotes_tmp[footnote_id];
+ }
+ footnotes += this.buildFootnote(
+ footnote_id,
+ footnote_text,
+ data,
+ editor
+ );
+ // Store the footnotes for later use (post cut/paste):
+ editor.footnotes_store[footnote_id] = footnote_text;
+ }
+
+ // Insert the footnotes into the list:
+ contents.findOne(".footnotes ol").setHtml(footnotes);
+
+ // Next we need to reinstate the 'editable' properties of the footnotes.
+ // (we have to do this individually due to Widgets 'fireOnce' for editable selectors)
+ var el = contents.findOne(".footnotes"),
+ n,
+ footnote_widget;
+ // So first we need to find the right Widget instance:
+ // (I hope there's a better way of doing this but I can't find one)
+ for (i in editor.widgets.instances) {
+ if (editor.widgets.instances[i].name == "footnotes") {
+ footnote_widget = editor.widgets.instances[i];
+ break;
+ }
+ }
+ // Then we `initEditable` each footnote, giving it a unique selector:
+ for (i in data.order) {
+ n = parseInt(i) + 1;
+ footnote_widget.initEditable("footnote_" + n, {
+ selector: "#footnote" + prefix + "-" + n + " cite",
+ allowedContent: "a[*]; cite[*](*); em strong span",
+ });
+ }
+
+ editor.fire("unlockSnapshot");
+ },
+ });
+})();
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/filter/default.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/filter/default.js
new file mode 100644
index 00000000..69353f59
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/filter/default.js
@@ -0,0 +1,1961 @@
+/**
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+ * CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+ */
+
+/* globals CKEDITOR */
+
+( function() {
+ 'use strict';
+
+ var tools = CKEDITOR.tools,
+ pastetools = CKEDITOR.plugins.pastetools,
+ commonFilter = pastetools.filters.common,
+ Style = commonFilter.styles,
+ createAttributeStack = commonFilter.createAttributeStack,
+ getElementIndentation = commonFilter.lists.getElementIndentation,
+ invalidTags = [
+ 'o:p',
+ 'xml',
+ 'script',
+ 'meta',
+ 'link'
+ ],
+ shapeTags = [
+ 'v:arc',
+ 'v:curve',
+ 'v:line',
+ 'v:oval',
+ 'v:polyline',
+ 'v:rect',
+ 'v:roundrect',
+ 'v:group'
+ ],
+ links = {},
+ inComment = 0,
+ plug = {},
+ List,
+ Heuristics;
+
+ /**
+ * Set of Paste from Word plugin helpers.
+ *
+ * @since 4.13.0
+ * @private
+ * @member CKEDITOR.plugins.pastetools.filters
+ */
+ CKEDITOR.plugins.pastetools.filters.word = plug;
+
+ /**
+ * Set of Paste from Word plugin helpers.
+ *
+ * See {@link CKEDITOR.plugins.pastetools.filters.word}.
+ *
+ * @since 4.6.0
+ * @deprecated 4.13.0
+ * @private
+ * @member CKEDITOR.plugins
+ */
+ CKEDITOR.plugins.pastefromword = plug;
+
+ /**
+ * Rules for the Paste from Word filter.
+ *
+ * @since 4.13.0
+ * @private
+ * @member CKEDITOR.plugins.pastetools.filters.word
+ */
+ plug.rules = function( html, editor, filter ) {
+ var msoListsDetected = Boolean( html.match( /mso-list:\s*l\d+\s+level\d+\s+lfo\d+/ ) ),
+ shapesIds = [],
+ rules = {
+ root: function( element ) {
+ element.filterChildren( filter );
+
+ CKEDITOR.plugins.pastefromword.lists.cleanup( List.createLists( element, editor ) );
+ },
+ elementNames: [
+ [ ( /^\?xml:namespace$/ ), '' ],
+ [ /^v:shapetype/, '' ],
+ [ new RegExp( invalidTags.join( '|' ) ), '' ] // Remove invalid tags.
+ ],
+ elements: {
+ 'a': function( element ) {
+ // Redundant anchor created by IE8.
+ if ( element.attributes.name ) {
+ if ( element.attributes.name == '_GoBack' ) {
+ delete element.name;
+ return;
+ }
+
+ // Garbage links that go nowhere.
+ if ( element.attributes.name.match( /^OLE_LINK\d+$/ ) ) {
+ delete element.name;
+ return;
+ }
+ }
+
+ if ( element.attributes.href && element.attributes.href.match( /#.+$/ ) ) {
+ var name = element.attributes.href.match( /#(.+)$/ )[ 1 ];
+ links[ name ] = element;
+ }
+
+ if ( element.attributes.name && links[ element.attributes.name ] ) {
+ var link = links[ element.attributes.name ];
+ link.attributes.href = link.attributes.href.replace( /.*#(.*)$/, '#$1' );
+ }
+
+ },
+ 'div': function( element ) {
+ // Don't allow to delete page break element (#3220).
+ if ( editor.plugins.pagebreak && element.attributes[ 'data-cke-pagebreak' ] ) {
+ return element;
+ }
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'img': function( element ) {
+ // If the parent is DocumentFragment it does not have any attributes. (https://dev.ckeditor.com/ticket/16912)
+ if ( element.parent && element.parent.attributes ) {
+ var attrs = element.parent.attributes,
+ style = attrs.style || attrs.STYLE;
+ if ( style && style.match( /mso\-list:\s?Ignore/ ) ) {
+ element.attributes[ 'cke-ignored' ] = true;
+ }
+ }
+
+ Style.mapCommonStyles( element );
+
+ if ( element.attributes.src && element.attributes.src.match( /^file:\/\// ) &&
+ element.attributes.alt && element.attributes.alt.match( /^https?:\/\// ) ) {
+ element.attributes.src = element.attributes.alt;
+ }
+
+ var imgShapesIds = element.attributes[ 'v:shapes' ] ? element.attributes[ 'v:shapes' ].split( ' ' ) : [];
+ // Check whether attribute contains shapes recognised earlier (stored in global list of shapesIds).
+ // If so, add additional data-attribute to img tag.
+ var isShapeFromList = CKEDITOR.tools.array.every( imgShapesIds, function( shapeId ) {
+ return shapesIds.indexOf( shapeId ) > -1;
+ } );
+ if ( imgShapesIds.length && isShapeFromList ) {
+ // As we don't know how to process shapes we can remove them.
+ return false;
+ }
+
+ },
+ 'p': function( element ) {
+ element.filterChildren( filter );
+
+ if ( element.attributes.style && element.attributes.style.match( /display:\s*none/i ) ) {
+ return false;
+ }
+
+ if ( List.thisIsAListItem( editor, element ) ) {
+ if ( Heuristics.isEdgeListItem( editor, element ) ) {
+ Heuristics.cleanupEdgeListItem( element );
+ }
+
+ List.convertToFakeListItem( editor, element );
+
+ // IE pastes nested paragraphs in list items, which is different from other browsers. (https://dev.ckeditor.com/ticket/16826)
+ // There's a possibility that list item will contain multiple paragraphs, in that case we want
+ // to split them with BR.
+ tools.array.reduce( element.children, function( paragraphsReplaced, node ) {
+ if ( node.name === 'p' ) {
+ // If there were already paragraphs replaced, put a br before this paragraph, so that
+ // it's inline children are displayed in a next line.
+ if ( paragraphsReplaced > 0 ) {
+ var br = new CKEDITOR.htmlParser.element( 'br' );
+ br.insertBefore( node );
+ }
+
+ node.replaceWithChildren();
+ paragraphsReplaced += 1;
+ }
+
+ return paragraphsReplaced;
+ }, 0 );
+ } else {
+ // In IE list level information is stored in
elements inside
elements.
+ var container = element.getAscendant( function( element ) {
+ return element.name == 'ul' || element.name == 'ol';
+ } ),
+ style = tools.parseCssText( element.attributes.style );
+ if ( container &&
+ !container.attributes[ 'cke-list-level' ] &&
+ style[ 'mso-list' ] &&
+ style[ 'mso-list' ].match( /level/ ) ) {
+ container.attributes[ 'cke-list-level' ] = style[ 'mso-list' ].match( /level(\d+)/ )[ 1 ];
+ }
+
+ // Adapt paragraph formatting to editor's convention according to enter-mode (#423).
+ if ( editor.config.enterMode == CKEDITOR.ENTER_BR ) {
+ // We suffer from attribute/style lost in this situation.
+ delete element.name;
+ element.add( new CKEDITOR.htmlParser.element( 'br' ) );
+ }
+
+ }
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'pre': function( element ) {
+ if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element );
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'h1': function( element ) {
+ if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element );
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'h2': function( element ) {
+ if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element );
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'h3': function( element ) {
+ if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element );
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'h4': function( element ) {
+ if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element );
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'h5': function( element ) {
+ if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element );
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'h6': function( element ) {
+ if ( List.thisIsAListItem( editor, element ) ) List.convertToFakeListItem( editor, element );
+
+ Style.createStyleStack( element, filter, editor );
+ },
+ 'font': function( element ) {
+ if ( element.getHtml().match( /^\s*$/ ) ) {
+ // There might be font tag directly in document fragment, we cannot replace it with a textnode as this generates
+ // superfluous spaces in output. What later might be transformed into empty paragraphs, so just remove such element.
+ if ( element.parent.type === CKEDITOR.NODE_ELEMENT ) {
+ new CKEDITOR.htmlParser.text( ' ' ).insertAfter( element );
+ }
+ return false;
+ }
+
+ if ( editor && editor.config.pasteFromWordRemoveFontStyles === true && element.attributes.size ) {
+ // font[size] are still used by old IEs for font size.
+ delete element.attributes.size;
+ }
+
+ // Create style stack for td/th > font if only class
+ // and style attributes are present. Such markup is produced by Excel.
+ if ( CKEDITOR.dtd.tr[ element.parent.name ] &&
+ CKEDITOR.tools.arrayCompare( CKEDITOR.tools.object.keys( element.attributes ), [ 'class', 'style' ] ) ) {
+
+ Style.createStyleStack( element, filter, editor );
+ } else {
+ createAttributeStack( element, filter );
+ }
+ },
+ 'ul': function( element ) {
+ if ( !msoListsDetected ) {
+ // List should only be processed if we're sure we're working with Word. (https://dev.ckeditor.com/ticket/16593)
+ return;
+ }
+
+ // Edge case from 11683 - an unusual way to create a level 2 list.
+ if ( element.parent.name == 'li' && tools.indexOf( element.parent.children, element ) === 0 ) {
+ Style.setStyle( element.parent, 'list-style-type', 'none' );
+ }
+
+ List.dissolveList( element );
+ return false;
+ },
+ 'li': function( element ) {
+ Heuristics.correctLevelShift( element );
+
+ if ( !msoListsDetected ) {
+ return;
+ }
+
+ element.attributes.style = Style.normalizedStyles( element, editor );
+
+ Style.pushStylesLower( element );
+ },
+ 'ol': function( element ) {
+ if ( !msoListsDetected ) {
+ // List should only be processed if we're sure we're working with Word. (https://dev.ckeditor.com/ticket/16593)
+ return;
+ }
+
+ // Fix edge-case where when a list skips a level in IE11, the element
+ // is implicitly surrounded by a
.
+ if ( element.parent.name == 'li' && tools.indexOf( element.parent.children, element ) === 0 ) {
+ Style.setStyle( element.parent, 'list-style-type', 'none' );
+ }
+
+ List.dissolveList( element );
+ return false;
+ },
+ 'span': function( element ) {
+ element.filterChildren( filter );
+
+ element.attributes.style = Style.normalizedStyles( element, editor );
+
+ if ( !element.attributes.style ||
+ // Remove garbage bookmarks that disrupt the content structure.
+ element.attributes.style.match( /^mso\-bookmark:OLE_LINK\d+$/ ) ||
+ element.getHtml().match( /^(\s| )+$/ ) ) {
+
+ commonFilter.elements.replaceWithChildren( element );
+ return false;
+ }
+
+ if ( element.attributes.style.match( /FONT-FAMILY:\s*Symbol/i ) ) {
+ element.forEach( function( node ) {
+ node.value = node.value.replace( / /g, '' );
+ }, CKEDITOR.NODE_TEXT, true );
+ }
+
+ Style.createStyleStack( element, filter, editor );
+ },
+
+ 'v:imagedata': remove,
+ // This is how IE8 presents images.
+ 'v:shape': function( element ) {
+ // There are 3 paths:
+ // 1. There is regular `v:shape` (no `v:imagedata` inside).
+ // 2. There is a simple situation with `v:shape` with `v:imagedata` inside. We can remove such element and rely on `img` tag found later on.
+ // 3. There is a complicated situation where we cannot find proper `img` tag after `v:shape` or there is some canvas element.
+ // a) If shape is a child of v:group, then most probably it belongs to canvas, so we need to treat it as in path 1.
+ // b) In other cases, most probably there is no related `img` tag. We need to transform `v:shape` into `img` tag (IE8 integration).
+
+ var duplicate = false,
+ child = element.getFirst( 'v:imagedata' );
+
+ // Path 1:
+ if ( child === null ) {
+ shapeTagging( element );
+ return;
+ }
+
+ // Path 2:
+ // Sometimes a child with proper ID might be nested in other tag.
+ element.parent.find( function( child ) {
+ if ( child.name == 'img' && child.attributes &&
+ child.attributes[ 'v:shapes' ] == element.attributes.id ) {
+
+ duplicate = true;
+ }
+ }, true );
+
+ if ( duplicate ) {
+ return false;
+ } else {
+
+ // Path 3:
+ var src = '';
+
+ // 3.a) Filter out situation when canvas is used. In such scenario there is v:group containing v:shape containing v:imagedata.
+ // We streat such v:shapes as in Path 1.
+ if ( element.parent.name === 'v:group' ) {
+ shapeTagging( element );
+ return;
+ }
+
+ // 3.b) Most probably there is no img tag later on, so we need to transform this v:shape into img. This should only happen on IE8.
+ element.forEach( function( child ) {
+ if ( child.attributes && child.attributes.src ) {
+ src = child.attributes.src;
+ }
+ }, CKEDITOR.NODE_ELEMENT, true );
+
+ element.filterChildren( filter );
+
+ element.name = 'img';
+ element.attributes.src = element.attributes.src || src;
+
+ delete element.attributes.type;
+ }
+
+ return;
+ },
+
+ 'style': function() {
+ // We don't want to let any styles in. Firefox tends to add some.
+ return false;
+ },
+
+ 'object': function( element ) {
+ // The specs about object `data` attribute:
+ // Address of the resource as a valid URL. At least one of data and type must be defined.
+ // If there is not `data`, skip the object element. (https://dev.ckeditor.com/ticket/17001)
+ return !!( element.attributes && element.attributes.data );
+ },
+
+ // Integrate page breaks with `pagebreak` plugin (#2598).
+ 'br': function( element ) {
+ if ( !editor.plugins.pagebreak ) {
+ return;
+ }
+
+ var styles = tools.parseCssText( element.attributes.style, true );
+
+ // Safari uses `break-before` instead of `page-break-before` to recognize page breaks.
+ if ( styles[ 'page-break-before' ] === 'always' || styles[ 'break-before' ] === 'page' ) {
+ var pagebreakEl = CKEDITOR.plugins.pagebreak.createElement( editor );
+ return CKEDITOR.htmlParser.fragment.fromHtml( pagebreakEl.getOuterHtml() ).children[ 0 ];
+ }
+ }
+ },
+ attributes: {
+ 'style': function( styles, element ) {
+ // Returning false deletes the attribute.
+ return Style.normalizedStyles( element, editor ) || false;
+ },
+ 'class': function( classes ) {
+ // The (el\d+)|(font\d+) are default Excel classes for table cells and text.
+ return falseIfEmpty( classes.replace( /(el\d+)|(font\d+)|msonormal|msolistparagraph\w*/ig, '' ) );
+ },
+ 'cellspacing': remove,
+ 'cellpadding': remove,
+ 'border': remove,
+ 'v:shapes': remove,
+ 'o:spid': remove
+ },
+ comment: function( element ) {
+ if ( element.match( /\[if.* supportFields.*\]/ ) ) {
+ inComment++;
+ }
+ if ( element == '[endif]' ) {
+ inComment = inComment > 0 ? inComment - 1 : 0;
+ }
+ return false;
+ },
+ text: function( content, node ) {
+ if ( inComment ) {
+ return '';
+ }
+
+ var grandparent = node.parent && node.parent.parent;
+
+ if ( grandparent && grandparent.attributes && grandparent.attributes.style && grandparent.attributes.style.match( /mso-list:\s*ignore/i ) ) {
+ return content.replace( / /g, ' ' );
+ }
+
+ return content;
+ }
+ };
+
+ tools.array.forEach( shapeTags, function( shapeTag ) {
+ rules.elements[ shapeTag ] = shapeTagging;
+ } );
+
+ return rules;
+
+ function shapeTagging( element ) {
+ // Check if regular or canvas shape (#1088).
+ if ( element.attributes[ 'o:gfxdata' ] || element.parent.name === 'v:group' ) {
+ shapesIds.push( element.attributes.id );
+ }
+ }
+ };
+
+ /**
+ * Namespace containing list-oriented helper methods.
+ *
+ * @private
+ * @since 4.13.0
+ * @member CKEDITOR.plugins.pastetools.filters.word
+ */
+ plug.lists = {
+ /**
+ * Checks if a given element is a list item-alike.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.editor} editor
+ * @param {CKEDITOR.htmlParser.element} element
+ * @returns {Boolean}
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ thisIsAListItem: function( editor, element ) {
+ if ( Heuristics.isEdgeListItem( editor, element ) ) {
+ return true;
+ }
+
+ /*jshint -W024 */
+ // Normally a style of the sort that looks like "mso-list: l0 level1 lfo1"
+ // indicates a list element, but the same style may appear in a
that's within a
.
+ if ( ( element.attributes.style && element.attributes.style.match( /mso\-list:\s?l\d/ ) &&
+ element.parent.name !== 'li' ) ||
+ element.attributes[ 'cke-dissolved' ] ||
+ element.getHtml().match( // )
+ ) {
+ return true;
+ }
+
+ return false;
+ /*jshint +W024 */
+ },
+
+ /**
+ * Converts an element to an element with the `cke:li` tag name.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.editor} editor
+ * @param {CKEDITOR.htmlParser.element} element
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ convertToFakeListItem: function( editor, element ) {
+ if ( Heuristics.isDegenerateListItem( editor, element ) ) {
+ Heuristics.assignListLevels( editor, element );
+ }
+
+ // A dummy call to cache parsed list info inside of cke-list-* attributes.
+ this.getListItemInfo( element );
+
+ if ( !element.attributes[ 'cke-dissolved' ] ) {
+ // The symbol is usually the first text node descendant
+ // of the element that doesn't start with a whitespace character;
+ var symbol;
+
+ element.forEach( function( element ) {
+ // Sometimes there are custom markers represented as images.
+ // They can be recognized by the distinctive alt attribute value.
+ if ( !symbol && element.name == 'img' &&
+ element.attributes[ 'cke-ignored' ] &&
+ element.attributes.alt == '*' ) {
+ symbol = '·';
+ // Remove the "symbol" now, since it's the best opportunity to do so.
+ element.remove();
+ }
+ }, CKEDITOR.NODE_ELEMENT );
+
+ element.forEach( function( element ) {
+ if ( !symbol && !element.value.match( /^ / ) ) {
+ symbol = element.value;
+ }
+ }, CKEDITOR.NODE_TEXT );
+
+ // Without a symbol this isn't really a list item.
+ if ( typeof symbol == 'undefined' ) {
+ return;
+ }
+
+ element.attributes[ 'cke-symbol' ] = symbol.replace( /(?: | ).*$/, '' );
+
+ List.removeSymbolText( element );
+ }
+
+ var styles = element.attributes && tools.parseCssText( element.attributes.style );
+
+ // Default list has 40px padding. To correct indentation we need to reduce margin-left by 40px for each list level.
+ // Additionally margin has to be reduced by sum of margins of each parent, however it can't be done until list are structured in a tree (#2870).
+ // Note margin left is absent in IE pasted content.
+ if ( styles[ 'margin-left' ] ) {
+ var margin = styles[ 'margin-left' ],
+ level = element.attributes[ 'cke-list-level' ];
+
+ // Ignore negative margins (#2870).
+ margin = Math.max( CKEDITOR.tools.convertToPx( margin ) - 40 * level, 0 );
+
+ if ( margin ) {
+ styles[ 'margin-left' ] = margin + 'px';
+ } else {
+ delete styles[ 'margin-left' ];
+ }
+
+ element.attributes.style = CKEDITOR.tools.writeCssText( styles );
+ }
+
+ // Converting to a normal list item would implicitly wrap the element around an
.
+ element.name = 'cke:li';
+ },
+
+ /**
+ * Converts any fake list items contained within `root` into real `
` elements.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element} root
+ * @returns {CKEDITOR.htmlParser.element[]} An array of converted elements.
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ convertToRealListItems: function( root ) {
+ var listElements = [];
+ // Select and clean up list elements.
+ root.forEach( function( element ) {
+ if ( element.name == 'cke:li' ) {
+ element.name = 'li';
+
+ listElements.push( element );
+ }
+ }, CKEDITOR.NODE_ELEMENT, false );
+
+ return listElements;
+ },
+
+ removeSymbolText: function( element ) { // ...from a list element.
+ var symbol = element.attributes[ 'cke-symbol' ],
+ // Find the first element which contains symbol to be replaced (#2690).
+ node = element.findOne( function( node ) {
+ // Since symbol may contains special characters we use `indexOf` (instead of RegExp) which is sufficient (#877).
+ return node.value && node.value.indexOf( symbol ) > -1;
+ }, true ),
+ parent;
+
+ if ( node ) {
+ node.value = node.value.replace( symbol, '' );
+ parent = node.parent;
+
+ if ( parent.getHtml().match( /^(\s| )*$/ ) && parent !== element ) {
+ parent.remove();
+ } else if ( !node.value ) {
+ node.remove();
+ }
+ }
+ },
+
+ setListSymbol: function( list, symbol, level ) {
+ level = level || 1;
+
+ var style = tools.parseCssText( list.attributes.style );
+
+ if ( list.name == 'ol' ) {
+ if ( list.attributes.type || style[ 'list-style-type' ] ) return;
+
+ var typeMap = {
+ '[ivx]': 'lower-roman',
+ '[IVX]': 'upper-roman',
+ '[a-z]': 'lower-alpha',
+ '[A-Z]': 'upper-alpha',
+ '\\d': 'decimal'
+ };
+
+ for ( var type in typeMap ) {
+ if ( List.getSubsectionSymbol( symbol ).match( new RegExp( type ) ) ) {
+ style[ 'list-style-type' ] = typeMap[ type ];
+ break;
+ }
+ }
+
+ list.attributes[ 'cke-list-style-type' ] = style[ 'list-style-type' ];
+ } else {
+ var symbolMap = {
+ '·': 'disc',
+ 'o': 'circle',
+ '§': 'square' // In Word this is a square.
+ };
+
+ if ( !style[ 'list-style-type' ] && symbolMap[ symbol ] ) {
+ style[ 'list-style-type' ] = symbolMap[ symbol ];
+ }
+
+ }
+
+ List.setListSymbol.removeRedundancies( style, level );
+
+ ( list.attributes.style = CKEDITOR.tools.writeCssText( style ) ) || delete list.attributes.style;
+ },
+
+ setListStart: function( list ) {
+ var symbols = [],
+ offset = 0;
+
+ for ( var i = 0; i < list.children.length; i++ ) {
+ symbols.push( list.children[ i ].attributes[ 'cke-symbol' ] || '' );
+ }
+
+ // When a list starts with a sublist, use the next element as a start indicator.
+ if ( !symbols[ 0 ] ) {
+ offset++;
+ }
+
+ // Attribute set in setListSymbol()
+ switch ( list.attributes[ 'cke-list-style-type' ] ) {
+ case 'lower-roman':
+ case 'upper-roman':
+ list.attributes.start = List.toArabic( List.getSubsectionSymbol( symbols[ offset ] ) ) - offset;
+ break;
+ case 'lower-alpha':
+ case 'upper-alpha':
+ list.attributes.start = List.getSubsectionSymbol( symbols[ offset ] ).replace( /\W/g, '' ).toLowerCase().charCodeAt( 0 ) - 96 - offset;
+ break;
+ case 'decimal':
+ list.attributes.start = ( parseInt( List.getSubsectionSymbol( symbols[ offset ] ), 10 ) - offset ) || 1;
+ break;
+ }
+
+ if ( list.attributes.start == '1' ) {
+ delete list.attributes.start;
+ }
+
+ delete list.attributes[ 'cke-list-style-type' ];
+ },
+
+ /**
+ * Numbering helper.
+ *
+ * @since 4.13.0
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ numbering: {
+ /**
+ * Converts the list marker value into a decimal number.
+ *
+ * var toNumber = CKEDITOR.plugins.pastefromword.lists.numbering.toNumber;
+ *
+ * console.log( toNumber( 'XIV', 'upper-roman' ) ); // Logs 14.
+ * console.log( toNumber( 'd', 'lower-alpha' ) ); // Logs 4.
+ * console.log( toNumber( '35', 'decimal' ) ); // Logs 35.
+ * console.log( toNumber( '404', 'foo' ) ); // Logs 1.
+ *
+ * @param {String} marker
+ * @param {String} markerType Marker type according to CSS `list-style-type` values.
+ * @returns {Number}
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists.numbering
+ */
+ toNumber: function( marker, markerType ) {
+ // Functions copied straight from old PFW implementation, no need to reinvent the wheel.
+ function fromAlphabet( str ) {
+ var alpahbets = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+ str = str.toUpperCase();
+ var l = alpahbets.length,
+ retVal = 1;
+ for ( var x = 1; str.length > 0; x *= l ) {
+ retVal += alpahbets.indexOf( str.charAt( str.length - 1 ) ) * x;
+ str = str.substr( 0, str.length - 1 );
+ }
+ return retVal;
+ }
+
+ function fromRoman( str ) {
+ var romans = [
+ [ 1000, 'M' ],
+ [ 900, 'CM' ],
+ [ 500, 'D' ],
+ [ 400, 'CD' ],
+ [ 100, 'C' ],
+ [ 90, 'XC' ],
+ [ 50, 'L' ],
+ [ 40, 'XL' ],
+ [ 10, 'X' ],
+ [ 9, 'IX' ],
+ [ 5, 'V' ],
+ [ 4, 'IV' ],
+ [ 1, 'I' ]
+ ];
+
+ str = str.toUpperCase();
+ var l = romans.length,
+ retVal = 0;
+ for ( var i = 0; i < l; ++i ) {
+ for ( var j = romans[ i ], k = j[ 1 ].length; str.substr( 0, k ) == j[ 1 ]; str = str.substr( k ) )
+ retVal += j[ 0 ];
+ }
+ return retVal;
+ }
+
+ if ( markerType == 'decimal' ) {
+ return Number( marker );
+ } else if ( markerType == 'upper-roman' || markerType == 'lower-roman' ) {
+ return fromRoman( marker.toUpperCase() );
+ } else if ( markerType == 'lower-alpha' || markerType == 'upper-alpha' ) {
+ return fromAlphabet( marker );
+ } else {
+ return 1;
+ }
+ },
+
+ /**
+ * Returns a list style based on the Word marker content.
+ *
+ * var getStyle = CKEDITOR.plugins.pastefromword.lists.numbering.getStyle;
+ *
+ * console.log( getStyle( '4' ) ); // Logs: "decimal"
+ * console.log( getStyle( 'b' ) ); // Logs: "lower-alpha"
+ * console.log( getStyle( 'P' ) ); // Logs: "upper-alpha"
+ * console.log( getStyle( 'i' ) ); // Logs: "lower-roman"
+ * console.log( getStyle( 'X' ) ); // Logs: "upper-roman"
+ *
+ *
+ * **Implementation note:** Characters `c` and `d` are not converted to roman on purpose. It is 100 and 500 respectively, so
+ * you rarely go with a list up until this point, while it is common to start with `c` and `d` in alpha.
+ *
+ * @param {String} marker Marker content retained from Word, e.g. `1`, `7`, `XI`, `b`.
+ * @returns {String} Resolved marker type.
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists.numbering
+ */
+ getStyle: function( marker ) {
+ var typeMap = {
+ 'i': 'lower-roman',
+ 'v': 'lower-roman',
+ 'x': 'lower-roman',
+ 'l': 'lower-roman',
+ 'm': 'lower-roman',
+ 'I': 'upper-roman',
+ 'V': 'upper-roman',
+ 'X': 'upper-roman',
+ 'L': 'upper-roman',
+ 'M': 'upper-roman'
+ },
+ firstCharacter = marker.slice( 0, 1 ),
+ type = typeMap[ firstCharacter ];
+
+ if ( !type ) {
+ type = 'decimal';
+
+ if ( firstCharacter.match( /[a-z]/ ) ) {
+ type = 'lower-alpha';
+ }
+ if ( firstCharacter.match( /[A-Z]/ ) ) {
+ type = 'upper-alpha';
+ }
+ }
+
+ return type;
+ }
+ },
+
+ // Taking into account cases like "1.1.2." etc. - get the last element.
+ getSubsectionSymbol: function( symbol ) {
+ return ( symbol.match( /([\da-zA-Z]+).?$/ ) || [ 'placeholder', '1' ] )[ 1 ];
+ },
+
+ setListDir: function( list ) {
+ var dirs = { ltr: 0, rtl: 0 };
+
+ list.forEach( function( child ) {
+ if ( child.name == 'li' ) {
+ var dir = child.attributes.dir || child.attributes.DIR || '';
+ if ( dir.toLowerCase() == 'rtl' ) {
+ dirs.rtl++;
+ } else {
+ dirs.ltr++;
+ }
+ }
+ }, CKEDITOR.ELEMENT_NODE );
+
+ if ( dirs.rtl > dirs.ltr ) {
+ list.attributes.dir = 'rtl';
+ }
+ },
+
+ createList: function( element ) {
+ // "o" symbolizes a circle in unordered lists.
+ if ( ( element.attributes[ 'cke-symbol' ].match( /([\da-np-zA-NP-Z]).?/ ) || [] )[ 1 ] ) {
+ return new CKEDITOR.htmlParser.element( 'ol' );
+ }
+ return new CKEDITOR.htmlParser.element( 'ul' );
+ },
+
+ /**
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element} root An element to be looked through for lists.
+ * @param {CKEDITOR.editor} editor The editor instance.
+ * @returns {CKEDITOR.htmlParser.element[]} An array of created list items.
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ createLists: function( root, editor ) {
+ var element, level, i, j,
+ listElements = List.convertToRealListItems( root );
+
+ if ( listElements.length === 0 ) {
+ return [];
+ }
+
+ // Chop data into continuous lists.
+ var lists = List.groupLists( listElements );
+
+ // Create nested list structures.
+ for ( i = 0; i < lists.length; i++ ) {
+ var list = lists[ i ],
+ firstLevel1Element = list[ 0 ];
+
+ // To determine the type of the top-level list a level 1 element is needed.
+ for ( j = 0; j < list.length; j++ ) {
+ if ( list[ j ].attributes[ 'cke-list-level' ] == 1 ) {
+ firstLevel1Element = list[ j ];
+ break;
+ }
+ }
+
+ var containerStack = [ List.createList( firstLevel1Element ) ],
+ // List wrapper (ol/ul).
+ innermostContainer = containerStack[ 0 ],
+ allContainers = [ containerStack[ 0 ] ],
+ marginTop = getMargin( list[ 0 ], 'top' ),
+ marginBottom = getMargin( list[ list.length - 1 ], 'bottom' );
+
+ // Insert first known list item before the list wrapper.
+ innermostContainer.insertBefore( list[ 0 ] );
+ innermostContainer.attributes.style = marginTop + marginBottom;
+
+ for ( j = 0; j < list.length; j++ ) {
+ element = list[ j ];
+
+ level = element.attributes[ 'cke-list-level' ];
+
+ while ( level > containerStack.length ) {
+ var content = List.createList( element );
+
+ var children = innermostContainer.children;
+ if ( children.length > 0 ) {
+ children[ children.length - 1 ].add( content );
+ } else {
+ var container = new CKEDITOR.htmlParser.element( 'li', {
+ style: 'list-style-type:none'
+ } );
+ container.add( content );
+ innermostContainer.add( container );
+ }
+
+ containerStack.push( content );
+ allContainers.push( content );
+ innermostContainer = content;
+
+ if ( level == containerStack.length ) {
+ List.setListSymbol( content, element.attributes[ 'cke-symbol' ], level );
+ }
+ }
+
+ while ( level < containerStack.length ) {
+ containerStack.pop();
+ innermostContainer = containerStack[ containerStack.length - 1 ];
+
+ if ( level == containerStack.length ) {
+ List.setListSymbol( innermostContainer, element.attributes[ 'cke-symbol' ], level );
+ }
+ }
+
+ // For future reference this is where the list elements are actually put into the lists.
+ element.remove();
+ innermostContainer.add( element );
+ }
+
+ // Try to set the symbol for the root (level 1) list.
+ var level1Symbol;
+ if ( containerStack[ 0 ].children.length ) {
+ level1Symbol = containerStack[ 0 ].children[ 0 ].attributes[ 'cke-symbol' ];
+
+ if ( !level1Symbol && containerStack[ 0 ].children.length > 1 ) {
+ level1Symbol = containerStack[ 0 ].children[ 1 ].attributes[ 'cke-symbol' ];
+ }
+
+ if ( level1Symbol ) {
+ List.setListSymbol( containerStack[ 0 ], level1Symbol );
+ }
+ }
+
+ // This can be done only after all the list elements are where they should be.
+ for ( j = 0; j < allContainers.length; j++ ) {
+ List.setListStart( allContainers[ j ] );
+ }
+
+ // Last but not least apply li[start] if needed, also this needs to be done once ols are final.
+ for ( j = 0; j < list.length; j++ ) {
+ this.determineListItemValue( list[ j ] );
+ }
+ }
+
+ // Adjust left margin based on parents sum of parents left margin (#2870).
+ CKEDITOR.tools.array.forEach( listElements, function( element ) {
+ var listParents = getParentListItems( element ),
+ leftOffset = getTotalMarginLeft( listParents ),
+ styles, marginLeft;
+
+ if ( !leftOffset ) {
+ return;
+ }
+
+ element.attributes = element.attributes || {};
+
+ styles = CKEDITOR.tools.parseCssText( element.attributes.style );
+
+ marginLeft = styles[ 'margin-left' ] || 0;
+ marginLeft = Math.max( parseInt( marginLeft, 10 ) - leftOffset, 0 );
+
+ if ( marginLeft ) {
+ styles[ 'margin-left' ] = marginLeft + 'px';
+ } else {
+ delete styles[ 'margin-left' ];
+ }
+
+ element.attributes.style = CKEDITOR.tools.writeCssText( styles );
+ } );
+
+ return listElements;
+
+ function getParentListItems( element ) {
+ var parents = [],
+ parent = element.parent;
+
+ while ( parent ) {
+ if ( parent.name === 'li' ) {
+ parents.push( parent );
+ }
+ parent = parent.parent;
+ }
+
+ return parents;
+ }
+
+ function getTotalMarginLeft( elements ) {
+ return CKEDITOR.tools.array.reduce( elements, function( total, element ) {
+ if ( element.attributes && element.attributes.style ) {
+ var marginLeft = CKEDITOR.tools.parseCssText( element.attributes.style )[ 'margin-left' ];
+ }
+ return marginLeft ? total + parseInt( marginLeft, 10 ) : total;
+ }, 0 );
+ }
+
+ function getMargin( element, margin ) {
+ var parsedStyles = CKEDITOR.tools.parseCssText( element.attributes.style ),
+ keepZeroMargins = CKEDITOR.plugins.pastetools.getConfigValue( editor, 'keepZeroMargins' ),
+ searchedMargin = 'margin-' + margin;
+
+ if ( !( searchedMargin in parsedStyles ) ) {
+ return '';
+ }
+
+ var value = CKEDITOR.tools.convertToPx( parsedStyles[ searchedMargin ] );
+
+ // Preserve keeping zero list margins when pasteTools_keepZeroMargins is ON (#5316).
+ if ( value === 0 && keepZeroMargins ) {
+ return searchedMargin + ': ' + value + '; ';
+ }
+
+ // Preserve keeping margins by default.
+ if ( value > 0 ) {
+ return searchedMargin + ': ' + value + 'px; ';
+ }
+
+ return '';
+ }
+ },
+
+ /**
+ * Final cleanup — removes all `cke-*` helper attributes.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element[]} listElements
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ cleanup: function( listElements ) {
+ var tempAttributes = [
+ 'cke-list-level',
+ 'cke-symbol',
+ 'cke-list-id',
+ 'cke-indentation',
+ 'cke-dissolved'
+ ],
+ i,
+ j;
+
+ for ( i = 0; i < listElements.length; i++ ) {
+ for ( j = 0; j < tempAttributes.length; j++ ) {
+ delete listElements[ i ].attributes[ tempAttributes[ j ] ];
+ }
+ }
+ },
+
+ /**
+ * Tries to determine the `li[value]` attribute for a given list item. The `element` given must
+ * have a parent in order for this function to work properly.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element} element
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ determineListItemValue: function( element ) {
+ if ( element.parent.name !== 'ol' ) {
+ // li[value] make sense only for list items in ordered list.
+ return;
+ }
+
+ var assumedValue = this.calculateValue( element ),
+ cleanSymbol = element.attributes[ 'cke-symbol' ].match( /[a-z0-9]+/gi ),
+ computedValue,
+ listType;
+
+ if ( cleanSymbol ) {
+ // Note that we always want to use last match, just because of markers like "1.1.4" "1.A.a.IV" etc.
+ cleanSymbol = cleanSymbol[ cleanSymbol.length - 1 ];
+
+ // We can determine proper value only if we know what type of list is it.
+ // So we need to check list wrapper if it has this information.
+ listType = element.parent.attributes[ 'cke-list-style-type' ] || this.numbering.getStyle( cleanSymbol );
+
+ computedValue = this.numbering.toNumber( cleanSymbol, listType );
+
+ if ( computedValue !== assumedValue ) {
+ element.attributes.value = computedValue;
+ }
+ }
+ },
+
+ /**
+ * Calculates the value for a given `
` element based on preceding list items (e.g. the `value`
+ * attribute). It could also look at the start attribute of its parent list (``).
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element} element The `
` element.
+ * @returns {Number}
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ calculateValue: function( element ) {
+ if ( !element.parent ) {
+ return 1;
+ }
+
+ var list = element.parent,
+ elementIndex = element.getIndex(),
+ valueFound = null,
+ // Index of the element with value attribute.
+ valueElementIndex,
+ curElement,
+ i;
+
+ // Look for any preceding li[value].
+ for ( i = elementIndex; i >= 0 && valueFound === null; i-- ) {
+ curElement = list.children[ i ];
+
+ if ( curElement.attributes && curElement.attributes.value !== undefined ) {
+ valueElementIndex = i;
+ valueFound = parseInt( curElement.attributes.value, 10 );
+ }
+ }
+
+ // Still if no li[value] was found, we'll check the list.
+ if ( valueFound === null ) {
+ valueFound = list.attributes.start !== undefined ? parseInt( list.attributes.start, 10 ) : 1;
+ valueElementIndex = 0;
+ }
+
+ return valueFound + ( elementIndex - valueElementIndex );
+ },
+
+ /**
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element} element
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ dissolveList: function( element ) {
+ var nameIs = function( name ) {
+ return function( element ) {
+ return element.name == name;
+ };
+ },
+ isList = function( element ) {
+ return nameIs( 'ul' )( element ) || nameIs( 'ol' )( element );
+ },
+ arrayTools = CKEDITOR.tools.array,
+ elements = [],
+ children,
+ i;
+
+ element.forEach( function( child ) {
+ elements.push( child );
+ }, CKEDITOR.NODE_ELEMENT, false );
+
+ var items = arrayTools.filter( elements, nameIs( 'li' ) ),
+ lists = arrayTools.filter( elements, isList );
+
+ arrayTools.forEach( lists, function( list ) {
+ var type = list.attributes.type,
+ start = parseInt( list.attributes.start, 10 ) || 1,
+ level = countParents( isList, list ) + 1;
+
+ if ( !type ) {
+ var style = tools.parseCssText( list.attributes.style );
+ type = style[ 'list-style-type' ];
+ }
+
+ arrayTools.forEach( arrayTools.filter( list.children, nameIs( 'li' ) ), function( child, index ) {
+ var symbol;
+
+ switch ( type ) {
+ case 'disc':
+ symbol = '·';
+ break;
+ case 'circle':
+ symbol = 'o';
+ break;
+ case 'square':
+ symbol = '§';
+ break;
+ case '1':
+ case 'decimal':
+ symbol = ( start + index ) + '.';
+ break;
+ case 'a':
+ case 'lower-alpha':
+ symbol = String.fromCharCode( 'a'.charCodeAt( 0 ) + start - 1 + index ) + '.';
+ break;
+ case 'A':
+ case 'upper-alpha':
+ symbol = String.fromCharCode( 'A'.charCodeAt( 0 ) + start - 1 + index ) + '.';
+ break;
+ case 'i':
+ case 'lower-roman':
+ symbol = toRoman( start + index ) + '.';
+ break;
+ case 'I':
+ case 'upper-roman':
+ symbol = toRoman( start + index ).toUpperCase() + '.';
+ break;
+ default:
+ symbol = list.name == 'ul' ? '·' : ( start + index ) + '.';
+ }
+
+ child.attributes[ 'cke-symbol' ] = symbol;
+ child.attributes[ 'cke-list-level' ] = level;
+ } );
+ } );
+
+ children = arrayTools.reduce( items, function( acc, listElement ) {
+ var child = listElement.children[ 0 ];
+
+ if ( child && child.name && child.attributes.style && child.attributes.style.match( /mso-list:/i ) ) {
+ Style.pushStylesLower( listElement, {
+ 'list-style-type': true,
+ 'display': true
+ } );
+
+ var childStyle = tools.parseCssText( child.attributes.style, true );
+
+ Style.setStyle( listElement, 'mso-list', childStyle[ 'mso-list' ], true );
+ Style.setStyle( child, 'mso-list', '' );
+ // mso-list takes precedence in determining the level.
+ delete listElement[ 'cke-list-level' ];
+
+ // If this style has a value it's usually "none". This marks such list elements for deletion.
+ var styleName = childStyle.display ? 'display' : childStyle.DISPLAY ? 'DISPLAY' : '';
+ if ( styleName ) {
+ Style.setStyle( listElement, 'display', childStyle[ styleName ], true );
+ }
+ }
+
+ // Don't include elements put there only to contain another list.
+ if ( listElement.children.length === 1 && isList( listElement.children[ 0 ] ) ) {
+ return acc;
+ }
+
+ listElement.name = 'p';
+ listElement.attributes[ 'cke-dissolved' ] = true;
+ acc.push( listElement );
+
+ return acc;
+ }, [] );
+
+ for ( i = children.length - 1; i >= 0; i-- ) {
+ children[ i ].insertAfter( element );
+ }
+ for ( i = lists.length - 1; i >= 0; i-- ) {
+ delete lists[ i ].name;
+ }
+
+ function toRoman( number ) {
+ if ( number >= 50 ) return 'l' + toRoman( number - 50 );
+ if ( number >= 40 ) return 'xl' + toRoman( number - 40 );
+ if ( number >= 10 ) return 'x' + toRoman( number - 10 );
+ if ( number == 9 ) return 'ix';
+ if ( number >= 5 ) return 'v' + toRoman( number - 5 );
+ if ( number == 4 ) return 'iv';
+ if ( number >= 1 ) return 'i' + toRoman( number - 1 );
+ return '';
+ }
+
+ function countParents( condition, element ) {
+ return count( element, 0 );
+
+ function count( parent, number ) {
+ if ( !parent || !parent.parent ) {
+ return number;
+ }
+
+ if ( condition( parent.parent ) ) {
+ return count( parent.parent, number + 1 );
+ } else {
+ return count( parent.parent, number );
+ }
+ }
+ }
+ },
+
+ groupLists: function( listElements ) {
+ // Chop data into continuous lists.
+ var i, element,
+ lists = [ [ listElements[ 0 ] ] ],
+ lastList = lists[ 0 ];
+
+ element = listElements[ 0 ];
+ element.attributes[ 'cke-indentation' ] = element.attributes[ 'cke-indentation' ] || getElementIndentation( element );
+
+ for ( i = 1; i < listElements.length; i++ ) {
+ element = listElements[ i ];
+ var previous = listElements[ i - 1 ];
+
+ element.attributes[ 'cke-indentation' ] = element.attributes[ 'cke-indentation' ] || getElementIndentation( element );
+
+ if ( element.previous !== previous ) {
+ List.chopDiscontinuousLists( lastList, lists );
+ lists.push( lastList = [] );
+ }
+
+ lastList.push( element );
+ }
+
+ List.chopDiscontinuousLists( lastList, lists );
+
+ return lists;
+ },
+
+ /**
+ * Converts a single, flat list items array into an array with a hierarchy of items.
+ *
+ * As the list gets chopped, it will be forced to render as a separate list, even if it has a deeper nesting level.
+ * For example, for level 3 it will create a structure like `ol > li > ol > li > ol > li`.
+ *
+ * Note that list items within a single list but with different levels that did not get chopped
+ * will still be rendered as a list tree later.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element[]} list An array containing list items.
+ * @param {CKEDITOR.htmlParser.element[]} lists All the lists in the pasted content represented by an array of arrays
+ * of list items. Modified by this method.
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ chopDiscontinuousLists: function( list, lists ) {
+ var levelSymbols = {};
+ var choppedLists = [ [] ],
+ lastListInfo;
+
+ for ( var i = 0; i < list.length; i++ ) {
+ var lastSymbol = levelSymbols[ list[ i ].attributes[ 'cke-list-level' ] ],
+ currentListInfo = this.getListItemInfo( list[ i ] ),
+ currentSymbol,
+ forceType;
+
+ if ( lastSymbol ) {
+ // An "h" before an "i".
+ forceType = lastSymbol.type.match( /alpha/ ) && lastSymbol.index == 7 ? 'alpha' : forceType;
+ // An "n" before an "o".
+ forceType = list[ i ].attributes[ 'cke-symbol' ] == 'o' && lastSymbol.index == 14 ? 'alpha' : forceType;
+
+ currentSymbol = List.getSymbolInfo( list[ i ].attributes[ 'cke-symbol' ], forceType );
+ currentListInfo = this.getListItemInfo( list[ i ] );
+
+ // Based on current and last index we'll decide if we want to chop list.
+ if (
+ // If the last list was a different list type then chop it!
+ lastSymbol.type != currentSymbol.type ||
+ // If those are logically different lists, and current list is not a continuation (https://dev.ckeditor.com/ticket/7918):
+ ( lastListInfo && currentListInfo.id != lastListInfo.id && !this.isAListContinuation( list[ i ] ) ) ) {
+ choppedLists.push( [] );
+ }
+ } else {
+ currentSymbol = List.getSymbolInfo( list[ i ].attributes[ 'cke-symbol' ] );
+ }
+
+ // Reset all higher levels
+ for ( var j = parseInt( list[ i ].attributes[ 'cke-list-level' ], 10 ) + 1; j < 20; j++ ) {
+ if ( levelSymbols[ j ] ) {
+ delete levelSymbols[ j ];
+ }
+ }
+
+ levelSymbols[ list[ i ].attributes[ 'cke-list-level' ] ] = currentSymbol;
+ choppedLists[ choppedLists.length - 1 ].push( list[ i ] );
+
+ lastListInfo = currentListInfo;
+ }
+
+ [].splice.apply( lists, [].concat( [ tools.indexOf( lists, list ), 1 ], choppedLists ) );
+ },
+
+ /**
+ * Checks if this list is a direct continuation of a list interrupted by a list with a different ID and
+ * with a different level. So if you look at the following list:
+ *
+ * * list1 level1
+ * * list1 level1
+ * * list2 level2
+ * * list2 level2
+ * * list1 level1
+ *
+ * It would return `true`, which means it is a continuation, and should not be chopped. However, if any paragraph or
+ * anything else appears in-between, it should be broken into different lists.
+ *
+ * You can see fixtures from issue https://dev.ckeditor.com/ticket/7918 as an example.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element} listElement The list to be checked.
+ * @returns {Boolean}
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ isAListContinuation: function( listElement ) {
+ var prev = listElement;
+
+ do {
+ prev = prev.previous;
+
+ if ( prev && prev.type === CKEDITOR.NODE_ELEMENT ) {
+ if ( prev.attributes[ 'cke-list-level' ] === undefined ) {
+ // Not a list, so looks like an interrupted list.
+ return false;
+ }
+
+ if ( prev.attributes[ 'cke-list-level' ] === listElement.attributes[ 'cke-list-level' ] ) {
+ // Same level, so we want to check if this is a continuation.
+ return prev.attributes[ 'cke-list-id' ] === listElement.attributes[ 'cke-list-id' ];
+ }
+ }
+
+ } while ( prev );
+
+ return false;
+ },
+
+ // Source: http://stackoverflow.com/a/17534350/3698944
+ toArabic: function( symbol ) {
+ if ( !symbol.match( /[ivxl]/i ) ) return 0;
+ if ( symbol.match( /^l/i ) ) return 50 + List.toArabic( symbol.slice( 1 ) );
+ if ( symbol.match( /^lx/i ) ) return 40 + List.toArabic( symbol.slice( 1 ) );
+ if ( symbol.match( /^x/i ) ) return 10 + List.toArabic( symbol.slice( 1 ) );
+ if ( symbol.match( /^ix/i ) ) return 9 + List.toArabic( symbol.slice( 2 ) );
+ if ( symbol.match( /^v/i ) ) return 5 + List.toArabic( symbol.slice( 1 ) );
+ if ( symbol.match( /^iv/i ) ) return 4 + List.toArabic( symbol.slice( 2 ) );
+ if ( symbol.match( /^i/i ) ) return 1 + List.toArabic( symbol.slice( 1 ) );
+ // Ignore other characters.
+ return List.toArabic( symbol.slice( 1 ) );
+ },
+
+ /**
+ * Returns an object describing the given `symbol`.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {String} symbol
+ * @param {String} type
+ * @returns {Object} ret
+ * @returns {Number} ret.index Identified numbering value
+ * @returns {String} ret.type One of: `decimal`, `disc`, `circle`, `square`, `roman`, `alpha`.
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ getSymbolInfo: function( symbol, type ) {
+ var symbolCase = symbol.toUpperCase() == symbol ? 'upper-' : 'lower-',
+ symbolMap = {
+ '·': [ 'disc', -1 ],
+ 'o': [ 'circle', -2 ],
+ '§': [ 'square', -3 ]
+ };
+
+ if ( symbol in symbolMap || ( type && type.match( /(disc|circle|square)/ ) ) ) {
+ return {
+ index: symbolMap[ symbol ][ 1 ],
+ type: symbolMap[ symbol ][ 0 ]
+ };
+ }
+
+ if ( symbol.match( /\d/ ) ) {
+ return {
+ index: symbol ? parseInt( List.getSubsectionSymbol( symbol ) , 10 ) : 0,
+ type: 'decimal'
+ };
+ }
+
+ symbol = symbol.replace( /\W/g, '' ).toLowerCase();
+
+ if ( ( !type && symbol.match( /[ivxl]+/i ) ) || ( type && type != 'alpha' ) || type == 'roman' ) {
+ return {
+ index: List.toArabic( symbol ),
+ type: symbolCase + 'roman'
+ };
+ }
+
+ if ( symbol.match( /[a-z]/i ) ) {
+ return {
+ index: symbol.charCodeAt( 0 ) - 97,
+ type: symbolCase + 'alpha'
+ };
+ }
+
+ return {
+ index: -1,
+ type: 'disc'
+ };
+ },
+
+ /**
+ * Returns Word-generated information about the given list item, mainly by parsing the `mso-list`
+ * CSS property.
+ *
+ * Note: Paragraphs with `mso-list` are also counted as list items because Word serves
+ * list items as paragraphs.
+ *
+ * @private
+ * @since 4.13.0
+ * @param {CKEDITOR.htmlParser.element} list
+ * @returns ret
+ * @returns {String} ret.id List ID. Usually it is a decimal string.
+ * @returns {String} ret.level List nesting level. `0` means it is the outermost list. Usually it is
+ * a decimal string.
+ * @member CKEDITOR.plugins.pastetools.filters.word.lists
+ */
+ getListItemInfo: function( list ) {
+ if ( list.attributes[ 'cke-list-id' ] !== undefined ) {
+ // List was already resolved.
+ return {
+ id: list.attributes[ 'cke-list-id' ],
+ level: list.attributes[ 'cke-list-level' ]
+ };
+ }
+
+ var propValue = tools.parseCssText( list.attributes.style )[ 'mso-list' ],
+ ret = {
+ id: '0',
+ level: '1'
+ };
+
+ if ( propValue ) {
+ // Add one whitespace so it's easier to match values assuming that all of these are separated with \s.
+ propValue += ' ';
+
+ ret.level = propValue.match( /level(.+?)\s+/ )[ 1 ];
+ ret.id = propValue.match( /l(\d+?)\s+/ )[ 1 ];
+ }
+
+ // Store values. List level will be reused if present to prevent regressions.
+ list.attributes[ 'cke-list-level' ] = list.attributes[ 'cke-list-level' ] !== undefined ? list.attributes[ 'cke-list-level' ] : ret.level;
+ list.attributes[ 'cke-list-id' ] = ret.id;
+
+ return ret;
+ }
+ };
+ List = plug.lists;
+
+ /**
+ * Namespace containing methods used to process the pasted content using heuristics.
+ *
+ * @private
+ * @since 4.13.0
+ * @member CKEDITOR.plugins.pastetools.filters.word
+ */
+ plug.heuristics = {
+ /**
+ * Decides if an `item` looks like a list item in Microsoft Edge.
+ *
+ * Note: It will return `false` when run in a browser other than Microsoft Edge, despite the configuration.
+ *
+ * @param {CKEDITOR.editor} editor
+ * @param {CKEDITOR.htmlParser.element} item
+ * @returns {Boolean}
+ * @member CKEDITOR.plugins.pastetools.filters.word.heuristics
+ * @private
+ */
+ isEdgeListItem: function( editor, item ) {
+ if ( !CKEDITOR.env.edge || !editor.config.pasteFromWord_heuristicsEdgeList ) {
+ return false;
+ }
+
+ var innerText = '';
+
+ // Edge doesn't provide any list-specific markup, so the only way to guess if it's a list is to check the text structure.
+ item.forEach && item.forEach( function( text ) {
+ innerText += text.value;
+ }, CKEDITOR.NODE_TEXT );
+
+ if ( innerText.match( /^(?: | )*\(?[a-zA-Z0-9]+?[\.\)](?: | ){2,}/ ) ) {
+ return true;
+ }
+
+ return Heuristics.isDegenerateListItem( editor, item );
+ },
+
+ /**
+ * Cleans up a given list `item`. It is needed to remove Edge pre-marker indentation, since Edge pastes
+ * list items as plain paragraphs with multiple ` `s before the list marker.
+ *
+ * @since 4.7.0
+ * @param {CKEDITOR.htmlParser.element} item The pre-processed list-like item, like a paragraph.
+ * @member CKEDITOR.plugins.pastetools.filters.word.heuristics
+ * @private
+ */
+ cleanupEdgeListItem: function( item ) {
+ var textOccurred = false;
+
+ item.forEach( function( node ) {
+ if ( !textOccurred ) {
+ node.value = node.value.replace( /^(?: |[\s])+/, '' );
+
+ // If there's any remaining text beside nbsp it means that we can stop filtering.
+ if ( node.value.length ) {
+ textOccurred = true;
+ }
+ }
+ }, CKEDITOR.NODE_TEXT );
+ },
+
+ /**
+ * Checks whether an element is a degenerate list item.
+ *
+ * Degenerate list items are elements that have some styles specific to list items,
+ * but lack the ones that could be used to determine their features (like list level etc.).
+ *
+ * @param {CKEDITOR.editor} editor
+ * @param {CKEDITOR.htmlParser.element} item
+ * @returns {Boolean}
+ * @member CKEDITOR.plugins.pastetools.filters.word.heuristics
+ * @private
+ * */
+ isDegenerateListItem: function( editor, item ) {
+ return !!item.attributes[ 'cke-list-level' ] || ( item.attributes.style && !item.attributes.style.match( /mso\-list/ ) && !!item.find( function( child ) {
+ // In rare cases there's no indication that a heading is a list item other than
+ // the fact that it has a child element containing only a list symbol.
+ if ( child.type == CKEDITOR.NODE_ELEMENT && item.name.match( /h\d/i ) &&
+ child.getHtml().match( /^[a-zA-Z0-9]+?[\.\)]$/ ) ) {
+ return true;
+ }
+
+ var css = tools.parseCssText( child.attributes && child.attributes.style, true );
+
+ if ( !css ) {
+ return false;
+ }
+ var fontSize = css.font || css[ 'font-size' ] || '',
+ fontFamily = css[ 'font-family' ] || '';
+
+ return ( fontSize.match( /7pt/i ) && !!child.previous ) ||
+ fontFamily.match( /symbol/i );
+ }, true ).length );
+ },
+
+ /**
+ * Assigns list levels to the `item` and all directly subsequent nodes for which {@link #isEdgeListItem} returns `true`.
+ *
+ * The algorithm determines list item level based on the lowest common non-zero difference in indentation
+ * of two or more subsequent list-like elements.
+ *
+ * @param {CKEDITOR.editor} editor
+ * @param {CKEDITOR.htmlParser.element} item The first item of the list.
+ * @returns {Object/null} `null` if list levels were already applied, or an object used to verify results in tests.
+ * @returns {Number[]} return.indents
+ * @returns {Number[]} return.levels
+ * @returns {Number[]} return.diffs
+ * @member CKEDITOR.plugins.pastetools.filters.word.heuristics
+ * @private
+ */
+ assignListLevels: function( editor, item ) {
+ // If levels were already calculated, it means that this function was called for preceeding element. There's
+ // no need to do this heavy work.
+ if ( item.attributes && item.attributes[ 'cke-list-level' ] !== undefined ) {
+ return;
+ }
+
+ var indents = [ getElementIndentation( item ) ],
+ items = [ item ],
+ levels = [],
+ array = CKEDITOR.tools.array,
+ map = array.map;
+
+ while ( item.next && item.next.attributes && !item.next.attributes[ 'cke-list-level' ] && Heuristics.isDegenerateListItem( editor, item.next ) ) {
+ item = item.next;
+ indents.push( getElementIndentation( item ) );
+ items.push( item );
+ }
+
+ // An array with indentation difference between n and n-1 list item. It's 0 for the first one.
+ var indentationDiffs = map( indents, function( curIndent, i ) {
+ return i === 0 ? 0 : curIndent - indents[ i - 1 ];
+ } ),
+ // Guess indentation step, but it must not be equal to 0.
+ indentationPerLevel = this.guessIndentationStep( array.filter( indents, function( val ) {
+ return val !== 0;
+ } ) );
+
+ // Here's the tricky part, we need to magically figure out what is the indentation difference between list level.
+ levels = map( indents, function( val ) {
+ // Make sure that the level is a full number.
+ return Math.round( val / indentationPerLevel );
+ } );
+
+ // Level can not be equal to 0, in case if it happens bump all the levels by 1,
+ if ( array.indexOf( levels, 0 ) !== -1 ) {
+ levels = map( levels, function( val ) {
+ return val + 1;
+ } );
+ }
+
+ // Assign levels to a proper place.
+ array.forEach( items, function( curItem, index ) {
+ curItem.attributes[ 'cke-list-level' ] = levels[ index ];
+ } );
+
+ return {
+ indents: indents,
+ levels: levels,
+ diffs: indentationDiffs
+ };
+ },
+
+ /**
+ * Given an array of list indentations, this method tries to guess what the indentation difference per list level is.
+ * E.g. assuming that you have something like:
+ *
+ * * foo (indentation 30px)
+ * * bar (indentation 90px)
+ * * baz (indentation 90px)
+ * * baz (indentation 115px)
+ * * baz (indentation 60px)
+ *
+ * The method will return `30`.
+ *
+ * @param {Number[]} indentations An array of indentation sizes.
+ * @returns {Number/null} A number or `null` if empty `indentations` was given.
+ * @member CKEDITOR.plugins.pastetools.filters.word.heuristics
+ * @private
+ */
+ guessIndentationStep: function( indentations ) {
+ return indentations.length ? Math.min.apply( null, indentations ) : null;
+ },
+
+ /**
+ * Shifts lists that were deformed during pasting one level down
+ * so that the list structure matches the content copied from Word.
+ *
+ * @param {CKEDITOR.htmlParser.element} element
+ * @member CKEDITOR.plugins.pastetools.filters.word.heuristics
+ * @private
+ * */
+ correctLevelShift: function( element ) {
+ var isShiftedList = function( list ) {
+ return list.children && list.children.length == 1 && Heuristics.isShifted( list.children[ 0 ] );
+ };
+
+ if ( this.isShifted( element ) ) {
+ var lists = CKEDITOR.tools.array.filter( element.children, function( child ) {
+ return ( child.name == 'ul' || child.name == 'ol' );
+ } );
+
+ var listChildren = CKEDITOR.tools.array.reduce( lists, function( acc, list ) {
+ var preceding = isShiftedList( list ) ? [ list ] : list.children;
+ return preceding.concat( acc );
+ }, [] );
+
+ CKEDITOR.tools.array.forEach( lists, function( list ) {
+ list.remove();
+ } );
+
+ CKEDITOR.tools.array.forEach( listChildren, function( child ) {
+ // `Add` method without index always append child at the end (#796).
+ element.add( child );
+ } );
+
+ delete element.name;
+ }
+ },
+
+ /**
+ * Determines if the list is malformed in a manner that its items
+ * are one level deeper than they should be.
+ *
+ * @param {CKEDITOR.htmlParser.element} element
+ * @returns {Boolean}
+ * @member CKEDITOR.plugins.pastetools.filters.word.heuristics
+ * @private
+ */
+ isShifted: function( element ) {
+ if ( element.name !== 'li' ) {
+ return false;
+ }
+
+ return CKEDITOR.tools.array.filter( element.children, function( child ) {
+ if ( child.name ) {
+ if ( child.name == 'ul' || child.name == 'ol' ) {
+ return false;
+ }
+
+ if ( child.name == 'p' && child.children.length === 0 ) {
+ return false;
+ }
+ }
+ return true;
+ } ).length === 0;
+ }
+ };
+
+ Heuristics = plug.heuristics;
+
+ // Expose this function since it's useful in other places.
+ List.setListSymbol.removeRedundancies = function( style, level ) {
+ // 'disc' and 'decimal' are the default styles in some cases - remove redundancy.
+ if ( ( level === 1 && style[ 'list-style-type' ] === 'disc' ) || style[ 'list-style-type' ] === 'decimal' ) {
+ delete style[ 'list-style-type' ];
+ }
+ };
+
+ function falseIfEmpty( value ) {
+ if ( value === '' ) {
+ return false;
+ }
+ return value;
+ }
+
+ // Used when filtering attributes - returning false deletes the attribute.
+ function remove() {
+ return false;
+ }
+
+ CKEDITOR.cleanWord = CKEDITOR.pasteFilters.word = pastetools.createFilter( {
+ rules: [
+ commonFilter.rules,
+ plug.rules
+ ],
+ additionalTransforms: function( html ) {
+ // Before filtering inline all the styles to allow because some of them are available only in style
+ // sheets. This step is skipped in IEs due to their flaky support for custom types in dataTransfer. (https://dev.ckeditor.com/ticket/16847)
+ if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported ) {
+ html = commonFilter.styles.inliner.inline( html ).getBody().getHtml();
+ }
+
+ // Sometimes Word malforms the comments.
+ return html.replace( //g, ']-->' );
+ }
+ } );
+
+ /**
+ * See {@link CKEDITOR.plugins.pastetools.filters.word.lists}.
+ *
+ * @property {Object} lists
+ * @private
+ * @deprecated 4.13.0
+ * @since 4.6.0
+ * @member CKEDITOR.plugins.pastefromword
+ */
+
+ /**
+ * See {@link CKEDITOR.plugins.pastetools.filters.word.images}.
+ *
+ * @property {Object} images
+ * @private
+ * @deprecated 4.13.0
+ * @removed 4.16.0
+ * @since 4.8.0
+ * @member CKEDITOR.plugins.pastefromword
+ */
+
+ /**
+ * See {@link CKEDITOR.plugins.pastetools.filters.image}.
+ *
+ * @property {Object} images
+ * @private
+ * @removed 4.16.0
+ * @since 4.13.0
+ * @member CKEDITOR.plugins.pastetools.filters.word
+ */
+
+ /**
+ * See {@link CKEDITOR.plugins.pastetools.filters.image#extractFromRtf}.
+ *
+ * @property {Function} extractFromRtf
+ * @private
+ * @removed 4.16.0
+ * @since 4.13.0
+ * @member CKEDITOR.plugins.pastetools.filters.word.images
+ */
+
+ /**
+ * See {@link CKEDITOR.plugins.pastetools.filters.image#extractTagsFromHtml}.
+ *
+ * @property {Function} extractTagsFromHtml
+ * @private
+ * @removed 4.16.0
+ * @since 4.13.0
+ * @member CKEDITOR.plugins.pastetools.filters.word.images
+ */
+
+ /**
+ * See {@link CKEDITOR.plugins.pastetools.filters.word.heuristics}.
+ *
+ * @property {Object} heuristics
+ * @private
+ * @deprecated 4.13.0
+ * @since 4.6.2
+ * @member CKEDITOR.plugins.pastefromword
+ */
+
+ /**
+ * See {@link CKEDITOR.plugins.pastetools.filters.common.styles}.
+ *
+ * @property {Object} styles
+ * @private
+ * @deprecated 4.13.0
+ * @since 4.6.0
+ * @member CKEDITOR.plugins.pastefromword
+ */
+
+
+
+ /**
+ * See {@link #pasteTools_removeFontStyles}.
+ *
+ * **Important note:** Prior to version 4.6.0 this configuration option defaulted to `true`.
+ *
+ * @deprecated 4.13.0
+ * @since 3.1.0
+ * @cfg {Boolean} [pasteFromWordRemoveFontStyles=false]
+ * @member CKEDITOR.config
+ */
+
+ /**
+ * Whether to transform Microsoft Word outline numbered headings into lists.
+ *
+ * config.pasteFromWordNumberedHeadingToList = true;
+ *
+ * @removed 4.6.0
+ * @since 3.1.0
+ * @cfg {Boolean} [pasteFromWordNumberedHeadingToList=false]
+ * @member CKEDITOR.config
+ */
+
+ /**
+ * Whether to remove element styles that cannot be managed with the editor. Note
+ * that this option does not handle font-specific styles, which depend on the
+ * {@link #pasteTools_removeFontStyles} setting instead.
+ *
+ * config.pasteFromWordRemoveStyles = false;
+ *
+ * @removed 4.6.0
+ * @since 3.1.0
+ * @cfg {Boolean} [pasteFromWordRemoveStyles=true]
+ * @member CKEDITOR.config
+ */
+
+ /**
+ * Activates a heuristic that helps detect lists pasted into the editor in Microsoft Edge.
+ *
+ * The reason why this heuristic is needed is that on pasting Microsoft Edge removes any Word-specific
+ * metadata allowing to identify lists.
+ *
+ * // Disables list heuristics for Edge.
+ * config.pasteFromWord_heuristicsEdgeList = false;
+ *
+ * @since 4.6.2
+ * @cfg {Boolean} [pasteFromWord_heuristicsEdgeList=true]
+ * @member CKEDITOR.config
+ */
+ CKEDITOR.config.pasteFromWord_heuristicsEdgeList = true;
+} )();
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/hidpi/pastefromword-rtl.png b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/hidpi/pastefromword-rtl.png
new file mode 100644
index 00000000..f1e41909
Binary files /dev/null and b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/hidpi/pastefromword-rtl.png differ
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/hidpi/pastefromword.png b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/hidpi/pastefromword.png
new file mode 100644
index 00000000..dd844347
Binary files /dev/null and b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/hidpi/pastefromword.png differ
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/pastefromword-rtl.png b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/pastefromword-rtl.png
new file mode 100644
index 00000000..663ce733
Binary files /dev/null and b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/pastefromword-rtl.png differ
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/pastefromword.png b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/pastefromword.png
new file mode 100644
index 00000000..0ede58c3
Binary files /dev/null and b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/icons/pastefromword.png differ
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/af.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/af.js
new file mode 100644
index 00000000..42b6207d
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/af.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'af', {
+ confirmCleanup: 'Die teks wat u wil byvoeg lyk asof dit uit Word gekopiëer is. Wil u dit eers skoonmaak voordat dit bygevoeg word?',
+ error: 'Die bygevoegte teks kon nie skoongemaak word nie, weens \'n interne fout',
+ title: 'Uit Word byvoeg',
+ toolbar: 'Uit Word byvoeg'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ar.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ar.js
new file mode 100644
index 00000000..c99009c2
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ar.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ar', {
+ confirmCleanup: 'يبدو أن النص المراد لصقه منسوخ من برنامج وورد. هل تود تنظيفه قبل الشروع في عملية اللصق؟',
+ error: 'لم يتم مسح المعلومات الملصقة لخلل داخلي',
+ title: 'لصق من وورد',
+ toolbar: 'لصق من وورد'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/az.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/az.js
new file mode 100644
index 00000000..45a8a255
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/az.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'az', {
+ confirmCleanup: 'Əlavə edilən mətn Word-dan köçürülənə oxşayır. Təmizləmək istəyirsinizmi?',
+ error: 'Daxili səhvə görə əlavə edilən məlumatların təmizlənməsi mümkün deyil',
+ title: 'Word-dan əlavəetmə',
+ toolbar: 'Word-dan əlavəetmə'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bg.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bg.js
new file mode 100644
index 00000000..9a81835a
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bg.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'bg', {
+ confirmCleanup: 'Текстът, който искате да поставите, изглежда е копиран от Word. Искате ли да се почисти преди поставянето?',
+ error: 'Вмъкваните данни не могат да бъдат почистени поради вътрешна грешка',
+ title: 'Вмъкни от Word',
+ toolbar: 'Вмъкни от Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bn.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bn.js
new file mode 100644
index 00000000..ac4b2d5a
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bn.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'bn', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'পেস্ট (শব্দ)',
+ toolbar: 'পেস্ট (শব্দ)'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bs.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bs.js
new file mode 100644
index 00000000..72a0498a
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/bs.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'bs', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'Zalijepi iz Word-a',
+ toolbar: 'Zalijepi iz Word-a'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ca.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ca.js
new file mode 100644
index 00000000..1a4c620a
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ca.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ca', {
+ confirmCleanup: 'El text que voleu enganxar sembla provenir de Word. Voleu netejar aquest text abans que sigui enganxat?',
+ error: 'No ha estat possible netejar les dades enganxades degut a un error intern',
+ title: 'Enganxa des del Word',
+ toolbar: 'Enganxa des del Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/cs.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/cs.js
new file mode 100644
index 00000000..fb592a41
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/cs.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'cs', {
+ confirmCleanup: 'Jak je vidět, vkládaný text je kopírován z Wordu. Chcete jej před vložením vyčistit?',
+ error: 'Z důvodu vnitřní chyby nebylo možné provést vyčištění vkládaného textu.',
+ title: 'Vložit z Wordu',
+ toolbar: 'Vložit z Wordu'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/cy.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/cy.js
new file mode 100644
index 00000000..163dbe9e
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/cy.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'cy', {
+ confirmCleanup: 'Mae\'r testun rydych chi am ludo wedi\'i gopïo o Word. Ydych chi am ei lanhau cyn ei ludo?',
+ error: 'Doedd dim modd glanhau y data a ludwyd oherwydd gwall mewnol',
+ title: 'Gludo o Word',
+ toolbar: 'Gludo o Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/da.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/da.js
new file mode 100644
index 00000000..a01f7d68
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/da.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'da', {
+ confirmCleanup: 'Den tekst du forsøger at indsætte ser ud til at komme fra Word. Vil du rense teksten før den indsættes?',
+ error: 'Det var ikke muligt at fjerne formatteringen på den indsatte tekst grundet en intern fejl',
+ title: 'Indsæt fra Word',
+ toolbar: 'Indsæt fra Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/de-ch.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/de-ch.js
new file mode 100644
index 00000000..00135a9b
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/de-ch.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'de-ch', {
+ confirmCleanup: 'Der Text, den Sie einfügen möchten, scheint aus MS-Word kopiert zu sein. Möchten Sie ihn zuvor bereinigen lassen?',
+ error: 'Aufgrund eines internen Fehlers war es nicht möglich, die eingefügten Daten zu bereinigen',
+ title: 'Aus Word einfügen',
+ toolbar: 'Aus Word einfügen'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/de.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/de.js
new file mode 100644
index 00000000..53e6f8ad
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/de.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'de', {
+ confirmCleanup: 'Der Text, den Sie einfügen möchten, scheint aus MS-Word kopiert zu sein. Möchten Sie ihn zuvor bereinigen lassen?',
+ error: 'Aufgrund eines internen Fehlers war es nicht möglich die eingefügten Daten zu bereinigen',
+ title: 'Aus Word einfügen',
+ toolbar: 'Aus Word einfügen'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/el.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/el.js
new file mode 100644
index 00000000..8aecb59d
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/el.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'el', {
+ confirmCleanup: 'Το κείμενο που επικολλάται φαίνεται να είναι αντιγραμμένο από το Word. Μήπως θα θέλατε να καθαριστεί προτού επικολληθεί;',
+ error: 'Δεν ήταν δυνατό να καθαριστούν τα δεδομένα λόγω ενός εσωτερικού σφάλματος',
+ title: 'Επικόλληση από το Word',
+ toolbar: 'Επικόλληση από το Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-au.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-au.js
new file mode 100644
index 00000000..aa5078cf
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-au.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'en-au', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?',
+ error: 'It was not possible to clean up the pasted data due to an internal error',
+ title: 'Paste from Word',
+ toolbar: 'Paste from Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-ca.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-ca.js
new file mode 100644
index 00000000..74f0efa6
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-ca.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'en-ca', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'Paste from Word',
+ toolbar: 'Paste from Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-gb.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-gb.js
new file mode 100644
index 00000000..59e2decd
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en-gb.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'en-gb', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?',
+ error: 'It was not possible to clean up the pasted data due to an internal error',
+ title: 'Paste from Word',
+ toolbar: 'Paste from Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en.js
new file mode 100644
index 00000000..1da124a9
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/en.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'en', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?',
+ error: 'It was not possible to clean up the pasted data due to an internal error',
+ title: 'Paste from Word',
+ toolbar: 'Paste from Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/eo.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/eo.js
new file mode 100644
index 00000000..c60fc1b3
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/eo.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'eo', {
+ confirmCleanup: 'La teksto, kiun vi volas interglui, ŝajnas esti kopiita el Word. Ĉu vi deziras purigi ĝin antaŭ intergluo?',
+ error: 'Ne eblis purigi la intergluitajn datenojn pro interna eraro',
+ title: 'Interglui el Word',
+ toolbar: 'Interglui el Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/es-mx.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/es-mx.js
new file mode 100644
index 00000000..664c249f
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/es-mx.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'es-mx', {
+ confirmCleanup: 'El texto que desea pegar parece estar copiado de Word. ¿Quieres limpiarlo antes de pegarlo?',
+ error: 'No fue posible limpiar los datos pegados debido a un error interno',
+ title: 'Pegar desde word',
+ toolbar: 'Pegar desde word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/es.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/es.js
new file mode 100644
index 00000000..cacc4630
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/es.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'es', {
+ confirmCleanup: 'El texto que desea parece provenir de Word.\r\n¿Desea depurarlo antes de pegarlo?',
+ error: 'No ha sido posible limpiar los datos debido a un error interno',
+ title: 'Pegar desde Word',
+ toolbar: 'Pegar desde Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/et.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/et.js
new file mode 100644
index 00000000..4f281913
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/et.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'et', {
+ confirmCleanup: 'Tekst, mida tahad asetada näib pärinevat Wordist. Kas tahad selle enne asetamist puhastada?',
+ error: 'Asetatud andmete puhastamine ei olnud sisemise vea tõttu võimalik',
+ title: 'Asetamine Wordist',
+ toolbar: 'Asetamine Wordist'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/eu.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/eu.js
new file mode 100644
index 00000000..4774f376
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/eu.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'eu', {
+ confirmCleanup: 'Itsatsi nahi duzun testua Word-etik kopiatua dela dirudi. Itsatsi baino lehen garbitu nahi duzu?',
+ error: 'Barne-errore bat dela eta ezin izan da itsatsitako testua garbitu',
+ title: 'Itsatsi Word-etik',
+ toolbar: 'Itsatsi Word-etik'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fa.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fa.js
new file mode 100644
index 00000000..0c1c88a4
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fa.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'fa', {
+ confirmCleanup: 'متنی که میخواهید بچسبانید به نظر میرسد که از Word کپی شده است. آیا میخواهید قبل از چسباندن آن را پاکسازی کنید؟',
+ error: 'به دلیل بروز خطای داخلی امکان پاکسازی اطلاعات بازنشانی شده وجود ندارد.',
+ title: 'چسباندن از Word',
+ toolbar: 'چسباندن از Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fi.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fi.js
new file mode 100644
index 00000000..c3f0d187
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fi.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'fi', {
+ confirmCleanup: 'Liittämäsi teksti näyttäisi olevan Word-dokumentista. Haluatko siivota sen ennen liittämistä? (Suositus: Kyllä)',
+ error: 'Liitetyn tiedon siivoaminen ei onnistunut sisäisen virheen takia',
+ title: 'Liitä Word-dokumentista',
+ toolbar: 'Liitä Word-dokumentista'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fo.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fo.js
new file mode 100644
index 00000000..9832a8c7
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fo.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'fo', {
+ confirmCleanup: 'Teksturin, tú roynir at seta inn, sýnist at stava frá Word. Skal teksturin reinsast fyrst?',
+ error: 'Tað eydnaðist ikki at reinsa tekstin vegna ein internan feil',
+ title: 'Innrita frá Word',
+ toolbar: 'Innrita frá Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fr-ca.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fr-ca.js
new file mode 100644
index 00000000..be3b119c
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fr-ca.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'fr-ca', {
+ confirmCleanup: 'Le texte que vous tentez de coller semble provenir de Word. Désirez vous le nettoyer avant de coller?',
+ error: 'Il n\'a pas été possible de nettoyer les données collées du à une erreur interne',
+ title: 'Coller de Word',
+ toolbar: 'Coller de Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fr.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fr.js
new file mode 100644
index 00000000..eca4eadd
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/fr.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'fr', {
+ confirmCleanup: 'Le texte à coller semble provenir de Word. Désirez-vous le nettoyer avant de coller ?',
+ error: 'Les données collées n\'ont pas pu être nettoyées à cause d\'une erreur interne',
+ title: 'Coller depuis Word',
+ toolbar: 'Coller depuis Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/gl.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/gl.js
new file mode 100644
index 00000000..459b2f6d
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/gl.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'gl', {
+ confirmCleanup: 'O texto que quere pegar semella ser copiado desde o Word. Quere depuralo antes de pegalo?',
+ error: 'Non foi posíbel depurar os datos pegados por mor dun erro interno',
+ title: 'Pegar desde Word',
+ toolbar: 'Pegar desde Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/gu.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/gu.js
new file mode 100644
index 00000000..3e784049
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/gu.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'gu', {
+ confirmCleanup: 'તમે જે ટેક્ષ્ત્ કોપી કરી રહ્યા છો ટે વર્ડ ની છે. કોપી કરતા પેહલા સાફ કરવી છે?',
+ error: 'પેસ્ટ કરેલો ડેટા ઇન્ટરનલ એરર ના લીથે સાફ કરી શકાયો નથી.',
+ title: 'પેસ્ટ (વડૅ ટેક્સ્ટ)',
+ toolbar: 'પેસ્ટ (વડૅ ટેક્સ્ટ)'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/he.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/he.js
new file mode 100644
index 00000000..31cf6c3a
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/he.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'he', {
+ confirmCleanup: 'נראה הטקסט שבכוונתך להדביק מקורו בקובץ וורד. האם ברצונך לנקות אותו טרם ההדבקה?',
+ error: 'לא ניתן היה לנקות את המידע בשל תקלה פנימית.',
+ title: 'הדבקה מ-Word',
+ toolbar: 'הדבקה מ-Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hi.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hi.js
new file mode 100644
index 00000000..b863d5d0
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hi.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'hi', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'पेस्ट (वर्ड से)',
+ toolbar: 'पेस्ट (वर्ड से)'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hr.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hr.js
new file mode 100644
index 00000000..d2b7de8a
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hr.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'hr', {
+ confirmCleanup: 'Tekst koji želite zalijepiti čini se da je kopiran iz Worda. Želite li prije očistiti tekst?',
+ error: 'Nije moguće očistiti podatke za ljepljenje zbog interne greške',
+ title: 'Zalijepi iz Worda',
+ toolbar: 'Zalijepi iz Worda'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hu.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hu.js
new file mode 100644
index 00000000..b3594477
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/hu.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'hu', {
+ confirmCleanup: 'Úgy tűnik a beillesztett szöveget Word-ből másolta át. Meg szeretné tisztítani a szöveget? (ajánlott)',
+ error: 'Egy belső hiba miatt nem sikerült megtisztítani a szöveget',
+ title: 'Beillesztés Word-ből',
+ toolbar: 'Beillesztés Word-ből'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/id.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/id.js
new file mode 100644
index 00000000..8fba6500
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/id.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'id', {
+ confirmCleanup: 'Teks yang ingin anda tempel sepertinya di salin dari Word. Apakah anda mau membersihkannya sebelum menempel?',
+ error: 'Tidak mungkin membersihkan data yang ditempel dikerenakan kesalahan internal',
+ title: 'Tempel dari Word',
+ toolbar: 'Tempel dari Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/is.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/is.js
new file mode 100644
index 00000000..b55ba9e0
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/is.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'is', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'Líma úr Word',
+ toolbar: 'Líma úr Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/it.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/it.js
new file mode 100644
index 00000000..cd7848b8
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/it.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'it', {
+ confirmCleanup: 'Il testo da incollare sembra provenire da Word. Desideri pulirlo prima di incollare?',
+ error: 'Non è stato possibile eliminare il testo incollato a causa di un errore interno.',
+ title: 'Incolla da Word',
+ toolbar: 'Incolla da Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ja.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ja.js
new file mode 100644
index 00000000..92967b26
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ja.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ja', {
+ confirmCleanup: '貼り付けを行うテキストはワード文章からコピーされようとしています。貼り付ける前にクリーニングを行いますか?',
+ error: '内部エラーにより貼り付けたデータをクリアできませんでした',
+ title: 'ワード文章から貼り付け',
+ toolbar: 'ワード文章から貼り付け'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ka.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ka.js
new file mode 100644
index 00000000..14ce0b9f
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ka.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ka', {
+ confirmCleanup: 'ჩასასმელი ტექსტი ვორდიდან გადმოტანილს გავს - გინდათ მისი წინასწარ გაწმენდა?',
+ error: 'შიდა შეცდომის გამო ვერ მოხერხდა ტექსტის გაწმენდა',
+ title: 'ვორდიდან ჩასმა',
+ toolbar: 'ვორდიდან ჩასმა'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/km.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/km.js
new file mode 100644
index 00000000..3c730ac5
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/km.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'km', {
+ confirmCleanup: 'អត្ថបទដែលអ្នកចង់បិទភ្ជាប់នេះ ទំនងដូចជាចម្លងមកពី Word។ តើអ្នកចង់សម្អាតវាមុនបិទភ្ជាប់ទេ?',
+ error: 'ដោយសារមានបញ្ហាផ្នែកក្នុងធ្វើឲ្យមិនអាចសម្អាតទិន្នន័យដែលបានបិទភ្ជាប់',
+ title: 'បិទភ្ជាប់ពី Word',
+ toolbar: 'បិទភ្ជាប់ពី Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ko.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ko.js
new file mode 100644
index 00000000..304ac950
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ko.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ko', {
+ confirmCleanup: '붙여 넣을 내용은 MS Word에서 복사 한 것입니다. 붙여 넣기 전에 정리 하시겠습니까?',
+ error: '내부 오류로 붙여 넣은 데이터를 정리 할 수 없습니다.',
+ title: 'MS Word 에서 붙여넣기',
+ toolbar: 'MS Word 에서 붙여넣기'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ku.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ku.js
new file mode 100644
index 00000000..530e273c
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ku.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ku', {
+ confirmCleanup: 'ئەم دەقەی بەتەمای بیلکێنی پێدەچێت له word هێنرابێت. دەتەوێت پاکی بکەیوه پێش ئەوەی بیلکێنی؟',
+ error: 'هیچ ڕێگەیەك نەبوو لەلکاندنی دەقەکه بەهۆی هەڵەیەکی ناوەخۆیی',
+ title: 'لکاندنی لەلایەن Word',
+ toolbar: 'لکاندنی لەڕێی Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/lt.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/lt.js
new file mode 100644
index 00000000..11d90115
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/lt.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'lt', {
+ confirmCleanup: 'Tekstas, kurį įkeliate yra kopijuojamas iš Word. Ar norite jį išvalyti prieš įkeliant?',
+ error: 'Dėl vidinių sutrikimų, nepavyko išvalyti įkeliamo teksto',
+ title: 'Įdėti iš Word',
+ toolbar: 'Įdėti iš Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/lv.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/lv.js
new file mode 100644
index 00000000..2f88d64b
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/lv.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'lv', {
+ confirmCleanup: 'Teksts, kuru vēlaties ielīmēt, izskatās ir nokopēts no Word. Vai vēlaties to iztīrīt pirms ielīmēšanas?',
+ error: 'Iekšējas kļūdas dēļ, neizdevās iztīrīt ielīmētos datus.',
+ title: 'Ievietot no Worda',
+ toolbar: 'Ievietot no Worda'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/mk.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/mk.js
new file mode 100644
index 00000000..b4b00adb
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/mk.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'mk', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'Paste from Word', // MISSING
+ toolbar: 'Paste from Word' // MISSING
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/mn.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/mn.js
new file mode 100644
index 00000000..6cf9218c
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/mn.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'mn', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'Word-оос буулгах',
+ toolbar: 'Word-оос буулгах'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ms.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ms.js
new file mode 100644
index 00000000..ec9aaabb
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ms.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ms', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'Tampal dari Word',
+ toolbar: 'Tampal dari Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/nb.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/nb.js
new file mode 100644
index 00000000..41a1d731
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/nb.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'nb', {
+ confirmCleanup: 'Teksten du limer inn ser ut til å være kopiert fra Word. Vil du renske den før du limer den inn?',
+ error: 'Det var ikke mulig å renske den innlimte teksten på grunn av en intern feil',
+ title: 'Lim inn fra Word',
+ toolbar: 'Lim inn fra Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/nl.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/nl.js
new file mode 100644
index 00000000..b7343b0a
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/nl.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'nl', {
+ confirmCleanup: 'De tekst die u wilt plakken lijkt gekopieerd te zijn vanuit Word. Wilt u de tekst opschonen voordat deze geplakt wordt?',
+ error: 'Het was niet mogelijk om de geplakte tekst op te schonen door een interne fout',
+ title: 'Plakken vanuit Word',
+ toolbar: 'Plakken vanuit Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/no.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/no.js
new file mode 100644
index 00000000..c7bc3b00
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/no.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'no', {
+ confirmCleanup: 'Teksten du limer inn ser ut til å være kopiert fra Word. Vil du renske den før du limer den inn?',
+ error: 'Det var ikke mulig å renske den innlimte teksten på grunn av en intern feil',
+ title: 'Lim inn fra Word',
+ toolbar: 'Lim inn fra Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/oc.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/oc.js
new file mode 100644
index 00000000..f27fb935
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/oc.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'oc', {
+ confirmCleanup: 'Sembla que lo tèxte de pegar proven de Word. Lo volètz netejar abans de lo pegar ?',
+ error: 'Las donadas pegadas an pas pogut èsser netejadas a causa d\'una error intèrna',
+ title: 'Pegar dempuèi Word',
+ toolbar: 'Pegar dempuèi Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pl.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pl.js
new file mode 100644
index 00000000..211ea669
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pl.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'pl', {
+ confirmCleanup: 'Tekst, który chcesz wkleić, prawdopodobnie pochodzi z programu Microsoft Word. Czy chcesz go wyczyścić przed wklejeniem?',
+ error: 'Wyczyszczenie wklejonych danych nie było możliwe z powodu wystąpienia błędu.',
+ title: 'Wklej z programu MS Word',
+ toolbar: 'Wklej z programu MS Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pt-br.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pt-br.js
new file mode 100644
index 00000000..dac89da4
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pt-br.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'pt-br', {
+ confirmCleanup: 'O texto que você deseja colar parece ter sido copiado do Word. Você gostaria de remover a formatação antes de colar?',
+ error: 'Não foi possível limpar os dados colados devido a um erro interno',
+ title: 'Colar do Word',
+ toolbar: 'Colar do Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pt.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pt.js
new file mode 100644
index 00000000..2d7ec93f
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/pt.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'pt', {
+ confirmCleanup: 'O texto que pretende colar parece ter sido copiado do Word. Deseja limpar o código antes de o colar?',
+ error: 'Não foi possível limpar a informação colada devido a um erro interno.',
+ title: 'Colar do Word',
+ toolbar: 'Colar do Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ro.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ro.js
new file mode 100644
index 00000000..6925a410
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ro.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ro', {
+ confirmCleanup: 'Textul pe care doriți să-l lipiți este din Word. Doriți curățarea textului înante de a-l adăuga?',
+ error: 'Nu a fost posibilă curățarea datelor adăugate datorită unei erori interne',
+ title: 'Adaugă din Word',
+ toolbar: 'Adaugă din Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ru.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ru.js
new file mode 100644
index 00000000..7b01ddf0
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ru.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ru', {
+ confirmCleanup: 'Текст, который вы желаете вставить, по всей видимости, был скопирован из Word. Следует ли очистить его перед вставкой?',
+ error: 'Невозможно очистить вставленные данные из-за внутренней ошибки',
+ title: 'Вставить из Word',
+ toolbar: 'Вставить из Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/si.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/si.js
new file mode 100644
index 00000000..9ae44f8c
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/si.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'si', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'වචන වලින් අලවන්න',
+ toolbar: 'වචන වලින් අලවන්න'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sk.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sk.js
new file mode 100644
index 00000000..d0e51f10
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sk.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'sk', {
+ confirmCleanup: 'Zdá sa, že vkladaný text pochádza z programu MS Word. Chcete ho pred vkladaním automaticky vyčistiť?',
+ error: 'Kvôli internej chybe nebolo možné vložené dáta vyčistiť',
+ title: 'Vložiť z Wordu',
+ toolbar: 'Vložiť z Wordu'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sl.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sl.js
new file mode 100644
index 00000000..6f0bc1ed
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sl.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'sl', {
+ confirmCleanup: 'Besedilo, ki ga želite prilepiti, je kopirano iz Worda. Ali ga želite očistiti, preden ga prilepite?',
+ error: 'Ni bilo mogoče očistiti prilepljenih podatkov zaradi notranje napake',
+ title: 'Prilepi iz Worda',
+ toolbar: 'Prilepi iz Worda'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sq.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sq.js
new file mode 100644
index 00000000..e1efc365
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sq.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'sq', {
+ confirmCleanup: 'Teksti që dëshironi të e hidhni siç duket është kopjuar nga Word-i. Dëshironi të e pastroni para se të e hidhni?',
+ error: 'Nuk ishte e mundur të fshiheshin të dhënat e hedhura për shkak të një gabimi të brendshëm',
+ title: 'Hidhe nga Word-i',
+ toolbar: 'Hidhe nga Word-i'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sr-latn.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sr-latn.js
new file mode 100644
index 00000000..c495923b
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sr-latn.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'sr-latn', {
+ confirmCleanup: 'Kopirani tekst je iz Word-a. Želite ga očistiti? ',
+ error: 'Zbog interne greške tekst nije očišćen.',
+ title: 'Zalepi iz Worda',
+ toolbar: 'Zalepi iz Worda'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sr.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sr.js
new file mode 100644
index 00000000..ea222149
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sr.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'sr', {
+ confirmCleanup: 'Уметнути текст је копиран из Word-а. Желите га очитити? ',
+ error: 'Због интерне грешке текст није очишћен.',
+ title: 'Залепи из Worda',
+ toolbar: 'Залепи из Worda'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sv.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sv.js
new file mode 100644
index 00000000..0886f1fb
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/sv.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'sv', {
+ confirmCleanup: 'Texten du vill klistra in verkar vara kopierad från Word. Vill du rensa den innan du klistrar in den?',
+ error: 'Det var inte möjligt att städa upp den inklistrade data på grund av ett internt fel',
+ title: 'Klistra in från Word',
+ toolbar: 'Klistra in från Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/th.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/th.js
new file mode 100644
index 00000000..d25d4d50
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/th.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'th', {
+ confirmCleanup: 'ข้อความที่คุณต้องการวางลงไปเป็นข้อความที่คัดลอกมาจากโปรแกรมไมโครซอฟท์เวิร์ด คุณต้องการล้างค่าข้อความดังกล่าวก่อนวางลงไปหรือไม่?',
+ error: 'ไม่สามารถล้างข้อมูลที่ต้องการวางได้เนื่องจากเกิดข้อผิดพลาดภายในระบบ',
+ title: 'วางสำเนาจากตัวอักษรเวิร์ด',
+ toolbar: 'วางสำเนาจากตัวอักษรเวิร์ด'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/tr.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/tr.js
new file mode 100644
index 00000000..98b73fc9
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/tr.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'tr', {
+ confirmCleanup: 'Yapıştırmaya çalıştığınız metin Word\'den kopyalanmıştır. Yapıştırmadan önce silmek istermisiniz?',
+ error: 'Yapıştırmadaki veri bilgisi hata düzelene kadar silinmeyecektir',
+ title: 'Word\'den Yapıştır',
+ toolbar: 'Word\'den Yapıştır'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/tt.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/tt.js
new file mode 100644
index 00000000..d397b225
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/tt.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'tt', {
+ confirmCleanup: 'The text you want to paste seems to be copied from Word. Do you want to clean it before pasting?', // MISSING
+ error: 'It was not possible to clean up the pasted data due to an internal error', // MISSING
+ title: 'Word\'тан өстәү',
+ toolbar: 'Word\'тан өстәү'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ug.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ug.js
new file mode 100644
index 00000000..746f632e
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/ug.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'ug', {
+ confirmCleanup: 'سىز چاپلىماقچى بولغان مەزمۇن MS Word تىن كەلگەندەك قىلىدۇ، MS Word پىچىمىنى تازىلىۋەتكەندىن كېيىن ئاندىن چاپلامدۇ؟',
+ error: 'ئىچكى خاتالىق سەۋەبىدىن چاپلايدىغان سانلىق مەلۇماتنى تازىلىيالمايدۇ',
+ title: 'MS Word تىن چاپلا',
+ toolbar: 'MS Word تىن چاپلا'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/uk.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/uk.js
new file mode 100644
index 00000000..82862ad2
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/uk.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'uk', {
+ confirmCleanup: 'Текст, що Ви намагаєтесь вставити, схожий на скопійований з Word. Бажаєте очистити його форматування перед вставлянням?',
+ error: 'Неможливо очистити форматування через внутрішню помилку.',
+ title: 'Вставити з Word',
+ toolbar: 'Вставити з Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/vi.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/vi.js
new file mode 100644
index 00000000..f81e77da
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/vi.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'vi', {
+ confirmCleanup: 'Văn bản bạn muốn dán có kèm định dạng của Word. Bạn có muốn loại bỏ định dạng Word trước khi dán?',
+ error: 'Không thể để làm sạch các dữ liệu dán do một lỗi nội bộ',
+ title: 'Dán với định dạng Word',
+ toolbar: 'Dán với định dạng Word'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/zh-cn.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/zh-cn.js
new file mode 100644
index 00000000..1d942799
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/zh-cn.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'zh-cn', {
+ confirmCleanup: '您要粘贴的内容好像是来自 MS Word,是否要清除 MS Word 格式后再粘贴?',
+ error: '由于内部错误无法清理要粘贴的数据',
+ title: '从 MS Word 粘贴',
+ toolbar: '从 MS Word 粘贴'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/zh.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/zh.js
new file mode 100644
index 00000000..9c526b91
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/lang/zh.js
@@ -0,0 +1,10 @@
+/*
+Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+*/
+CKEDITOR.plugins.setLang( 'pastefromword', 'zh', {
+ confirmCleanup: '您想貼上的文字似乎是自 Word 複製而來,請問您是否要先清除 Word 的格式後再行貼上?',
+ error: '由於發生內部錯誤,無法清除清除 Word 的格式。',
+ title: '自 Word 貼上',
+ toolbar: '自 Word 貼上'
+} );
diff --git a/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/plugin.js b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/plugin.js
new file mode 100644
index 00000000..827d14d2
--- /dev/null
+++ b/mod_app/static/ckeditor/ckeditor/plugins/pastefromword/plugin.js
@@ -0,0 +1,217 @@
+/**
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
+ * CKEditor 4 LTS ("Long Term Support") is available under the terms of the Extended Support Model.
+ */
+
+/**
+ * @fileOverview This plugin handles pasting content from Microsoft Office applications.
+ */
+
+( function() {
+ /* global confirm */
+
+ CKEDITOR.plugins.add( 'pastefromword', {
+ requires: 'pastetools',
+ // jscs:disable maximumLineLength
+ lang: 'af,ar,az,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,es-mx,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,oc,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
+ // jscs:enable maximumLineLength
+ icons: 'pastefromword,pastefromword-rtl', // %REMOVE_LINE_CORE%
+ hidpi: true, // %REMOVE_LINE_CORE%
+ init: function( editor ) {
+ // Flag indicate this command is actually been asked instead of a generic pasting.
+ var forceFromWord = 0,
+ pastetoolsPath = CKEDITOR.plugins.getPath( 'pastetools' ),
+ path = this.path,
+ configInlineImages = editor.config.pasteFromWord_inlineImages === undefined ? true : editor.config.pasteFromWord_inlineImages,
+ defaultFilters = [
+ CKEDITOR.getUrl( pastetoolsPath + 'filter/common.js' ),
+ CKEDITOR.getUrl( pastetoolsPath + 'filter/image.js' ),
+ CKEDITOR.getUrl( path + 'filter/default.js' )
+ ];
+
+ editor.addCommand( 'pastefromword', {
+ // Snapshots are done manually by editable.insertXXX methods.
+ canUndo: false,
+ async: true,
+
+ /**
+ * The Paste from Word command. It will determine its pasted content from Word automatically if possible.
+ *
+ * At the time of writing it was working correctly only in Internet Explorer browsers, due to their
+ * `paste` support in `document.execCommand`.
+ *
+ * @private
+ * @param {CKEDITOR.editor} editor An instance of the editor where the command is being executed.
+ * @param {Object} [data] The options object.
+ * @param {Boolean/String} [data.notification=true] Content for a notification shown after an unsuccessful
+ * paste attempt. If `false`, the notification will not be displayed. This parameter was added in 4.7.0.
+ * @member CKEDITOR.editor.commands.pastefromword
+ */
+ exec: function( editor, data ) {
+ forceFromWord = 1;
+ editor.execCommand( 'paste', {
+ type: 'html',
+ notification: data && typeof data.notification !== 'undefined' ? data.notification : true
+ } );
+ }
+ } );
+
+ // Register the toolbar button.
+ CKEDITOR.plugins.clipboard.addPasteButton( editor, 'PasteFromWord', {
+ label: editor.lang.pastefromword.toolbar,
+ command: 'pastefromword',
+ toolbar: 'clipboard,50'
+ } );
+
+ // Features brought by this command beside the normal process:
+ // 1. No more bothering of user about the clean-up.
+ // 2. Perform the clean-up even if content is not from Microsoft Word.
+ // (e.g. from a Microsoft Word similar application.)
+ // 3. Listen with high priority (3), so clean up is done before content
+ // type sniffing (priority = 6).
+ editor.pasteTools.register( {
+ filters: editor.config.pasteFromWordCleanupFile ? [ editor.config.pasteFromWordCleanupFile ] :
+ defaultFilters,
+
+ canHandle: function( evt ) {
+ var data = evt.data,
+ // Always get raw clipboard data (#3586).
+ mswordHtml = CKEDITOR.plugins.pastetools.getClipboardData( data, 'text/html' ),
+ generatorName = CKEDITOR.plugins.pastetools.getContentGeneratorName( mswordHtml ),
+ wordRegexp = /(class="?Mso|style=["'][^"]*?\bmso\-|w:WordDocument||<\/font>)/,
+ // Use wordRegexp only when there is no meta generator tag in the content
+ isOfficeContent = generatorName ? generatorName === 'microsoft' : wordRegexp.test( mswordHtml );
+
+ return mswordHtml && ( forceFromWord || isOfficeContent );
+ },
+
+ handle: function( evt, next ) {
+ var data = evt.data,
+ mswordHtml = CKEDITOR.plugins.pastetools.getClipboardData( data, 'text/html' ),
+ // Required in Paste from Word Image plugin (#662).
+ dataTransferRtf = CKEDITOR.plugins.pastetools.getClipboardData( data, 'text/rtf' ),
+ pfwEvtData = { dataValue: mswordHtml, dataTransfer: { 'text/rtf': dataTransferRtf } };
+
+ // PFW might still get prevented, if it's not forced.
+ if ( editor.fire( 'pasteFromWord', pfwEvtData ) === false && !forceFromWord ) {
+ return;
+ }
+
+ // Do not apply paste filter to data filtered by the Word filter (https://dev.ckeditor.com/ticket/13093).
+ data.dontFilter = true;
+
+ if ( forceFromWord || confirmCleanUp() ) {
+ pfwEvtData.dataValue = CKEDITOR.cleanWord( pfwEvtData.dataValue, editor );
+
+ // Paste From Word Image:
+ // RTF clipboard is required for embedding images.
+ // If img tags are not allowed there is no point to process images.
+ // Also skip embedding images if image filter is not loaded.
+ if ( CKEDITOR.plugins.clipboard.isCustomDataTypesSupported && configInlineImages &&
+ CKEDITOR.pasteFilters.image ) {
+ pfwEvtData.dataValue = CKEDITOR.pasteFilters.image( pfwEvtData.dataValue, editor, dataTransferRtf );
+ }
+
+ editor.fire( 'afterPasteFromWord', pfwEvtData );
+
+ data.dataValue = pfwEvtData.dataValue;
+
+ if ( editor.config.forcePasteAsPlainText === true ) {
+ // If `config.forcePasteAsPlainText` set to true, force plain text even on Word content (#1013).
+ data.type = 'text';
+ } else if ( !CKEDITOR.plugins.clipboard.isCustomCopyCutSupported && editor.config.forcePasteAsPlainText === 'allow-word' ) {
+ // In browsers using pastebin when pasting from Word, evt.data.type is 'auto' (not 'html') so it gets converted
+ // by 'pastetext' plugin to 'text'. We need to restore 'html' type (#1013) and (#1638).
+ data.type = 'html';
+ }
+ }
+
+ // Reset forceFromWord.
+ forceFromWord = 0;
+
+ next();
+
+ function confirmCleanUp() {
+ return !editor.config.pasteFromWordPromptCleanup ||
+ confirm( editor.lang.pastefromword.confirmCleanup );
+ }
+ }
+ } );
+ }
+ } );
+} )();
+
+
+/**
+ * Whether to prompt the user about the clean up of content being pasted from Microsoft Word.
+ *
+ * config.pasteFromWordPromptCleanup = true;
+ *
+ * @since 3.1.0
+ * @cfg {Boolean} [pasteFromWordPromptCleanup=false]
+ * @member CKEDITOR.config
+ */
+
+/**
+ * The file that provides the Microsoft Word cleanup function for pasting operations.
+ *
+ * **Note:** This is a global configuration shared by all editor instances present
+ * on the page.
+ *
+ * // Load from the 'pastefromword' plugin 'filter' sub folder (custom.js file) using a path relative to the CKEditor installation folder.
+ * CKEDITOR.config.pasteFromWordCleanupFile = 'plugins/pastefromword/filter/custom.js';
+ *
+ * // Load from the 'pastefromword' plugin 'filter' sub folder (custom.js file) using a full path (including the CKEditor installation folder).
+ * CKEDITOR.config.pasteFromWordCleanupFile = '/ckeditor/plugins/pastefromword/filter/custom.js';
+ *
+ * // Load custom.js file from the 'customFilters' folder (located in server's root) using the full URL.
+ * CKEDITOR.config.pasteFromWordCleanupFile = 'http://my.example.com/customFilters/custom.js';
+ *
+ * @since 3.1.0
+ * @cfg {String} [pasteFromWordCleanupFile= + 'filter/default.js']
+ * @member CKEDITOR.config
+ */
+
+/**
+ * Flag decides whether embedding images pasted with Word content is enabled or not.
+ *
+ * **Note:** Please be aware that embedding images requires Clipboard API support, available only in modern browsers, that is indicated by
+ * {@link CKEDITOR.plugins.clipboard#isCustomDataTypesSupported} flag.
+ *
+ * // Disable embedding images pasted from Word.
+ * config.pasteFromWord_inlineImages = false;
+ *
+ * @since 4.8.0
+ * @cfg {Boolean} [pasteFromWord_inlineImages=true]
+ * @member CKEDITOR.config
+ */
+
+/**
+ * See {@link #pasteTools_keepZeroMargins}.
+ * @since 4.12.0
+ * @deprecated 4.13.0
+ * @cfg {Boolean} [pasteFromWord_keepZeroMargins=false]
+ * @member CKEDITOR.config
+ */
+
+/**
+ * Fired when the pasted content was recognized as Microsoft Word content.
+ *
+ * This event is cancellable. If canceled, it will prevent Paste from Word processing.
+ *
+ * @since 4.6.0
+ * @event pasteFromWord
+ * @param data
+ * @param {String} data.dataValue Pasted content. Changes to this property will affect the pasted content.
+ * @member CKEDITOR.editor
+ */
+
+/**
+ * Fired after the Paste form Word filters have been applied.
+ *
+ * @since 4.6.0
+ * @event afterPasteFromWord
+ * @param data
+ * @param {String} data.dataValue Pasted content after processing. Changes to this property will affect the pasted content.
+ * @member CKEDITOR.editor
+ */
diff --git a/museum_of_dreams_project/settings/base.py b/museum_of_dreams_project/settings/base.py
index 846d8f50..1202bdcf 100644
--- a/museum_of_dreams_project/settings/base.py
+++ b/museum_of_dreams_project/settings/base.py
@@ -128,7 +128,9 @@
"uploadimage",
"uploadwidget",
"mentions",
+ "footnotes",
"popout",
+ "pastefromword",
]
),
"toolbar": [
@@ -143,7 +145,7 @@
"Undo",
"Redo",
],
- ["Link", "Unlink", "Anchor"],
+ ["Link", "Unlink", "Anchor", "Footnotes", "FindFootnotes"],
["Image", "Flash", "Table", "HorizontalRule"],
["TextColor", "BGColor"],
["Smiley", "SpecialChar"],