Merge branch 'MDL-47002-master' of git://github.com/merrill-oakland/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 23 Mar 2015 06:59:05 +0000 (14:59 +0800)
committerDavid Monllao <davidm@moodle.com>
Mon, 23 Mar 2015 06:59:05 +0000 (14:59 +0800)
179 files changed:
admin/tool/behat/cli/run.php
admin/user/user_bulk_cohortadd.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_stepslib.php
blocks/moodleblock.class.php
blocks/upgrade.txt
calendar/lib.php
calendar/tests/behat/calendar.feature
config-dist.php
course/modedit.php
course/modlib.php
course/moodleform_mod.php
enrol/cohort/edit.php
enrol/cohort/edit_form.php
enrol/meta/addinstance.php
enrol/meta/addinstance_form.php
filter/data/filter.php
filter/data/tests/filter_test.php [new file with mode: 0644]
grade/tests/behat/grade_to_pass.feature [new file with mode: 0644]
lang/en/enrol.php
lang/en/grades.php
lang/en/webservice.php
lib/ajax/service.php [new file with mode: 0644]
lib/amd/build/ajax.min.js [new file with mode: 0644]
lib/amd/build/config.min.js [new file with mode: 0644]
lib/amd/build/mustache.min.js [new file with mode: 0644]
lib/amd/build/notification.min.js [new file with mode: 0644]
lib/amd/build/str.min.js [new file with mode: 0644]
lib/amd/build/templates.min.js [new file with mode: 0644]
lib/amd/build/url.min.js [new file with mode: 0644]
lib/amd/build/yui.min.js [new file with mode: 0644]
lib/amd/src/ajax.js [new file with mode: 0644]
lib/amd/src/config.js [new file with mode: 0644]
lib/amd/src/first.js
lib/amd/src/mustache.js [new file with mode: 0644]
lib/amd/src/notification.js [new file with mode: 0644]
lib/amd/src/str.js [new file with mode: 0644]
lib/amd/src/templates.js [new file with mode: 0644]
lib/amd/src/url.js [new file with mode: 0644]
lib/amd/src/yui.js [new file with mode: 0644]
lib/behat/classes/behat_selectors.php
lib/blocklib.php
lib/classes/output/external.php [new file with mode: 0644]
lib/classes/output/mustache_filesystem_loader.php [new file with mode: 0644]
lib/classes/output/mustache_javascript_helper.php [new file with mode: 0644]
lib/classes/output/mustache_pix_helper.php [new file with mode: 0644]
lib/classes/output/mustache_string_helper.php [new file with mode: 0644]
lib/classes/output/mustache_uniqid_helper.php [new file with mode: 0644]
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/external/externallib.php
lib/externallib.php
lib/moodlelib.php
lib/mustache/CONTRIBUTING.md [new file with mode: 0644]
lib/mustache/LICENSE [new file with mode: 0644]
lib/mustache/README.md [new file with mode: 0644]
lib/mustache/composer.json [new file with mode: 0644]
lib/mustache/readme_moodle.txt [new file with mode: 0644]
lib/mustache/src/Mustache/Autoloader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/AbstractCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/FilesystemCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Cache/NoopCache.php [new file with mode: 0644]
lib/mustache/src/Mustache/Compiler.php [new file with mode: 0644]
lib/mustache/src/Mustache/Context.php [new file with mode: 0644]
lib/mustache/src/Mustache/Engine.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/InvalidArgumentException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/LogicException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/RuntimeException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/SyntaxException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownFilterException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownHelperException.php [new file with mode: 0644]
lib/mustache/src/Mustache/Exception/UnknownTemplateException.php [new file with mode: 0644]
lib/mustache/src/Mustache/HelperCollection.php [new file with mode: 0644]
lib/mustache/src/Mustache/LambdaHelper.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/ArrayLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/CascadingLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/FilesystemLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/InlineLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/MutableLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Loader/StringLoader.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger/AbstractLogger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Logger/StreamLogger.php [new file with mode: 0644]
lib/mustache/src/Mustache/Parser.php [new file with mode: 0644]
lib/mustache/src/Mustache/Template.php [new file with mode: 0644]
lib/mustache/src/Mustache/Tokenizer.php [new file with mode: 0644]
lib/outputcomponents.php
lib/outputfactories.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/setup.php
lib/tablelib.php
lib/templates/pix_icon.mustache [new file with mode: 0644]
lib/tests/behat/behat_general.php
lib/tests/tablelib_test.php
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/yui/build/moodle-core-handlebars/moodle-core-handlebars-debug.js
lib/yui/build/moodle-core-handlebars/moodle-core-handlebars-min.js
lib/yui/build/moodle-core-handlebars/moodle-core-handlebars.js
lib/yui/src/handlebars/js/handlebars.js
message/lib.php
mod/assign/feedback/editpdf/tests/behat/annotate_pdf.feature
mod/assign/gradingtable.php
mod/book/lib.php
mod/book/tests/lib_test.php [new file with mode: 0644]
mod/forum/classes/post_form.php
mod/forum/db/access.php
mod/forum/lang/en/forum.php
mod/forum/post.php
mod/forum/tests/behat/post_to_multiple_groups.feature [new file with mode: 0644]
mod/forum/tests/behat/separate_group_discussions.feature
mod/forum/tests/behat/separate_group_single_group_discussions.feature
mod/forum/version.php
mod/glossary/import.php
mod/glossary/lib.php
mod/lesson/backup/moodle2/backup_lesson_stepslib.php
mod/lesson/backup/moodle2/restore_lesson_stepslib.php
mod/lesson/db/install.xml
mod/lesson/db/upgrade.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/shortanswer.php
mod/lesson/renderer.php
mod/lesson/tests/behat/completion_condition_time_spent.feature [new file with mode: 0644]
mod/lesson/version.php
mod/lesson/view.php
mod/lti/view.php
mod/quiz/attemptlib.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/edit_rest.php
mod/quiz/lang/en/quiz.php
mod/quiz/repaginate.php
mod/quiz/styles.css
mod/quiz/tests/behat/attempt_require_previous.feature [new file with mode: 0644]
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/editing_require_previous.feature [new file with mode: 0644]
mod/quiz/tests/structure_test.php
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/section.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
mod/quiz/yui/src/util/js/slot.js
mod/scorm/locallib.php
mod/upgrade.txt
mod/wiki/files.php
mod/wiki/filesedit.php
mod/wiki/pagelib.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
mod/workshop/mod_form.php
question/behaviour/behaviourbase.php
question/behaviour/immediatefeedback/behaviour.php
question/behaviour/interactive/behaviour.php
question/behaviour/upgrade.txt
question/engine/questionattempt.php
question/engine/questionusage.php
question/type/multianswer/renderer.php
repository/filesystem/lib.php
version.php
webservice/renderer.php

index c83259f..92c393c 100644 (file)
@@ -128,7 +128,7 @@ array_walk($unrecognised, function (&$v) {
         $v = escapeshellarg($v);
     }
 });
-$extraopts = implode(' ', $unrecognised);
+$extraopts = $unrecognised;
 
 $tags = '';
 
@@ -139,10 +139,10 @@ if ($options['profile']) {
         exit(1);
     }
     $tags = $CFG->behat_config[$profile]['filters']['tags'];
-    $extraopts .= '--profile=\'' . $profile . "'";
+    $extraopts[] = '--profile=\'' . $profile . "'";
 } else if ($options['tags']) {
     $tags = $options['tags'];
-    $extraopts .= '--tags="' . $tags . '"';
+    $extraopts[] = '--tags="' . $tags . '"';
 }
 
 // Update config file if tags defined.
@@ -166,6 +166,7 @@ if ($tags) {
 }
 
 $cmds = array();
+$extraopts = implode(' ', $extraopts);
 echo "Running " . ($options['torun'] - $options['fromrun'] + 1) . " parallel behat sites:" . PHP_EOL;
 
 for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
index deb6143..958d6c1 100644 (file)
@@ -39,7 +39,8 @@ $users = $SESSION->bulk_users;
 $strnever = get_string('never');
 
 $cohorts = array(''=>get_string('choosedots'));
