├── .gitignore ├── pix ├── icon.png ├── logo.png └── icon.svg ├── .mdtconfig ├── amd ├── build │ ├── filepicker.min.js │ ├── imagepaster.min.js │ ├── filepicker.min.js.map │ ├── imagepaster.min.js.map │ ├── editor.min.js │ └── editor.min.js.map └── src │ ├── filepicker.js │ ├── imagepaster.js │ └── editor.js ├── styles.css ├── db └── services.php ├── version.php ├── README.md ├── .github └── workflows │ ├── moodle-release.yml │ └── moodle-ci.yml ├── classes ├── preferences_form.php ├── external │ └── get_preview.php └── privacy │ └── provider.php ├── preferences.php ├── tests ├── external │ └── get_preview_test.php └── privacy │ └── provider_test.php ├── CHANGES.md ├── lang └── en │ └── editor_marklar.php ├── lib.php └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /phpunit.xml 2 | /tests/phpunit.xml 3 | -------------------------------------------------------------------------------- /pix/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudrd8mz/moodle-editor_marklar/HEAD/pix/icon.png -------------------------------------------------------------------------------- /pix/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudrd8mz/moodle-editor_marklar/HEAD/pix/logo.png -------------------------------------------------------------------------------- /.mdtconfig: -------------------------------------------------------------------------------- 1 | MDTCOMPONENT=editor_marklar 2 | MDTRELPATH=lib/editor/marklar/ 3 | MDTEXPORT=origin/main 4 | -------------------------------------------------------------------------------- /amd/build/filepicker.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tiny AMD wrapper for the Moodle filepicker, based on Atto YUI module moodle-editor_atto-editor. 3 | * 4 | * @module editor_marklar/filepicker 5 | * @copyright 2016 David Mudrak 6 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 7 | */ 8 | define("editor_marklar/filepicker",["core/yui"],(function(Y){function EditorFilepicker(options){this.options=options}return EditorFilepicker.prototype.canShowFilepicker=function(type){return this.options&&void 0!==this.options[type]},EditorFilepicker.prototype.showFilepicker=function(type,callback,context){var self=this;Y.use("core_filepicker",(function(Y){var options=Y.clone(self.options[type],!0);options.formcallback=callback,context&&(options.magicscope=context),M.core_filepicker.show(Y,options)}))},{init:function(options){return new EditorFilepicker(options)}}})); 9 | 10 | //# sourceMappingURL=filepicker.min.js.map -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .marklar-wrapper { 2 | padding: 4px; 3 | background-color: #f2f2f2; 4 | border: 1px solid #bbb; 5 | } 6 | 7 | .marklar-panel .btn + .btn { 8 | margin-left: 0.25rem; 9 | } 10 | 11 | .marklar-panel::after { 12 | content: " "; 13 | display: block; 14 | height: 0; 15 | clear: both; 16 | } 17 | 18 | .marklar-wrapper [data-marklar-widget] { 19 | margin-bottom: 10px; 20 | } 21 | 22 | .marklar-preview { 23 | background-color: white; 24 | margin: 0 0 10px; 25 | border: 1px solid #ccc; 26 | padding: 7px; 27 | border-radius: 4px; 28 | overflow: auto; 29 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 30 | } 31 | 32 | .marklar-preview-controls { 33 | float: right; 34 | } 35 | 36 | .marklar-preview .marklar-preview-loading { 37 | display: inline-block; 38 | padding: 5px; 39 | color: #ccc; 40 | } 41 | 42 | .marklar-syntax-controls { 43 | display: inline-block; 44 | } 45 | 46 | .marklar-syntax-help { 47 | padding: 4px 10px; 48 | font-size: 80%; 49 | background-color: #f9f9f9; 50 | border: 1px solid #ccc; 51 | border-radius: 4px 4px 4px 4px; 52 | -moz-border-radius: 4px 4px 4px 4px; 53 | -webkit-border-radius: 4px 4px 4px 4px; 54 | } 55 | 56 | .marklar-syntax-help dd code { 57 | line-height: 23px; 58 | } 59 | -------------------------------------------------------------------------------- /db/services.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the list of the plugin's external services 19 | * 20 | * @package editor_marklar 21 | * @category external 22 | * @copyright 2016 David Mudrák 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | $functions = [ 29 | 'editor_marklar_get_preview' => [ 30 | 'classname' => 'editor_marklar\external\get_preview', 31 | 'methodname' => 'execute', 32 | 'description' => 'Returns the editor content as it will be displayed after saving the form', 33 | 'type' => 'read', 34 | 'ajax' => true, 35 | ], 36 | ]; 37 | -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Markdown friendly editor for Moodle. 19 | * 20 | * All editors marklar. This one just marklar less. It is not for everybody 21 | * though. Those who do not marklar about marklar should not marklar marklar. 22 | * There are other marklar better for them. 23 | * 24 | * @package editor_marklar 25 | * @copyright 2016 David Mudrak 26 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 | */ 28 | 29 | defined('MOODLE_INTERNAL') || die(); 30 | 31 | $plugin->component = 'editor_marklar'; 32 | $plugin->release = '1.0.2'; 33 | $plugin->maturity = MATURITY_STABLE; 34 | $plugin->version = 2024030603; 35 | $plugin->requires = 2023042400; 36 | $plugin->supported = [402, 405]; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Marklar - markdown friendly editor for Moodle 2 | ============================================= 3 | 4 | All online editors marklar. This one just marklar less. 5 | 6 | It is not for everybody though. Those who do not marklar about marklar should 7 | not marklar marklar. There are other marklar better for them. 8 | 9 | Usage 10 | ----- 11 | 12 | 1. As an admin, go to Site administration > Plugins > Text editors > Manage editors 13 | 2. Enable the Marklar editor 14 | 3. As the user, go to your Preferences > Editor preferences and set Marklar as your 15 | preferred editor. 16 | 4. Go to Preferences > Miscellaneous > Marklar editor preferences to eventually change 17 | the default behaviour of the editor. 18 | 19 | Features 20 | -------- 21 | 22 | Here are intentional features of the Marklar editor, just to make it clear they 23 | are not bugs. 24 | 25 | * Marklar is WYTIWYG - what you type is what you get (done). 26 | * Marklar does not have buttons. Markdown syntax is to be typed manually (done). 27 | * Marklar is not extensible via sub-plugins (done). 28 | * Marklar sets markdown as the preferred format for new texts (done). 29 | * Marklar can be also used for editing texts in other formats. Supported 30 | formats can be customised via user preferences (done). 31 | * Marklar is not used for editing HTML texts by default. Another editor such 32 | like Atto or TinyMCE is loaded to do that (done). 33 | * Marklar supports images and files embedding via filepicker (done). 34 | * Marklar supports server-side generated preview (done). 35 | * Marklar supports uploading images from clipboard (done). 36 | * Marklar will do just that and nothing more - 37 | [KISS principle](https://en.wikipedia.org/wiki/KISS_principle). 38 | 39 | Moodle compatibility 40 | -------------------- 41 | 42 | Marklar editor supports Moodle versions 3.0 and higher. 43 | -------------------------------------------------------------------------------- /amd/build/imagepaster.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows images to be pasted into textarea editor field. 3 | * 4 | * @module editor_marklar/imagepaster 5 | * @copyright 2018 David Mudrák 6 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 7 | */ 8 | define("editor_marklar/imagepaster",["jquery","core/log","core/config"],(function($,Log,Config){function ImagePaster(textarea,imagepickeroptions,callback){this.textarea=textarea,this.imagepickeroptions=imagepickeroptions,this.callback=callback,this.initPasteListener()}return ImagePaster.prototype.initPasteListener=function(){var self=this;self.textarea.on("paste",(function(e){for(var items=e.originalEvent.clipboardData.items,i=0;i> $GITHUB_OUTPUT 59 | 60 | - name: Evaluate the response 61 | id: evaluate-response 62 | env: 63 | RESPONSE: ${{ steps.add-version.outputs.response }} 64 | run: | 65 | jq <<< ${RESPONSE} 66 | jq --exit-status ".id" <<< ${RESPONSE} > /dev/null 67 | -------------------------------------------------------------------------------- /classes/preferences_form.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the editor_marklar_preferences_form class. 19 | * 20 | * @package editor_marklar 21 | * @copyright 2016 David Mudrák 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | require_once($CFG->dirroot.'/lib/formslib.php'); 28 | 29 | /** 30 | * Defines the Marklar editor user preferences form. 31 | * 32 | * @copyright 2016 David Mudrak 33 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 | */ 35 | class editor_marklar_preferences_form extends moodleform { 36 | 37 | /** 38 | * Defines the form fields. 39 | */ 40 | public function definition() { 41 | 42 | $mform = $this->_form; 43 | 44 | $mform->addGroup([ 45 | $mform->createElement('checkbox', 'format'.FORMAT_HTML, null, get_string('formathtml')), 46 | $mform->createElement('checkbox', 'format'.FORMAT_MOODLE, null, get_string('formattext')), 47 | $mform->createElement('checkbox', 'format'.FORMAT_PLAIN, null, get_string('formatplain')), 48 | ], 'formats', get_string('preferencesformat', 'editor_marklar'), '
', false); 49 | 50 | $mform->addHelpButton('formats', 'preferencesformat', 'editor_marklar'); 51 | $mform->setDefault('format'.FORMAT_HTML, 0); 52 | $mform->setDefault('format'.FORMAT_MOODLE, 1); 53 | $mform->setDefault('format'.FORMAT_PLAIN, 1); 54 | 55 | $mform->addGroup([ 56 | $mform->createElement('checkbox', 'monospace', get_string('preferencesmonospace', 'editor_marklar')), 57 | ], 'editing', get_string('preferencesediting', 'editor_marklar'), '
', false); 58 | 59 | if (!empty($this->_customdata['user'])) { 60 | $mform->addElement('hidden', 'userid', $this->_customdata['user']->id); 61 | $mform->setType('userid', PARAM_INT); 62 | } 63 | 64 | $this->add_action_buttons(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /preferences.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Marklar editor user preferences 19 | * 20 | * @package editor_marklar 21 | * @copyright 2016 David Mudrák 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require_once(__DIR__.'/../../../config.php'); 26 | require_once($CFG->dirroot.'/user/editlib.php'); 27 | 28 | require_login(SITEID, false); 29 | 30 | $userid = optional_param('userid', $USER->id, PARAM_INT); 31 | 32 | list($user, $course) = useredit_setup_preference_page($userid, SITEID); 33 | 34 | $PAGE->set_url('/lib/editor/marklar/preferences.php', ['id' => $userid]); 35 | $PAGE->navbar->includesettingsbase = true; 36 | $PAGE->set_title($course->shortname.': '.get_string('preferences', 'editor_marklar')); 37 | $PAGE->set_heading(fullname($user, true)); 38 | 39 | $form = new editor_marklar_preferences_form(null, ['user' => $user]); 40 | $data = [ 41 | 'monospace' => get_user_preferences('editor_marklar/monospace', false, $user), 42 | ]; 43 | 44 | $formats = json_decode(get_user_preferences('editor_marklar/formats', '', $user)); 45 | if (is_object($formats)) { 46 | $data = array_merge($data, (array)$formats); 47 | } 48 | 49 | $form->set_data($data); 50 | 51 | $redirect = new moodle_url('/user/preferences.php', ['userid' => $user->id]); 52 | 53 | if ($form->is_cancelled()) { 54 | redirect($redirect); 55 | 56 | } else if ($data = $form->get_data()) { 57 | $formats = [ 58 | 'format'.FORMAT_MOODLE => !empty($data->{'format'.FORMAT_MOODLE}), 59 | 'format'.FORMAT_HTML => !empty($data->{'format'.FORMAT_HTML}), 60 | 'format'.FORMAT_PLAIN => !empty($data->{'format'.FORMAT_PLAIN}), 61 | ]; 62 | set_user_preference('editor_marklar/formats', json_encode($formats), $user); 63 | set_user_preference('editor_marklar/monospace', !empty($data->monospace)); 64 | \core\event\user_updated::create_from_userid($user->id)->trigger(); 65 | redirect($redirect); 66 | } 67 | 68 | echo $OUTPUT->header(); 69 | echo $OUTPUT->heading(get_string('preferences', 'editor_marklar')); 70 | $form->display(); 71 | echo $OUTPUT->footer(); 72 | -------------------------------------------------------------------------------- /amd/src/filepicker.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | /** 17 | * Tiny AMD wrapper for the Moodle filepicker, based on Atto YUI module moodle-editor_atto-editor. 18 | * 19 | * @module editor_marklar/filepicker 20 | * @copyright 2016 David Mudrak 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | define(['core/yui'], function(Y) { 24 | 25 | "use strict"; 26 | 27 | /** 28 | * @constructor 29 | * @param {array} options List of filepicker options 30 | */ 31 | function EditorFilepicker(options) { 32 | this.options = options; 33 | } 34 | 35 | /** 36 | * Should we show the filepicker for the given type of content? 37 | * 38 | * @method canShowFilepicker 39 | * @param {string} type The content type for the file picker: image, link, media 40 | * @return {boolean} 41 | */ 42 | EditorFilepicker.prototype.canShowFilepicker = function(type) { 43 | return (this.options && (typeof this.options[type] !== 'undefined')); 44 | }; 45 | 46 | /** 47 | * Show the filepicker. 48 | * 49 | * This depends on core_filepicker, and then calls that module's show function. 50 | * 51 | * @method showFilepicker 52 | * @param {string} type The media type for the file picker. 53 | * @param {function} callback The callback to use when selecting an item of media. 54 | * @param {object} [context] The context from which to call the callback. 55 | */ 56 | EditorFilepicker.prototype.showFilepicker = function(type, callback, context) { 57 | var self = this; 58 | Y.use('core_filepicker', function(Y) { 59 | var options = Y.clone(self.options[type], true); 60 | options.formcallback = callback; 61 | if (context) { 62 | options.magicscope = context; 63 | } 64 | 65 | M.core_filepicker.show(Y, options); 66 | }); 67 | }; 68 | 69 | return /** @alias module:editor_marklar/filepicker */ { 70 | init: function(options) { 71 | return new EditorFilepicker(options); 72 | } 73 | }; 74 | }); 75 | -------------------------------------------------------------------------------- /tests/external/get_preview_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace editor_marklar\external; 18 | 19 | /** 20 | * Defines tests for the plugin external function editor_marklar_get_preview. 21 | * 22 | * @package editor_marklar 23 | * @covers \editor_marklar\external\get_preview 24 | * @copyright 2016 David Mudrak 25 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 26 | */ 27 | final class get_preview_test extends \advanced_testcase { 28 | 29 | /** 30 | * Test the editor_marklar_get_preview() external function. 31 | * 32 | * @runInSeparateProcess 33 | */ 34 | public function test_get_preview(): void { 35 | global $CFG; 36 | require_once($CFG->libdir.'/externallib.php'); 37 | 38 | $this->resetAfterTest(); 39 | 40 | $this->setUser($this->getDataGenerator()->create_user()); 41 | $syscontext = \context_system::instance(); 42 | 43 | $text = '## It works! ##'; 44 | $format = FORMAT_MARKDOWN; 45 | $contextid = $syscontext->id; 46 | 47 | $response = \core_external\external_api::clean_returnvalue(get_preview::execute_returns(), 48 | get_preview::execute($text, $format, $contextid)); 49 | 50 | $this->assertEquals('

It works!

', $response['html']); 51 | } 52 | 53 | /** 54 | * Test that draftfile.php links work in preview. 55 | * 56 | * @runInSeparateProcess 57 | */ 58 | public function test_embeded_images_preview(): void { 59 | global $CFG; 60 | require_once($CFG->libdir.'/externallib.php'); 61 | 62 | $this->resetAfterTest(); 63 | 64 | $this->setUser($this->getDataGenerator()->create_user()); 65 | $syscontext = \context_system::instance(); 66 | 67 | $text = '* '; 68 | $format = FORMAT_HTML; 69 | $contextid = $syscontext->id; 70 | 71 | $response = \core_external\external_api::clean_returnvalue(get_preview::execute_returns(), 72 | get_preview::execute($text, $format, $contextid)); 73 | 74 | $this->assertFalse(strpos($response['html'], 'brokenfile.php')); 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /pix/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 42 | 46 | 49 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /amd/build/filepicker.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"filepicker.min.js","sources":["../src/filepicker.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny AMD wrapper for the Moodle filepicker, based on Atto YUI module moodle-editor_atto-editor.\n *\n * @module editor_marklar/filepicker\n * @copyright 2016 David Mudrak \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['core/yui'], function(Y) {\n\n \"use strict\";\n\n /**\n * @constructor\n * @param {array} options List of filepicker options\n */\n function EditorFilepicker(options) {\n this.options = options;\n }\n\n /**\n * Should we show the filepicker for the given type of content?\n *\n * @method canShowFilepicker\n * @param {string} type The content type for the file picker: image, link, media\n * @return {boolean}\n */\n EditorFilepicker.prototype.canShowFilepicker = function(type) {\n return (this.options && (typeof this.options[type] !== 'undefined'));\n };\n\n /**\n * Show the filepicker.\n *\n * This depends on core_filepicker, and then calls that module's show function.\n *\n * @method showFilepicker\n * @param {string} type The media type for the file picker.\n * @param {function} callback The callback to use when selecting an item of media.\n * @param {object} [context] The context from which to call the callback.\n */\n EditorFilepicker.prototype.showFilepicker = function(type, callback, context) {\n var self = this;\n Y.use('core_filepicker', function(Y) {\n var options = Y.clone(self.options[type], true);\n options.formcallback = callback;\n if (context) {\n options.magicscope = context;\n }\n\n M.core_filepicker.show(Y, options);\n });\n };\n\n return /** @alias module:editor_marklar/filepicker */ {\n init: function(options) {\n return new EditorFilepicker(options);\n }\n };\n});\n"],"names":["define","Y","EditorFilepicker","options","prototype","canShowFilepicker","type","this","showFilepicker","callback","context","self","use","clone","formcallback","magicscope","M","core_filepicker","show","init"],"mappings":";;;;;;;AAsBAA,mCAAO,CAAC,aAAa,SAASC,YAQjBC,iBAAiBC,cACjBA,QAAUA,eAUnBD,iBAAiBE,UAAUC,kBAAoB,SAASC,aAC5CC,KAAKJ,cAA0C,IAAvBI,KAAKJ,QAAQG,OAajDJ,iBAAiBE,UAAUI,eAAiB,SAASF,KAAMG,SAAUC,aAC7DC,KAAOJ,KACXN,EAAEW,IAAI,mBAAmB,SAASX,OAC1BE,QAAUF,EAAEY,MAAMF,KAAKR,QAAQG,OAAO,GAC1CH,QAAQW,aAAeL,SACnBC,UACAP,QAAQY,WAAaL,SAGzBM,EAAEC,gBAAgBC,KAAKjB,EAAGE,aAIoB,CAClDgB,KAAM,SAAShB,gBACJ,IAAID,iBAAiBC"} -------------------------------------------------------------------------------- /tests/privacy/provider_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace editor_marklar\privacy; 18 | 19 | use core_privacy\local\metadata\collection; 20 | use core_privacy\local\request\writer; 21 | use core_privacy\local\request\approved_contextlist; 22 | use core_privacy\local\request\deletion_criteria; 23 | 24 | /** 25 | * Unit tests for the privacy API. 26 | * 27 | * @package editor_marklar 28 | * @covers \editor_marklar\privacy\provider 29 | * @copyright 2018 David Mudrak 30 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 31 | */ 32 | final class provider_test extends \advanced_testcase { 33 | 34 | /** 35 | * Assert that provider::get_metadata() returns valid content. 36 | */ 37 | public function test_get_metadata(): void { 38 | 39 | $items = new collection('editor_marklar'); 40 | $result = provider::get_metadata($items); 41 | $this->assertSame($items, $result); 42 | $this->assertInstanceOf(collection::class, $result); 43 | } 44 | 45 | /** 46 | * Assert that provider::export_user_preferences() gives no data if the user has no Marklar preferences. 47 | */ 48 | public function test_export_user_preferences_no_pref(): void { 49 | 50 | $user = \core_user::get_user_by_username('admin'); 51 | provider::export_user_preferences($user->id); 52 | $writer = writer::with_context(\context_system::instance()); 53 | 54 | $this->assertFalse($writer->has_any_data()); 55 | } 56 | 57 | 58 | /** 59 | * Assert that provider::export_user_preferences() gives user's Marklar preferences if they exist. 60 | */ 61 | public function test_export_user_preferences_has_pref(): void { 62 | $this->resetAfterTest(); 63 | 64 | $user = \core_user::get_user_by_username('admin'); 65 | $formats = [ 66 | 'format'.FORMAT_MOODLE => true, 67 | 'format'.FORMAT_HTML => false, 68 | 'format'.FORMAT_PLAIN => true, 69 | ]; 70 | set_user_preference('editor_marklar/formats', json_encode($formats), $user); 71 | 72 | provider::export_user_preferences($user->id); 73 | $writer = writer::with_context(\context_system::instance()); 74 | 75 | $this->assertTrue($writer->has_any_data()); 76 | 77 | $prefs = (array) $writer->get_user_preferences('editor_marklar'); 78 | 79 | $this->assertEquals(3, count($prefs)); 80 | $this->assertArrayHasKey('formats.format'.FORMAT_MOODLE, $prefs); 81 | $this->assertArrayHasKey('formats.format'.FORMAT_HTML, $prefs); 82 | $this->assertArrayHasKey('formats.format'.FORMAT_PLAIN, $prefs); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /classes/external/get_preview.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace editor_marklar\external; 18 | 19 | use context; 20 | use core_external\external_api; 21 | use core_external\external_function_parameters; 22 | use core_external\external_single_structure; 23 | use core_external\external_value; 24 | 25 | /** 26 | * Provides the plugin external functions. 27 | * 28 | * @package editor_marklar 29 | * @category external 30 | * @copyright 2016 David Mudrak 31 | * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 32 | */ 33 | class get_preview extends external_api { 34 | 35 | /** 36 | * Describes the input parameters. 37 | * 38 | * @return external_function_parameters 39 | */ 40 | public static function execute_parameters() { 41 | return new external_function_parameters([ 42 | 'text' => new external_value(PARAM_RAW, 'Input text', VALUE_REQUIRED), 43 | 'format' => new external_value(PARAM_INT, 'Format of the input text', VALUE_REQUIRED), 44 | 'contextid' => new external_value(PARAM_INT, 'Context where the text will appear', VALUE_REQUIRED), 45 | ]); 46 | } 47 | 48 | /** 49 | * Returns the formatted text for preview. 50 | * 51 | * @param string $text 52 | * @param int $format 53 | * @param int $contextid 54 | * @return string 55 | */ 56 | public static function execute($text, $format, $contextid) { 57 | global $CFG; 58 | 59 | // @codingStandardsIgnoreLine 60 | extract(self::validate_parameters(self::execute_parameters(), compact('text', 'format', 'contextid'))); 61 | 62 | $context = context::instance_by_id($contextid); 63 | self::validate_context($context); 64 | 65 | // We do not really know what exact options will be used. Guess 66 | // something reasonable for our purpose here. 67 | $options = [ 68 | 'context' => $context, 69 | 'para' => false, 70 | 'blanktarget' => true, 71 | ]; 72 | 73 | // Make sure the draftfile.php links are not replaced with brokenfile.php links. 74 | $text = str_replace($CFG->wwwroot.'/draftfile.php', '@@DRAFTFILE@@', $text); 75 | $html = format_text($text, $format, $options); 76 | $html = str_replace('@@DRAFTFILE@@', $CFG->wwwroot.'/draftfile.php', $html); 77 | 78 | return [ 79 | 'html' => $html, 80 | ]; 81 | } 82 | 83 | /** 84 | * Describes the returned value. 85 | * 86 | * @return external_single_structure 87 | */ 88 | public static function execute_returns() { 89 | return new external_single_structure([ 90 | 'html' => new external_value(PARAM_RAW, 'HTML formatted text to be displayed'), 91 | ]); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /classes/privacy/provider.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Provides the {@see editor_marklar\privacy\provider} class. 19 | * 20 | * @package editor_marklar 21 | * @category privacy 22 | * @copyright 2018 David Mudrák 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace editor_marklar\privacy; 27 | 28 | use core_privacy\local\request\writer; 29 | use core_privacy\local\metadata\collection; 30 | use core_privacy\local\request\transform; 31 | use core_privacy\local\legacy_polyfill; 32 | 33 | /** 34 | * Provides a response to a specific privacy related request from the user. 35 | * 36 | * @copyright 2018 David Mudrak 37 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 38 | */ 39 | class provider implements 40 | // This plugin stores user private data. 41 | \core_privacy\local\metadata\provider, 42 | 43 | // This plugin stores site-wide user preferences. 44 | \core_privacy\local\request\user_preference_provider { 45 | 46 | /** 47 | * Collect metadata describing personal data stored by the plugin. 48 | * 49 | * @param collection $collection 50 | * @return collection 51 | */ 52 | public static function get_metadata(collection $collection): collection { 53 | $collection->add_user_preference('editor_marklar/formats', 'preferencesformat'); 54 | $collection->add_user_preference('editor_marklar/monospace', 'preferencesmonospace'); 55 | return $collection; 56 | } 57 | 58 | /** 59 | * Export all user preferences controlled by the plugin. 60 | * 61 | * @param int $userid The id of the user whose data is to be exported. 62 | */ 63 | public static function export_user_preferences(int $userid) { 64 | 65 | $raw = (array) json_decode(get_user_preferences('editor_marklar/formats', '', $userid)); 66 | 67 | if (is_array($raw)) { 68 | foreach ($raw as $format => $enabled) { 69 | switch($format) { 70 | case 'format'.FORMAT_HTML: 71 | $name = get_string('formathtml'); 72 | break; 73 | case 'format'.FORMAT_MOODLE: 74 | $name = get_string('formattext'); 75 | break; 76 | case 'format'.FORMAT_PLAIN: 77 | $name = get_string('formatplain'); 78 | break; 79 | default: 80 | // This is not expected to happen. 81 | $name = $format; 82 | } 83 | 84 | $desc = get_string('privacy:export:preferences:format', 'editor_marklar', ['format' => $name]); 85 | 86 | writer::export_user_preference('editor_marklar', 'formats.'.$format, transform::yesno($enabled), $desc); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.github/workflows/moodle-ci.yml: -------------------------------------------------------------------------------- 1 | name: Moodle Plugin CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-22.04 8 | 9 | services: 10 | postgres: 11 | image: postgres:13 12 | env: 13 | POSTGRES_USER: 'postgres' 14 | POSTGRES_HOST_AUTH_METHOD: 'trust' 15 | ports: 16 | - 5432:5432 17 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 18 | 19 | mariadb: 20 | image: mariadb:10 21 | env: 22 | MYSQL_USER: 'root' 23 | MYSQL_ALLOW_EMPTY_PASSWORD: "true" 24 | MYSQL_CHARACTER_SET_SERVER: "utf8mb4" 25 | MYSQL_COLLATION_SERVER: "utf8mb4_unicode_ci" 26 | ports: 27 | - 3306:3306 28 | options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | php: ['8.1'] 34 | moodle-branch: ['MOODLE_402_STABLE', 'MOODLE_405_STABLE'] 35 | database: [pgsql] 36 | 37 | steps: 38 | - name: Check out repository code 39 | uses: actions/checkout@v4 40 | with: 41 | path: plugin 42 | 43 | - name: Setup PHP ${{ matrix.php }} 44 | uses: shivammathur/setup-php@v2 45 | with: 46 | php-version: ${{ matrix.php }} 47 | extensions: ${{ matrix.extensions }} 48 | ini-values: max_input_vars=5000 49 | # If you are not using code coverage, keep "none". Otherwise, use "pcov" (Moodle 3.10 and up) or "xdebug". 50 | # If you try to use code coverage with "none", it will fallback to phpdbg (which has known problems). 51 | coverage: none 52 | 53 | - name: Initialise moodle-plugin-ci 54 | run: | 55 | composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^4 56 | echo $(cd ci/bin; pwd) >> $GITHUB_PATH 57 | echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH 58 | sudo locale-gen en_AU.UTF-8 59 | echo "NVM_DIR=$HOME/.nvm" >> $GITHUB_ENV 60 | 61 | - name: Install moodle-plugin-ci 62 | run: moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 63 | env: 64 | DB: ${{ matrix.database }} 65 | MOODLE_BRANCH: ${{ matrix.moodle-branch }} 66 | # Uncomment this to run Behat tests using the Moodle App. 67 | # MOODLE_APP: 'true' 68 | 69 | - name: PHP Lint 70 | if: ${{ !cancelled() }} 71 | run: moodle-plugin-ci phplint 72 | 73 | - name: PHP Copy/Paste Detector 74 | continue-on-error: true # This step will show errors but will not fail 75 | if: ${{ !cancelled() }} 76 | run: moodle-plugin-ci phpcpd 77 | 78 | - name: PHP Mess Detector 79 | continue-on-error: true # This step will show errors but will not fail 80 | if: ${{ !cancelled() }} 81 | run: moodle-plugin-ci phpmd 82 | 83 | - name: Moodle Code Checker 84 | if: ${{ !cancelled() }} 85 | run: moodle-plugin-ci phpcs --max-warnings 0 86 | 87 | - name: Moodle PHPDoc Checker 88 | if: ${{ !cancelled() }} 89 | run: moodle-plugin-ci phpdoc --max-warnings 0 90 | 91 | - name: Validating 92 | if: ${{ !cancelled() }} 93 | run: moodle-plugin-ci validate 94 | 95 | - name: Check upgrade savepoints 96 | if: ${{ !cancelled() }} 97 | run: moodle-plugin-ci savepoints 98 | 99 | - name: Mustache Lint 100 | if: ${{ !cancelled() }} 101 | run: moodle-plugin-ci mustache 102 | 103 | - name: Grunt 104 | if: ${{ !cancelled() }} 105 | run: moodle-plugin-ci grunt --max-lint-warnings 0 106 | 107 | - name: PHPUnit tests 108 | if: ${{ !cancelled() }} 109 | run: moodle-plugin-ci phpunit --fail-on-warning 110 | 111 | - name: Behat features 112 | if: ${{ !cancelled() }} 113 | run: moodle-plugin-ci behat --profile chrome 114 | 115 | - name: Mark cancelled jobs as failed. 116 | if: ${{ cancelled() }} 117 | run: exit 1 118 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 1.0.2 ### 2 | 3 | * Fixed unexpected token error on pages with textareas with special chars in 4 | identifiers (#28). Credit goes to Juan Segarra Montesinos (@juancs). 5 | * Moodle 4.5 marked as supported. 6 | 7 | ### 1.0.1 ### 8 | 9 | * Removed obsolete option that started to print debugging output in Moodle 4.4 (#27). 10 | * Moodle 4.4 marked as supported. 11 | 12 | ### 1.0.0 ### 13 | 14 | * Added preference to use monospace font when editing the text (#11). 15 | * Fixed unit tests to work again. Credit goes to Juan Segarra Montesinos (@juancs). 16 | * Moodle 4.2 and higher is required. 17 | 18 | ### 1.0.0-beta.3 ### 19 | 20 | * Do not pass null as a string parameter to `json_decode()` to avoid deprecation warning. 21 | 22 | ### 1.0.0-beta.2 ### 23 | 24 | * Fixed "No permission to access this repository." error when browsing Content bank 25 | repository. Credit goes to Juan Segarra Montesinos (@juancs). 26 | 27 | ### 1.0.0-beta.1 ### 28 | 29 | * Fixed missing white space between the Insert image and the Insert file buttons. 30 | * The editor is now considered feature complete, releasing as the first beta. 31 | * Tested under Moodle 3.9. 32 | 33 | ### 0.9.0 ### 34 | 35 | * The master branch now contains the version with AMD modules compiled for Moodle 3.8. 36 | This will allow to start using ES6 features or APIs that require a polyfill. 37 | 38 | ### 0.8.x ### 39 | 40 | * The maintenance branch for Moodle 3.4 - 3.7 forked off as 0.8.x and uses the old way 41 | of building the AMD modules. 42 | 43 | ### 0.8.4 ### 44 | 45 | * Include the re-compiled AMD modules and the associated source maps. 46 | 47 | ### 0.8.3 ### 48 | 49 | * Stop using the httpswwwroot setting (deprecated in Moodle 3.4, removed in 3.8). 50 | * Perform travis-ci tests on PHP 7.2. 51 | * Supported are now Moodle versions 3.4 or higher. 52 | 53 | ### 0.8.2 ### 54 | 55 | * Notify filters about updated content in previews - makes filters like MathJax work 56 | then. Credit goes to Juan Segarra Montesinos (@juancs). 57 | 58 | ### 0.8.1 ### 59 | 60 | * Fixed reported eslint warnings. 61 | 62 | ### 0.8.0 ### 63 | 64 | * Allows to embed images from clipboard (such as screenshots). 65 | 66 | ### 0.7.0 ### 67 | 68 | * Implements the privacy API (GDPR compliance). 69 | * Supported are now Moodle versions 3.3 or higher. 70 | 71 | ### 0.6.2 ### 72 | 73 | * Fixed JS coding style warnings reported by eslint 74 | 75 | ### 0.6.1 ### 76 | 77 | * Only fixes the detected coding style issues. 78 | 79 | ### 0.6.0 ### 80 | 81 | * Displays quick reference for the currently selected format. 82 | * Internal refactoring and improvements in the JavaScript code. 83 | 84 | ### 0.5.1 ### 85 | 86 | * Fixed Moodle code precheck errors and warnings. 87 | * Tested functionality on Moodle 3.3beta. 88 | * Added travis checks against PHP 7.1. 89 | 90 | ### 0.5.0 ### 91 | 92 | * Do not throw database error when submitted in the preview mode (bug #9). 93 | * Add support for the Boost theme and generally make it less dependent on 94 | actual theme (bug #8). 95 | * Do not display it narrow in places like the Database module (bug #7). 96 | 97 | ### 0.4.3 ### 98 | 99 | * Fixed preview mode in clean-based themes in 3.2 (no boost support yet) 100 | * Format selector, image inserting button and file inserting button are now 101 | disabled in the preview mode. 102 | 103 | ### 0.4.2 ### 104 | 105 | * Marklar does not attempt to load when used outside of a moodle form. 106 | * Added self-check for the format selector presence before enabling the preview 107 | feature. 108 | 109 | ### 0.4.1 ### 110 | 111 | * Small CSS fixes on some themes. 112 | 113 | ### 0.4.0 ### 114 | 115 | * Added support for the preview of edited text. The preview is server-side 116 | generated so it correctly applies filters and other formatting rules. 117 | * Improved styling of the editor to make it look nicer next to file picker 118 | elements. 119 | 120 | ### 0.3.1 ### 121 | 122 | * The false debugging message "Too many params passed ..." not displayed any 123 | more (https://tracker.moodle.org/browse/MDL-53423). 124 | 125 | ### 0.3.0 ### 126 | 127 | * Added button to insert link to a file, credit goes to Juan Segarra Montesinos 128 | (Jaume I University) 129 | * Added user preferences page where any Marklar user can define additional 130 | supported Moodle formats to use Marklar with. 131 | 132 | ### 0.2.1 ### 133 | 134 | * Added editor icon and logo. 135 | 136 | ### 0.2.0 ### 137 | 138 | * Added support for embedding images via file picker. 139 | 140 | ### 0.1.0 ### 141 | 142 | * First functional version of a thin wrapper implementing the text editor 143 | interface. 144 | -------------------------------------------------------------------------------- /lang/en/editor_marklar.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Strings for the Marklar editor. 19 | * 20 | * @package editor_marklar 21 | * @category string 22 | * @copyright 2016 David Mudrak 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | $string['insertimage'] = 'Insert image'; 29 | $string['insertlink'] = 'Insert file'; 30 | $string['pluginname'] = 'Marklar'; 31 | $string['preferences'] = 'Marklar editor preferences'; 32 | $string['preferencesediting'] = 'Editing options'; 33 | $string['preferencesformat'] = 'Additional text formats to be also edited with Marklar'; 34 | $string['preferencesformat_help'] = 'Marklar natively supports Markdown formatted texts. It can be also used for editing texts in other formats. Select all additional text formats you want to edit with Marklar, too. 35 | 36 | By default, Marklar is used for Moodle auto-format and Plain text formatted fields. For editing HTML, another rich text editor (such as Atto or TinyMCE) will be used.'; 37 | $string['preferencesmonospace'] = 'Use monospace font'; 38 | $string['previewloading'] = 'Loading preview…'; 39 | $string['previewoff'] = 'Edit'; 40 | $string['previewon'] = 'Preview'; 41 | $string['privacy:export:preferences:format'] = 'Whether you prefer to use Marklar for editing texts with {$a->format} syntax.'; 42 | $string['syntax-format0'] = '

Moodle auto-format allows to type text normally, as if you were sending a plain-text email. Line breaks will be retained. You can still embed an HTML code if you want to and it will be applied.

'; 43 | $string['syntax-format1'] = '
44 |
Links
45 |
<a href="https://example.com">Link text</a>
46 |
Emphasis and importance
47 |
<em>Emphasized text</em>
48 |
<strong>Strongly important text</strong>
49 |
Headings
50 |
<h2>Level 2</h2>
51 |
<h3>Level 3</h2>
52 |
Paragraphs and line breaks
53 |
<p>Paragraph text</p>
54 |
Line<br>break
55 |
'; 56 | $string['syntax-format2'] = '

This format is useful when you need to include lots of code or HTML that you want to be displayed exactly as you wrote it. It still translates spaces and new lines, but otherwise your text isn\'t touched.

'; 57 | // @codingStandardsIgnoreStart 58 | $string['syntax-format4'] = '
59 |
Links
60 |
[link text](https://example.com)
61 |
[link text](https://example.com "Link title")
62 |
Emphasis and importance
63 |
_Emphasized text_
64 |
*Emphasized text*
65 |
__Strongly important text__
66 |
**Strongly important text**
67 |
Headings
68 |
## Level 2 ##
69 |
### Level 3 ###
70 |
Paragraphs and line breaks
71 |
Paragraphs are separated by a blank line. For a line break, end a line with two or more spaces.
72 |
Blockquotes
73 |
> Email-style of blockquoting
74 |
Lists
75 |
* Bullet list item
76 |
1. Numbered list item
77 |
Preformatted
78 |
`function_name()` (inline)
79 |
   code_block() (indent with four spaces)
80 |
Horizontal rule
81 |
---
82 |
***
83 |
HTML
84 |
For any markup that is not covered by Markdown syntax, simply use raw HTML.
85 |
<span class="badge badge-info">Notice</span>
86 |
<img class="img-responsive" src="…" alt="…" />
87 |
88 |
89 |

Full Markdown syntax documentation

'; 90 | // @codingStandardsIgnoreEnd 91 | $string['syntaxloading'] = 'Loading syntax help…'; 92 | $string['syntaxoff'] = 'Hide syntax'; 93 | $string['syntaxon'] = 'Show syntax'; 94 | -------------------------------------------------------------------------------- /lib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Text editor interface implementation. 19 | * 20 | * @package editor_marklar 21 | * @copyright 2016 David Mudrak 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | /** 26 | * Defines the Marklar editor behaviour in terms of Moodle text editor interface 27 | * 28 | * @copyright 2016 David Mudrak 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | class marklar_texteditor extends texteditor { 32 | 33 | /** 34 | * Is editor supported in current browser? 35 | * 36 | * @return bool 37 | */ 38 | public function supported_by_browser() { 39 | return true; 40 | } 41 | 42 | /** 43 | * Returns list of supported text formats. 44 | * 45 | * Marklar natively supports Markdown texts. As it is basically just a 46 | * textarea, we also declare it should be used for Moodle auto-formatted 47 | * and plain texts. For HTML, another rich text editor would be then used 48 | * (such as Atto). However, users can set this behaviour via their 49 | * preferences. 50 | * 51 | * @return array 52 | */ 53 | public function get_supported_formats() { 54 | 55 | // Marklar is always supported. 56 | $supported = [FORMAT_MARKDOWN => FORMAT_MARKDOWN]; 57 | 58 | // Other formats can be supported via user preferences. 59 | $formats = json_decode(get_user_preferences('editor_marklar/formats', '')); 60 | 61 | if (is_object($formats)) { 62 | if (!empty($formats->{'format'.FORMAT_MOODLE})) { 63 | $supported[FORMAT_MOODLE] = FORMAT_MOODLE; 64 | } 65 | if (!empty($formats->{'format'.FORMAT_HTML})) { 66 | $supported[FORMAT_HTML] = FORMAT_HTML; 67 | } 68 | if (!empty($formats->{'format'.FORMAT_PLAIN})) { 69 | $supported[FORMAT_PLAIN] = FORMAT_PLAIN; 70 | } 71 | 72 | } else { 73 | $supported[FORMAT_PLAIN] = FORMAT_PLAIN; 74 | $supported[FORMAT_MOODLE] = FORMAT_MOODLE; 75 | } 76 | 77 | return $supported; 78 | } 79 | 80 | /** 81 | * Returns main preferred text format. 82 | * 83 | * @return int text format 84 | */ 85 | public function get_preferred_format() { 86 | return FORMAT_MARKDOWN; 87 | } 88 | 89 | /** 90 | * Supports file picker and repos? 91 | * 92 | * @return bool 93 | */ 94 | public function supports_repositories() { 95 | return false; 96 | } 97 | 98 | /** 99 | * Add required JS needed for editor 100 | * 101 | * @param string $elementid id of text area to be converted to editor 102 | * @param array|null $options 103 | * @param object $fpoptions file picker options 104 | * @return void 105 | */ 106 | public function use_editor($elementid, array|null $options = null, $fpoptions = null) { 107 | global $PAGE; 108 | 109 | $initparams = [ 110 | 'elementid' => $elementid, 111 | 'contextid' => empty($options['context']) ? $PAGE->context->id : $options['context']->id, 112 | 'monospace' => !empty(get_user_preferences('editor_marklar/monospace')), 113 | ]; 114 | 115 | $PAGE->requires->js_call_amd('editor_marklar/editor', 'init', [$initparams]); 116 | 117 | // If we passed the $fpoptions via init's parameters, debugging warning 118 | // about the parameter size would be thrown. This is a really nasty 119 | // hack to work around that. See MDL-53423 for details. 120 | $PAGE->requires->js_init_code('M.editor_marklar = M.editor_marklar || {}'); 121 | $PAGE->requires->js_init_code('M.editor_marklar.fpoptions = M.editor_marklar.fpoptions || {}'); 122 | $PAGE->requires->js_init_code(js_writer::set_variable('M.editor_marklar.fpoptions['.json_encode($elementid).']', 123 | convert_to_array($fpoptions))); 124 | } 125 | } 126 | 127 | 128 | /** 129 | * Extends the user preferences page 130 | * 131 | * @param navigation_node $usersetting 132 | * @param stdClass $user 133 | * @param context_user $usercontext 134 | * @param stdClass $course 135 | * @param context_course $coursecontext 136 | */ 137 | function editor_marklar_extend_navigation_user_settings(navigation_node $usersetting, $user, context_user $usercontext, 138 | $course, context_course $coursecontext) { 139 | global $CFG; 140 | 141 | // Check if the user's preferred editor is Marklar. 142 | $preference = get_user_preferences('htmleditor', null, $user); 143 | 144 | // If the user's preferred editor is "default", check if it is Marklar. 145 | if (empty($preference) && !empty($CFG->texteditors)) { 146 | $editors = explode(',', $CFG->texteditors); 147 | if (reset($editors) === 'marklar') { 148 | $preference = 'marklar'; 149 | } 150 | } 151 | 152 | // If the user's preferred editor is Marklar, show a link to Marklar preferences page. 153 | if ($preference === 'marklar') { 154 | $prefurl = new moodle_url('/lib/editor/marklar/preferences.php', ['userid' => $user->id]); 155 | $usersetting->add(get_string('preferences', 'editor_marklar'), $prefurl); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /amd/src/imagepaster.js: -------------------------------------------------------------------------------- 1 | // This file is part of Moodle - http://moodle.org/ 2 | // 3 | // Moodle is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // Moodle is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License 14 | // along with Moodle. If not, see . 15 | 16 | /** 17 | * Allows images to be pasted into textarea editor field. 18 | * 19 | * @module editor_marklar/imagepaster 20 | * @copyright 2018 David Mudrák 21 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 22 | */ 23 | define(['jquery', 'core/log', 'core/config'], function($, Log, Config) { 24 | 25 | "use strict"; 26 | 27 | /** 28 | * Prepare a new image paster instance. 29 | * 30 | * @constructor 31 | * @param {jQuery} textarea - Editor's textarea element. 32 | * @param {Object} imagepickeroptions - Filepicker component used for uploading. 33 | * @param {function(Object)} callback - Function to run when the pasted image has been uploaded. 34 | */ 35 | function ImagePaster(textarea, imagepickeroptions, callback) { 36 | var self = this; 37 | 38 | self.textarea = textarea; 39 | self.imagepickeroptions = imagepickeroptions; 40 | self.callback = callback; 41 | 42 | self.initPasteListener(); 43 | } 44 | 45 | /** 46 | * Register a handler listening to the paste event in the textarea. 47 | */ 48 | ImagePaster.prototype.initPasteListener = function() { 49 | var self = this; 50 | 51 | self.textarea.on('paste', function(e) { 52 | var items = e.originalEvent.clipboardData.items; 53 | for (var i = 0; i < items.length; i++) { 54 | var item = items[i]; 55 | if (item.type.indexOf('image/') === 0) { 56 | self.uploadImage(item.getAsFile()); 57 | } 58 | } 59 | }); 60 | }; 61 | 62 | /** 63 | * Upload the pasted file to Moodle. 64 | * 65 | * @param {File} file - Pasted file. 66 | */ 67 | ImagePaster.prototype.uploadImage = function(file) { 68 | var self = this; 69 | 70 | var filename = (Math.random() * 1000).toString().replace('.', '') + '_' + file.name; 71 | var repositorykeys = window.Object.keys(self.imagepickeroptions.repositories); 72 | var formdata = new window.FormData(); 73 | var uploadrepofound = false; 74 | 75 | for (var i = 0; i < repositorykeys.length; i++) { 76 | if (self.imagepickeroptions.repositories[repositorykeys[i]].type === 'upload') { 77 | formdata.append('repo_id', self.imagepickeroptions.repositories[repositorykeys[i]].id); 78 | uploadrepofound = true; 79 | break; 80 | } 81 | } 82 | 83 | if (!uploadrepofound) { 84 | return; 85 | } 86 | 87 | formdata.append('repo_upload_file', file, filename); 88 | formdata.append('itemid', self.imagepickeroptions.itemid); 89 | formdata.append('author', self.imagepickeroptions.author); 90 | formdata.append('env', self.imagepickeroptions.env); 91 | formdata.append('sesskey', Config.sesskey); 92 | formdata.append('client_id', self.imagepickeroptions.client_id); 93 | 94 | if (self.imagepickeroptions.context.id) { 95 | formdata.append('ctx_id', self.imagepickeroptions.context.id); 96 | } 97 | 98 | $.ajax(Config.wwwroot + '/repository/repository_ajax.php?action=upload', { 99 | type: 'POST', 100 | data: formdata, 101 | dataType: 'json', 102 | processData: false, 103 | contentType: false, 104 | async: true 105 | }).done(function(res) { 106 | if ('error' in res) { 107 | Log.error('imagepaster: error uploading image: ' + res.errorcode + ': ' + res.error); 108 | } else { 109 | self.callback(res); 110 | } 111 | }).fail(function(error) { 112 | Log.error('imagepaster: error uploading image: ' + error.status + ' ' + error.statusText); 113 | }); 114 | }; 115 | 116 | return { 117 | /** 118 | * Initialize the image paster module. 119 | * 120 | * The callback will be run when the file is sucessfully uploaded. It will 121 | * be given the object returned by repository_upload::process_upload(): 122 | * - url: draftfile URL of the uploaded file 123 | * - id: itemid of the uploaded file 124 | * - file: name of the uploaded file 125 | * 126 | * @method 127 | * @param {jQuery|Element|string} textareaorid - Editor's textarea element or its id. 128 | * @param {Object} imagepickeroptions - Filepicker component used for uploading. 129 | * @param {function(Object)} callback - Function to run when the pasted image has been uploaded. 130 | * @returns {ImagePaster|bool} - ImagePaster instance or false on error. 131 | */ 132 | init: function(textareaorid, imagepickeroptions, callback) { 133 | 134 | var textarea; 135 | 136 | if (typeof textareaorid === 'string') { 137 | textarea = $(document.getElementById(textareaorid)); 138 | 139 | } else { 140 | textarea = $(textareaorid); 141 | } 142 | 143 | if (!textarea.length) { 144 | Log.error('imagepaster: invalid editor textarea element'); 145 | return false; 146 | } 147 | 148 | if (!imagepickeroptions) { 149 | Log.error('imagepaster: invalid image picker options'); 150 | return false; 151 | } 152 | 153 | if (!callback || typeof callback != 'function') { 154 | Log.error('imagepaster: invalid callback specified'); 155 | return false; 156 | } 157 | 158 | return new ImagePaster(textarea, imagepickeroptions, callback); 159 | } 160 | }; 161 | }); 162 | -------------------------------------------------------------------------------- /amd/build/imagepaster.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"imagepaster.min.js","sources":["../src/imagepaster.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Allows images to be pasted into textarea editor field.\n *\n * @module editor_marklar/imagepaster\n * @copyright 2018 David Mudrák \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/log', 'core/config'], function($, Log, Config) {\n\n \"use strict\";\n\n /**\n * Prepare a new image paster instance.\n *\n * @constructor\n * @param {jQuery} textarea - Editor's textarea element.\n * @param {Object} imagepickeroptions - Filepicker component used for uploading.\n * @param {function(Object)} callback - Function to run when the pasted image has been uploaded.\n */\n function ImagePaster(textarea, imagepickeroptions, callback) {\n var self = this;\n\n self.textarea = textarea;\n self.imagepickeroptions = imagepickeroptions;\n self.callback = callback;\n\n self.initPasteListener();\n }\n\n /**\n * Register a handler listening to the paste event in the textarea.\n */\n ImagePaster.prototype.initPasteListener = function() {\n var self = this;\n\n self.textarea.on('paste', function(e) {\n var items = e.originalEvent.clipboardData.items;\n for (var i = 0; i < items.length; i++) {\n var item = items[i];\n if (item.type.indexOf('image/') === 0) {\n self.uploadImage(item.getAsFile());\n }\n }\n });\n };\n\n /**\n * Upload the pasted file to Moodle.\n *\n * @param {File} file - Pasted file.\n */\n ImagePaster.prototype.uploadImage = function(file) {\n var self = this;\n\n var filename = (Math.random() * 1000).toString().replace('.', '') + '_' + file.name;\n var repositorykeys = window.Object.keys(self.imagepickeroptions.repositories);\n var formdata = new window.FormData();\n var uploadrepofound = false;\n\n for (var i = 0; i < repositorykeys.length; i++) {\n if (self.imagepickeroptions.repositories[repositorykeys[i]].type === 'upload') {\n formdata.append('repo_id', self.imagepickeroptions.repositories[repositorykeys[i]].id);\n uploadrepofound = true;\n break;\n }\n }\n\n if (!uploadrepofound) {\n return;\n }\n\n formdata.append('repo_upload_file', file, filename);\n formdata.append('itemid', self.imagepickeroptions.itemid);\n formdata.append('author', self.imagepickeroptions.author);\n formdata.append('env', self.imagepickeroptions.env);\n formdata.append('sesskey', Config.sesskey);\n formdata.append('client_id', self.imagepickeroptions.client_id);\n\n if (self.imagepickeroptions.context.id) {\n formdata.append('ctx_id', self.imagepickeroptions.context.id);\n }\n\n $.ajax(Config.wwwroot + '/repository/repository_ajax.php?action=upload', {\n type: 'POST',\n data: formdata,\n dataType: 'json',\n processData: false,\n contentType: false,\n async: true\n }).done(function(res) {\n if ('error' in res) {\n Log.error('imagepaster: error uploading image: ' + res.errorcode + ': ' + res.error);\n } else {\n self.callback(res);\n }\n }).fail(function(error) {\n Log.error('imagepaster: error uploading image: ' + error.status + ' ' + error.statusText);\n });\n };\n\n return {\n /**\n * Initialize the image paster module.\n *\n * The callback will be run when the file is sucessfully uploaded. It will\n * be given the object returned by repository_upload::process_upload():\n * - url: draftfile URL of the uploaded file\n * - id: itemid of the uploaded file\n * - file: name of the uploaded file\n *\n * @method\n * @param {jQuery|Element|string} textareaorid - Editor's textarea element or its id.\n * @param {Object} imagepickeroptions - Filepicker component used for uploading.\n * @param {function(Object)} callback - Function to run when the pasted image has been uploaded.\n * @returns {ImagePaster|bool} - ImagePaster instance or false on error.\n */\n init: function(textareaorid, imagepickeroptions, callback) {\n\n var textarea;\n\n if (typeof textareaorid === 'string') {\n textarea = $(document.getElementById(textareaorid));\n\n } else {\n textarea = $(textareaorid);\n }\n\n if (!textarea.length) {\n Log.error('imagepaster: invalid editor textarea element');\n return false;\n }\n\n if (!imagepickeroptions) {\n Log.error('imagepaster: invalid image picker options');\n return false;\n }\n\n if (!callback || typeof callback != 'function') {\n Log.error('imagepaster: invalid callback specified');\n return false;\n }\n\n return new ImagePaster(textarea, imagepickeroptions, callback);\n }\n };\n});\n"],"names":["define","$","Log","Config","ImagePaster","textarea","imagepickeroptions","callback","this","initPasteListener","prototype","self","on","e","items","originalEvent","clipboardData","i","length","item","type","indexOf","uploadImage","getAsFile","file","filename","Math","random","toString","replace","name","repositorykeys","window","Object","keys","repositories","formdata","FormData","uploadrepofound","append","id","itemid","author","env","sesskey","client_id","context","ajax","wwwroot","data","dataType","processData","contentType","async","done","res","error","errorcode","fail","status","statusText","init","textareaorid","document","getElementById"],"mappings":";;;;;;;AAsBAA,oCAAO,CAAC,SAAU,WAAY,gBAAgB,SAASC,EAAGC,IAAKC,iBAYlDC,YAAYC,SAAUC,mBAAoBC,UACpCC,KAENH,SAAWA,SAFLG,KAGNF,mBAAqBA,mBAHfE,KAIND,SAAWA,SAJLC,KAMNC,2BAMTL,YAAYM,UAAUD,kBAAoB,eAClCE,KAAOH,KAEXG,KAAKN,SAASO,GAAG,SAAS,SAASC,WAC3BC,MAAQD,EAAEE,cAAcC,cAAcF,MACjCG,EAAI,EAAGA,EAAIH,MAAMI,OAAQD,IAAK,KAC/BE,KAAOL,MAAMG,GACmB,IAAhCE,KAAKC,KAAKC,QAAQ,WAClBV,KAAKW,YAAYH,KAAKI,kBAWtCnB,YAAYM,UAAUY,YAAc,SAASE,cACrCb,KAAOH,KAEPiB,UAA4B,IAAhBC,KAAKC,UAAiBC,WAAWC,QAAQ,IAAK,IAAM,IAAML,KAAKM,KAC3EC,eAAiBC,OAAOC,OAAOC,KAAKvB,KAAKL,mBAAmB6B,cAC5DC,SAAW,IAAIJ,OAAOK,SACtBC,iBAAkB,EAEbrB,EAAI,EAAGA,EAAIc,eAAeb,OAAQD,OAC8B,WAAjEN,KAAKL,mBAAmB6B,aAAaJ,eAAed,IAAIG,KAAmB,CAC3EgB,SAASG,OAAO,UAAW5B,KAAKL,mBAAmB6B,aAAaJ,eAAed,IAAIuB,IACnFF,iBAAkB,QAKrBA,kBAILF,SAASG,OAAO,mBAAoBf,KAAMC,UAC1CW,SAASG,OAAO,SAAU5B,KAAKL,mBAAmBmC,QAClDL,SAASG,OAAO,SAAU5B,KAAKL,mBAAmBoC,QAClDN,SAASG,OAAO,MAAO5B,KAAKL,mBAAmBqC,KAC/CP,SAASG,OAAO,UAAWpC,OAAOyC,SAClCR,SAASG,OAAO,YAAa5B,KAAKL,mBAAmBuC,WAEjDlC,KAAKL,mBAAmBwC,QAAQN,IAChCJ,SAASG,OAAO,SAAU5B,KAAKL,mBAAmBwC,QAAQN,IAG9DvC,EAAE8C,KAAK5C,OAAO6C,QAAU,gDAAiD,CACrE5B,KAAM,OACN6B,KAAMb,SACNc,SAAU,OACVC,aAAa,EACbC,aAAa,EACbC,OAAO,IACRC,MAAK,SAASC,KACT,UAAWA,IACXrD,IAAIsD,MAAM,uCAAyCD,IAAIE,UAAY,KAAOF,IAAIC,OAE9E7C,KAAKJ,SAASgD,QAEnBG,MAAK,SAASF,OACbtD,IAAIsD,MAAM,uCAAyCA,MAAMG,OAAS,IAAMH,MAAMI,iBAI/E,CAgBHC,KAAM,SAASC,aAAcxD,mBAAoBC,cAEzCF,gBAGAA,SAAWJ,EADa,iBAAjB6D,aACMC,SAASC,eAAeF,cAGxBA,eAGH5C,OAKTZ,mBAKAC,UAA+B,mBAAZA,SAKjB,IAAIH,YAAYC,SAAUC,mBAAoBC,WAJjDL,IAAIsD,MAAM,4CACH,IANPtD,IAAIsD,MAAM,8CACH,IANPtD,IAAIsD,MAAM,iDACH"} -------------------------------------------------------------------------------- /amd/build/editor.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module editor_marklar/editor 3 | * @copyright 2016 David Mudrak 4 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 5 | */ 6 | define("editor_marklar/editor",["jquery","core/yui","core/str","core/log","core/ajax","core/event","editor_marklar/filepicker","editor_marklar/imagepaster"],(function($,Y,str,log,ajax,event,filepicker,ImagePaster){function MarklarEditor(textarea,initparams){this.initparams=initparams,void 0!==M.editor_marklar.fpoptions[initparams.elementid]&&(this.initparams.filepickeroptions=M.editor_marklar.fpoptions[initparams.elementid]),this.initTextArea(textarea),this.initFormatSelector(),this.initPanel(),this.initFilesEmbedding(),this.initPreview(),this.initSyntaxHelp(),this.initImagePaster()}return MarklarEditor.prototype.initTextArea=function(textarea){this.textarea=textarea.addClass("marklar-textarea").css("box-sizing","border-box").css("width","100%").css("background-color","white").css("margin-bottom","10px").css("padding","7px"),this.initparams.monospace&&this.textarea.css("font-family","monospace")},MarklarEditor.prototype.initFormatSelector=function(){var self=this,fname=self.textarea.attr("name").replace("[text]","[format]"),form=self.textarea.closest("form");if(fname!==self.textarea.attr("name")&&(self.formatSelector=form.find('select[name="'+fname+'"]'),!self.formatSelector.length)){var formatId,formatName,formatHidden=form.find('input[name="'+fname+'"]');if(formatHidden.length){switch(formatId=parseInt(formatHidden.attr("value"))){case 0:formatName="formattext";break;case 1:formatName="formathtml";break;case 2:formatName="formatplain";break;case 4:formatName="formatmarkdown";break;default:return log.error("marklar: unknown text format "+formatId),void(self.formatSelector=null)}self.formatSelector=$('').append($('")),formatHidden.remove(),str.get_string(formatName,"core_moodle").done((function(formatTitle){self.formatSelector.find('option[value="'+formatId+'"]').text(formatTitle)})).fail((function(ex){log.error(ex)}))}else log.error("marklar: format field not found: "+fname)}},MarklarEditor.prototype.initPanel=function(){this.textarea.wrap('
'),this.panel=$('
').insertAfter(this.textarea),this.editpanel=$('
').appendTo(this.panel),this.formatSelector&&(this.editpanel.append(this.formatSelector),this.formatSelector.attr("data-marklar-widget","format-select")),this.panel.prepend(''),this.editpanel.append(''),this.editpanel.append(''),this.editpanel.append('')},MarklarEditor.prototype.initFilesEmbedding=function(){if("filepickeroptions"in this.initparams){var self=this;Y.use("core_filepicker",(function(){self.filepicker=filepicker.init(self.initparams.filepickeroptions),self.filepicker.canShowFilepicker("image")&&str.get_string("insertimage","editor_marklar").done((function(strinsertimage){var button=$('