-$allcohorts = $DB->get_records('cohort');
+$allcohorts = $DB->get_records('cohort', null, 'name');
+
 foreach ($allcohorts as $c) {
     if (!empty($c->component)) {
         // external cohorts can not be modified
index e355871..124208a 100644 (file)
@@ -61,11 +61,8 @@ class restore_final_task extends restore_task {
             $this->add_step(new restore_grade_history_structure_step('grade_history', 'grade_history.xml'));
         }
 
-        // Course completion, executed conditionally if restoring to new course
-        if ($this->get_target() !== backup::TARGET_CURRENT_ADDING &&
-            $this->get_target() !== backup::TARGET_EXISTING_ADDING) {
-            $this->add_step(new restore_course_completion_structure_step('course_completion', 'completion.xml'));
-        }
+        // Course completion.
+        $this->add_step(new restore_course_completion_structure_step('course_completion', 'completion.xml'));
 
         // Conditionally restore course badges.
         if ($this->get_setting_value('badges')) {
index d779cfa..ca66102 100644 (file)
@@ -2453,11 +2453,12 @@ class restore_course_completion_structure_step extends restore_structure_step {
      *   2. The backup includes course completion information
      *   3. All modules are restorable
      *   4. All modules are marked for restore.
+     *   5. No completion criteria already exist for the course.
      *
      * @return bool True is safe to execute, false otherwise
      */
     protected function execute_condition() {
-        global $CFG;
+        global $CFG, $DB;
 
         // First check course completion is enabled on this site
         if (empty($CFG->enablecompletion)) {
@@ -2483,11 +2484,16 @@ class restore_course_completion_structure_step extends restore_structure_step {
             return false;
         }
 
-        // Finally check all modules within the backup are being restored.
+        // Check all modules within the backup are being restored.
         if ($this->task->is_excluding_activities()) {
             return false;
         }
 
+        // Check that no completion criteria is already set for the course.
+        if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) {
+            return false;
+        }
+
         return true;
     }
 
index 333893d..a8024a8 100644 (file)
@@ -343,10 +343,13 @@ class block_base {
      * Default behavior: save all variables as $CFG properties
      * You don't need to override this if you 're satisfied with the above
      *
+     * @deprecated since Moodle 2.9 MDL-49385 - Please use Admin Settings functionality to save block configuration.
+     * @todo MDL-49553 This will be deleted in Moodle 3.1
      * @param array $data
      * @return boolean
      */
     function config_save($data) {
+        debugging('config_save($data) is deprecated, use Admin Settings functionality to save block configuration.', DEBUG_DEVELOPER);
         foreach ($data as $name => $value) {
             set_config($name, $value);
         }
index 288eff3..573aaeb 100644 (file)
@@ -4,6 +4,7 @@ information provided here is intended especially for developers.
 === 2.9 ===
 
 * The obsolete method preferred_width() was removed (it was not doing anything)
+* Deprecated block_base::config_save as is not called anywhere and should not be used.
 
 === 2.8 ===
 
index 572238a..10805f6 100644 (file)
@@ -659,8 +659,14 @@ function calendar_add_event_metadata($event) {
         $event->courselink = calendar_get_courselink($event->courseid);
         $event->cssclass = 'calendar_event_course';
     } else if ($event->groupid) {                                    // Group event
-        $event->icon = '<img src="'.$OUTPUT->pix_url('i/groupevent') . '" alt="'.get_string('groupevent', 'calendar').'" class="icon" />';
-        $event->courselink = calendar_get_courselink($event->courseid);
+        if ($group = calendar_get_group_cached($event->groupid)) {
+            $groupname = format_string($group->name, true, context_course::instance($group->courseid));
+        } else {
+            $groupname = '';
+        }
+        $event->icon = html_writer::empty_tag('image', array('src' => $OUTPUT->pix_url('i/groupevent'),
+            'alt' => get_string('groupevent', 'calendar'), 'title' => $groupname, 'class' => 'icon'));
+        $event->courselink = calendar_get_courselink($event->courseid) . ', ' . $groupname;
         $event->cssclass = 'calendar_event_group';
     } else if($event->userid) {                                      // User event
         $event->icon = '<img src="'.$OUTPUT->pix_url('i/userevent') . '" alt="'.get_string('userevent', 'calendar').'" class="icon" />';
@@ -1415,6 +1421,20 @@ function calendar_get_course_cached(&$coursecache, $courseid) {
     return $coursecache[$courseid];
 }
 
+/**
+ * Get group from groupid for calendar display
+ *
+ * @param int $groupid
+ * @return stdClass group object with fields 'id', 'name' and 'courseid'
+ */
+function calendar_get_group_cached($groupid) {
+    static $groupscache = array();
+    if (!isset($groupscache[$groupid])) {
+        $groupscache[$groupid] = groups_get_group($groupid, 'id,name,courseid');
+    }
+    return $groupscache[$groupid];
+}
+
 /**
  * Returns the courses to load events for, the
  *
index 619590c..de0e752 100644 (file)
@@ -9,12 +9,20 @@ Feature: Perform basic calendar functionality
       | username | firstname | lastname | email |
       | student1 | Student | 1 | student1@asd.com |
       | student2 | Student | 2 | student2@asd.com |
+      | student3 | Student | 3 | student3@asd.com |
     And the following "courses" exist:
       | fullname | shortname | format |
       | Course 1 | C1 | topics |
     And the following "course enrolments" exist:
       | user | course | role |
       | student1 | C1 | student |
+      | student3 | C1 | student |
+    And the following "groups" exist:
+      | name | course | idnumber |
+      | Group 1 | C1 | G1 |
+    And the following "group members" exist:
+      | user | group |
+      | student1 | G1 |
     When I log in as "admin"
     And I follow "Course 1"
     And I turn editing mode on
@@ -50,6 +58,23 @@ Feature: Perform basic calendar functionality
     And I follow "This month"
     And I should not see "Really awesome event!"
 
+  Scenario: Create a group event
+    And I create a calendar event with form data:
+      | Type of event | group |
+      | Group | Group 1 |
+      | Event title | Really awesome event! |
+      | Description | Come join this awesome event |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "This month"
+    And I follow "Really awesome event!"
+    And "Group 1" "text" should exist in the ".eventlist" "css_element"
+    And I log out
+    And I log in as "student3"
+    And I follow "This month"
+    And I should not see "Really awesome event!"
+
   Scenario: Create a user event
     And I create a calendar event with form data:
       | Type of event | user |
index 716d3c5..9ba0d91 100644 (file)
@@ -346,15 +346,6 @@ $CFG->admin = 'admin';
 //       $CFG->forcefirstname = 'Bruce';
 //       $CFG->forcelastname  = 'Simpson';
 //
-// The following setting will turn SQL Error logging on. This will output an
-// entry in apache error log indicating the position of the error and the statement
-// called. This option will action disregarding error_reporting setting.
-//     $CFG->dblogerror = true;
-//
-// The following setting will log every database query to a table called adodb_logsql.
-// Use this setting on a development server only, the table grows quickly!
-//     $CFG->logsql = true;
-//
 // The following setting will turn on username logging into Apache log. For full details regarding setting
 // up of this function please refer to the install section of the document.
 //     $CFG->apacheloguser = 0; // Turn this feature off. Default value.
@@ -711,7 +702,7 @@ $CFG->admin = 'admin';
 // (the basic and behat_* ones) to avoid problems with production environments. This setting can be
 // used to expand the default white list with an array of extra settings.
 // Example:
-//   $CFG->behat_extraallowedsettings = array('logsql', 'dblogerror');
+//   $CFG->behat_extraallowedsettings = array('somecoresetting', ...);
 //
 // You should explicitly allow the usage of the deprecated behat steps, otherwise an exception will
 // be thrown when using them. The setting is disabled by default.
index 234e3ad..aaa87e9 100644 (file)
@@ -189,6 +189,10 @@ if (!empty($add)) {
                                              'iteminstance'=>$data->instance, 'courseid'=>$course->id))) {
         // add existing outcomes
         foreach ($items as $item) {
+            if (!empty($item->gradepass)) {
+                $decimalpoints = $item->get_decimals();
+                $data->gradepass = format_float($item->gradepass, $decimalpoints);
+            }
             if (!empty($item->outcomeid)) {
                 $data->{'outcome_'.$item->outcomeid} = 1;
             }
index 9a44714..ed653ce 100644 (file)
@@ -191,8 +191,16 @@ function edit_module_post_actions($moduleinfo, $course) {
     // Sync idnumber with grade_item.
     if ($hasgrades && $grade_item = grade_item::fetch(array('itemtype'=>'mod', 'itemmodule'=>$moduleinfo->modulename,
                  'iteminstance'=>$moduleinfo->instance, 'itemnumber'=>0, 'courseid'=>$course->id))) {
+        $gradeupdate = false;
         if ($grade_item->idnumber != $moduleinfo->cmidnumber) {
             $grade_item->idnumber = $moduleinfo->cmidnumber;
+            $gradeupdate = true;
+        }
+        if (isset($moduleinfo->gradepass) && $grade_item->gradepass != $moduleinfo->gradepass) {
+            $grade_item->gradepass = $moduleinfo->gradepass;
+            $gradeupdate = true;
+        }
+        if ($gradeupdate) {
             $grade_item->update();
         }
     }
index 6a3a845..ef638b3 100644 (file)
@@ -299,6 +299,22 @@ abstract class moodleform_mod extends moodleform {
             $errors['assessed'] = get_string('scaleselectionrequired', 'rating');
         }
 
+        // Grade to pass: ensure that the grade to pass is valid for points and scales.
+        // If we are working with a scale, convert into a positive number for validation.
+
+        if (isset($data['gradepass']) && (isset($data['grade']) || isset($data['scale']))) {
+            $scale = isset($data['grade']) ? $data['grade'] : $data['scale'];
+            if ($scale < 0) {
+                $scalevalues = $DB->get_record('scale', array('id' => -$scale));
+                $grade = count(explode(',', $scalevalues->scale));
+            } else {
+                $grade = $scale;
+            }
+            if ($data['gradepass'] > $grade) {
+                $errors['gradepass'] = get_string('gradepassgreaterthangrade', 'grades', $grade);
+            }
+        }
+
         // Completion: Don't let them choose automatic completion without turning
         // on some conditions. Ignore this check when completion settings are
         // locked, as the options are then disabled.
@@ -624,6 +640,9 @@ abstract class moodleform_mod extends moodleform {
                     $mform->addElement('select', 'advancedgradingmethod_'.$areaname,
                         get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods']);
                     $mform->addHelpButton('advancedgradingmethod_'.$areaname, 'gradingmethod', 'core_grading');
+                    if (!$this->_features->rating) {
+                        $mform->disabledIf('advancedgradingmethod_'.$areaname, 'grade[modgrade_type]', 'eq', 'none');
+                    }
 
                 } else {
                     // the module defines multiple gradable areas, display a selector
@@ -644,6 +663,19 @@ abstract class moodleform_mod extends moodleform {
                         get_string('gradecategoryonmodform', 'grades'),
                         grade_get_categories_menu($COURSE->id, $this->_outcomesused));
                 $mform->addHelpButton('gradecat', 'gradecategoryonmodform', 'grades');
+                if (!$this->_features->rating) {
+                    $mform->disabledIf('gradecat', 'grade[modgrade_type]', 'eq', 'none');
+                }
+            }
+
+            // Grade to pass.
+            $mform->addElement('text', 'gradepass', get_string('gradepass', 'grades'));
+            $mform->addHelpButton('gradepass', 'gradepass', 'grades');
+            $mform->setDefault('gradepass', '');
+            $mform->setType('gradepass', PARAM_FLOAT);
+            $mform->addRule('gradepass', null, 'numeric', null, 'client');
+            if (!$this->_features->rating) {
+                $mform->disabledIf('gradepass', 'grade[modgrade_type]', 'eq', 'none');
             }
         }
     }
index 050bdff..02db3a1 100644 (file)
@@ -29,6 +29,7 @@ require_once("$CFG->dirroot/group/lib.php");
 
 $courseid = required_param('courseid', PARAM_INT);
 $instanceid = optional_param('id', 0, PARAM_INT);
+$message = optional_param('message', null, PARAM_TEXT);
 
 $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
 $context = context_course::instance($course->id, MUST_EXIST);
@@ -91,6 +92,10 @@ if ($mform->is_cancelled()) {
         $DB->update_record('enrol', $instance);
     }  else {
         $enrol->add_instance($course, array('name'=>$data->name, 'status'=>$data->status, 'customint1'=>$data->customint1, 'roleid'=>$data->roleid, 'customint2'=>$data->customint2));
+        if (!empty($data->submitbuttonnext)) {
+            $returnurl = new moodle_url($PAGE->url);
+            $returnurl->param('message', 'added');
+        }
     }
     $trace = new null_progress_trace();
     enrol_cohort_sync($trace, $course->id);
@@ -102,5 +107,8 @@ $PAGE->set_heading($course->fullname);
 $PAGE->set_title(get_string('pluginname', 'enrol_cohort'));
 
 echo $OUTPUT->header();
+if ($message === 'added') {
+    echo $OUTPUT->notification(get_string('instanceadded', 'enrol'), 'notifysuccess');
+}
 $mform->display();
 echo $OUTPUT->footer();
index c7d0e2a..bb5ee8a 100644 (file)
@@ -31,7 +31,7 @@ class enrol_cohort_edit_form extends moodleform {
     function definition() {
         global $CFG, $DB;
 
-        $mform  = $this->_form;
+        $mform = $this->_form;
 
         list($instance, $plugin, $course) = $this->_customdata;
         $coursecontext = context_course::instance($course->id);
@@ -97,12 +97,25 @@ class enrol_cohort_edit_form extends moodleform {
         if ($instance->id) {
             $this->add_action_buttons(true);
         } else {
-            $this->add_action_buttons(true, get_string('addinstance', 'enrol'));
+            $this->add_add_buttons();
         }
 
         $this->set_data($instance);
     }
 
+    /**
+     * Adds buttons on create new method form
+     */
+    protected function add_add_buttons() {
+        $mform = $this->_form;
+        $buttonarray = array();
+        $buttonarray[0] = $mform->createElement('submit', 'submitbutton', get_string('addinstance', 'enrol'));
+        $buttonarray[1] = $mform->createElement('submit', 'submitbuttonnext', get_string('addinstanceanother', 'enrol'));
+        $buttonarray[2] = $mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+    }
+
     function validation($data, $files) {
         global $DB;
 
index ac2f2ff..3ded040 100644 (file)
@@ -27,6 +27,7 @@ require_once("$CFG->dirroot/enrol/meta/addinstance_form.php");
 require_once("$CFG->dirroot/enrol/meta/locallib.php");
 
 $id = required_param('id', PARAM_INT); // course id
+$message = optional_param('message', null, PARAM_TEXT);
 
 $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
 $context = context_course::instance($course->id, MUST_EXIST);
@@ -52,7 +53,12 @@ if ($mform->is_cancelled()) {
 } else if ($data = $mform->get_data()) {
     $eid = $enrol->add_instance($course, array('customint1'=>$data->link));
     enrol_meta_sync($course->id);
-    redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id)));
+    if (!empty($data->submitbuttonnext)) {
+        redirect(new moodle_url('/enrol/meta/addinstance.php',
+                array('id' => $course->id, 'message' => 'added')));
+    } else {
+        redirect(new moodle_url('/enrol/instances.php', array('id' => $course->id)));
+    }
 }
 
 $PAGE->set_heading($course->fullname);
@@ -60,6 +66,10 @@ $PAGE->set_title(get_string('pluginname', 'enrol_meta'));
 
 echo $OUTPUT->header();
 
+if ($message === 'added') {
+    echo $OUTPUT->notification(get_string('instanceadded', 'enrol'), 'notifysuccess');
+}
+
 $mform->display();
 
 echo $OUTPUT->footer();
index ecc3f54..fb4b748 100644 (file)
@@ -72,11 +72,24 @@ class enrol_meta_addinstance_form extends moodleform {
         $mform->addElement('hidden', 'id', null);
         $mform->setType('id', PARAM_INT);
 
-        $this->add_action_buttons(true, get_string('addinstance', 'enrol'));
+        $this->add_add_buttons();
 
         $this->set_data(array('id'=>$course->id));
     }
 
+    /**
+     * Adds buttons on create new method form
+     */
+    protected function add_add_buttons() {
+        $mform = $this->_form;
+        $buttonarray = array();
+        $buttonarray[0] = $mform->createElement('submit', 'submitbutton', get_string('addinstance', 'enrol'));
+        $buttonarray[1] = $mform->createElement('submit', 'submitbuttonnext', get_string('addinstanceanother', 'enrol'));
+        $buttonarray[2] = $mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+    }
+
     function validation($data, $files) {
         global $DB, $CFG;
 
index e703d92..4eabac0 100644 (file)
@@ -32,26 +32,36 @@ defined('MOODLE_INTERNAL') || die();
 class filter_data extends moodle_text_filter {
 
     public function filter($text, array $options = array()) {
-        global $CFG, $DB;
+        global $CFG, $DB, $USER;
 
-        // Trivial-cache - keyed on $cachedcontextid
-        static $cachedcontextid;
-        static $contentlist;
+        // Trivial-cache - keyed on $cachedcourseid + $cacheduserid.
+        static $cachedcourseid = null;
+        static $cacheduserid = null;
+        static $coursecontentlist = array();
+        static $sitecontentlist = array();
 
         static $nothingtodo;
 
         // Try to get current course.
         $coursectx = $this->context->get_course_context(false);
         if (!$coursectx) {
+            // We could be in a course category so no entries for courseid == 0 will be found.
             $courseid = 0;
         } else {
             $courseid = $coursectx->instanceid;
         }
 
-        // Initialise/invalidate our trivial cache if dealing with a different context
-        if (!isset($cachedcontextid) || $cachedcontextid !== $this->context->id) {
-            $cachedcontextid = $this->context->id;
-            $contentlist = array();
+        if ($cacheduserid !== $USER->id) {
+            // Invalidate all caches if the user changed.
+            $coursecontentlist = array();
+            $sitecontentlist = array();
+            $cacheduserid = $USER->id;
+            $cachedcourseid = $courseid;
+            $nothingtodo = false;
+        } else if ($courseid != get_site()->id && $courseid != 0 && $cachedcourseid != $courseid) {
+            // Invalidate course-level caches if the course id changed.
+            $coursecontentlist = array();
+            $cachedcourseid = $courseid;
             $nothingtodo = false;
         }
 
@@ -59,6 +69,13 @@ class filter_data extends moodle_text_filter {
             return $text;
         }
 
+        // If courseid == 0 only site entries will be returned.
+        if ($courseid == get_site()->id || $courseid == 0) {
+            $contentlist = & $sitecontentlist;
+        } else {
+            $contentlist = & $coursecontentlist;
+        }
+
         // Create a list of all the resources to search for. It may be cached already.
         if (empty($contentlist)) {
             $coursestosearch = $courseid ? array($courseid) : array(); // Add courseid if found
diff --git a/filter/data/tests/filter_test.php b/filter/data/tests/filter_test.php
new file mode 100644 (file)
index 0000000..17c279c
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests.
+ *
+ * @package filter_data
+ * @category test
+ * @copyright 2015 David Monllao
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/filter/data/filter.php');
+
+/**
+ * Tests for filter_data.
+ *
+ * @package filter_data
+ * @copyright 2015 David Monllao
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filter_data_filter_testcase extends advanced_testcase {
+
+    /**
+     * Tests that the filter applies the required changes.
+     *
+     * @return void
+     */
+    public function test_filter() {
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        filter_manager::reset_caches();
+
+        filter_set_global_state('data', TEXTFILTER_ON);
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $coursecontext1 = context_course::instance($course1->id);
+
+        $course2 = $this->getDataGenerator()->create_course();
+        $coursecontext2 = context_course::instance($course2->id);
+
+        $sitecontext = context_course::instance(SITEID);
+
+        $site = get_site();
+        $this->add_simple_database_instance($site, array('SiteEntry'));
+        $this->add_simple_database_instance($course1, array('CourseEntry'));
+
+        $html = '<p>I like CourseEntry and SiteEntry</p>';
+
+        // Testing at course level (both site and course).
+        $filtered = format_text($html, FORMAT_HTML, array('context' => $coursecontext1));
+        $this->assertRegExp('/title=(\'|")CourseEntry(\'|")/', $filtered);
+        $this->assertRegExp('/title=(\'|")SiteEntry(\'|")/', $filtered);
+
+        // Testing at site level (only site).
+        $filtered = format_text($html, FORMAT_HTML, array('context' => $sitecontext));
+        $this->assertNotRegExp('/title=(\'|")CourseEntry(\'|")/', $filtered);
+        $this->assertRegExp('/title=(\'|")SiteEntry(\'|")/', $filtered);
+
+        // Changing to another course to test the caches invalidation (only site).
+        $filtered = format_text($html, FORMAT_HTML, array('context' => $coursecontext2));
+        $this->assertNotRegExp('/title=(\'|")CourseEntry(\'|")/', $filtered);
+        $this->assertRegExp('/title=(\'|")SiteEntry(\'|")/', $filtered);
+    }
+
+    /**
+     * Adds a database instance to the provided course + a text field + adds all attached entries.
+     *
+     * @param stdClass $course
+     * @param array $entries A list of entry names.
+     * @return void
+     */
+    protected function add_simple_database_instance($course, $entries = false) {
+        global $DB;
+
+        $database = $this->getDataGenerator()->create_module('data',
+                array('course' => $course->id));
+
+        // A database field.
+        $field = data_get_field_new('text', $database);
+        $fielddetail = new stdClass();
+        $fielddetail->d = $database->id;
+        $fielddetail->mode = 'add';
+        $fielddetail->type = 'text';
+        $fielddetail->sesskey = sesskey();
+        $fielddetail->name = 'Name';
+        $fielddetail->description = 'Some name';
+        $fielddetail->param1 = '1';
+        $field->define_field($fielddetail);
+        $field->insert_field();
+        $recordid = data_add_record($database);
+
+        // Database entries.
+        foreach ($entries as $entrytext) {
+            $datacontent = array();
+            $datacontent['fieldid'] = $field->field->id;
+            $datacontent['recordid'] = $recordid;
+            $datacontent['content'] = $entrytext;
+            $contentid = $DB->insert_record('data_content', $datacontent);
+        }
+    }
+}
diff --git a/grade/tests/behat/grade_to_pass.feature b/grade/tests/behat/grade_to_pass.feature
new file mode 100644 (file)
index 0000000..8ac6e30
--- /dev/null
@@ -0,0 +1,251 @@
+@core @core_grades
+Feature: We can set the grade to pass value
+  In order to set the grade to pass value
+  As a teacher
+  I assign a grade to pass to an activity while editing the activity.
+  I need to ensure that the grade to pass is visible in the gradebook.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | format | numsections |
+      | Course 1 | C1 | weeks | 5 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And the following "scales" exist:
+      | name | scale |
+      | Test Scale 1 | Disappointing, Good, Very good, Excellent |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+
+  @javascript
+  Scenario: Validate that switching the type of grading used correctly disables grade to pass
+    When I turn editing mode on
+    And I add a "Assignment" to section "1"
+    And I expand all fieldsets
+    And I set the field "grade[modgrade_type]" to "Point"
+    Then the "Grade to pass" "field" should be enabled
+    And I set the field "grade[modgrade_type]" to "None"
+    And the "Grade to pass" "field" should be disabled
+    And I press "Save and return to course"
+
+  @javascript
+  Scenario: Create an activity with a Grade to pass value greater than the maximum grade
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | grade[modgrade_type] | Point |
+      | grade[modgrade_point] | 50 |
+      | Grade to pass | 100 |
+    Then I should see "The grade to pass can not be greater than the maximum possible grade 50"
+    And I press "Cancel"
+
+  @javascript
+  Scenario: Set a valid grade to pass for an assignment activity using points
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | grade[modgrade_type] | Point |
+      | grade[modgrade_point] | 50 |
+      | Grade to pass | 25 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  assign Test Assignment 1" "link"
+    Then the field "Grade to pass" matches value "25"
+    And I follow "Course 1"
+    And I follow "Test Assignment 1"
+    And I follow "Edit settings"
+    And I expand all fieldsets
+    And I set the field "Grade to pass" to "30"
+    And I press "Save and return to course"
+    And I follow "Grades"
+    And I click on "Edit  assign Test Assignment 1" "link"
+    And the field "Grade to pass" matches value "30"
+
+  @javascript
+  Scenario: Set a valid grade to pass for an assignment activity using scales
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | grade[modgrade_type] | Scale |
+      | grade[modgrade_scale] | Test Scale 1 |
+      | Grade to pass | 3 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  assign Test Assignment 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "3"
+    And I set the field "Grade to pass" to "4"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Assignment 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "4"
+
+  @javascript
+  Scenario: Set a invalid grade to pass for an assignment activity using scales
+    When I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test Assignment 1 |
+      | Description | Submit your online text |
+      | grade[modgrade_type] | Scale |
+      | grade[modgrade_scale] | Test Scale 1 |
+      | Grade to pass | 10 |
+    Then I should see "The grade to pass can not be greater than the maximum possible grade 4"
+
+  @javascript
+  Scenario: Set a valid grade to pass for workshop activity
+    When I turn editing mode on
+    And I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Test Workshop 1 |
+      | Description | Test workshop |
+      | grade | 80 |
+      | Submission grade to pass | 40 |
+      | gradinggrade | 20 |
+      | Assessment grade to pass | 10 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  workshop Test Workshop 1 (submission)" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "40"
+    And I set the field "Grade to pass" to "45"
+    And I press "Save changes"
+    And I click on "Edit  workshop Test Workshop 1 (assessment)" "link"
+    And I follow "Show more..."
+    And the field "Grade to pass" matches value "10"
+    And I set the field "Grade to pass" to "15"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Workshop 1"
+    And I follow "Edit settings"
+    And the field "Submission grade to pass" matches value "45"
+    And the field "Assessment grade to pass" matches value "15"
+
+  @javascript
+  Scenario: Set an invalid grade to pass for workshop activity
+    When I turn editing mode on
+    And I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Test Workshop 1 |
+      | Description | Test workshop |
+      | grade | 80 |
+      | Submission grade to pass | 90 |
+      | gradinggrade | 20 |
+      | Assessment grade to pass | 30 |
+    Then "The grade to pass can not be greater than the maximum possible grade 80" "text" should exist in the "#fitem_id_submissiongradepass .error" "css_element"
+    Then "The grade to pass can not be greater than the maximum possible grade 20" "text" should exist in the "#fitem_id_gradinggradepass .error" "css_element"
+
+  @javascript
+  Scenario: Set a valid grade to pass for quiz activity
+    When I turn editing mode on
+    And I add a "Quiz" to section "1" and I fill the form with:
+      | Name | Test Quiz 1 |
+      | Grade to pass | 9.5 |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  quiz Test Quiz 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "9.5"
+    And I set the field "Grade to pass" to "8"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Quiz 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "8.00"
+
+  @javascript
+  Scenario: Set a valid grade to pass for lesson activity
+    When I turn editing mode on
+    And I add a "Lesson" to section "1" and I fill the form with:
+      | Name          | Test Lesson 1 |
+      | Description   | Test          |
+      | Grade to pass | 90            |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  lesson Test Lesson 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Lesson 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
+
+  @javascript
+  Scenario: Set a valid grade to pass for database activity
+    When I turn editing mode on
+    And I add a "Database" to section "1" and I fill the form with:
+      | Name           | Test Database 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  data Test Database 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Database 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
+
+  @javascript
+  Scenario: Set an invalid grade to pass for forum activity
+    When I turn editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name     | Test Forum 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+      | scale[modgrade_point] | 60 |
+    Then I should see "The grade to pass can not be greater than the maximum possible grade 60"
+
+  @javascript
+  Scenario: Set a valid grade to pass for forum activity
+    When I turn editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name     | Test Forum 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  forum Test Forum 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Forum 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
+
+  @javascript
+  Scenario: Set a valid grade to pass for glossary activity
+    When I turn editing mode on
+    And I add a "Glossary" to section "1" and I fill the form with:
+      | Name           | Test Glossary 1    |
+      | Description    | Test               |
+      | Grade to pass  | 90                 |
+      | Aggregate type | Average of ratings |
+    And I follow "Grades"
+    And I turn editing mode on
+    And I click on "Edit  glossary Test Glossary 1" "link"
+    And I follow "Show more..."
+    Then the field "Grade to pass" matches value "90"
+    And I set the field "Grade to pass" to "80"
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Test Glossary 1"
+    And I follow "Edit settings"
+    And the field "Grade to pass" matches value "80"
index 2da9eee..b0498eb 100644 (file)
@@ -25,6 +25,7 @@
 
 $string['actenrolshhdr'] = 'Available course enrolment plugins';
 $string['addinstance'] = 'Add method';
+$string['addinstanceanother'] = 'Add method and create another';
 $string['ajaxoneuserfound'] = '1 user found';
 $string['ajaxxusersfound'] = '{$a} users found';
 $string['ajaxnext25'] = 'Next 25...';
@@ -83,6 +84,7 @@ $string['expirythreshold'] = 'Notification threshold';
 $string['expirythreshold_help'] = 'How long before enrolment expiry should users be notified?';
 $string['finishenrollingusers'] = 'Finish enrolling users';
 $string['foundxcohorts'] = 'Found {$a} cohorts';
+$string['instanceadded'] = 'Method added';
 $string['instanceeditselfwarning'] = 'Warning:';
 $string['instanceeditselfwarningtext'] = 'You are enrolled into this course through this enrolment method, changes may affect your access to this course.';
 $string['invalidenrolinstance'] = 'Invalid enrolment instance';
index e80d92e..a15d522 100644 (file)
@@ -296,6 +296,7 @@ $string['gradeoutcomes'] = 'Outcomes';
 $string['gradeoutcomescourses'] = 'Course outcomes';
 $string['gradepass'] = 'Grade to pass';
 $string['gradepass_help'] = 'This setting determines the minimum grade required to pass. The value is used in activity and course completion, and in the gradebook, where pass grades are highlighted in green and fail grades in red.';
+$string['gradepassgreaterthangrade'] = 'The grade to pass can not be greater than the maximum possible grade {$a}';
 $string['gradepointdefault'] = 'Grade point default';
 $string['gradepointdefault_help'] = 'This setting determines the default value for the grade point value available in an activity.';
 $string['gradepointdefault_validateerror'] = 'This setting must be an integer between 1 and the grade point maximum.';
index b059127..83e1056 100644 (file)
@@ -40,6 +40,7 @@ $string['apiexplorer'] = 'API explorer';
 $string['apiexplorernotavalaible'] = 'API explorer not available yet.';
 $string['arguments'] = 'Arguments';
 $string['authmethod'] = 'Authentication method';
+$string['callablefromajax'] = 'Callable from AJAX';
 $string['cannotcreatetoken'] = 'No permission to create web service token for the service {$a}.';
 $string['cannotgetcoursecontents'] = 'Cannot get course contents';
 $string['configwebserviceplugins'] = 'For security reasons, only protocols that are in use should be enabled.';
diff --git a/lib/ajax/service.php b/lib/ajax/service.php
new file mode 100644 (file)
index 0000000..7a72e88
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file is used to call any registered externallib function in Moodle.
+ *
+ * It will process more than one request and return more than one response if required.
+ * It is recommended to add webservice functions and re-use this script instead of
+ * writing any new custom ajax scripts.
+ *
+ * @since Moodle 2.9
+ * @package core
+ * @copyright 2015 Damyon Wiese
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->libdir . '/externallib.php');
+
+require_login(null, true, null, true, true);
+
+$rawjson = file_get_contents('php://input');
+
+$requests = json_decode($rawjson, true);
+if ($requests === null) {
+    $lasterror = json_last_error_msg();
+    throw new coding_exception('Invalid json in request: ' . $lasterror);
+}
+$responses = array();
+
+
+foreach ($requests as $request) {
+    $response = array();
+    $methodname = clean_param($request['methodname'], PARAM_ALPHANUMEXT);
+    $index = clean_param($request['index'], PARAM_INT);
+    $args = $request['args'];
+
+    try {
+        $externalfunctioninfo = external_function_info($methodname);
+
+        if (!$externalfunctioninfo->allowed_from_ajax) {
+            throw new moodle_exception('servicenotavailable', 'webservice');
+        }
+
+        // Validate params, this also sorts the params properly, we need the correct order in the next part.
+        $callable = array($externalfunctioninfo->classname, 'validate_parameters');
+        $params = call_user_func($callable,
+                                 $externalfunctioninfo->parameters_desc,
+                                 $args);
+
+        // Execute - gulp!
+        $callable = array($externalfunctioninfo->classname, $externalfunctioninfo->methodname);
+        $result = call_user_func_array($callable,
+                                       array_values($params));
+
+        $response['error'] = false;
+        $response['data'] = $result;
+        $responses[$index] = $response;
+    } catch (Exception $e) {
+        $jsonexception = get_exception_info($e);
+        unset($jsonexception->a);
+        if (!debugging('', DEBUG_DEVELOPER)) {
+            unset($jsonexception->debuginfo);
+            unset($jsonexception->backtrace);
+        }
+        $response['error'] = true;
+        $response['exception'] = $jsonexception;
+        $responses[$index] = $response;
+        // Do not process the remaining requests.
+        break;
+    }
+}
+
+echo json_encode($responses);
diff --git a/lib/amd/build/ajax.min.js b/lib/amd/build/ajax.min.js
new file mode 100644 (file)
index 0000000..72274b8
Binary files /dev/null and b/lib/amd/build/ajax.min.js differ
diff --git a/lib/amd/build/config.min.js b/lib/amd/build/config.min.js
new file mode 100644 (file)
index 0000000..03f444f
Binary files /dev/null and b/lib/amd/build/config.min.js differ
diff --git a/lib/amd/build/mustache.min.js b/lib/amd/build/mustache.min.js
new file mode 100644 (file)
index 0000000..e321d41
Binary files /dev/null and b/lib/amd/build/mustache.min.js differ
diff --git a/lib/amd/build/notification.min.js b/lib/amd/build/notification.min.js
new file mode 100644 (file)
index 0000000..37952ec
Binary files /dev/null and b/lib/amd/build/notification.min.js differ
diff --git a/lib/amd/build/str.min.js b/lib/amd/build/str.min.js
new file mode 100644 (file)
index 0000000..bfdb826
Binary files /dev/null and b/lib/amd/build/str.min.js differ
diff --git a/lib/amd/build/templates.min.js b/lib/amd/build/templates.min.js
new file mode 100644 (file)
index 0000000..c64573a
Binary files /dev/null and b/lib/amd/build/templates.min.js differ
diff --git a/lib/amd/build/url.min.js b/lib/amd/build/url.min.js
new file mode 100644 (file)
index 0000000..e287766
Binary files /dev/null and b/lib/amd/build/url.min.js differ
diff --git a/lib/amd/build/yui.min.js b/lib/amd/build/yui.min.js
new file mode 100644 (file)
index 0000000..c2bc6fa
Binary files /dev/null and b/lib/amd/build/yui.min.js differ
diff --git a/lib/amd/src/ajax.js b/lib/amd/src/ajax.js
new file mode 100644 (file)
index 0000000..4f2cb77
--- /dev/null
@@ -0,0 +1,161 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Standard Ajax wrapper for Moodle. It calls the central Ajax script,
+ * which can call any existing webservice using the current session.
+ * In addition, it can batch multiple requests and return multiple responses.
+ *
+ * @module     core/ajax
+ * @class      ajax
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['jquery', 'core/config'], function($, config) {
+
+    /**
+     * Success handler. Called when the ajax call succeeds. Checks each response and
+     * resolves or rejects the deferred from that request.
+     *
+     * @method requestSuccess
+     * @private
+     * @param {Object[]} responses Array of responses containing error, exception and data attributes.
+     */
+    var requestSuccess = function(responses) {
+        // Call each of the success handlers.
+        var requests = this;
+        var exception = null;
+        var i = 0;
+        var request;
+        var response;
+
+        for (i = 0; i < requests.length; i++) {
+            request = requests[i];
+
+            response = responses[i];
+            // We may not have responses for all the requests.
+            if (typeof response !== "undefined") {
+                if (response.error === false) {
+                    // Call the done handler if it was provided.
+                    request.deferred.resolve(response.data);
+                } else {
+                    exception = response.exception;
+                    break;
+                }
+            } else {
+                // This is not an expected case.
+                exception = new Error('missing response');
+                break;
+            }
+        }
+        // Something failed, reject the remaining promises.
+        if (exception !== null) {
+            for (; i < requests.length; i++) {
+                request = requests[i];
+                request.deferred.reject(exception);
+            }
+        }
+    };
+
+    /**
+     * Fail handler. Called when the ajax call fails. Rejects all deferreds.
+     *
+     * @method requestFail
+     * @private
+     * @param {jqXHR} jqXHR The ajax object.
+     * @param {string} textStatus The status string.
+     */
+    var requestFail = function(jqXHR, textStatus) {
+        // Reject all the promises.
+        var requests = this;
+
+        var i = 0;
+        for (i = 0; i < requests.length; i++) {
+            var request = requests[i];
+
+            if (typeof request.fail != "undefined") {
+                request.deferred.reject(textStatus);
+            }
+        }
+    };
+
+    return /** @alias module:core/ajax */ {
+        // Public variables and functions.
+        /**
+         * Make a series of ajax requests and return all the responses.
+         *
+         * @method call
+         * @param {Object[]} Array of requests with each containing methodname and args properties.
+         *                   done and fail callbacks can be set for each element in the array, or the
+         *                   can be attached to the promises returned by this function.
+         * @param {Boolean} async Optional, defaults to true.
+         *                  If false - this function will not return until the promises are resolved.
+         * @return {Promise[]} Array of promises that will be resolved when the ajax call returns.
+         */
+        call: function(requests, async) {
+            var ajaxRequestData = [],
+                i,
+                promises = [];
+
+            if (typeof async === "undefined") {
+                async = true;
+            }
+            for (i = 0; i < requests.length; i++) {
+                var request = requests[i];
+                ajaxRequestData.push({
+                    index: i,
+                    methodname: request.methodname,
+                    args: request.args
+                });
+                request.deferred = $.Deferred();
+                promises.push(request.deferred.promise());
+                // Allow setting done and fail handlers as arguments.
+                // This is just a shortcut for the calling code.
+                if (typeof request.done !== "undefined") {
+                    request.deferred.done(request.done);
+                }
+                if (typeof request.fail !== "undefined") {
+                    request.deferred.fail(request.fail);
+                }
+                request.index = i;
+            }
+
+            ajaxRequestData = JSON.stringify(ajaxRequestData);
+            var settings = {
+                type: 'POST',
+                data: ajaxRequestData,
+                context: requests,
+                dataType: 'json',
+                processData: false,
+                async: async
+            };
+
+            // Jquery deprecated done and fail with async=false so we need to do this 2 ways.
+            if (async) {
+                $.ajax(config.wwwroot + '/lib/ajax/service.php', settings)
+                    .done(requestSuccess)
+                    .fail(requestFail);
+            } else {
+                settings.success = requestSuccess;
+                settings.error = requestFail;
+                $.ajax(config.wwwroot + '/lib/ajax/service.php', settings);
+            }
+
+            return promises;
+        }
+    };
+});
diff --git a/lib/amd/src/config.js b/lib/amd/src/config.js
new file mode 100644 (file)
index 0000000..203326b
--- /dev/null
@@ -0,0 +1,30 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Expose the M.cfg global variable.
+ *
+ * @module     core/config
+ * @class      config
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(function() {
+
+    // This module exposes only the raw data from M.cfg;
+    return /** @alias module:core/config */ M.cfg;
+});
index 22188f3..704c0c5 100644 (file)
@@ -22,5 +22,6 @@
  * @package    core
  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
  */
 define(function() { });
diff --git a/lib/amd/src/mustache.js b/lib/amd/src/mustache.js
new file mode 100644 (file)
index 0000000..6c96ca4
--- /dev/null
@@ -0,0 +1,608 @@
+// The MIT License
+//
+// Copyright (c) 2009 Chris Wanstrath (Ruby)
+// Copyright (c) 2010-2014 Jan Lehnardt (JavaScript)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+// Description of import into Moodle:
+// Download from https://github.com/janl/mustache.js/releases
+// Copy mustache.js into lib/amd/src/ in Moodle folder.
+// Add the license as a comment to the file and these instructions.
+
+/*!
+ * mustache.js - Logic-less {{mustache}} templates with JavaScript
+ * http://github.com/janl/mustache.js
+ */
+/* jshint ignore:start */
+
+(function (global, factory) {
+  if (typeof exports === "object" && exports) {
+    factory(exports); // CommonJS
+  } else if (typeof define === "function" && define.amd) {
+    define(['exports'], factory); // AMD
+  } else {
+    factory(global.Mustache = {}); // <script>
+  }
+}(this, function (mustache) {
+
+  var Object_toString = Object.prototype.toString;
+  var isArray = Array.isArray || function (object) {
+    return Object_toString.call(object) === '[object Array]';
+  };
+
+  function isFunction(object) {
+    return typeof object === 'function';
+  }
+
+  function escapeRegExp(string) {
+    return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+  }
+
+  // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
+  // See https://github.com/janl/mustache.js/issues/189
+  var RegExp_test = RegExp.prototype.test;
+  function testRegExp(re, string) {
+    return RegExp_test.call(re, string);
+  }
+
+  var nonSpaceRe = /\S/;
+  function isWhitespace(string) {
+    return !testRegExp(nonSpaceRe, string);
+  }
+
+  var entityMap = {
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': '&quot;',
+    "'": '&#39;',
+    "/": '&#x2F;'
+  };
+
+  function escapeHtml(string) {
+    return String(string).replace(/[&<>"'\/]/g, function (s) {
+      return entityMap[s];
+    });
+  }
+
+  var whiteRe = /\s*/;
+  var spaceRe = /\s+/;
+  var equalsRe = /\s*=/;
+  var curlyRe = /\s*\}/;
+  var tagRe = /#|\^|\/|>|\{|&|=|!/;
+
+  /**
+   * Breaks up the given `template` string into a tree of tokens. If the `tags`
+   * argument is given here it must be an array with two string values: the
+   * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
+   * course, the default is to use mustaches (i.e. mustache.tags).
+   *
+   * A token is an array with at least 4 elements. The first element is the
+   * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
+   * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
+   * all text that appears outside a symbol this element is "text".
+   *
+   * The second element of a token is its "value". For mustache tags this is
+   * whatever else was inside the tag besides the opening symbol. For text tokens
+   * this is the text itself.
+   *
+   * The third and fourth elements of the token are the start and end indices,
+   * respectively, of the token in the original template.
+   *
+   * Tokens that are the root node of a subtree contain two more elements: 1) an
+   * array of tokens in the subtree and 2) the index in the original template at
+   * which the closing tag for that section begins.
+   */
+  function parseTemplate(template, tags) {
+    if (!template)
+      return [];
+
+    var sections = [];     // Stack to hold section tokens
+    var tokens = [];       // Buffer to hold the tokens
+    var spaces = [];       // Indices of whitespace tokens on the current line
+    var hasTag = false;    // Is there a {{tag}} on the current line?
+    var nonSpace = false;  // Is there a non-space char on the current line?
+
+    // Strips all whitespace tokens array for the current line
+    // if there was a {{#tag}} on it and otherwise only space.
+    function stripSpace() {
+      if (hasTag && !nonSpace) {
+        while (spaces.length)
+          delete tokens[spaces.pop()];
+      } else {
+        spaces = [];
+      }
+
+      hasTag = false;
+      nonSpace = false;
+    }
+
+    var openingTagRe, closingTagRe, closingCurlyRe;
+    function compileTags(tags) {
+      if (typeof tags === 'string')
+        tags = tags.split(spaceRe, 2);
+
+      if (!isArray(tags) || tags.length !== 2)
+        throw new Error('Invalid tags: ' + tags);
+
+      openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
+      closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
+      closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
+    }
+
+    compileTags(tags || mustache.tags);
+
+    var scanner = new Scanner(template);
+
+    var start, type, value, chr, token, openSection;
+    while (!scanner.eos()) {
+      start = scanner.pos;
+
+      // Match any text between tags.
+      value = scanner.scanUntil(openingTagRe);
+
+      if (value) {
+        for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
+          chr = value.charAt(i);
+
+          if (isWhitespace(chr)) {
+            spaces.push(tokens.length);
+          } else {
+            nonSpace = true;
+          }
+
+          tokens.push([ 'text', chr, start, start + 1 ]);
+          start += 1;
+
+          // Check for whitespace on the current line.
+          if (chr === '\n')
+            stripSpace();
+        }
+      }
+
+      // Match the opening tag.
+      if (!scanner.scan(openingTagRe))
+        break;
+
+      hasTag = true;
+
+      // Get the tag type.
+      type = scanner.scan(tagRe) || 'name';
+      scanner.scan(whiteRe);
+
+      // Get the tag value.
+      if (type === '=') {
+        value = scanner.scanUntil(equalsRe);
+        scanner.scan(equalsRe);
+        scanner.scanUntil(closingTagRe);
+      } else if (type === '{') {
+        value = scanner.scanUntil(closingCurlyRe);
+        scanner.scan(curlyRe);
+        scanner.scanUntil(closingTagRe);
+        type = '&';
+      } else {
+        value = scanner.scanUntil(closingTagRe);
+      }
+
+      // Match the closing tag.
+      if (!scanner.scan(closingTagRe))
+        throw new Error('Unclosed tag at ' + scanner.pos);
+
+      token = [ type, value, start, scanner.pos ];
+      tokens.push(token);
+
+      if (type === '#' || type === '^') {
+        sections.push(token);
+      } else if (type === '/') {
+        // Check section nesting.
+        openSection = sections.pop();
+
+        if (!openSection)
+          throw new Error('Unopened section "' + value + '" at ' + start);
+
+        if (openSection[1] !== value)
+          throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
+      } else if (type === 'name' || type === '{' || type === '&') {
+        nonSpace = true;
+      } else if (type === '=') {
+        // Set the tags for the next time around.
+        compileTags(value);
+      }
+    }
+
+    // Make sure there are no open sections when we're done.
+    openSection = sections.pop();
+
+    if (openSection)
+      throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
+
+    return nestTokens(squashTokens(tokens));
+  }
+
+  /**
+   * Combines the values of consecutive text tokens in the given `tokens` array
+   * to a single token.
+   */
+  function squashTokens(tokens) {
+    var squashedTokens = [];
+
+    var token, lastToken;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      if (token) {
+        if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
+          lastToken[1] += token[1];
+          lastToken[3] = token[3];
+        } else {
+          squashedTokens.push(token);
+          lastToken = token;
+        }
+      }
+    }
+
+    return squashedTokens;
+  }
+
+  /**
+   * Forms the given array of `tokens` into a nested tree structure where
+   * tokens that represent a section have two additional items: 1) an array of
+   * all tokens that appear in that section and 2) the index in the original
+   * template that represents the end of that section.
+   */
+  function nestTokens(tokens) {
+    var nestedTokens = [];
+    var collector = nestedTokens;
+    var sections = [];
+
+    var token, section;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+      case '^':
+        collector.push(token);
+        sections.push(token);
+        collector = token[4] = [];
+        break;
+      case '/':
+        section = sections.pop();
+        section[5] = token[2];
+        collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
+        break;
+      default:
+        collector.push(token);
+      }
+    }
+
+    return nestedTokens;
+  }
+
+  /**
+   * A simple string scanner that is used by the template parser to find
+   * tokens in template strings.
+   */
+  function Scanner(string) {
+    this.string = string;
+    this.tail = string;
+    this.pos = 0;
+  }
+
+  /**
+   * Returns `true` if the tail is empty (end of string).
+   */
+  Scanner.prototype.eos = function () {
+    return this.tail === "";
+  };
+
+  /**
+   * Tries to match the given regular expression at the current position.
+   * Returns the matched text if it can match, the empty string otherwise.
+   */
+  Scanner.prototype.scan = function (re) {
+    var match = this.tail.match(re);
+
+    if (!match || match.index !== 0)
+      return '';
+
+    var string = match[0];
+
+    this.tail = this.tail.substring(string.length);
+    this.pos += string.length;
+
+    return string;
+  };
+
+  /**
+   * Skips all text until the given regular expression can be matched. Returns
+   * the skipped string, which is the entire tail if no match can be made.
+   */
+  Scanner.prototype.scanUntil = function (re) {
+    var index = this.tail.search(re), match;
+
+    switch (index) {
+    case -1:
+      match = this.tail;
+      this.tail = "";
+      break;
+    case 0:
+      match = "";
+      break;
+    default:
+      match = this.tail.substring(0, index);
+      this.tail = this.tail.substring(index);
+    }
+
+    this.pos += match.length;
+
+    return match;
+  };
+
+  /**
+   * Represents a rendering context by wrapping a view object and
+   * maintaining a reference to the parent context.
+   */
+  function Context(view, parentContext) {
+    this.view = view == null ? {} : view;
+    this.cache = { '.': this.view };
+    this.parent = parentContext;
+  }
+
+  /**
+   * Creates a new context using the given view with this context
+   * as the parent.
+   */
+  Context.prototype.push = function (view) {
+    return new Context(view, this);
+  };
+
+  /**
+   * Returns the value of the given name in this context, traversing
+   * up the context hierarchy if the value is absent in this context's view.
+   */
+  Context.prototype.lookup = function (name) {
+    var cache = this.cache;
+
+    var value;
+    if (name in cache) {
+      value = cache[name];
+    } else {
+      var context = this, names, index;
+
+      while (context) {
+        if (name.indexOf('.') > 0) {
+          value = context.view;
+          names = name.split('.');
+          index = 0;
+
+          while (value != null && index < names.length)
+            value = value[names[index++]];
+        } else if (typeof context.view == 'object') {
+          value = context.view[name];
+        }
+
+        if (value != null)
+          break;
+
+        context = context.parent;
+      }
+
+      cache[name] = value;
+    }
+
+    if (isFunction(value))
+      value = value.call(this.view);
+
+    return value;
+  };
+
+  /**
+   * A Writer knows how to take a stream of tokens and render them to a
+   * string, given a context. It also maintains a cache of templates to
+   * avoid the need to parse the same template twice.
+   */
+  function Writer() {
+    this.cache = {};
+  }
+
+  /**
+   * Clears all cached templates in this writer.
+   */
+  Writer.prototype.clearCache = function () {
+    this.cache = {};
+  };
+
+  /**
+   * Parses and caches the given `template` and returns the array of tokens
+   * that is generated from the parse.
+   */
+  Writer.prototype.parse = function (template, tags) {
+    var cache = this.cache;
+    var tokens = cache[template];
+
+    if (tokens == null)
+      tokens = cache[template] = parseTemplate(template, tags);
+
+    return tokens;
+  };
+
+  /**
+   * High-level method that is used to render the given `template` with
+   * the given `view`.
+   *
+   * The optional `partials` argument may be an object that contains the
+   * names and templates of partials that are used in the template. It may
+   * also be a function that is used to load partial templates on the fly
+   * that takes a single argument: the name of the partial.
+   */
+  Writer.prototype.render = function (template, view, partials) {
+    var tokens = this.parse(template);
+    var context = (view instanceof Context) ? view : new Context(view);
+    return this.renderTokens(tokens, context, partials, template);
+  };
+
+  /**
+   * Low-level method that renders the given array of `tokens` using
+   * the given `context` and `partials`.
+   *
+   * Note: The `originalTemplate` is only ever used to extract the portion
+   * of the original template that was contained in a higher-order section.
+   * If the template doesn't use higher-order sections, this argument may
+   * be omitted.
+   */
+  Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
+    var buffer = '';
+
+    // This function is used to render an arbitrary template
+    // in the current context by higher-order sections.
+    var self = this;
+    function subRender(template) {
+      return self.render(template, context, partials);
+    }
+
+    var token, value;
+    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+      token = tokens[i];
+
+      switch (token[0]) {
+      case '#':
+        value = context.lookup(token[1]);
+
+        if (!value)
+          continue;
+
+        if (isArray(value)) {
+          for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
+            buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
+          }
+        } else if (typeof value === 'object' || typeof value === 'string') {
+          buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
+        } else if (isFunction(value)) {
+          if (typeof originalTemplate !== 'string')
+            throw new Error('Cannot use higher-order sections without the original template');
+
+          // Extract the portion of the original template that the section contains.
+          value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
+
+          if (value != null)
+            buffer += value;
+        } else {
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+        }
+
+        break;
+      case '^':
+        value = context.lookup(token[1]);
+
+        // Use JavaScript's definition of falsy. Include empty arrays.
+        // See https://github.com/janl/mustache.js/issues/186
+        if (!value || (isArray(value) && value.length === 0))
+          buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+
+        break;
+      case '>':
+        if (!partials)
+          continue;
+
+        value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+
+        if (value != null)
+          buffer += this.renderTokens(this.parse(value), context, partials, value);
+
+        break;
+      case '&':
+        value = context.lookup(token[1]);
+
+        if (value != null)
+          buffer += value;
+
+        break;
+      case 'name':
+        value = context.lookup(token[1]);
+
+        if (value != null)
+          buffer += mustache.escape(value);
+
+        break;
+      case 'text':
+        buffer += token[1];
+        break;
+      }
+    }
+
+    return buffer;
+  };
+
+  mustache.name = "mustache.js";
+  mustache.version = "1.0.0";
+  mustache.tags = [ "{{", "}}" ];
+
+  // All high-level mustache.* functions use this writer.
+  var defaultWriter = new Writer();
+
+  /**
+   * Clears all cached templates in the default writer.
+   */
+  mustache.clearCache = function () {
+    return defaultWriter.clearCache();
+  };
+
+  /**
+   * Parses and caches the given template in the default writer and returns the
+   * array of tokens it contains. Doing this ahead of time avoids the need to
+   * parse templates on the fly as they are rendered.
+   */
+  mustache.parse = function (template, tags) {
+    return defaultWriter.parse(template, tags);
+  };
+
+  /**
+   * Renders the `template` with the given `view` and `partials` using the
+   * default writer.
+   */
+  mustache.render = function (template, view, partials) {
+    return defaultWriter.render(template, view, partials);
+  };
+
+  // This is here for backwards compatibility with 0.4.x.
+  mustache.to_html = function (template, view, partials, send) {
+    var result = mustache.render(template, view, partials);
+
+    if (isFunction(send)) {
+      send(result);
+    } else {
+      return result;
+    }
+  };
+
+  // Export the escaping function so that the user may override it.
+  // See https://github.com/janl/mustache.js/issues/244
+  mustache.escape = escapeHtml;
+
+  // Export these mainly for testing, but also for advanced usage.
+  mustache.Scanner = Scanner;
+  mustache.Context = Context;
+  mustache.Writer = Writer;
+
+}));
+/* jshint ignore:end */
diff --git a/lib/amd/src/notification.js b/lib/amd/src/notification.js
new file mode 100644 (file)
index 0000000..1880b80
--- /dev/null
@@ -0,0 +1,105 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Wrapper for the YUI M.core.notification class. Allows us to
+ * use the YUI version in AMD code until it is replaced.
+ *
+ * @module     core/notification
+ * @class      notification
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['core/yui'], function(Y) {
+
+    // Private variables and functions.
+
+    return /** @alias module:core/notification */ {
+        // Public variables and functions.
+        /**
+         * Wrap M.core.alert.
+         *
+         * @method alert
+         * @param {string} title
+         * @param {string} message
+         * @param {string} yesLabel
+         */
+        alert: function(title, message, yesLabel) {
+            // Here we are wrapping YUI. This allows us to start transitioning, but
+            // wait for a good alternative without having inconsistent dialogues.
+            Y.use('moodle-core-notification-alert', function () {
+                var alert = new M.core.alert({
+                    title : title,
+                    message : message,
+                    yesLabel: yesLabel
+                });
+
+                alert.show();
+            });
+        },
+
+        /**
+         * Wrap M.core.confirm.
+         *
+         * @method confirm
+         * @param {string} title
+         * @param {string} question
+         * @param {string} yesLabel
+         * @param {string} noLabel
+         * @param {function} callback
+         */
+        confirm: function(title, question, yesLabel, noLabel, callback) {
+            // Here we are wrapping YUI. This allows us to start transitioning, but
+            // wait for a good alternative without having inconsistent dialogues.
+            Y.use('moodle-core-notification-confirm', function () {
+                var modal = new M.core.confirm({
+                    title : title,
+                    question : question,
+                    yesLabel: yesLabel,
+                    noLabel: noLabel
+                });
+
+                modal.on('complete-yes', function() {
+                    callback();
+                });
+                modal.show();
+            });
+        },
+
+        /**
+         * Wrap M.core.exception.
+         *
+         * @method exception
+         * @param {Error} ex
+         */
+        exception: function(ex) {
+            // Fudge some parameters.
+            if (ex.backtrace) {
+                ex.lineNumber = ex.backtrace[0].line;
+                ex.fileName = ex.backtrace[0].file;
+                ex.fileName = '...' + ex.fileName.substr(ex.fileName.length - 20);
+                ex.stack = ex.debuginfo;
+                ex.name = ex.errorcode;
+            }
+            Y.use('moodle-core-notification-exception', function () {
+                var modal = new M.core.exception(ex);
+
+                modal.show();
+            });
+        }
+    };
+});
diff --git a/lib/amd/src/str.js b/lib/amd/src/str.js
new file mode 100644 (file)
index 0000000..d342de1
--- /dev/null
@@ -0,0 +1,141 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Fetch and render language strings.
+ * Hooks into the old M.str global - but can also fetch missing strings on the fly.
+ *
+ * @module     core/str
+ * @class      str
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['jquery', 'core/ajax'], function($, ajax) {
+
+
+    return /** @alias module:core/str */ {
+        // Public variables and functions.
+        /**
+         * Return a promise object that will be resolved into a string eventually (maybe immediately).
+         *
+         * @method get_string
+         * @param {string} key The language string key
+         * @param {string} component The language string component
+         * @param {string} param The param for variable expansion in the string.
+         * @param {string} lang The users language - if not passed it is deduced.
+         * @return {Promise}
+         */
+        get_string: function(key, component, param, lang) {
+            var deferred = $.Deferred();
+
+            var request = this.get_strings([{
+                key: key,
+                component: component,
+                param: param,
+                lang: lang
+            }]);
+
+            request.done(function(results) {
+                deferred.resolve(results[0]);
+            }).fail(function(ex) {
+                deferred.reject(ex);
+            });
+
+            return deferred.promise();
+        },
+
+        /**
+         * Make a batch request to load a set of strings
+         *
+         * @method get_strings
+         * @param {Object[]} requests Array of { key: key, component: component, param: param, lang: lang };
+         *                                      See get_string for more info on these args.
+         * @return {Promise}
+         */
+        get_strings: function(requests) {
+
+            var deferred = $.Deferred();
+            var results = [];
+            var i = 0;
+            var missing = false;
+            var request;
+
+            for (i = 0; i < requests.length; i++) {
+                request = requests[i];
+                if (typeof M.str[request.component] === "undefined" ||
+                        typeof M.str[request.component][request.key] === "undefined") {
+                    missing = true;
+                }
+            }
+
+            if (!missing) {
+                // We have all the strings already.
+                for (i = 0; i < requests.length; i++) {
+                    request = requests[i];
+
+                    results[i] = M.util.get_string(request.key, request.component, request.param);
+                }
+                deferred.resolve(results);
+            } else {
+                // Something is missing, we might as well load them all.
+                var ajaxrequests = [];
+
+                for (i = 0; i < requests.length; i++) {
+                    request = requests[i];
+
+                    if (typeof request.lang === "undefined") {
+                        request.lang = $('html').attr('lang');
+                    }
+                    ajaxrequests.push({
+                        methodname: 'core_get_string',
+                        args: {
+                            stringid: request.key,
+                            component: request.component,
+                            lang: request.lang,
+                            stringparams: []
+                        }
+                    });
+                }
+
+                var deferreds = ajax.call(ajaxrequests);
+                $.when.apply(null, deferreds).done(
+                    function() {
+                        // Turn the list of arguments (unknown length) into a real array.
+                        var i = 0;
+                        for (i = 0; i < arguments.length; i++) {
+                            request = requests[i];
+                            // Cache all the string templates.
+                            if (typeof M.str[request.component] === "undefined") {
+                                M.str[request.component] = [];
+                            }
+                            M.str[request.component][request.key] = arguments[i];
+                            // And set the results.
+                            results[i] = M.util.get_string(request.key, request.component, request.param).trim();
+                        }
+                        deferred.resolve(results);
+                    }
+                ).fail(
+                    function(ex) {
+                        deferred.reject(ex);
+                    }
+                );
+            }
+
+            return deferred.promise();
+        }
+    };
+});
diff --git a/lib/amd/src/templates.js b/lib/amd/src/templates.js
new file mode 100644 (file)
index 0000000..4f581e5
--- /dev/null
@@ -0,0 +1,374 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Template renderer for Moodle. Load and render Moodle templates with Mustache.
+ *
+ * @module     core/templates
+ * @package    core
+ * @class      templates
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define([ 'core/mustache',
+         'jquery',
+         'core/ajax',
+         'core/str',
+         'core/notification',
+         'core/url',
+         'core/config'
+       ],
+       function(mustache, $, ajax, str, notification, coreurl, config) {
+
+    // Private variables and functions.
+
+    /** @var {string[]} templateCache - Cache of already loaded templates */
+    var templateCache = {};
+
+    /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
+    var requiredStrings = [];
+
+    /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
+    var requiredJS = [];
+
+    /** @var {Number} uniqid Incrementing value that is changed for every call to render */
+    var uniqid = 1;
+
+    /** @var {String} themeName for the current render */
+    var currentThemeName = '';
+
+    /**
+     * Render image icons.
+     *
+     * @method pixHelper
+     * @private
+     * @param {string} sectionText The text to parse arguments from.
+     * @return {string}
+     */
+    var pixHelper = function(sectionText) {
+        var parts = sectionText.split(',');
+        var key = '';
+        var component = '';
+        var text = '';
+        var result;
+
+        if (parts.length > 0) {
+            key = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            component = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            text = parts.join(',').trim();
+        }
+        var url = coreurl.imageUrl(key, component);
+
+        var templatecontext = {
+            attributes: [
+                { name: 'src', value: url},
+                { name: 'alt', value: text},
+                { name: 'class', value: 'smallicon'}
+            ]
+        };
+        // We forced loading of this early, so it will be in the cache.
+        var template = templateCache[currentThemeName + '/core/pix_icon'];
+        result = mustache.render(template, templatecontext, partialHelper);
+        return result.trim();
+    };
+
+    /**
+     * Load a partial from the cache or ajax.
+     *
+     * @method partialHelper
+     * @private
+     * @param {string} name The partial name to load.
+     * @return {string}
+     */
+    var partialHelper = function(name) {
+        var template = '';
+
+        getTemplate(name, false).done(
+            function(source) {
+                template = source;
+            }
+        ).fail(notification.exception);
+
+        return template;
+    };
+
+    /**
+     * Render blocks of javascript and save them in an array.
+     *
+     * @method jsHelper
+     * @private
+     * @param {string} sectionText The text to save as a js block.
+     * @param {function} helper Used to render the block.
+     * @return {string}
+     */
+    var jsHelper = function(sectionText, helper) {
+        requiredJS.push(helper(sectionText, this));
+        return '';
+    };
+
+    /**
+     * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
+     * into a get_string call.
+     *
+     * @method stringHelper
+     * @private
+     * @param {string} sectionText The text to parse the arguments from.
+     * @param {function} helper Used to render subsections of the text.
+     * @return {string}
+     */
+    var stringHelper = function(sectionText, helper) {
+        var parts = sectionText.split(',');
+        var key = '';
+        var component = '';
+        var param = '';
+        if (parts.length > 0) {
+            key = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            component = parts.shift().trim();
+        }
+        if (parts.length > 0) {
+            param = parts.join(',').trim();
+        }
+
+        if (param !== '') {
+            // Allow variable expansion in the param part only.
+            param = helper(param, this);
+        }
+        // Allow json formatted $a arguments.
+        if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
+            param = JSON.parse(param);
+        }
+
+        var index = requiredStrings.length;
+        requiredStrings.push({key: key, component: component, param: param});
+        return '{{_s' + index + '}}';
+    };
+
+    /**
+     * Add some common helper functions to all context objects passed to templates.
+     * These helpers match exactly the helpers available in php.
+     *
+     * @method addHelpers
+     * @private
+     * @param {Object} context Simple types used as the context for the template.
+     * @param {String} themeName We set this multiple times, because there are async calls.
+     */
+    var addHelpers = function(context, themeName) {
+        currentThemeName = themeName;
+        requiredStrings = [];
+        requiredJS = [];
+        context.uniqid = uniqid++;
+        context.str = function() { return stringHelper; };
+        context.pix = function() { return pixHelper; };
+        context.js = function() { return jsHelper; };
+        context.globals = { config : config };
+        context.currentTheme = themeName;
+    };
+
+    /**
+     * Get all the JS blocks from the last rendered template.
+     *
+     * @method getJS
+     * @private
+     * @param {string[]} strings Replacement strings.
+     * @return {string}
+     */
+    var getJS = function(strings) {
+        var js = '';
+        if (requiredJS.length > 0) {
+            js = requiredJS.join(";\n");
+        }
+
+        var i = 0;
+
+        for (i = 0; i < strings.length; i++) {
+            js = js.replace('{{_s' + i + '}}', strings[i]);
+        }
+        // Re-render to get the final strings.
+        return js;
+    };
+
+    /**
+     * Render a template and then call the callback with the result.
+     *
+     * @method doRender
+     * @private
+     * @param {string} templateSource The mustache template to render.
+     * @param {Object} context Simple types used as the context for the template.
+     * @param {String} themeName Name of the current theme.
+     * @return {Promise} object
+     */
+    var doRender = function(templateSource, context, themeName) {
+        var deferred = $.Deferred();
+
+        currentThemeName = themeName;
+
+        // Make sure we fetch this first.
+        var loadPixTemplate = getTemplate('core/pix_icon', true);
+
+        loadPixTemplate.done(
+            function() {
+                addHelpers(context, themeName);
+                var result = '';
+                try {
+                    result = mustache.render(templateSource, context, partialHelper);
+                } catch (ex) {
+                    deferred.reject(ex);
+                }
+
+                if (requiredStrings.length > 0) {
+                    str.get_strings(requiredStrings).done(
+                        function(strings) {
+                            var i;
+
+                            // Why do we not do another call the render here?
+                            //
+                            // Because that would expose DOS holes. E.g.
+                            // I create an assignment called "{{fish" which
+                            // would get inserted in the template in the first pass
+                            // and cause the template to die on the second pass (unbalanced).
+                            for (i = 0; i < strings.length; i++) {
+                                result = result.replace('{{_s' + i + '}}', strings[i]);
+                            }
+                            deferred.resolve(result.trim(), getJS(strings));
+                        }
+                    ).fail(
+                        function(ex) {
+                            deferred.reject(ex);
+                        }
+                    );
+                } else {
+                    deferred.resolve(result.trim(), getJS([]));
+                }
+            }
+        ).fail(
+            function(ex) {
+                deferred.reject(ex);
+            }
+        );
+        return deferred.promise();
+    };
+
+    /**
+     * Load a template from the cache or ajax request.
+     *
+     * @method getTemplate
+     * @private
+     * @param {string} templateName - should consist of the component and the name of the template like this:
+     *                              core/menu (lib/templates/menu.mustache) or
+     *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
+     * @return {Promise} JQuery promise object resolved when the template has been fetched.
+     */
+    var getTemplate = function(templateName, async) {
+        var deferred = $.Deferred();
+        var parts = templateName.split('/');
+        var component = parts.shift();
+        var name = parts.shift();
+
+        var searchKey = currentThemeName + '/' + templateName;
+
+        if (searchKey in templateCache) {
+            deferred.resolve(templateCache[searchKey]);
+        } else {
+            var promises = ajax.call([{
+                methodname: 'core_output_load_template',
+                args:{
+                    component: component,
+                    template: name,
+                    themename: currentThemeName
+                }
+            }], async);
+            promises[0].done(
+                function (templateSource) {
+                    templateCache[searchKey] = templateSource;
+                    deferred.resolve(templateSource);
+                }
+            ).fail(
+                function (ex) {
+                    deferred.reject(ex);
+                }
+            );
+        }
+        return deferred.promise();
+    };
+
+    return /** @alias module:core/templates */ {
+        // Public variables and functions.
+        /**
+         * Load a template and call doRender on it.
+         *
+         * @method render
+         * @private
+         * @param {string} templateName - should consist of the component and the name of the template like this:
+         *                              core/menu (lib/templates/menu.mustache) or
+         *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
+         * @param {Object} context - Could be array, string or simple value for the context of the template.
+         * @param {string} themeName - Name of the current theme.
+         * @return {Promise} JQuery promise object resolved when the template has been rendered.
+         */
+        render: function(templateName, context, themeName) {
+            var deferred = $.Deferred();
+
+            if (typeof (themeName) === "undefined") {
+                // System context by default.
+                themeName = config.theme;
+            }
+
+            currentThemeName = themeName;
+
+            var loadTemplate = getTemplate(templateName, true);
+
+            loadTemplate.done(
+                function(templateSource) {
+                    var renderPromise = doRender(templateSource, context, themeName);
+
+                    renderPromise.done(
+                        function(result, js) {
+                            deferred.resolve(result, js);
+                        }
+                    ).fail(
+                        function(ex) {
+                            deferred.reject(ex);
+                        }
+                    );
+                }
+            ).fail(
+                function(ex) {
+                    deferred.reject(ex);
+                }
+            );
+            return deferred.promise();
+        },
+
+        /**
+         * Execute a block of JS returned from a template.
+         * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
+         *
+         * @method runTemplateJS
+         * @private
+         * @param {string} source - A block of javascript.
+         */
+        runTemplateJS: function(source) {
+            var newscript = $('<script>').attr('type','text/javascript').html(source);
+            $('head').append(newscript);
+        }
+    };
+});
diff --git a/lib/amd/src/url.js b/lib/amd/src/url.js
new file mode 100644 (file)
index 0000000..908c898
--- /dev/null
@@ -0,0 +1,91 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * URL utility functions.
+ *
+ * @module     core/url
+ * @package    core
+ * @class      url
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(['core/config'], function(config) {
+
+
+    return /** @alias module:core/url */ {
+        // Public variables and functions.
+        /**
+         * Generate a style tag referencing this sheet and add it to the head of the page.
+         *
+         * @method fileUrl
+         * @param {string} sheet The style sheet name. Must exist in the theme, or one of it's parents.
+         * @return {string}
+         */
+        fileUrl: function(relativeScript, slashArg) {
+
+            var url = config.wwwroot + relativeScript;
+
+            // Force a /
+            if (slashArg.charAt(0) != '/') {
+                slashArg = '/' + slashArg;
+            }
+            if (config.slasharguments) {
+                url += slashArg;
+            } else {
+                url += '?file=' + encodeURIComponent(slashArg);
+            }
+            return url;
+        },
+
+        /**
+         * Take a path relative to the moodle basedir and do some fixing (see class moodle_url in php).
+         *
+         * @method relativeUrl
+         * @param {string} relativePath The path relative to the moodle basedir.
+         * @return {string}
+         */
+        relativeUrl: function(relativePath) {
+
+            if (relativePath.indexOf('http:') === 0 || relativePath.indexOf('https:') === 0 || relativePath.indexOf('://')) {
+                throw new Error('relativeUrl function does not accept absolute urls');
+            }
+
+            // Fix non-relative paths;
+            if (relativePath.charAt(0) != '/') {
+                relativePath = '/' + relativePath;
+            }
+
+            // Fix admin urls.
+            if (config.admin !== 'admin') {
+                relativePath = relativePath.replace(/^\/admin\//, '/' + config.admin + '/');
+            }
+            return config.wwwroot + relativePath;
+        },
+
+        /**
+         * Wrapper for image_url function.
+         *
+         * @method imageUrl
+         * @param {string} imagename The image name (e.g. t/edit).
+         * @param {string} component The component (e.g. mod_feedback).
+         * @return {string}
+         */
+        imageUrl: function(imagename, component) {
+            return M.util.image_url(imagename, component);
+        }
+    };
+});
diff --git a/lib/amd/src/yui.js b/lib/amd/src/yui.js
new file mode 100644 (file)
index 0000000..af58326
--- /dev/null
@@ -0,0 +1,30 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Expose the global YUI variable. Note: This is only for scripts that are writing AMD
+ * wrappers for YUI functionality. This is not for plugins.
+ *
+ * @module     core/yui
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+define(function() {
+
+    // This module exposes only the global yui instance.
+    return /** @alias module:core/yui */ Y;
+});
index 5fd807e..add2130 100644 (file)
@@ -93,51 +93,51 @@ class behat_selectors {
      */
     protected static $moodleselectors = array(
         'activity' => <<<XPATH
-//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][normalize-space(.) = %locator% ]
+.//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][normalize-space(.) = %locator% ]
 XPATH
         , 'block' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' block ') and
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' block ') and
     (contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
      descendant::h2[normalize-space(.) = %locator%] or
      @aria-label = %locator%)]
 XPATH
         , 'dialogue' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ') and
     normalize-space(descendant::div[
         contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue-hd ')
         ]) = %locator%] |
-//div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' yui-dialog ') and
     normalize-space(descendant::div[@class='hd']) = %locator%]
 XPATH
         , 'filemanager' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ')]
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ')]
     /descendant::input[@id = //label[contains(normalize-space(string(.)), %locator%)]/@for]
 XPATH
         , 'list_item' => <<<XPATH
-.//li[contains(normalize-space(.), %locator%)]
+.//li[contains(normalize-space(.), %locator%) and not(.//li[contains(normalize-space(.), %locator%)])]
 XPATH
         , 'question' => <<<XPATH
-//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]
     [contains(div[@class='content']/div[@class='formulation'], %locator%)]
 XPATH
         , 'region' => <<<XPATH
-//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
+.//*[self::div | self::section | self::aside | self::header | self::footer][./@id = %locator%]
 XPATH
         , 'section' => <<<XPATH
-//li[contains(concat(' ', normalize-space(@class), ' '), ' section ')][./descendant::*[self::h3]
+.//li[contains(concat(' ', normalize-space(@class), ' '), ' section ')][./descendant::*[self::h3]
     [normalize-space(.) = %locator%][contains(concat(' ', normalize-space(@class), ' '), ' sectionname ') or
     contains(concat(' ', normalize-space(@class), ' '), ' section-title ')]] |
-//div[contains(concat(' ', normalize-space(@class), ' '), ' sitetopic ')]
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' sitetopic ')]
     [./descendant::*[self::h2][normalize-space(.) = %locator%] or %locator% = 'frontpage']
 XPATH
         , 'table' => <<<XPATH
 .//table[(./@id = %locator% or contains(.//caption, %locator%) or contains(concat(' ', normalize-space(@class), ' '), %locator% ))]
 XPATH
         , 'table_row' => <<<XPATH
-.//tr[contains(normalize-space(.), %locator%)]
+.//tr[contains(normalize-space(.), %locator%) and not(.//tr[contains(normalize-space(.), %locator%)])]
 XPATH
         , 'text' => <<<XPATH
-//*[contains(., %locator%)][count(./descendant::*[contains(., %locator%)]) = 0]
+.//*[contains(., %locator%) and not(.//*[contains(., %locator%)])]
 XPATH
     );
 
index 5d1fb24..a44c185 100644 (file)
@@ -1599,8 +1599,10 @@ class block_manager {
         if ($bestgap < $newweight) {
             $newweight = floor($newweight);
             for ($weight = $bestgap + 1; $weight <= $newweight; $weight++) {
-                foreach ($usedweights[$weight] as $biid) {
-                    $this->reposition_block($biid, $newregion, $weight - 1);
+                if (array_key_exists($weight, $usedweights)) {
+                    foreach ($usedweights[$weight] as $biid) {
+                        $this->reposition_block($biid, $newregion, $weight - 1);
+                    }
                 }
             }
             $this->reposition_block($block->instance->id, $newregion, $newweight);
diff --git a/lib/classes/output/external.php b/lib/classes/output/external.php
new file mode 100644 (file)
index 0000000..8e53119
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper to load strings from string_manager.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use external_api;
+use external_function_parameters;
+use external_value;
+use core_component;
+use moodle_exception;
+use context_system;
+use theme_config;
+
+/**
+ * This class contains a list of webservice functions related to output.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class external extends external_api {
+    /**
+     * Returns description of load_template() parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function load_template_parameters() {
+        return new external_function_parameters(
+                array('component' => new external_value(PARAM_COMPONENT, 'component containing the template'),
+                      'template' => new external_value(PARAM_ALPHANUMEXT, 'name of the template'),
+                      'themename' => new external_value(PARAM_ALPHANUMEXT, 'The current theme.'),
+                         )
+            );
+    }
+
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function load_template_is_allowed_from_ajax() {
+        return true;
+    }
+
+    /**
+     * Return a mustache template, and all the strings it requires.
+     *
+     * @param string $component The component that holds the template.
+     * @param string $templatename The name of the template.
+     * @param string $themename The name of the current theme.
+     * @return string the template
+     */
+    public static function load_template($component, $template, $themename) {
+        global $DB, $CFG, $PAGE;
+
+        $params = self::validate_parameters(self::load_template_parameters(),
+                                            array('component' => $component,
+                                                  'template' => $template,
+                                                  'themename' => $themename));
+
+        $component = $params['component'];
+        $template = $params['template'];
+        $themename = $params['themename'];
+
+        // Check if this is a valid component.
+        $componentdir = core_component::get_component_directory($component);
+        if (empty($componentdir)) {
+            throw new moodle_exception('filenotfound', 'error');
+        }
+        // Places to look.
+        $candidates = array();
+        // Theme dir.
+        $root = $CFG->dirroot;
+
+        $themeconfig = theme_config::load($themename);
+
+        $candidate = "${root}/theme/${themename}/templates/${component}/${template}.mustache";
+        $candidates[] = $candidate;
+        // Theme parents dir.
+        foreach ($themeconfig->parents as $theme) {
+            $candidate = "${root}/theme/${theme}/templates/${component}/${template}.mustache";
+            $candidates[] = $candidate;
+        }
+        // Component dir.
+        $candidate = "${componentdir}/templates/${template}.mustache";
+        $candidates[] = $candidate;
+        $templatestr = false;
+        foreach ($candidates as $candidate) {
+            if (file_exists($candidate)) {
+                $templatestr = file_get_contents($candidate);
+                break;
+            }
+        }
+        if ($templatestr === false) {
+            throw new moodle_exception('filenotfound', 'error');
+        }
+
+        return $templatestr;
+    }
+
+    /**
+     * Returns description of load_template() result value.
+     *
+     * @return external_description
+     */
+    public static function load_template_returns() {
+        return new external_value(PARAM_RAW, 'template');
+    }
+}
+
diff --git a/lib/classes/output/mustache_filesystem_loader.php b/lib/classes/output/mustache_filesystem_loader.php
new file mode 100644 (file)
index 0000000..efaab81
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Perform some custom name mapping for template file names (strip leading component/).
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use coding_exception;
+
+/**
+ * Perform some custom name mapping for template file names (strip leading component/).
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_filesystem_loader extends \Mustache_Loader_FilesystemLoader {
+
+    /**
+     * Helper function for getting a Mustache template file name.
+     * Strips the leading component as we are already limited to the correct directories.
+     *
+     * @param string $name
+     *
+     * @return string Template file name
+     */
+    protected function getFileName($name) {
+        if (strpos($name, '/') === false) {
+            throw new coding_exception('Templates names must be specified as "componentname/templatename" (' . $name . ' requested) ');
+        }
+        list($component, $templatename) = explode('/', $name, 2);
+        return parent::getFileName($templatename);
+    }
+}
diff --git a/lib/classes/output/mustache_javascript_helper.php b/lib/classes/output/mustache_javascript_helper.php
new file mode 100644 (file)
index 0000000..7ba9b2a
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper that will add JS to the end of the page.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+/**
+ * Store a list of JS calls to insert at the end of the page.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_javascript_helper {
+
+    /** @var page_requirements_manager $requires - Page requirements manager for collecting JS calls. */
+    private $requires = null;
+
+    /**
+     * Create new instance of mustache javascript helper.
+     *
+     * @param page_requirements_manager $requires Page requirements manager.
+     */
+    public function __construct($requires) {
+        $this->requires = $requires;
+    }
+
+    /**
+     * Add the block of text to the page requires so it is appended in the footer. The
+     * content of the block can contain further mustache tags which will be resolved.
+     *
+     * @param string $text The script content of the section.
+     * @param \Mustache_LambdaHelper $helper Used to render the content of this block.
+     */
+    public function help($text, \Mustache_LambdaHelper $helper) {
+        $this->requires->js_amd_inline($helper->render($text));
+    }
+}
diff --git a/lib/classes/output/mustache_pix_helper.php b/lib/classes/output/mustache_pix_helper.php
new file mode 100644 (file)
index 0000000..3b0d261
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper render pix icons.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use Mustache_LambdaHelper;
+use renderer_base;
+
+/**
+ * This class will call pix_icon with the section content.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_pix_helper {
+
+    /** @var renderer_base $renderer A reference to the renderer in use */
+    private $renderer;
+
+    /**
+     * Save a reference to the renderer.
+     * @param renderer_base $renderer
+     */
+    public function __construct(renderer_base $renderer) {
+        $this->renderer = $renderer;
+    }
+
+    /**
+     * Read a pix icon name from a template and get it from pix_icon.
+     *
+     * {{#pix}}t/edit,component,Anything else is alt text{{/pix}}
+     *
+     * The args are comma separated and only the first is required.
+     *
+     * @param string $text The text to parse for arguments.
+     * @param Mustache_LambdaHelper $helper Used to render nested mustache variables.
+     * @return string
+     */
+    public function pix($text, Mustache_LambdaHelper $helper) {
+        // Split the text into an array of variables.
+        $key = strtok($text, ",");
+        $key = trim($key);
+        $component = strtok(",");
+        $component = trim($component);
+        if (!$component) {
+            $component = '';
+        }
+        $text = strtok("");
+        // Allow mustache tags in the last argument.
+        $text = $helper->render($text);
+
+        return trim($this->renderer->pix_icon($key, $text, $component));
+    }
+}
+
diff --git a/lib/classes/output/mustache_string_helper.php b/lib/classes/output/mustache_string_helper.php
new file mode 100644 (file)
index 0000000..1b5d52a
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper to load strings from string_manager.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use Mustache_LambdaHelper;
+use stdClass;
+
+/**
+ * This class will load language strings in a template.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_string_helper {
+
+    /**
+     * Read a lang string from a template and get it from get_string.
+     *
+     * Some examples for calling this from a template are:
+     *
+     * {{#str}}activity{{/str}}
+     * {{#str}}actionchoice, core, {{#str}}delete{{/str}}{{/str}} (Nested)
+     * {{#str}}addinganewto, core, {"what":"This", "to":"That"}{{/str}} (Complex $a)
+     *
+     * The args are comma separated and only the first is required.
+     * The last is a $a argument for get string. For complex data here, use JSON.
+     *
+     * @param string $text The text to parse for arguments.
+     * @param Mustache_LambdaHelper $helper Used to render nested mustache variables.
+     * @return string
+     */
+    public function str($text, Mustache_LambdaHelper $helper) {
+        // Split the text into an array of variables.
+        $key = strtok($text, ",");
+        $key = trim($key);
+        $component = strtok(",");
+        $component = trim($component);
+        if (!$component) {
+            $component = '';
+        }
+
+        $a = new stdClass();
+
+        $next = strtok('');
+        $next = trim($next);
+        if ((strpos($next, '{') === 0) && (strpos($next, '{{') !== 0)) {
+            $rawjson = $helper->render($next);
+            $a = json_decode($rawjson);
+        } else {
+            $a = $helper->render($next);
+        }
+        return get_string($key, $component, $a);
+    }
+}
+
diff --git a/lib/classes/output/mustache_uniqid_helper.php b/lib/classes/output/mustache_uniqid_helper.php
new file mode 100644 (file)
index 0000000..0dbb773
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Mustache helper that will add JS to the end of the page.
+ *
+ * @package    core
+ * @category   output
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+/**
+ * Lazy create a uniqid per instance of the class. The id is only generated
+ * when this class it converted to a string.
+ *
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      2.9
+ */
+class mustache_uniqid_helper {
+
+    /** @var string $uniqid The unique id */
+    private $uniqid = null;
+
+    /**
+     * Init the random variable and return it as a string.
+     *
+     * @return string random id.
+     */
+    public function __toString() {
+        if ($this->uniqid === null) {
+            $this->uniqid = \html_writer::random_id(uniqid());
+        }
+        return $this->uniqid;
+    }
+}
index a6391b9..a9ea1a8 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150224" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150306" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
         <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
       </KEYS>
       <INDEXES>
-        <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
         <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
index 0631dd8..00d90d1 100644 (file)
@@ -950,6 +950,13 @@ $functions = array(
         'type'        => 'write',
         'capabilities'=> 'moodle/calendar:manageentries', 'moodle/calendar:manageownentries', 'moodle/calendar:managegroupentries'
     ),
+
+    'core_output_load_template' => array(
+        'classname'   => 'core\output\external',
+        'methodname'  => 'load_template',
+        'description' => 'Load a template for a renderable',
+        'type'        => 'read'
+    ),
 );
 
 $services = array(
index a0aed78..a198c09 100644 (file)
@@ -4223,5 +4223,29 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015031100.00);
     }
 
+    if ($oldversion < 2015031400.00) {
+
+        // Define index useridfrom (not unique) to be dropped form message.
+        $table = new xmldb_table('message');
+        $index = new xmldb_index('useridfrom', XMLDB_INDEX_NOTUNIQUE, array('useridfrom'));
+
+        // Conditionally launch drop index useridfrom.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Define index useridfrom (not unique) to be dropped form message_read.
+        $table = new xmldb_table('message_read');
+        $index = new xmldb_index('useridfrom', XMLDB_INDEX_NOTUNIQUE, array('useridfrom'));
+
+        // Conditionally launch drop index useridfrom.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015031400.00);
+    }
+
     return true;
 }
index 5116da8..7751186 100644 (file)
@@ -4100,3 +4100,29 @@ function enrol_cohort_search_cohorts(course_enrolment_manager $manager, $offset
     }
     return array('more' => !(bool)$limit, 'offset' => $offset, 'cohorts' => $cohorts);
 }
+
+/**
+ * Is $USER one of the supplied users?
+ *
+ * $user2 will be null if viewing a user's recent conversations
+ *
+ * @deprecated since Moodle 2.9 MDL-49371 - please do not use this function any more.
+ * @todo MDL-49290 This will be deleted in Moodle 3.1.
+ * @param stdClass the first user
+ * @param stdClass the second user or null
+ * @return bool True if the current user is one of either $user1 or $user2
+ */
+function message_current_user_is_involved($user1, $user2) {
+    global $USER;
+
+    debugging('message_current_user_is_involved() is deprecated, please do not use this function.', DEBUG_DEVELOPER);
+
+    if (empty($user1->id) || (!empty($user2) && empty($user2->id))) {
+        throw new coding_exception('Invalid user object detected. Missing id.');
+    }
+
+    if ($user1->id != $USER->id && (empty($user2) || $user2->id != $USER->id)) {
+        return false;
+    }
+    return true;
+}
index a0526c5..1909c2f 100644 (file)
@@ -77,6 +77,16 @@ class core_external extends external_api {
         return $strparams;
     }
 
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function get_string_is_allowed_from_ajax() {
+        return true;
+    }
+
     /**
      * Returns description of get_string parameters
      *
@@ -153,6 +163,17 @@ class core_external extends external_api {
         );
     }
 
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function get_strings_is_allowed_from_ajax() {
+        return true;
+    }
+
+
     /**
      * Return multiple call to core get_string()
      *
@@ -216,6 +237,16 @@ class core_external extends external_api {
         );
     }
 
+    /**
+     * Can this function be called directly from ajax?
+     *
+     * @return boolean
+     * @since Moodle 2.9
+     */
+    public static function get_component_strings_is_allowed_from_ajax() {
+        return true;
+    }
+
     /**
      * Return all lang strings of a component - call to core get_component_strings().
      *
index a9ed4bf..d666932 100644 (file)
@@ -56,6 +56,7 @@ function external_function_info($function, $strictness=MUST_EXIST) {
         }
     }
 
+    $function->ajax_method = $function->methodname.'_is_allowed_from_ajax';
     $function->parameters_method = $function->methodname.'_parameters';
     $function->returns_method    = $function->methodname.'_returns';
     $function->deprecated_method = $function->methodname.'_is_deprecated';
@@ -75,6 +76,12 @@ function external_function_info($function, $strictness=MUST_EXIST) {
             $function->deprecated = true;
         }
     }
+    $function->allowed_from_ajax = false;
+    if (method_exists($function->classname, $function->ajax_method)) {
+        if (call_user_func(array($function->classname, $function->ajax_method)) === true) {
+            $function->allowed_from_ajax = true;
+        }
+    }
 
     // fetch the parameters description
     $function->parameters_desc = call_user_func(array($function->classname, $function->parameters_method));
index 7d8ea2f..675ba26 100644 (file)
@@ -6842,6 +6842,26 @@ function get_string_manager($forcereload=false) {
                 $translist = explode(',', $CFG->langlist);
             }
 
+            if (!empty($CFG->config_php_settings['customstringmanager'])) {
+                $classname = $CFG->config_php_settings['customstringmanager'];
+
+                if (class_exists($classname)) {
+                    $implements = class_implements($classname);
+
+                    if (isset($implements['core_string_manager'])) {
+                        $singleton = new $classname($CFG->langotherroot, $CFG->langlocalroot, $translist);
+                        return $singleton;
+
+                    } else {
+                        debugging('Unable to instantiate custom string manager: class '.$classname.
+                            ' does not implement the core_string_manager interface.');
+                    }
+
+                } else {
+                    debugging('Unable to instantiate custom string manager: class '.$classname.' can not be found.');
+                }
+            }
+
             $singleton = new core_string_manager_standard($CFG->langotherroot, $CFG->langlocalroot, $translist);
 
         } else {
diff --git a/lib/mustache/CONTRIBUTING.md b/lib/mustache/CONTRIBUTING.md
new file mode 100644 (file)
index 0000000..c0b323d
--- /dev/null
@@ -0,0 +1,35 @@
+# Contributions welcome!
+
+
+### Here's a quick guide:
+
+ 1. [Fork the repo on GitHub](https://github.com/bobthecow/mustache.php).
+
+ 2. Update submodules: `git submodule update --init`
+
+ 3. Run the test suite. We only take pull requests with passing tests, and it's great to know that you have a clean slate. Make sure you have PHPUnit 3.5+, then run `phpunit` from the project directory.
+
+ 4. Add tests for your change. Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, add a test!
+
+ 5. Make the tests pass.
+
+ 6. Push your fork to GitHub and submit a pull request against the `dev` branch.
+
+
+### You can do some things to increase the chance that your pull request is accepted the first time:
+
+ * Submit one pull request per fix or feature.
+ * To help with that, do all your work in a feature branch (e.g. `feature/my-alsome-feature`).
+ * Follow the conventions you see used in the project.
+ * Use `phpcs --standard=PSR2` to check your changes against the coding standard.
+ * Write tests that fail without your code, and pass with it.
+ * Don't bump version numbers. Those will be updated — per [semver](http://semver.org) — once your change is merged into `master`.
+ * Update any documentation: docblocks, README, examples, etc.
+ * ... Don't update the wiki until your change is merged and released, but make a note in your pull request so we don't forget.
+
+
+### Mustache.php follows the PSR-* coding standards:
+
+ * [PSR-0: Class and file naming conventions](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md)
+ * [PSR-1: Basic coding standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md)
+ * [PSR-2: Coding style guide](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
diff --git a/lib/mustache/LICENSE b/lib/mustache/LICENSE
new file mode 100644 (file)
index 0000000..0bdbc04
--- /dev/null
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2010-2014 Justin Hileman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/mustache/README.md b/lib/mustache/README.md
new file mode 100644 (file)
index 0000000..b33b355
--- /dev/null
@@ -0,0 +1,71 @@
+Mustache.php
+============
+
+A [Mustache](http://mustache.github.com/) implementation in PHP.
+
+[![Package version](http://img.shields.io/packagist/v/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
+[![Build status](http://img.shields.io/travis/bobthecow/mustache.php/dev.svg)](http://travis-ci.org/bobthecow/mustache.php)
+[![Monthly downloads](http://img.shields.io/packagist/dm/mustache/mustache.svg)](https://packagist.org/packages/mustache/mustache)
+
+
+Usage
+-----
+
+A quick example:
+
+```php
+<?php
+$m = new Mustache_Engine;
+echo $m->render('Hello {{planet}}', array('planet' => 'World!')); // "Hello World!"
+```
+
+
+And a more in-depth example -- this is the canonical Mustache template:
+
+```html+jinja
+Hello {{name}}
+You have just won ${{value}}!
+{{#in_ca}}
+Well, ${{taxed_value}}, after taxes.
+{{/in_ca}}
+```
+
+
+Create a view "context" object -- which could also be an associative array, but those don't do functions quite as well:
+
+```php
+<?php
+class Chris {
+    public $name  = "Chris";
+    public $value = 10000;
+
+    public function taxed_value() {
+        return $this->value - ($this->value * 0.4);
+    }
+
+    public $in_ca = true;
+}
+```
+
+
+And render it:
+
+```php
+<?php
+$m = new Mustache_Engine;
+$chris = new Chris;
+echo $m->render($template, $chris);
+```
+
+
+And That's Not All!
+-------------------
+
+Read [the Mustache.php documentation](https://github.com/bobthecow/mustache.php/wiki/Home) for more information.
+
+
+See Also
+--------
+
+ * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md).
+ * [mustache(5)](http://mustache.github.com/mustache.5.html) man page.
diff --git a/lib/mustache/composer.json b/lib/mustache/composer.json
new file mode 100644 (file)
index 0000000..2969d03
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    "name": "mustache/mustache",
+    "description": "A Mustache implementation in PHP.",
+    "keywords": ["templating", "mustache"],
+    "homepage": "https://github.com/bobthecow/mustache.php",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Justin Hileman",
+            "email": "justin@justinhileman.info",
+            "homepage": "http://justinhileman.com"
+        }
+    ],
+    "require": {
+        "php": ">=5.2.4"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "*"
+    },
+    "autoload": {
+        "psr-0": { "Mustache": "src/" }
+    }
+}
diff --git a/lib/mustache/readme_moodle.txt b/lib/mustache/readme_moodle.txt
new file mode 100644 (file)
index 0000000..31ce19d
--- /dev/null
@@ -0,0 +1,11 @@
+Description of Mustache library import into moodle.
+
+Download from https://github.com/bobthecow/mustache.php
+
+Delete folder "test"
+
+Delete phpunit.xml.dist
+
+Delete hidden files ".*"
+
+Delete folder "bin"
diff --git a/lib/mustache/src/Mustache/Autoloader.php b/lib/mustache/src/Mustache/Autoloader.php
new file mode 100644 (file)
index 0000000..8c8e906
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache class autoloader.
+ */
+class Mustache_Autoloader
+{
+    private $baseDir;
+
+    /**
+     * Autoloader constructor.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     */
+    public function __construct($baseDir = null)
+    {
+        if ($baseDir === null) {
+            $baseDir = dirname(__FILE__).'/..';
+        }
+
+        // realpath doesn't always work, for example, with stream URIs
+        $realDir = realpath($baseDir);
+        if (is_dir($realDir)) {
+            $this->baseDir = $realDir;
+        } else {
+            $this->baseDir = $baseDir;
+        }
+    }
+
+    /**
+     * Register a new instance as an SPL autoloader.
+     *
+     * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
+     *
+     * @return Mustache_Autoloader Registered Autoloader instance
+     */
+    public static function register($baseDir = null)
+    {
+        $loader = new self($baseDir);
+        spl_autoload_register(array($loader, 'autoload'));
+
+        return $loader;
+    }
+
+    /**
+     * Autoload Mustache classes.
+     *
+     * @param string $class
+     */
+    public function autoload($class)
+    {
+        if ($class[0] === '\\') {
+            $class = substr($class, 1);
+        }
+
+        if (strpos($class, 'Mustache') !== 0) {
+            return;
+        }
+
+        $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
+        if (is_file($file)) {
+            require $file;
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache.php b/lib/mustache/src/Mustache/Cache.php
new file mode 100644 (file)
index 0000000..c8fc5d5
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache interface.
+ *
+ * Interface for caching and loading Mustache_Template classes
+ * generated by the Mustache_Compiler.
+ */
+interface Mustache_Cache
+{
+    /**
+     * Load a compiled Mustache_Template class from cache.
+     *
+     * @param string $key
+     *
+     * @return boolean indicates successfully class load
+     */
+    public function load($key);
+
+    /**
+     * Cache and load a compiled Mustache_Template class.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value);
+}
diff --git a/lib/mustache/src/Mustache/Cache/AbstractCache.php b/lib/mustache/src/Mustache/Cache/AbstractCache.php
new file mode 100644 (file)
index 0000000..98b6451
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Cache class.
+ *
+ * Provides logging support to child implementations.
+ *
+ * @abstract
+ */
+abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
+{
+    private $logger = null;
+
+    /**
+     * Get the current logger instance.
+     *
+     * @return Mustache_Logger|Psr\Log\LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set a logger instance.
+     *
+     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+     */
+    public function setLogger($logger = null)
+    {
+        if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        $this->logger = $logger;
+    }
+
+    /**
+     * Add a log record if logging is enabled.
+     *
+     * @param integer $level   The logging level
+     * @param string  $message The log message
+     * @param array   $context The log context
+     */
+    protected function log($level, $message, array $context = array())
+    {
+        if (isset($this->logger)) {
+            $this->logger->log($level, $message, $context);
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache/FilesystemCache.php b/lib/mustache/src/Mustache/Cache/FilesystemCache.php
new file mode 100644 (file)
index 0000000..120ad2d
--- /dev/null
@@ -0,0 +1,159 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache filesystem implementation.
+ *
+ * A FilesystemCache instance caches Mustache Template classes from the filesystem by name:
+ *
+ *     $cache = new Mustache_Cache_FilesystemCache(dirname(__FILE__).'/cache');
+ *     $cache->cache($className, $compiledSource);
+ *
+ * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k?
+ */
+class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache
+{
+    private $baseDir;
+    private $fileMode;
+
+    /**
+     * Filesystem cache constructor.
+     *
+     * @param string $baseDir  Directory for compiled templates.
+     * @param int    $fileMode Override default permissions for cache files. Defaults to using the system-defined umask.
+     */
+    public function __construct($baseDir, $fileMode = null)
+    {
+        $this->baseDir = $baseDir;
+        $this->fileMode = $fileMode;
+    }
+
+    /**
+     * Load the class from cache using `require_once`.
+     *
+     * @param string $key
+     *
+     * @return boolean
+     */
+    public function load($key)
+    {
+        $fileName = $this->getCacheFilename($key);
+        if (!is_file($fileName)) {
+            return false;
+        }
+
+        require_once $fileName;
+
+        return true;
+    }
+
+    /**
+     * Cache and load the compiled class
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value)
+    {
+        $fileName = $this->getCacheFilename($key);
+
+        $this->log(
+            Mustache_Logger::DEBUG,
+            'Writing to template cache: "{fileName}"',
+            array('fileName' => $fileName)
+        );
+
+        $this->writeFile($fileName, $value);
+        $this->load($key);
+    }
+
+    /**
+     * Build the cache filename.
+     * Subclasses should override for custom cache directory structures.
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    protected function getCacheFilename($name)
+    {
+        return sprintf('%s/%s.php', $this->baseDir, $name);
+    }
+
+    /**
+     * Create cache directory
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to create directory
+     *
+     * @param string $fileName
+     *
+     * @return string
+     */
+    private function buildDirectoryForFilename($fileName)
+    {
+        $dirName = dirname($fileName);
+        if (!is_dir($dirName)) {
+            $this->log(
+                Mustache_Logger::INFO,
+                'Creating Mustache template cache directory: "{dirName}"',
+                array('dirName' => $dirName)
+            );
+
+            @mkdir($dirName, 0777, true);
+            if (!is_dir($dirName)) {
+                throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
+            }
+        }
+
+        return $dirName;
+    }
+
+    /**
+     * Write cache file
+     *
+     * @throws Mustache_Exception_RuntimeException If unable to write file
+     *
+     * @param string $fileName
+     * @param string $value
+     *
+     * @return void
+     */
+    private function writeFile($fileName, $value)
+    {
+        $dirName = $this->buildDirectoryForFilename($fileName);
+
+        $this->log(
+            Mustache_Logger::DEBUG,
+            'Caching compiled template to "{fileName}"',
+            array('fileName' => $fileName)
+        );
+
+        $tempFile = tempnam($dirName, basename($fileName));
+        if (false !== @file_put_contents($tempFile, $value)) {
+            if (@rename($tempFile, $fileName)) {
+                $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask());
+                @chmod($fileName, $mode);
+
+                return;
+            }
+
+            $this->log(
+                Mustache_Logger::ERROR,
+                'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
+                array('tempName' => $tempFile, 'fileName' => $fileName)
+            );
+        }
+
+        throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
+    }
+}
diff --git a/lib/mustache/src/Mustache/Cache/NoopCache.php b/lib/mustache/src/Mustache/Cache/NoopCache.php
new file mode 100644 (file)
index 0000000..d3a7e1f
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Cache in-memory implementation.
+ *
+ * The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not
+ * recommended for production use.
+ */
+class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache
+{
+    /**
+     * Loads nothing. Move along.
+     *
+     * @param string $key
+     *
+     * @return boolean
+     */
+    public function load($key)
+    {
+        return false;
+    }
+
+    /**
+     * Loads the compiled Mustache Template class without caching.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return void
+     */
+    public function cache($key, $value)
+    {
+        $this->log(
+            Mustache_Logger::WARNING,
+            'Template cache disabled, evaluating "{className}" class at runtime',
+            array('className' => $key)
+        );
+        eval('?>' . $value);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Compiler.php b/lib/mustache/src/Mustache/Compiler.php
new file mode 100644 (file)
index 0000000..72d3414
--- /dev/null
@@ -0,0 +1,646 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Compiler class.
+ *
+ * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
+ */
+class Mustache_Compiler
+{
+
+    private $pragmas;
+    private $defaultPragmas = array();
+    private $sections;
+    private $source;
+    private $indentNextLine;
+    private $customEscape;
+    private $entityFlags;
+    private $charset;
+    private $strictCallables;
+
+    /**
+     * Compile a Mustache token parse tree into PHP source code.
+     *
+     * @param string $source          Mustache Template source code
+     * @param string $tree            Parse tree of Mustache tokens
+     * @param string $name            Mustache Template class name
+     * @param bool   $customEscape    (default: false)
+     * @param string $charset         (default: 'UTF-8')
+     * @param bool   $strictCallables (default: false)
+     * @param int    $entityFlags     (default: ENT_COMPAT)
+     *
+     * @return string Generated PHP source code
+     */
+    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
+    {
+        $this->pragmas         = $this->defaultPragmas;
+        $this->sections        = array();
+        $this->source          = $source;
+        $this->indentNextLine  = true;
+        $this->customEscape    = $customEscape;
+        $this->entityFlags     = $entityFlags;
+        $this->charset         = $charset;
+        $this->strictCallables = $strictCallables;
+
+        return $this->writeCode($tree, $name);
+    }
+
+    /**
+     * Enable pragmas across all templates, regardless of the presence of pragma
+     * tags in the individual templates.
+     *
+     * @internal Users should set global pragmas in Mustache_Engine, not here :)
+     *
+     * @param string[] $pragmas
+     */
+    public function setPragmas(array $pragmas)
+    {
+        $this->pragmas = array();
+        foreach ($pragmas as $pragma) {
+            $this->pragmas[$pragma] = true;
+        }
+        $this->defaultPragmas = $this->pragmas;
+    }
+
+    /**
+     * Helper function for walking the Mustache token parse tree.
+     *
+     * @throws Mustache_Exception_SyntaxException upon encountering unknown token types.
+     *
+     * @param array $tree  Parse tree of Mustache tokens
+     * @param int   $level (default: 0)
+     *
+     * @return string Generated PHP source code
+     */
+    private function walk(array $tree, $level = 0)
+    {
+        $code = '';
+        $level++;
+        foreach ($tree as $node) {
+            switch ($node[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_PRAGMA:
+                    $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true;
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                    $code .= $this->section(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_INVERTED:
+                    $code .= $this->invertedSection(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                    $code .= $this->partial(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT:
+                    $code .= $this->parent(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $node[Mustache_Tokenizer::NODES],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_ARG:
+                    $code .= $this->blockArg(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_VAR:
+                    $code .= $this->blockVar(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_COMMENT:
+                    break;
+
+                case Mustache_Tokenizer::T_ESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED:
+                case Mustache_Tokenizer::T_UNESCAPED_2:
+                    $code .= $this->variable(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
+                        $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED,
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_TEXT:
+                    $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
+                    break;
+
+                default:
+                    throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
+            }
+        }
+
+        return $code;
+    }
+
+    const KLASS = '<?php
+
+        class %s extends Mustache_Template
+        {
+            private $lambdaHelper;%s
+
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
+                $buffer = \'\';
+                $newContext = array();
+        %s
+
+                return $buffer;
+            }
+        %s
+        }';
+
+    const KLASS_NO_LAMBDAS = '<?php
+
+        class %s extends Mustache_Template
+        {%s
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $buffer = \'\';
+                $newContext = array();
+        %s
+
+                return $buffer;
+            }
+        }';
+
+    const STRICT_CALLABLE = 'protected $strictCallables = true;';
+
+    /**
+     * Generate Mustache Template class PHP source.
+     *
+     * @param array  $tree Parse tree of Mustache tokens
+     * @param string $name Mustache Template class name
+     *
+     * @return string Generated PHP source code
+     */
+    private function writeCode($tree, $name)
+    {
+        $code     = $this->walk($tree);
+        $sections = implode("\n", $this->sections);
+        $klass    = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+
+        $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
+
+        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
+    }
+
+    const BLOCK_VAR = '
+        $value = $this->resolveValue($context->findInBlock(%s), $context, $indent);
+        if ($value && !is_array($value) && !is_object($value)) {
+            $buffer .= $value;
+        } else {
+            %s
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block variable PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $start Section start offset
+     * @param int    $end   Section end offset
+     * @param string $otag  Current Mustache opening tag
+     * @param string $ctag  Current Mustache closing tag
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $id = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $this->walk($nodes, 2));
+    }
+
+    const BLOCK_ARG = '
+        // %s block_arg
+        $value = $this->section%s($context, $indent, true);
+        $newContext[%s] = %s$value;
+    ';
+
+    /**
+     * Generate Mustache Template inheritance block argument PHP source.
+     *
+     * @param array  $nodes Array of child tokens
+     * @param string $id    Section name
+     * @param int    $start Section start offset
+     * @param int    $end   Section end offset
+     * @param string $otag  Current Mustache opening tag
+     * @param string $ctag  Current Mustache closing tag
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $key = $this->section($nodes, $id, array(), $start, $end, $otag, $ctag, $level, true);
+        $id  = var_export($id, true);
+
+        return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent());
+    }
+
+    const SECTION_CALL = '
+        // %s section
+        $value = $context->%s(%s);%s
+        $buffer .= $this->section%s($context, $indent, $value);
+    ';
+
+    const SECTION = '
+        private function section%s(Mustache_Context $context, $indent, $value)
+        {
+            $buffer = \'\';
+            if (%s) {
+                $source = %s;
+                $result = call_user_func($value, $source, $this->lambdaHelper);
+                if (strpos($result, \'{{\') === false) {
+                    $buffer .= $result;
+                } else {
+                    $buffer .= $this->mustache
+                        ->loadLambda((string) $result%s)
+                        ->renderInternal($context);
+                }
+            } elseif (!empty($value)) {
+                $values = $this->isIterable($value) ? $value : array($value);
+                foreach ($values as $value) {
+                    $context->push($value);
+                    %s
+                    $context->pop();
+                }
+            }
+
+            return $buffer;
+        }';
+
+    /**
+     * Generate Mustache Template section PHP source.
+     *
+     * @param array    $nodes   Array of child tokens
+     * @param string   $id      Section name
+     * @param string[] $filters Array of filters
+     * @param int      $start   Section start offset
+     * @param int      $end     Section end offset
+     * @param string   $otag    Current Mustache opening tag
+     * @param string   $ctag    Current Mustache closing tag
+     * @param int      $level
+     * @param bool     $arg     (default: false)
+     *
+     * @return string Generated section PHP source code
+     */
+    private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level, $arg = false)
+    {
+        $source   = var_export(substr($this->source, $start, $end - $start), true);
+        $callable = $this->getCallable();
+
+        if ($otag !== '{{' || $ctag !== '}}') {
+            $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
+        } else {
+            $delims = '';
+        }
+
+        $key = ucfirst(md5($delims."\n".$source));
+
+        if (!isset($this->sections[$key])) {
+            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
+        }
+
+        if ($arg === true) {
+            return $key;
+        } else {
+            $method  = $this->getFindMethod($id);
+            $id      = var_export($id, true);
+            $filters = $this->getFilters($filters, $level);
+
+            return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
+        }
+    }
+
+    const INVERTED_SECTION = '
+        // %s inverted section
+        $value = $context->%s(%s);%s
+        if (empty($value)) {
+            %s
+        }';
+
+    /**
+     * Generate Mustache Template inverted section PHP source.
+     *
+     * @param array    $nodes   Array of child tokens
+     * @param string   $id      Section name
+     * @param string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated inverted section PHP source code
+     */
+    private function invertedSection($nodes, $id, $filters, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = var_export($id, true);
+        $filters = $this->getFilters($filters, $level);
+
+        return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
+    }
+
+    const PARTIAL_INDENT = ', $indent . %s';
+    const PARTIAL = '
+        if ($partial = $this->mustache->loadPartial(%s)) {
+            $buffer .= $partial->renderInternal($context%s);
+        }
+    ';
+
+    /**
+     * Generate Mustache Template partial call PHP source.
+     *
+     * @param string $id     Partial name
+     * @param string $indent Whitespace indent to apply to partial
+     * @param int    $level
+     *
+     * @return string Generated partial call PHP source code
+     */
+    private function partial($id, $indent, $level)
+    {
+        if ($indent !== '') {
+            $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true));
+        } else {
+            $indentParam = '';
+        }
+
+        return sprintf(
+            $this->prepare(self::PARTIAL, $level),
+            var_export($id, true),
+            $indentParam
+        );
+    }
+
+    const PARENT = '
+        %s
+
+        if ($parent = $this->mustache->LoadPartial(%s)) {
+            $context->pushBlockContext($newContext);
+            $buffer .= $parent->renderInternal($context, $indent);
+            $context->popBlockContext();
+        }
+    ';
+
+    /**
+     * Generate Mustache Template inheritance parent call PHP source.
+     *
+     * @param string $id       Parent tag name
+     * @param string $indent   Whitespace indent to apply to parent
+     * @param array  $children Child nodes
+     * @param int    $level
+     *
+     * @return string Generated PHP source code
+     */
+    private function parent($id, $indent, array $children, $level)
+    {
+        $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs'));
+
+        return sprintf(
+            $this->prepare(self::PARENT, $level),
+            $this->walk($realChildren, $level),
+            var_export($id, true),
+            var_export($indent, true)
+        );
+    }
+
+    /**
+     * Helper method for filtering out non-block-arg tokens.
+     *
+     * @param array $node
+     *
+     * @return boolean True if $node is a block arg token.
+     */
+    private static function onlyBlockArgs(array $node)
+    {
+        return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG;
+    }
+
+    const VARIABLE = '
+        $value = $this->resolveValue($context->%s(%s), $context, $indent);%s
+        $buffer .= %s%s;
+    ';
+
+    /**
+     * Generate Mustache Template variable interpolation PHP source.
+     *
+     * @param string   $id      Variable name
+     * @param string[] $filters Array of filters
+     * @param boolean  $escape  Escape the variable value for output?
+     * @param int      $level
+     *
+     * @return string Generated variable interpolation PHP source
+     */
+    private function variable($id, $filters, $escape, $level)
+    {
+        $method  = $this->getFindMethod($id);
+        $id      = ($method !== 'last') ? var_export($id, true) : '';
+        $filters = $this->getFilters($filters, $level);
+        $value   = $escape ? $this->getEscape() : '$value';
+
+        return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
+    }
+
+    const FILTER = '
+        $filter = $context->%s(%s);
+        if (!(%s)) {
+            throw new Mustache_Exception_UnknownFilterException(%s);
+        }
+        $value = call_user_func($filter, $value);%s
+    ';
+
+    /**
+     * Generate Mustache Template variable filtering PHP source.
+     *
+     * @param string[] $filters Array of filters
+     * @param int      $level
+     *
+     * @return string Generated filter PHP source
+     */
+    private function getFilters(array $filters, $level)
+    {
+        if (empty($filters)) {
+            return '';
+        }
+
+        $name     = array_shift($filters);
+        $method   = $this->getFindMethod($name);
+        $filter   = ($method !== 'last') ? var_export($name, true) : '';
+        $callable = $this->getCallable('$filter');
+        $msg      = var_export($name, true);
+
+        return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level));
+    }
+
+    const LINE = '$buffer .= "\n";';
+    const TEXT = '$buffer .= %s%s;';
+
+    /**
+     * Generate Mustache Template output Buffer call PHP source.
+     *
+     * @param string $text
+     * @param int    $level
+     *
+     * @return string Generated output Buffer call PHP source
+     */
+    private function text($text, $level)
+    {
+        $indentNextLine = (substr($text, -1) === "\n");
+        $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
+        $this->indentNextLine = $indentNextLine;
+
+        return $code;
+    }
+
+    /**
+     * Prepare PHP source code snippet for output.
+     *
+     * @param string  $text
+     * @param int     $bonus          Additional indent level (default: 0)
+     * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
+     * @param boolean $appendNewline  Append a newline to the snippet? (default: false)
+     *
+     * @return string PHP source code snippet
+     */
+    private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
+    {
+        $text = ($prependNewline ? "\n" : '').trim($text);
+        if ($prependNewline) {
+            $bonus++;
+        }
+        if ($appendNewline) {
+            $text .= "\n";
+        }
+
+        return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
+    }
+
+    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
+    const CUSTOM_ESCAPE  = 'call_user_func($this->mustache->getEscape(), %s)';
+
+    /**
+     * Get the current escaper.
+     *
+     * @param string $value (default: '$value')
+     *
+     * @return string Either a custom callback, or an inline call to `htmlspecialchars`
+     */
+    private function getEscape($value = '$value')
+    {
+        if ($this->customEscape) {
+            return sprintf(self::CUSTOM_ESCAPE, $value);
+        }
+
+        return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
+    }
+
+    /**
+     * Select the appropriate Context `find` method for a given $id.
+     *
+     * The return value will be one of `find`, `findDot` or `last`.
+     *
+     * @see Mustache_Context::find
+     * @see Mustache_Context::findDot
+     * @see Mustache_Context::last
+     *
+     * @param string $id Variable name
+     *
+     * @return string `find` method name
+     */
+    private function getFindMethod($id)
+    {
+        if ($id === '.') {
+            return 'last';
+        }
+
+        if (strpos($id, '.') === false) {
+            return 'find';
+        }
+
+        return 'findDot';
+    }
+
+    const IS_CALLABLE        = '!is_string(%s) && is_callable(%s)';
+    const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)';
+
+    /**
+     * Helper function to compile strict vs lax "is callable" logic.
+     *
+     * @param string $variable (default: '$value')
+     *
+     * @return string "is callable" logic
+     */
+    private function getCallable($variable = '$value')
+    {
+        $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE;
+
+        return sprintf($tpl, $variable, $variable);
+    }
+
+    const LINE_INDENT = '$indent . ';
+
+    /**
+     * Get the current $indent prefix to write to the buffer.
+     *
+     * @return string "$indent . " or ""
+     */
+    private function flushIndent()
+    {
+        if (!$this->indentNextLine) {
+            return '';
+        }
+
+        $this->indentNextLine = false;
+
+        return self::LINE_INDENT;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Context.php b/lib/mustache/src/Mustache/Context.php
new file mode 100644 (file)
index 0000000..db03acc
--- /dev/null
@@ -0,0 +1,206 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template rendering Context.
+ */
+class Mustache_Context
+{
+    private $stack      = array();
+    private $blockStack = array();
+
+    /**
+     * Mustache rendering Context constructor.
+     *
+     * @param mixed $context Default rendering context (default: null)
+     */
+    public function __construct($context = null)
+    {
+        if ($context !== null) {
+            $this->stack = array($context);
+        }
+    }
+
+    /**
+     * Push a new Context frame onto the stack.
+     *
+     * @param mixed $value Object or array to use for context
+     */
+    public function push($value)
+    {
+        array_push($this->stack, $value);
+    }
+
+    /**
+     * Push a new Context frame onto the block context stack.
+     *
+     * @param mixed $value Object or array to use for block context
+     */
+    public function pushBlockContext($value)
+    {
+        array_push($this->blockStack, $value);
+    }
+
+    /**
+     * Pop the last Context frame from the stack.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function pop()
+    {
+        return array_pop($this->stack);
+    }
+
+    /**
+     * Pop the last block Context frame from the stack.
+     *
+     * @return mixed Last block Context frame (object or array)
+     */
+    public function popBlockContext()
+    {
+        return array_pop($this->blockStack);
+    }
+
+    /**
+     * Get the last Context frame.
+     *
+     * @return mixed Last Context frame (object or array)
+     */
+    public function last()
+    {
+        return end($this->stack);
+    }
+
+    /**
+     * Find a variable in the Context stack.
+     *
+     * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
+     * rendering context, look for a variable with the given name:
+     *
+     *  * If the Context frame is an associative array which contains the key $id, returns the value of that element.
+     *  * If the Context frame is an object, this will check first for a public method, then a public property named
+     *    $id. Failing both of these, it will try `__isset` and `__get` magic methods.
+     *  * If a value named $id is not found in any Context frame, returns an empty string.
+     *
+     * @param string $id Variable name
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function find($id)
+    {
+        return $this->findVariableInStack($id, $this->stack);
+    }
+
+    /**
+     * Find a 'dot notation' variable in the Context stack.
+     *
+     * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
+     * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
+     * result. For example, given the following context stack:
+     *
+     *     $data = array(
+     *         'name' => 'Fred',
+     *         'child' => array(
+     *             'name' => 'Bob'
+     *         ),
+     *     );
+     *
+     * ... and the Mustache following template:
+     *
+     *     {{ child.name }}
+     *
+     * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
+     * Context frames.
+     *
+     * @param string $id Dotted variable selector
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    public function findDot($id)
+    {
+        $chunks = explode('.', $id);
+        $first  = array_shift($chunks);
+        $value  = $this->findVariableInStack($first, $this->stack);
+
+        foreach ($chunks as $chunk) {
+            if ($value === '') {
+                return $value;
+            }
+
+            $value = $this->findVariableInStack($chunk, array($value));
+        }
+
+        return $value;
+    }
+
+    /**
+     * Find an argument in the block context stack.
+     *
+     * @param string $id
+     *
+     * @return mixed Variable value, or '' if not found.
+     */
+    public function findInBlock($id)
+    {
+        foreach ($this->blockStack as $context) {
+            if (array_key_exists($id, $context)) {
+                return $context[$id];
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * Helper function to find a variable in the Context stack.
+     *
+     * @see Mustache_Context::find
+     *
+     * @param string $id    Variable name
+     * @param array  $stack Context stack
+     *
+     * @return mixed Variable value, or '' if not found
+     */
+    private function findVariableInStack($id, array $stack)
+    {
+        for ($i = count($stack) - 1; $i >= 0; $i--) {
+            $frame = &$stack[$i];
+
+            switch (gettype($frame)) {
+                case 'object':
+                    if (!($frame instanceof Closure)) {
+                        // Note that is_callable() *will not work here*
+                        // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
+                        if (method_exists($frame, $id)) {
+                            return $frame->$id();
+                        }
+
+                        if (isset($frame->$id)) {
+                            return $frame->$id;
+                        }
+
+                        if ($frame instanceof ArrayAccess && isset($frame[$id])) {
+                            return $frame[$id];
+                        }
+                    }
+                    break;
+
+                case 'array':
+                    if (array_key_exists($id, $frame)) {
+                        return $frame[$id];
+                    }
+                    break;
+            }
+        }
+
+        return '';
+    }
+}
diff --git a/lib/mustache/src/Mustache/Engine.php b/lib/mustache/src/Mustache/Engine.php
new file mode 100644 (file)
index 0000000..4cac394
--- /dev/null
@@ -0,0 +1,785 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache implementation in PHP.
+ *
+ * {@link http://defunkt.github.com/mustache}
+ *
+ * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
+ * logic from template files. In fact, it is not even possible to embed logic in the template.
+ *
+ * This is very, very rad.
+ *
+ * @author Justin Hileman {@link http://justinhileman.com}
+ */
+class Mustache_Engine
+{
+    const VERSION        = '2.7.0';
+    const SPEC_VERSION   = '1.1.2';
+
+    const PRAGMA_FILTERS = 'FILTERS';
+    const PRAGMA_BLOCKS  = 'BLOCKS';
+
+    // Known pragmas
+    private static $knownPragmas = array(
+        self::PRAGMA_FILTERS => true,
+        self::PRAGMA_BLOCKS  => true,
+    );
+
+    // Template cache
+    private $templates = array();
+
+    // Environment
+    private $templateClassPrefix = '__Mustache_';
+    private $cache;
+    private $lambdaCache;
+    private $cacheLambdaTemplates = false;
+    private $loader;
+    private $partialsLoader;
+    private $helpers;
+    private $escape;
+    private $entityFlags = ENT_COMPAT;
+    private $charset = 'UTF-8';
+    private $logger;
+    private $strictCallables = false;
+    private $pragmas = array();
+
+    // Services
+    private $tokenizer;
+    private $parser;
+    private $compiler;
+
+    /**
+     * Mustache class constructor.
+     *
+     * Passing an $options array allows overriding certain Mustache options during instantiation:
+     *
+     *     $options = array(
+     *         // The class prefix for compiled templates. Defaults to '__Mustache_'.
+     *         'template_class_prefix' => '__MyTemplates_',
+     *
+     *         // A Mustache cache instance or a cache directory string for compiled templates.
+     *         // Mustache will not cache templates unless this is set.
+     *         'cache' => dirname(__FILE__).'/tmp/cache/mustache',
+     *
+     *         // Override default permissions for cache files. Defaults to using the system-defined umask. It is
+     *         // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
+     *         'cache_file_mode' => 0666,
+     *
+     *         // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda
+     *         // sections are often too dynamic to benefit from caching.
+     *         'cache_lambda_templates' => true,
+     *
+     *         // A Mustache template loader instance. Uses a StringLoader if not specified.
+     *         'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+     *
+     *         // A Mustache loader instance for partials.
+     *         'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+     *
+     *         // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
+     *         // efficient or lazy as a Filesystem (or database) loader.
+     *         'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
+     *
+     *         // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
+     *         // sections), or any other valid Mustache context value. They will be prepended to the context stack,
+     *         // so they will be available in any template loaded by this Mustache instance.
+     *         'helpers' => array('i18n' => function ($text) {
+     *             // do something translatey here...
+     *         }),
+     *
+     *         // An 'escape' callback, responsible for escaping double-mustache variables.
+     *         'escape' => function ($value) {
+     *             return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
+     *         },
+     *
+     *         // Type argument for `htmlspecialchars`.  Defaults to ENT_COMPAT.  You may prefer ENT_QUOTES.
+     *         'entity_flags' => ENT_QUOTES,
+     *
+     *         // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
+     *         'charset' => 'ISO-8859-1',
+     *
+     *         // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
+     *         // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
+     *         // available as well:
+     *         'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
+     *
+     *         // Only treat Closure instances and invokable classes as callable. If true, values like
+     *         // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
+     *         // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
+     *         // helps protect against arbitrary code execution when user input is passed directly into the template.
+     *         // This currently defaults to false, but will default to true in v3.0.
+     *         'strict_callables' => true,
+     *
+     *         // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual
+     *         // templates.
+     *         'pragmas' => [Mustache_Engine::PRAGMA_FILTERS],
+     *     );
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
+     *
+     * @param array $options (default: array())
+     */
+    public function __construct(array $options = array())
+    {
+        if (isset($options['template_class_prefix'])) {
+            $this->templateClassPrefix = $options['template_class_prefix'];
+        }
+
+        if (isset($options['cache'])) {
+            $cache = $options['cache'];
+
+            if (is_string($cache)) {
+                $mode  = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
+                $cache = new Mustache_Cache_FilesystemCache($cache, $mode);
+            }
+
+            $this->setCache($cache);
+        }
+
+        if (isset($options['cache_lambda_templates'])) {
+            $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
+        }
+
+        if (isset($options['loader'])) {
+            $this->setLoader($options['loader']);
+        }
+
+        if (isset($options['partials_loader'])) {
+            $this->setPartialsLoader($options['partials_loader']);
+        }
+
+        if (isset($options['partials'])) {
+            $this->setPartials($options['partials']);
+        }
+
+        if (isset($options['helpers'])) {
+            $this->setHelpers($options['helpers']);
+        }
+
+        if (isset($options['escape'])) {
+            if (!is_callable($options['escape'])) {
+                throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
+            }
+
+            $this->escape = $options['escape'];
+        }
+
+        if (isset($options['entity_flags'])) {
+          $this->entityFlags = $options['entity_flags'];
+        }
+
+        if (isset($options['charset'])) {
+            $this->charset = $options['charset'];
+        }
+
+        if (isset($options['logger'])) {
+            $this->setLogger($options['logger']);
+        }
+
+        if (isset($options['strict_callables'])) {
+            $this->strictCallables = $options['strict_callables'];
+        }
+
+        if (isset($options['pragmas'])) {
+            foreach ($options['pragmas'] as $pragma) {
+                if (!isset(self::$knownPragmas[$pragma])) {
+                    throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma));
+                }
+                $this->pragmas[$pragma] = true;
+            }
+        }
+    }
+
+    /**
+     * Shortcut 'render' invocation.
+     *
+     * Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Template::render
+     *
+     * @param string $template
+     * @param mixed  $context  (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function render($template, $context = array())
+    {
+        return $this->loadTemplate($template)->render($context);
+    }
+
+    /**
+     * Get the current Mustache escape callback.
+     *
+     * @return callable|null
+     */
+    public function getEscape()
+    {
+        return $this->escape;
+    }
+
+    /**
+     * Get the current Mustache entitity type to escape.
+     *
+     * @return int
+     */
+    public function getEntityFlags()
+    {
+        return $this->entityFlags;
+    }
+
+    /**
+     * Get the current Mustache character set.
+     *
+     * @return string
+     */
+    public function getCharset()
+    {
+        return $this->charset;
+    }
+
+    /**
+     * Get the current globally enabled pragmas.
+     *
+     * @return array
+     */
+    public function getPragmas()
+    {
+        return array_keys($this->pragmas);
+    }
+
+    /**
+     * Set the Mustache template Loader instance.
+     *
+     * @param Mustache_Loader $loader
+     */
+    public function setLoader(Mustache_Loader $loader)
+    {
+        $this->loader = $loader;
+    }
+
+    /**
+     * Get the current Mustache template Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * a StringLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getLoader()
+    {
+        if (!isset($this->loader)) {
+            $this->loader = new Mustache_Loader_StringLoader();
+        }
+
+        return $this->loader;
+    }
+
+    /**
+     * Set the Mustache partials Loader instance.
+     *
+     * @param Mustache_Loader $partialsLoader
+     */
+    public function setPartialsLoader(Mustache_Loader $partialsLoader)
+    {
+        $this->partialsLoader = $partialsLoader;
+    }
+
+    /**
+     * Get the current Mustache partials Loader instance.
+     *
+     * If no Loader instance has been explicitly specified, this method will instantiate and return
+     * an ArrayLoader instance.
+     *
+     * @return Mustache_Loader
+     */
+    public function getPartialsLoader()
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+        }
+
+        return $this->partialsLoader;
+    }
+
+    /**
+     * Set partials for the current partials Loader instance.
+     *
+     * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable
+     *
+     * @param array $partials (default: array())
+     */
+    public function setPartials(array $partials = array())
+    {
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader();
+        }
+
+        if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
+            throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
+        }
+
+        $this->partialsLoader->setTemplates($partials);
+    }
+
+    /**
+     * Set an array of Mustache helpers.
+     *
+     * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
+     * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
+     * any template loaded by this Mustache instance.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
+     *
+     * @param array|Traversable $helpers
+     */
+    public function setHelpers($helpers)
+    {
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
+        }
+
+        $this->getHelpers()->clear();
+
+        foreach ($helpers as $name => $helper) {
+            $this->addHelper($name, $helper);
+        }
+    }
+
+    /**
+     * Get the current set of Mustache helpers.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @return Mustache_HelperCollection
+     */
+    public function getHelpers()
+    {
+        if (!isset($this->helpers)) {
+            $this->helpers = new Mustache_HelperCollection();
+        }
+
+        return $this->helpers;
+    }
+
+    /**
+     * Add a new Mustache helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function addHelper($name, $helper)
+    {
+        $this->getHelpers()->add($name, $helper);
+    }
+
+    /**
+     * Get a Mustache helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function getHelper($name)
+    {
+        return $this->getHelpers()->get($name);
+    }
+
+    /**
+     * Check whether this Mustache instance has a helper.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     *
+     * @return boolean True if the helper is present
+     */
+    public function hasHelper($name)
+    {
+        return $this->getHelpers()->has($name);
+    }
+
+    /**
+     * Remove a helper by name.
+     *
+     * @see Mustache_Engine::setHelpers
+     *
+     * @param string $name
+     */
+    public function removeHelper($name)
+    {
+        $this->getHelpers()->remove($name);
+    }
+
+    /**
+     * Set the Mustache Logger instance.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface.
+     *
+     * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
+     */
+    public function setLogger($logger = null)
+    {
+        if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
+            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+        }
+
+        if ($this->getCache()->getLogger() === null) {
+            $this->getCache()->setLogger($logger);
+        }
+
+        $this->logger = $logger;
+    }
+
+    /**
+     * Get the current Mustache Logger instance.
+     *
+     * @return Mustache_Logger|Psr\Log\LoggerInterface
+     */
+    public function getLogger()
+    {
+        return $this->logger;
+    }
+
+    /**
+     * Set the Mustache Tokenizer instance.
+     *
+     * @param Mustache_Tokenizer $tokenizer
+     */
+    public function setTokenizer(Mustache_Tokenizer $tokenizer)
+    {
+        $this->tokenizer = $tokenizer;
+    }
+
+    /**
+     * Get the current Mustache Tokenizer instance.
+     *
+     * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Tokenizer
+     */
+    public function getTokenizer()
+    {
+        if (!isset($this->tokenizer)) {
+            $this->tokenizer = new Mustache_Tokenizer();
+        }
+
+        return $this->tokenizer;
+    }
+
+    /**
+     * Set the Mustache Parser instance.
+     *
+     * @param Mustache_Parser $parser
+     */
+    public function setParser(Mustache_Parser $parser)
+    {
+        $this->parser = $parser;
+    }
+
+    /**
+     * Get the current Mustache Parser instance.
+     *
+     * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Parser
+     */
+    public function getParser()
+    {
+        if (!isset($this->parser)) {
+            $this->parser = new Mustache_Parser();
+        }
+
+        return $this->parser;
+    }
+
+    /**
+     * Set the Mustache Compiler instance.
+     *
+     * @param Mustache_Compiler $compiler
+     */
+    public function setCompiler(Mustache_Compiler $compiler)
+    {
+        $this->compiler = $compiler;
+    }
+
+    /**
+     * Get the current Mustache Compiler instance.
+     *
+     * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Compiler
+     */
+    public function getCompiler()
+    {
+        if (!isset($this->compiler)) {
+            $this->compiler = new Mustache_Compiler();
+        }
+
+        return $this->compiler;
+    }
+
+    /**
+     * Set the Mustache Cache instance.
+     *
+     * @param Mustache_Cache $cache
+     */
+    public function setCache(Mustache_Cache $cache)
+    {
+        if (isset($this->logger) && $cache->getLogger() === null) {
+            $cache->setLogger($this->getLogger());
+        }
+
+        $this->cache = $cache;
+    }
+
+    /**
+     * Get the current Mustache Cache instance.
+     *
+     * If no Cache instance has been explicitly specified, this method will instantiate and return a new one.
+     *
+     * @return Mustache_Cache
+     */
+    public function getCache()
+    {
+        if (!isset($this->cache)) {
+            $this->setCache(new Mustache_Cache_NoopCache());
+        }
+
+        return $this->cache;
+    }
+
+    /**
+     * Get the current Lambda Cache instance.
+     *
+     * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache.
+     *
+     * @see Mustache_Engine::getCache
+     *
+     * @return Mustache_Cache
+     */
+    protected function getLambdaCache()
+    {
+        if ($this->cacheLambdaTemplates) {
+            return $this->getCache();
+        }
+
+        if (!isset($this->lambdaCache)) {
+            $this->lambdaCache = new Mustache_Cache_NoopCache();
+        }
+
+        return $this->lambdaCache;
+    }
+
+    /**
+     * Helper method to generate a Mustache template class.
+     *
+     * @param string $source
+     *
+     * @return string Mustache Template class name
+     */
+    public function getTemplateClassName($source)
+    {
+        return $this->templateClassPrefix . md5(sprintf(
+            'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,pragmas:%s,source:%s',
+            self::VERSION,
+            isset($this->escape) ? 'custom' : 'default',
+            $this->entityFlags,
+            $this->charset,
+            $this->strictCallables ? 'true' : 'false',
+            implode(' ', $this->getPragmas()),
+            $source
+        ));
+    }
+
+    /**
+     * Load a Mustache Template by name.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadTemplate($name)
+    {
+        return $this->loadSource($this->getLoader()->load($name));
+    }
+
+    /**
+     * Load a Mustache partial Template by name.
+     *
+     * This is a helper method used internally by Template instances for loading partial templates. You can most likely
+     * ignore it completely.
+     *
+     * @param string $name
+     *
+     * @return Mustache_Template
+     */
+    public function loadPartial($name)
+    {
+        try {
+            if (isset($this->partialsLoader)) {
+                $loader = $this->partialsLoader;
+            } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
+                $loader = $this->loader;
+            } else {
+                throw new Mustache_Exception_UnknownTemplateException($name);
+            }
+
+            return $this->loadSource($loader->load($name));
+        } catch (Mustache_Exception_UnknownTemplateException $e) {
+            // If the named partial cannot be found, log then return null.
+            $this->log(
+                Mustache_Logger::WARNING,
+                'Partial not found: "{name}"',
+                array('name' => $e->getTemplateName())
+            );
+        }
+    }
+
+    /**
+     * Load a Mustache lambda Template by source.
+     *
+     * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
+     * likely ignore it completely.
+     *
+     * @param string $source
+     * @param string $delims (default: null)
+     *
+     * @return Mustache_Template
+     */
+    public function loadLambda($source, $delims = null)
+    {
+        if ($delims !== null) {
+            $source = $delims . "\n" . $source;
+        }
+
+        return $this->loadSource($source, $this->getLambdaCache());
+    }
+
+    /**
+     * Instantiate and return a Mustache Template instance by source.
+     *
+     * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect
+     * the 'cache_lambda_templates' configuration option.
+     *
+     * @see Mustache_Engine::loadTemplate
+     * @see Mustache_Engine::loadPartial
+     * @see Mustache_Engine::loadLambda
+     *
+     * @param string         $source
+     * @param Mustache_Cache $cache  (default: null)
+     *
+     * @return Mustache_Template
+     */
+    private function loadSource($source, Mustache_Cache $cache = null)
+    {
+        $className = $this->getTemplateClassName($source);
+
+        if (!isset($this->templates[$className])) {
+            if ($cache === null) {
+                $cache = $this->getCache();
+            }
+
+            if (!class_exists($className, false)) {
+                if (!$cache->load($className)) {
+                    $compiled = $this->compile($source);
+                    $cache->cache($className, $compiled);
+                }
+            }
+
+            $this->log(
+                Mustache_Logger::DEBUG,
+                'Instantiating template: "{className}"',
+                array('className' => $className)
+            );
+
+            $this->templates[$className] = new $className($this);
+        }
+
+        return $this->templates[$className];
+    }
+
+    /**
+     * Helper method to tokenize a Mustache template.
+     *
+     * @see Mustache_Tokenizer::scan
+     *
+     * @param string $source
+     *
+     * @return array Tokens
+     */
+    private function tokenize($source)
+    {
+        return $this->getTokenizer()->scan($source);
+    }
+
+    /**
+     * Helper method to parse a Mustache template.
+     *
+     * @see Mustache_Parser::parse
+     *
+     * @param string $source
+     *
+     * @return array Token tree
+     */
+    private function parse($source)
+    {
+        $parser = $this->getParser();
+        $parser->setPragmas($this->getPragmas());
+
+        return $parser->parse($this->tokenize($source));
+    }
+
+    /**
+     * Helper method to compile a Mustache template.
+     *
+     * @see Mustache_Compiler::compile
+     *
+     * @param string $source
+     *
+     * @return string generated Mustache template class code
+     */
+    private function compile($source)
+    {
+        $tree = $this->parse($source);
+        $name = $this->getTemplateClassName($source);
+
+        $this->log(
+            Mustache_Logger::INFO,
+            'Compiling template to "{className}" class',
+            array('className' => $name)
+        );
+
+        $compiler = $this->getCompiler();
+        $compiler->setPragmas($this->getPragmas());
+
+        return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
+    }
+
+    /**
+     * Add a log record if logging is enabled.
+     *
+     * @param integer $level   The logging level
+     * @param string  $message The log message
+     * @param array   $context The log context
+     */
+    private function log($level, $message, array $context = array())
+    {
+        if (isset($this->logger)) {
+            $this->logger->log($level, $message, $context);
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception.php b/lib/mustache/src/Mustache/Exception.php
new file mode 100644 (file)
index 0000000..8a2b01c
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Exception interface.
+ */
+interface Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/InvalidArgumentException.php b/lib/mustache/src/Mustache/Exception/InvalidArgumentException.php
new file mode 100644 (file)
index 0000000..9bd1107
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Invalid argument exception.
+ */
+class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/LogicException.php b/lib/mustache/src/Mustache/Exception/LogicException.php
new file mode 100644 (file)
index 0000000..255ce54
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Logic exception.
+ */
+class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/RuntimeException.php b/lib/mustache/src/Mustache/Exception/RuntimeException.php
new file mode 100644 (file)
index 0000000..a3c48f7
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Runtime exception.
+ */
+class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}
diff --git a/lib/mustache/src/Mustache/Exception/SyntaxException.php b/lib/mustache/src/Mustache/Exception/SyntaxException.php
new file mode 100644 (file)
index 0000000..7a16f7e
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache syntax exception.
+ */
+class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception
+{
+    protected $token;
+
+    /**
+     * @param string $msg
+     * @param array  $token
+     */
+    public function __construct($msg, array $token)
+    {
+        $this->token = $token;
+        parent::__construct($msg);
+    }
+
+    /**
+     * @return array
+     */
+    public function getToken()
+    {
+        return $this->token;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownFilterException.php b/lib/mustache/src/Mustache/Exception/UnknownFilterException.php
new file mode 100644 (file)
index 0000000..b9a315a
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown filter exception.
+ */
+class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception
+{
+    protected $filterName;
+
+    /**
+     * @param string $filterName
+     */
+    public function __construct($filterName)
+    {
+        $this->filterName = $filterName;
+        parent::__construct(sprintf('Unknown filter: %s', $filterName));
+    }
+
+    public function getFilterName()
+    {
+        return $this->filterName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownHelperException.php b/lib/mustache/src/Mustache/Exception/UnknownHelperException.php
new file mode 100644 (file)
index 0000000..226d774
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown helper exception.
+ */
+class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $helperName;
+
+    /**
+     * @param string $helperName
+     */
+    public function __construct($helperName)
+    {
+        $this->helperName = $helperName;
+        parent::__construct(sprintf('Unknown helper: %s', $helperName));
+    }
+
+    public function getHelperName()
+    {
+        return $this->helperName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Exception/UnknownTemplateException.php b/lib/mustache/src/Mustache/Exception/UnknownTemplateException.php
new file mode 100644 (file)
index 0000000..5dafe89
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown template exception.
+ */
+class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $templateName;
+
+    /**
+     * @param string $templateName
+     */
+    public function __construct($templateName)
+    {
+        $this->templateName = $templateName;
+        parent::__construct(sprintf('Unknown template: %s', $templateName));
+    }
+
+    public function getTemplateName()
+    {
+        return $this->templateName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/HelperCollection.php b/lib/mustache/src/Mustache/HelperCollection.php
new file mode 100644 (file)
index 0000000..c7c7950
--- /dev/null
@@ -0,0 +1,172 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A collection of helpers for a Mustache instance.
+ */
+class Mustache_HelperCollection
+{
+    private $helpers = array();
+
+    /**
+     * Helper Collection constructor.
+     *
+     * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
+     *
+     * @param array|Traversable $helpers (default: null)
+     */
+    public function __construct($helpers = null)
+    {
+        if ($helpers === null) {
+            return;
+        }
+
+        if (!is_array($helpers) && !$helpers instanceof Traversable) {
+            throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers');
+        }
+
+        foreach ($helpers as $name => $helper) {
+            $this->add($name, $helper);
+        }
+    }
+
+    /**
+     * Magic mutator.
+     *
+     * @see Mustache_HelperCollection::add
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function __set($name, $helper)
+    {
+        $this->add($name, $helper);
+    }
+
+    /**
+     * Add a helper to this collection.
+     *
+     * @param string $name
+     * @param mixed  $helper
+     */
+    public function add($name, $helper)
+    {
+        $this->helpers[$name] = $helper;
+    }
+
+    /**
+     * Magic accessor.
+     *
+     * @see Mustache_HelperCollection::get
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function __get($name)
+    {
+        return $this->get($name);
+    }
+
+    /**
+     * Get a helper by name.
+     *
+     * @throws Mustache_Exception_UnknownHelperException If helper does not exist.
+     *
+     * @param string $name
+     *
+     * @return mixed Helper
+     */
+    public function get($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($name);
+        }
+
+        return $this->helpers[$name];
+    }
+
+    /**
+     * Magic isset().
+     *
+     * @see Mustache_HelperCollection::has
+     *
+     * @param string $name
+     *
+     * @return boolean True if helper is present
+     */
+    public function __isset($name)
+    {
+        return $this->has($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @param string $name
+     *
+     * @return boolean True if helper is present
+     */
+    public function has($name)
+    {
+        return array_key_exists($name, $this->helpers);
+    }
+
+    /**
+     * Magic unset().
+     *
+     * @see Mustache_HelperCollection::remove
+     *
+     * @param string $name
+     */
+    public function __unset($name)
+    {
+        $this->remove($name);
+    }
+
+    /**
+     * Check whether a given helper is present in the collection.
+     *
+     * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present.
+     *
+     * @param string $name
+     */
+    public function remove($name)
+    {
+        if (!$this->has($name)) {
+            throw new Mustache_Exception_UnknownHelperException($name);
+        }
+
+        unset($this->helpers[$name]);
+    }
+
+    /**
+     * Clear the helper collection.
+     *
+     * Removes all helpers from this collection
+     */
+    public function clear()
+    {
+        $this->helpers = array();
+    }
+
+    /**
+     * Check whether the helper collection is empty.
+     *
+     * @return boolean True if the collection is empty
+     */
+    public function isEmpty()
+    {
+        return empty($this->helpers);
+    }
+}
diff --git a/lib/mustache/src/Mustache/LambdaHelper.php b/lib/mustache/src/Mustache/LambdaHelper.php
new file mode 100644 (file)
index 0000000..7cd8092
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Lambda Helper.
+ *
+ * Passed as the second argument to section lambdas (higher order sections),
+ * giving them access to a `render` method for rendering a string with the
+ * current context.
+ */
+class Mustache_LambdaHelper
+{
+    private $mustache;
+    private $context;
+
+    /**
+     * Mustache Lambda Helper constructor.
+     *
+     * @param Mustache_Engine  $mustache Mustache engine instance.
+     * @param Mustache_Context $context  Rendering context.
+     */
+    public function __construct(Mustache_Engine $mustache, Mustache_Context $context)
+    {
+        $this->mustache = $mustache;
+        $this->context  = $context;
+    }
+
+    /**
+     * Render a string as a Mustache template with the current rendering context.
+     *
+     * @param string $string
+     *
+     * @return string Rendered template.
+     */
+    public function render($string)
+    {
+        return $this->mustache
+            ->loadLambda((string) $string)
+            ->renderInternal($this->context);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader.php b/lib/mustache/src/Mustache/Loader.php
new file mode 100644 (file)
index 0000000..e75ee3f
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template Loader interface.
+ */
+interface Mustache_Loader
+{
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name);
+}
diff --git a/lib/mustache/src/Mustache/Loader/ArrayLoader.php b/lib/mustache/src/Mustache/Loader/ArrayLoader.php
new file mode 100644 (file)
index 0000000..e7ece91
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template array Loader implementation.
+ *
+ * An ArrayLoader instance loads Mustache Template source by name from an initial array:
+ *
+ *     $loader = new ArrayLoader(
+ *         'foo' => '{{ bar }}',
+ *         'baz' => 'Hey {{ qux }}!'
+ *     );
+ *
+ *     $tpl = $loader->load('foo'); // '{{ bar }}'
+ *
+ * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
+ * is set. It can also be used as a quick-and-dirty Template loader.
+ */
+class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
+{
+    private $templates;
+
+    /**
+     * ArrayLoader constructor.
+     *
+     * @param array $templates Associative array of Template source (default: array())
+     */
+    public function __construct(array $templates = array())
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Load a Template.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Set an associative array of Template sources for this loader.
+     *
+     * @param array $templates
+     */
+    public function setTemplates(array $templates)
+    {
+        $this->templates = $templates;
+    }
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     */
+    public function setTemplate($name, $template)
+    {
+        $this->templates[$name] = $template;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/CascadingLoader.php b/lib/mustache/src/Mustache/Loader/CascadingLoader.php
new file mode 100644 (file)
index 0000000..d02a273
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template cascading loader implementation, which delegates to other
+ * Loader instances.
+ */
+class Mustache_Loader_CascadingLoader implements Mustache_Loader
+{
+    private $loaders;
+
+    /**
+     * Construct a CascadingLoader with an array of loaders:
+     *
+     *     $loader = new Mustache_Loader_CascadingLoader(array(
+     *         new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
+     *         new Mustache_Loader_FilesystemLoader(__DIR__.'/templates')
+     *     ));
+     *
+     * @param Mustache_Loader[] $loaders
+     */
+    public function __construct(array $loaders = array())
+    {
+        $this->loaders = array();
+        foreach ($loaders as $loader) {
+            $this->addLoader($loader);
+        }
+    }
+
+    /**
+     * Add a Loader instance.
+     *
+     * @param Mustache_Loader $loader
+     */
+    public function addLoader(Mustache_Loader $loader)
+    {
+        $this->loaders[] = $loader;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        foreach ($this->loaders as $loader) {
+            try {
+                return $loader->load($name);
+            } catch (Mustache_Exception_UnknownTemplateException $e) {
+                // do nothing, check the next loader.
+            }
+        }
+
+        throw new Mustache_Exception_UnknownTemplateException($name);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/FilesystemLoader.php b/lib/mustache/src/Mustache/Loader/FilesystemLoader.php
new file mode 100644 (file)
index 0000000..7cbf9cd
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template filesystem Loader implementation.
+ *
+ * A FilesystemLoader instance loads Mustache Template source from the filesystem by name:
+ *
+ *     $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
+ *     $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
+ *
+ * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
+ *
+ *     $m = new Mustache(array(
+ *          'loader'          => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
+ *          'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
+ *     ));
+ */
+class Mustache_Loader_FilesystemLoader implements Mustache_Loader
+{
+    private $baseDir;
+    private $extension = '.mustache';
+    private $templates = array();
+
+    /**
+     * Mustache filesystem Loader constructor.
+     *
+     * Passing an $options array allows overriding certain Loader options during instantiation:
+     *
+     *     $options = array(
+     *         // The filename extension used for Mustache templates. Defaults to '.mustache'
+     *         'extension' => '.ms',
+     *     );
+     *
+     * @throws Mustache_Exception_RuntimeException if $baseDir does not exist.
+     *
+     * @param string $baseDir Base directory containing Mustache template files.
+     * @param array  $options Array of Loader options (default: array())
+     */
+    public function __construct($baseDir, array $options = array())
+    {
+        $this->baseDir = $baseDir;
+
+        if (strpos($this->baseDir, '://') === false) {
+            $this->baseDir = realpath($this->baseDir);
+        }
+
+        if (!is_dir($this->baseDir)) {
+            throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
+        }
+
+        if (array_key_exists('extension', $options)) {
+            if (empty($options['extension'])) {
+                $this->extension = '';
+            } else {
+                $this->extension = '.' . ltrim($options['extension'], '.');
+            }
+        }
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     *     $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views');
+     *     $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        if (!isset($this->templates[$name])) {
+            $this->templates[$name] = $this->loadFile($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Helper function for loading a Mustache file by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    protected function loadFile($name)
+    {
+        $fileName = $this->getFileName($name);
+
+        if (!file_exists($fileName)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return file_get_contents($fileName);
+    }
+
+    /**
+     * Helper function for getting a Mustache template file name.
+     *
+     * @param string $name
+     *
+     * @return string Template file name
+     */
+    protected function getFileName($name)
+    {
+        $fileName = $this->baseDir . '/' . $name;
+        if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
+            $fileName .= $this->extension;
+        }
+
+        return $fileName;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/InlineLoader.php b/lib/mustache/src/Mustache/Loader/InlineLoader.php
new file mode 100644 (file)
index 0000000..9e4ab42
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template loader for inline templates.
+ *
+ * With the InlineLoader, templates can be defined at the end of any PHP source
+ * file:
+ *
+ *     $loader  = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+ *     $hello   = $loader->load('hello');
+ *     $goodbye = $loader->load('goodbye');
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ planet }}!
+ *
+ *     @@ goodbye
+ *     Goodbye, cruel {{ planet }}
+ *
+ * Templates are deliniated by lines containing only `@@ name`.
+ *
+ * The InlineLoader is well-suited to micro-frameworks such as Silex:
+ *
+ *     $app->register(new MustacheServiceProvider, array(
+ *         'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
+ *     ));
+ *
+ *     $app->get('/{name}', function ($name) use ($app) {
+ *         return $app['mustache']->render('hello', compact('name'));
+ *     })
+ *     ->value('name', 'world');
+ *
+ *     // ...
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ name }}!
+ *
+ */
+class Mustache_Loader_InlineLoader implements Mustache_Loader
+{
+    protected $fileName;
+    protected $offset;
+    protected $templates;
+
+    /**
+     * The InlineLoader requires a filename and offset to process templates.
+     * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
+     * perfectly suited to the job:
+     *
+     *     $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+     *
+     * Note that this only works if the loader is instantiated inside the same
+     * file as the inline templates. If the templates are located in another
+     * file, it would be necessary to manually specify the filename and offset.
+     *
+     * @param string $fileName The file to parse for inline templates
+     * @param int    $offset   A string offset for the start of the templates.
+     *                         This usually coincides with the `__halt_compiler`
+     *                         call, and the `__COMPILER_HALT_OFFSET__`.
+     */
+    public function __construct($fileName, $offset)
+    {
+        if (!is_file($fileName)) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.');
+        }
+
+        if (!is_int($offset) || $offset < 0) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.');
+        }
+
+        $this->fileName = $fileName;
+        $this->offset   = $offset;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        $this->loadTemplates();
+
+        if (!array_key_exists($name, $this->templates)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Parse and load templates from the end of a source file.
+     */
+    protected function loadTemplates()
+    {
+        if ($this->templates === null) {
+            $this->templates = array();
+            $data = file_get_contents($this->fileName, false, null, $this->offset);
+            foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
+                if (trim($chunk)) {
+                    list($name, $content)         = explode("\n", $chunk, 2);
+                    $this->templates[trim($name)] = trim($content);
+                }
+            }
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Loader/MutableLoader.php b/lib/mustache/src/Mustache/Loader/MutableLoader.php
new file mode 100644 (file)
index 0000000..78901be
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template mutable Loader interface.
+ */
+interface Mustache_Loader_MutableLoader
+{
+    /**
+     * Set an associative array of Template sources for this loader.
+     *
+     * @param array $templates
+     *
+     * @return void
+     */
+    public function setTemplates(array $templates);
+
+    /**
+     * Set a Template source by name.
+     *
+     * @param string $name
+     * @param string $template Mustache Template source
+     *
+     * @return void
+     */
+    public function setTemplate($name, $template);
+}
diff --git a/lib/mustache/src/Mustache/Loader/StringLoader.php b/lib/mustache/src/Mustache/Loader/StringLoader.php
new file mode 100644 (file)
index 0000000..72d105d
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Template string Loader implementation.
+ *
+ * A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
+ *
+ *     $loader = new StringLoader;
+ *     $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
+ *
+ * This is the default Template Loader instance used by Mustache:
+ *
+ *     $m = new Mustache;
+ *     $tpl = $m->loadTemplate('{{ foo }}');
+ *     echo $tpl->render(array('foo' => 'bar')); // "bar"
+ */
+class Mustache_Loader_StringLoader implements Mustache_Loader
+{
+    /**
+     * Load a Template by source.
+     *
+     * @param string $name Mustache Template source
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        return $name;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Logger.php b/lib/mustache/src/Mustache/Logger.php
new file mode 100644 (file)
index 0000000..2e5d674
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Describes a Mustache logger instance
+ *
+ * This is identical to the Psr\Log\LoggerInterface.
+ *
+ * The message MUST be a string or object implementing __toString().
+ *
+ * The message MAY contain placeholders in the form: {foo} where foo
+ * will be replaced by the context data in key "foo".
+ *
+ * The context array can contain arbitrary data, the only assumption that
+ * can be made by implementors is that if an Exception instance is given
+ * to produce a stack trace, it MUST be in a key named "exception".
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
+ * for the full interface specification.
+ */
+interface Mustache_Logger
+{
+    /**
+     * Psr\Log compatible log levels
+     */
+    const EMERGENCY = 'emergency';
+    const ALERT     = 'alert';
+    const CRITICAL  = 'critical';
+    const ERROR     = 'error';
+    const WARNING   = 'warning';
+    const NOTICE    = 'notice';
+    const INFO      = 'info';
+    const DEBUG     = 'debug';
+
+    /**
+     * System is unusable.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function emergency($message, array $context = array());
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function alert($message, array $context = array());
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function critical($message, array $context = array());
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function error($message, array $context = array());
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function warning($message, array $context = array());
+
+    /**
+     * Normal but significant events.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function notice($message, array $context = array());
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function info($message, array $context = array());
+
+    /**
+     * Detailed debug information.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function debug($message, array $context = array());
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @param mixed  $level
+     * @param string $message
+     * @param array  $context
+     *
+     * @return null
+     */
+    public function log($level, $message, array $context = array());
+}
diff --git a/lib/mustache/src/Mustache/Logger/AbstractLogger.php b/lib/mustache/src/Mustache/Logger/AbstractLogger.php
new file mode 100644 (file)
index 0000000..3dd96e7
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * This is a simple Logger implementation that other Loggers can inherit from.
+ *
+ * This is identical to the Psr\Log\AbstractLogger.
+ *
+ * It simply delegates all log-level-specific methods to the `log` method to
+ * reduce boilerplate code that a simple Logger that does the same thing with
+ * messages regardless of the error level has to implement.
+ */
+abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger
+{
+    /**
+     * System is unusable.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function emergency($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::EMERGENCY, $message, $context);
+    }
+
+    /**
+     * Action must be taken immediately.
+     *
+     * Example: Entire website down, database unavailable, etc. This should
+     * trigger the SMS alerts and wake you up.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function alert($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::ALERT, $message, $context);
+    }
+
+    /**
+     * Critical conditions.
+     *
+     * Example: Application component unavailable, unexpected exception.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function critical($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::CRITICAL, $message, $context);
+    }
+
+    /**
+     * Runtime errors that do not require immediate action but should typically
+     * be logged and monitored.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function error($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::ERROR, $message, $context);
+    }
+
+    /**
+     * Exceptional occurrences that are not errors.
+     *
+     * Example: Use of deprecated APIs, poor use of an API, undesirable things
+     * that are not necessarily wrong.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function warning($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::WARNING, $message, $context);
+    }
+
+    /**
+     * Normal but significant events.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function notice($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::NOTICE, $message, $context);
+    }
+
+    /**
+     * Interesting events.
+     *
+     * Example: User logs in, SQL logs.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function info($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::INFO, $message, $context);
+    }
+
+    /**
+     * Detailed debug information.
+     *
+     * @param string $message
+     * @param array  $context
+     */
+    public function debug($message, array $context = array())
+    {
+        $this->log(Mustache_Logger::DEBUG, $message, $context);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Logger/StreamLogger.php b/lib/mustache/src/Mustache/Logger/StreamLogger.php
new file mode 100644 (file)
index 0000000..d422340
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Stream Logger.
+ *
+ * The Stream Logger wraps a file resource instance (such as a stream) or a
+ * stream URL. All log messages over the threshold level will be appended to
+ * this stream.
+ *
+ * Hint: Try `php://stderr` for your stream URL.
+ */
+class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
+{
+    protected static $levels = array(
+        self::DEBUG     => 100,
+        self::INFO      => 200,
+        self::NOTICE    => 250,
+        self::WARNING   => 300,
+        self::ERROR     => 400,
+        self::CRITICAL  => 500,
+        self::ALERT     => 550,
+        self::EMERGENCY => 600,
+    );
+
+    protected $level;
+    protected $stream = null;
+    protected $url    = null;
+
+    /**
+     * @throws InvalidArgumentException if the logging level is unknown.
+     *
+     * @param resource|string $stream Resource instance or URL
+     * @param integer         $level  The minimum logging level at which this handler will be triggered
+     */
+    public function __construct($stream, $level = Mustache_Logger::ERROR)
+    {
+        $this->setLevel($level);
+
+        if (is_resource($stream)) {
+            $this->stream = $stream;
+        } else {
+            $this->url = $stream;
+        }
+    }
+
+    /**
+     * Close stream resources.
+     */
+    public function __destruct()
+    {
+        if (is_resource($this->stream)) {
+            fclose($this->stream);
+        }
+    }
+
+    /**
+     * Set the minimum logging level.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown.
+     *
+     * @param integer $level The minimum logging level which will be written
+     */
+    public function setLevel($level)
+    {
+        if (!array_key_exists($level, self::$levels)) {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+        }
+
+        $this->level = $level;
+    }
+
+    /**
+     * Get the current minimum logging level.
+     *
+     * @return integer
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Logs with an arbitrary level.
+     *
+     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown.
+     *
+     * @param mixed  $level
+     * @param string $message
+     * @param array  $context
+     */
+    public function log($level, $message, array $context = array())
+    {
+        if (!array_key_exists($level, self::$levels)) {
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
+        }
+
+        if (self::$levels[$level] >= self::$levels[$this->level]) {
+            $this->writeLog($level, $message, $context);
+        }
+    }
+
+    /**
+     * Write a record to the log.
+     *
+     * @throws Mustache_Exception_LogicException   If neither a stream resource nor url is present.
+     * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened.
+     *
+     * @param integer $level   The logging level
+     * @param string  $message The log message
+     * @param array   $context The log context
+     */
+    protected function writeLog($level, $message, array $context = array())
+    {
+        if (!is_resource($this->stream)) {
+            if (!isset($this->url)) {
+                throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
+            }
+
+            $this->stream = fopen($this->url, 'a');
+            if (!is_resource($this->stream)) {
+                // @codeCoverageIgnoreStart
+                throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
+                // @codeCoverageIgnoreEnd
+            }
+        }
+
+        fwrite($this->stream, self::formatLine($level, $message, $context));
+    }
+
+    /**
+     * Gets the name of the logging level.
+     *
+     * @throws InvalidArgumentException if the logging level is unknown.
+     *
+     * @param integer $level
+     *
+     * @return string
+     */
+    protected static function getLevelName($level)
+    {
+        return strtoupper($level);
+    }
+
+    /**
+     * Format a log line for output.
+     *
+     * @param integer $level   The logging level
+     * @param string  $message The log message
+     * @param array   $context The log context
+     *
+     * @return string
+     */
+    protected static function formatLine($level, $message, array $context = array())
+    {
+        return sprintf(
+            "%s: %s\n",
+            self::getLevelName($level),
+            self::interpolateMessage($message, $context)
+        );
+    }
+
+    /**
+     * Interpolate context values into the message placeholders.
+     *
+     * @param string $message
+     * @param array  $context
+     *
+     * @return string
+     */
+    protected static function interpolateMessage($message, array $context = array())
+    {
+        if (strpos($message, '{') === false) {
+            return $message;
+        }
+
+        // build a replacement array with braces around the context keys
+        $replace = array();
+        foreach ($context as $key => $val) {
+            $replace['{' . $key . '}'] = $val;
+        }
+
+        // interpolate replacement values into the the message and return
+        return strtr($message, $replace);
+    }
+}
diff --git a/lib/mustache/src/Mustache/Parser.php b/lib/mustache/src/Mustache/Parser.php
new file mode 100644 (file)
index 0000000..0c134ec
--- /dev/null
@@ -0,0 +1,317 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Parser class.
+ *
+ * This class is responsible for turning a set of Mustache tokens into a parse tree.
+ */
+class Mustache_Parser
+{
+    private $lineNum;
+    private $lineTokens;
+    private $pragmas;
+    private $defaultPragmas = array();
+
+    private $pragmaFilters;
+    private $pragmaBlocks;
+
+    /**
+     * Process an array of Mustache tokens and convert them into a parse tree.
+     *
+     * @param array $tokens Set of Mustache tokens
+     *
+     * @return array Mustache token parse tree
+     */
+    public function parse(array $tokens = array())
+    {
+        $this->lineNum    = -1;
+        $this->lineTokens = 0;
+        $this->pragmas    = $this->defaultPragmas;
+
+        $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]);
+        $this->pragmaBlocks  = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]);
+
+        return $this->buildTree($tokens);
+    }
+
+    /**
+     * Enable pragmas across all templates, regardless of the presence of pragma
+     * tags in the individual templates.
+     *
+     * @internal Users should set global pragmas in Mustache_Engine, not here :)
+     *
+     * @param string[] $pragmas
+     */
+    public function setPragmas(array $pragmas)
+    {
+        $this->pragmas = array();
+        foreach ($pragmas as $pragma) {
+            $this->enablePragma($pragma);
+        }
+        $this->defaultPragmas = $this->pragmas;
+    }
+
+    /**
+     * Helper method for recursively building a parse tree.
+     *
+     * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered.
+     *
+     * @param array &$tokens Set of Mustache tokens
+     * @param array $parent  Parent token (default: null)
+     *
+     * @return array Mustache Token parse tree
+     */
+    private function buildTree(array &$tokens, array $parent = null)
+    {
+        $nodes = array();
+
+        while (!empty($tokens)) {
+            $token = array_shift($tokens);
+
+            if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) {
+                $this->lineTokens++;
+            } else {
+                $this->lineNum    = $token[Mustache_Tokenizer::LINE];
+                $this->lineTokens = 0;
+            }
+
+            if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) {
+                list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]);
+                if (!empty($filters)) {
+                    $token[Mustache_Tokenizer::NAME]    = $name;
+                    $token[Mustache_Tokenizer::FILTERS] = $filters;
+                }
+            }
+
+            switch ($token[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_DELIM_CHANGE:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                case Mustache_Tokenizer::T_INVERTED:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_END_SECTION:
+                    if (!isset($parent)) {
+                        $msg = sprintf(
+                            'Unexpected closing tag: /%s on line %d',
+                            $token[Mustache_Tokenizer::NAME],
+                            $token[Mustache_Tokenizer::LINE]
+                        );
+                        throw new Mustache_Exception_SyntaxException($msg, $token);
+                    }
+
+                    if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
+                        $msg = sprintf(
+                            'Nesting error: %s (on line %d) vs. %s (on line %d)',
+                            $parent[Mustache_Tokenizer::NAME],
+                            $parent[Mustache_Tokenizer::LINE],
+                            $token[Mustache_Tokenizer::NAME],
+                            $token[Mustache_Tokenizer::LINE]
+                        );
+                        throw new Mustache_Exception_SyntaxException($msg, $token);
+                    }
+
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $parent[Mustache_Tokenizer::END]   = $token[Mustache_Tokenizer::INDEX];
+                    $parent[Mustache_Tokenizer::NODES] = $nodes;
+
+                    return $parent;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    //store the whitespace prefix for laters!
+                    if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
+                        $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
+                    }
+                    $nodes[] = $token;
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT:
+                    $this->checkIfTokenIsAllowedInParent($parent, $token);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_BLOCK_VAR:
+                    if ($this->pragmaBlocks) {
+                        // BLOCKS pragma is enabled, let's do this!
+                        if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+                            $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG;
+                        }
+                        $this->clearStandaloneLines($nodes, $tokens);
+                        $nodes[] = $this->buildTree($tokens, $token);
+                    } else {
+                        // pretend this was just a normal "escaped" token...
+                        $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED;
+                        // TODO: figure out how to figure out if there was a space after this dollar:
+                        $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME];
+                        $nodes[] = $token;
+                    }
+                    break;
+
+                case Mustache_Tokenizer::T_PRAGMA:
+                    $this->enablePragma($token[Mustache_Tokenizer::NAME]);
+                    // no break
+
+                case Mustache_Tokenizer::T_COMMENT:
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $token;
+                    break;
+
+                default:
+                    $nodes[] = $token;
+                    break;
+            }
+        }
+
+        if (isset($parent)) {
+            $msg = sprintf(
+                'Missing closing tag: %s opened on line %d',
+                $parent[Mustache_Tokenizer::NAME],
+                $parent[Mustache_Tokenizer::LINE]
+            );
+            throw new Mustache_Exception_SyntaxException($msg, $parent);
+        }
+
+        return $nodes;
+    }
+
+    /**
+     * Clear standalone line tokens.
+     *
+     * Returns a whitespace token for indenting partials, if applicable.
+     *
+     * @param array $nodes  Parsed nodes.
+     * @param array $tokens Tokens to be parsed.
+     *
+     * @return array|null Resulting indent token, if any.
+     */
+    private function clearStandaloneLines(array &$nodes, array &$tokens)
+    {
+        if ($this->lineTokens > 1) {
+            // this is the third or later node on this line, so it can't be standalone
+            return;
+        }
+
+        $prev = null;
+        if ($this->lineTokens === 1) {
+            // this is the second node on this line, so it can't be standalone
+            // unless the previous node is whitespace.
+            if ($prev = end($nodes)) {
+                if (!$this->tokenIsWhitespace($prev)) {
+                    return;
+                }
+            }
+        }
+
+        if ($next = reset($tokens)) {
+            // If we're on a new line, bail.
+            if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) {
+                return;
+            }
+
+            // If the next token isn't whitespace, bail.
+            if (!$this->tokenIsWhitespace($next)) {
+                return;
+            }
+
+            if (count($tokens) !== 1) {
+                // Unless it's the last token in the template, the next token
+                // must end in newline for this to be standalone.
+                if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
+                    return;
+                }
+            }
+
+            // Discard the whitespace suffix
+            array_shift($tokens);
+        }
+
+        if ($prev) {
+            // Return the whitespace prefix, if any
+            return array_pop($nodes);
+        }
+    }
+
+    /**
+     * Check whether token is a whitespace token.
+     *
+     * True if token type is T_TEXT and value is all whitespace characters.
+     *
+     * @param array $token
+     *
+     * @return boolean True if token is a whitespace token
+     */
+    private function tokenIsWhitespace(array $token)
+    {
+        if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) {
+            return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
+        }
+
+        return false;
+    }
+
+    /**
+     * Check whether a token is allowed inside a parent tag.
+     *
+     * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag.
+     *
+     * @param array|null $parent
+     * @param array      $token
+     */
+    private function checkIfTokenIsAllowedInParent($parent, array $token)
+    {
+        if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) {
+            throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token);
+        }
+    }
+
+    /**
+     * Split a tag name into name and filters.
+     *
+     * @param string $name
+     *
+     * @return array [Tag name, Array of filters]
+     */
+    private function getNameAndFilters($name)
+    {
+        $filters = array_map('trim', explode('|', $name));
+        $name    = array_shift($filters);
+
+        return array($name, $filters);
+    }
+
+    /**
+     * Enable a pragma.
+     *
+     * @param string $name
+     */
+    private function enablePragma($name)
+    {
+        $this->pragmas[$name] = true;
+
+        switch ($name) {
+            case Mustache_Engine::PRAGMA_BLOCKS:
+                $this->pragmaBlocks = true;
+                break;
+
+            case Mustache_Engine::PRAGMA_FILTERS:
+                $this->pragmaFilters = true;
+                break;
+        }
+    }
+}
diff --git a/lib/mustache/src/Mustache/Template.php b/lib/mustache/src/Mustache/Template.php
new file mode 100644 (file)
index 0000000..e7156cf
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Abstract Mustache Template class.
+ *
+ * @abstract
+ */
+abstract class Mustache_Template
+{
+    /**
+     * @var Mustache_Engine
+     */
+    protected $mustache;
+
+    /**
+     * @var boolean
+     */
+    protected $strictCallables = false;
+
+    /**
+     * Mustache Template constructor.
+     *
+     * @param Mustache_Engine $mustache
+     */
+    public function __construct(Mustache_Engine $mustache)
+    {
+        $this->mustache = $mustache;
+    }
+
+    /**
+     * Mustache Template instances can be treated as a function and rendered by simply calling them:
+     *
+     *     $m = new Mustache_Engine;
+     *     $tpl = $m->loadTemplate('Hello, {{ name }}!');
+     *     echo $tpl(array('name' => 'World')); // "Hello, World!"
+     *
+     * @see Mustache_Template::render
+     *
+     * @param mixed $context Array or object rendering context (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function __invoke($context = array())
+    {
+        return $this->render($context);
+    }
+
+    /**
+     * Render this template given the rendering context.
+     *
+     * @param mixed $context Array or object rendering context (default: array())
+     *
+     * @return string Rendered template
+     */
+    public function render($context = array())
+    {
+        return $this->renderInternal(
+            $this->prepareContextStack($context)
+        );
+    }
+
+    /**
+     * Internal rendering method implemented by Mustache Template concrete subclasses.
+     *
+     * This is where the magic happens :)
+     *
+     * NOTE: This method is not part of the Mustache.php public API.
+     *
+     * @param Mustache_Context $context
+     * @param string           $indent  (default: '')
+     *
+     * @return string Rendered template
+     */
+    abstract public function renderInternal(Mustache_Context $context, $indent = '');
+
+    /**
+     * Tests whether a value should be iterated over (e.g. in a section context).
+     *
+     * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
+     * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
+     * Java, Python, etc.
+     *
+     * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
+     * between between a list of things (numeric, normalized array) and a set of variables to be used as section context
+     * (associative array). In other words, this will be iterated over:
+     *
+     *     $items = array(
+     *         array('name' => 'foo'),
+     *         array('name' => 'bar'),
+     *         array('name' => 'baz'),
+     *     );
+     *
+     * ... but this will be used as a section context block:
+     *
+     *     $items = array(
+     *         1        => array('name' => 'foo'),
+     *         'banana' => array('name' => 'bar'),
+     *         42       => array('name' => 'baz'),
+     *     );
+     *
+     * @param mixed $value
+     *
+     * @return boolean True if the value is 'iterable'
+     */
+    protected function isIterable($value)
+    {
+        switch (gettype($value)) {
+            case 'object':
+                return $value instanceof Traversable;
+
+            case 'array':
+                $i = 0;
+                foreach ($value as $k => $v) {
+                    if ($k !== $i++) {
+                        return false;
+                    }
+                }
+
+                return true;
+
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Helper method to prepare the Context stack.
+     *
+     * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
+     *
+     * @param mixed $context Optional first context frame (default: null)
+     *
+     * @return Mustache_Context
+     */
+    protected function prepareContextStack($context = null)
+    {
+        $stack = new Mustache_Context();
+
+        $helpers = $this->mustache->getHelpers();
+        if (!$helpers->isEmpty()) {
+            $stack->push($helpers);
+        }
+
+        if (!empty($context)) {
+            $stack->push($context);
+        }
+
+        return $stack;
+    }
+
+    /**
+     * Resolve a context value.
+     *
+     * Invoke the value if it is callable, otherwise return the value.
+     *
+     * @param mixed            $value
+     * @param Mustache_Context $context
+     * @param string           $indent
+     *
+     * @return string
+     */
+    protected function resolveValue($value, Mustache_Context $context, $indent = '')
+    {
+        if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
+            return $this->mustache
+                ->loadLambda((string) call_user_func($value))
+                ->renderInternal($context, $indent);
+        }
+
+        return $value;
+    }
+}
diff --git a/lib/mustache/src/Mustache/Tokenizer.php b/lib/mustache/src/Mustache/Tokenizer.php
new file mode 100644 (file)
index 0000000..3175a03
--- /dev/null
@@ -0,0 +1,331 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2014 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache Tokenizer class.
+ *
+ * This class is responsible for turning raw template source into a set of Mustache tokens.
+ */