diff --git a/config/copy.config.js b/config/copy.config.js index 7f0f0686f..ddbcafc88 100644 --- a/config/copy.config.js +++ b/config/copy.config.js @@ -12,5 +12,9 @@ module.exports = { copyConfig: { src: ['{{ROOT}}/src/config.json'], dest: '{{WWW}}/' - } + }, + copyMathJax: { + src: ['{{ROOT}}/node_modules/mathjax/**/*'], + dest: '{{WWW}}/lib/mathjax' + }, }; diff --git a/package-lock.json b/package-lock.json index a5a04e889..acf72dd24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.7.1", + "version": "3.8.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -503,8 +503,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -525,14 +524,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -547,20 +544,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -677,8 +671,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -690,7 +683,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -705,7 +697,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -713,14 +704,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -739,7 +728,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -820,8 +808,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -833,7 +820,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -919,8 +905,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -956,7 +941,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -976,7 +960,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1020,14 +1003,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -5243,8 +5224,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -5262,13 +5242,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5281,18 +5259,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -5395,8 +5370,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -5406,7 +5380,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5419,20 +5392,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5449,7 +5419,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5528,8 +5497,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -5539,7 +5507,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5615,8 +5582,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -5646,7 +5612,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5664,7 +5629,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5703,13 +5667,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -6136,8 +6098,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -6158,14 +6119,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6180,20 +6139,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6310,8 +6266,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6323,7 +6278,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6338,7 +6292,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6346,14 +6299,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6372,7 +6323,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6453,8 +6403,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6466,7 +6415,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6552,8 +6500,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6589,7 +6536,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6609,7 +6555,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6653,14 +6598,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -8863,6 +8806,11 @@ "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" }, + "mathjax": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.2.tgz", + "integrity": "sha512-Z9te7r3lsjZibugO1uKsdyqICKXVNr7M1Ll2GtjJu9cUKvOvwDqEp0YIjBD4H58NNuPg5DP5/AQ2Tu8K52Mqng==" + }, "md5-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-4.0.0.tgz", @@ -12921,8 +12869,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -12943,14 +12890,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12965,20 +12910,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -13095,8 +13037,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -13108,7 +13049,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -13123,7 +13063,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -13131,14 +13070,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -13157,7 +13094,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -13238,8 +13174,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -13251,7 +13186,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -13337,8 +13271,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -13374,7 +13307,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -13394,7 +13326,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -13438,14 +13369,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/package.json b/package.json index 28931d637..bede60886 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "ionic-angular": "3.9.3", "ionicons": "^3.0.0", "jszip": "^3.1.5", + "mathjax": "2.7.2", "moment": "^2.22.2", "nl.kingsquare.cordova.background-audio": "^1.0.1", "phonegap-plugin-multidex": "^1.0.0", diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 04027cae0..293d1fd62 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -245,8 +245,8 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { const subPromises = []; courses.forEach((course) => { subPromises.push(this.filterHelper.getFiltersAndFormatText(course.fullname, 'course', course.id) - .then((text) => { - course.fullname = text; + .then((result) => { + course.fullname = result.text; }).catch(() => { // Ignore errors. })); diff --git a/src/addon/filter/filter.module.ts b/src/addon/filter/filter.module.ts index f2646d346..57e177598 100644 --- a/src/addon/filter/filter.module.ts +++ b/src/addon/filter/filter.module.ts @@ -20,6 +20,7 @@ import { AddonFilterDataModule } from './data/data.module'; import { AddonFilterEmailProtectModule } from './emailprotect/emailprotect.module'; import { AddonFilterEmoticonModule } from './emoticon/emoticon.module'; import { AddonFilterGlossaryModule } from './glossary/glossary.module'; +import { AddonFilterMathJaxLoaderModule } from './mathjaxloader/mathjaxloader.module'; import { AddonFilterMediaPluginModule } from './mediaplugin/mediaplugin.module'; import { AddonFilterMultilangModule } from './multilang/multilang.module'; import { AddonFilterTexModule } from './tex/tex.module'; @@ -36,6 +37,7 @@ import { AddonFilterUrlToLinkModule } from './urltolink/urltolink.module'; AddonFilterEmailProtectModule, AddonFilterEmoticonModule, AddonFilterGlossaryModule, + AddonFilterMathJaxLoaderModule, AddonFilterMediaPluginModule, AddonFilterMultilangModule, AddonFilterTexModule, diff --git a/src/addon/filter/mathjaxloader/mathjaxloader.module.ts b/src/addon/filter/mathjaxloader/mathjaxloader.module.ts new file mode 100644 index 000000000..5d78641ca --- /dev/null +++ b/src/addon/filter/mathjaxloader/mathjaxloader.module.ts @@ -0,0 +1,34 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicModule } from 'ionic-angular'; +import { CoreFilterDelegate } from '@core/filter/providers/delegate'; +import { AddonFilterMathJaxLoaderHandler } from './providers/handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule + ], + providers: [ + AddonFilterMathJaxLoaderHandler + ] +}) +export class AddonFilterMathJaxLoaderModule { + constructor(filterDelegate: CoreFilterDelegate, handler: AddonFilterMathJaxLoaderHandler) { + filterDelegate.registerHandler(handler); + } +} diff --git a/src/addon/filter/mathjaxloader/providers/handler.ts b/src/addon/filter/mathjaxloader/providers/handler.ts new file mode 100644 index 000000000..72f67ba66 --- /dev/null +++ b/src/addon/filter/mathjaxloader/providers/handler.ts @@ -0,0 +1,371 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreFilterDefaultHandler } from '@core/filter/providers/default-filter'; +import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@core/filter/providers/filter'; +import { CoreEventsProvider } from '@providers/events'; +import { CoreLangProvider } from '@providers/lang'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreSite } from '@classes/site'; + +/** + * Handler to support the MathJax filter. + */ +@Injectable() +export class AddonFilterMathJaxLoaderHandler extends CoreFilterDefaultHandler { + name = 'AddonFilterMathJaxLoaderHandler'; + filterName = 'mathjaxloader'; + + // Default values for MathJax config for sites where we cannot retrieve it. + protected DEFAULT_URL = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js'; + protected DEFAULT_CONFIG = 'MathJax.Hub.Config({' + + 'config: ["Accessible.js", "Safe.js"],' + + 'errorSettings: { message: ["!"] },' + + 'skipStartupTypeset: true,' + + 'messageStyle: "none"' + + '});'; + + // List of language codes found in the MathJax/localization/ directory. + protected MATHJAX_LANG_CODES = [ + 'ar', 'ast', 'bcc', 'bg', 'br', 'ca', 'cdo', 'ce', 'cs', 'cy', 'da', 'de', 'diq', 'en', 'eo', 'es', 'fa', + 'fi', 'fr', 'gl', 'he', 'ia', 'it', 'ja', 'kn', 'ko', 'lb', 'lki', 'lt', 'mk', 'nl', 'oc', 'pl', 'pt', + 'pt-br', 'qqq', 'ru', 'scn', 'sco', 'sk', 'sl', 'sv', 'th', 'tr', 'uk', 'vi', 'zh-hans', 'zh-hant' + ]; + + // List of explicit mappings and known exceptions (moodle => mathjax). + protected EXPLICIT_MAPPING = { + 'zh-tw': 'zh-hant', + 'zh-cn': 'zh-hans', + }; + + protected window: any = window; // Convert the window to to be able to use non-standard properties like MathJax. + + constructor(eventsProvider: CoreEventsProvider, + private langProvider: CoreLangProvider, + private sitesProvider: CoreSitesProvider, + private textUtils: CoreTextUtilsProvider, + private utils: CoreUtilsProvider) { + super(); + + // Load the JS. + this.loadJS(); + + // Get the current language. + this.langProvider.getCurrentLanguage().then((lang) => { + lang = this.mapLanguageCode(lang); + + // Now call the configure function. + this.window.M.filter_mathjaxloader.configure({ + mathjaxconfig: this.DEFAULT_CONFIG, + lang: lang + }); + }); + + // Update MathJax locale if app language changes. + eventsProvider.on(CoreEventsProvider.LANGUAGE_CHANGED, (lang) => { + if (typeof this.window.MathJax != 'undefined') { + lang = this.mapLanguageCode(lang); + + this.window.MathJax.Hub.Queue(() => { + this.window.MathJax.Localization.setLocale(lang); + }); + } + }); + } + + /** + * Filter some text. + * + * @param text The text to filter. + * @param filter The filter. + * @param options Options passed to the filters. + * @param siteId Site ID. If not defined, current site. + * @return Filtered text (or promise resolved with the filtered text). + */ + filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string) + : string | Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + + // Don't apply this filter if Moodle is 3.7 or higher and the WS already filtered the content. + if (!options.wsNotFiltered && site.isVersionGreaterEqualThan('3.7')) { + return text; + } + + if (text.indexOf('class="filter_mathjaxloader_equation"') != -1) { + // The content seems to have treated mathjax already, don't do it. + return text; + } + + // We cannot get the filter settings, so we cannot know if it can be used as a replacement for the TeX filter. + // Assume it cannot (default value). + let hasDisplayOrInline = false; + if (text.match(/\\[\[\(]/) || text.match(/\$\$/)) { + // Only parse the text if there are mathjax symbols in it. + // The recognized math environments are \[ \] and $$ $$ for display mathematics and \( \) for inline mathematics. + // Wrap display and inline math environments in nolink spans. + const result = this.wrapMathInNoLink(text); + text = result.text; + hasDisplayOrInline = result.changed; + } + + if (hasDisplayOrInline) { + return '' + text + ''; + } + + return text; + }); + } + + /** + * Handle HTML. This function is called after "filter", and it will receive an HTMLElement containing the text that was + * filtered. + * + * @param container The HTML container to handle. + * @param filter The filter. + * @param options Options passed to the filters. + * @param siteId Site ID. If not defined, current site. + * @return If async, promise resolved when done. + */ + handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string) + : void | Promise { + + return this.waitForReady().then(() => { + this.window.M.filter_mathjaxloader.typeset(container); + }); + } + + /** + * Wrap a portion of the $text inside a no link span. The whole text is then returned. + * + * @param text The text to modify. + * @param start The start index of the substring in text that should be wrapped in the span. + * @param end The end index of the substring in text that should be wrapped in the span. + * @return The whole text with the span inserted around the defined substring. + */ + protected insertSpan(text: string, start: number, end: number): string { + return this.textUtils.substrReplace(text, + '' + text.substr(start, end - start + 1) + '', + start, + end - start + 1); + } + + /** + * Check if the JS library has been loaded. + * + * @return Whether the library has been loaded. + */ + protected jsLoaded(): boolean { + return this.window.M && this.window.M.filter_mathjaxloader; + } + + /** + * Load the JS to make MathJax work in the app. The JS loaded is extracted from Moodle filter's loader JS file. + */ + protected loadJS(): void { + // tslint:disable: no-this-assignment + const that = this; + + this.window.M = this.window.M || {}; + this.window.M.filter_mathjaxloader = this.window.M.filter_mathjaxloader || { + _lang: '', + _configured: false, + // Add the configuration to the head and set the lang. + configure: function (params: any): void { + // Add a js configuration object to the head. + const script = document.createElement('script'); + script.type = 'text/x-mathjax-config'; + script.text = params.mathjaxconfig; + document.head.appendChild(script); + + // Save the lang config until MathJax is actually loaded. + this._lang = params.lang; + }, + // Set the correct language for the MathJax menus. + _setLocale: function (): void { + if (!this._configured) { + const lang = this._lang; + + if (typeof that.window.MathJax != 'undefined') { + that.window.MathJax.Hub.Queue(() => { + that.window.MathJax.Localization.setLocale(lang); + }); + that.window.MathJax.Hub.Configured(); + this._configured = true; + } + } + }, + // Called by the filter when an equation is found while rendering the page. + typeset: function (container: HTMLElement): void { + if (!this._configured) { + this._setLocale(); + } + + if (typeof that.window.MathJax != 'undefined') { + const processDelay = that.window.MathJax.Hub.processSectionDelay; + // Set the process section delay to 0 when updating the formula. + that.window.MathJax.Hub.processSectionDelay = 0; + + const equations = Array.from(container.querySelectorAll('.filter_mathjaxloader_equation')); + equations.forEach((node) => { + that.window.MathJax.Hub.Queue(['Typeset', that.window.MathJax.Hub, node]); + }); + + // Set the delay back to normal after processing. + that.window.MathJax.Hub.processSectionDelay = processDelay; + } + } + }; + } + + /** + * Perform a mapping of the app language code to the equivalent for MathJax. + * + * @param langCode The app language code. + * @return The MathJax language code. + */ + protected mapLanguageCode(langCode: string): string { + + // If defined, explicit mapping takes the highest precedence. + if (this.EXPLICIT_MAPPING[langCode]) { + return this.EXPLICIT_MAPPING[langCode]; + } + + // If there is exact match, it will be probably right. + if (this.MATHJAX_LANG_CODES.indexOf(langCode) != -1) { + return langCode; + } + + // Finally try to find the best matching mathjax pack. + const parts = langCode.split('-'); + if (this.MATHJAX_LANG_CODES.indexOf(parts[0]) != -1) { + return parts[0]; + } + + // No more guessing, use default language. + return this.langProvider.getDefaultLanguage(); + } + + /** + * Check if the filter should be applied in a certain site based on some filter options. + * + * @param options Options. + * @param site Site. + * @return Whether filter should be applied. + */ + shouldBeApplied(options: CoreFilterFormatTextOptions, site?: CoreSite): boolean { + // Only apply the filter if logged in and we're filtering current site. + return site && site.getId() == this.sitesProvider.getCurrentSiteId(); + } + + /** + * Wait for the MathJax library and our JS object to be loaded. + * + * @param retries Number of times this has been retried. + * @return Promise resolved when ready or if it took too long to load. + */ + protected waitForReady(retries: number = 0): Promise { + if ((this.window.MathJax && this.jsLoaded()) || retries >= 20) { + // Loaded or too many retries, stop. + return Promise.resolve(); + } + + const deferred = this.utils.promiseDefer(); + + setTimeout(() => { + return this.waitForReady(retries + 1).finally(() => { + deferred.resolve(); + }); + }, 250); + + return deferred.promise; + } + + /** + * Find math environments in the $text and wrap them in no link spans + * (). If math environments are nested, only + * the outer environment is wrapped in the span. + * + * The recognized math environments are \[ \] and $$ $$ for display + * mathematics and \( \) for inline mathematics. + * + * @param text The text to filter. + * @return Object containing the potentially modified text and a boolean that is true if any changes were made to the text. + */ + protected wrapMathInNoLink(text: string): {text: string, changed: boolean} { + let len = text.length, + i = 1, + displayStart = -1, + displayBracket = false, + displayDollar = false, + inlineStart = -1, + changesDone = false; + + // Loop over the $text once. + while (i < len) { + if (displayStart === -1) { + // No display math has started yet. + if (text[i - 1] === '\\') { + + if (text[i] === '[') { + // Display mode \[ begins. + displayStart = i - 1; + displayBracket = true; + } else if (text[i] === '(') { + // Inline math \( begins, not nested inside display math. + inlineStart = i - 1; + } else if (text[i] === ')' && inlineStart > -1) { + // Inline math ends, not nested inside display math. Wrap the span around it. + text = this.insertSpan(text, inlineStart, i); + + inlineStart = -1; // Reset. + i += 28; // The text length changed due to the . + len += 28; + changesDone = true; + } + + } else if (text[i - 1] === '$' && text[i] === '$') { + // Display mode $$ begins. + displayStart = i - 1; + displayDollar = true; + } + + } else { + // Display math open. + if ((text[i - 1] === '\\' && text[i] === ']' && displayBracket) || + (text[i - 1] === '$' && text[i] === '$' && displayDollar)) { + // Display math ends, wrap the span around it. + text = this.insertSpan(text, displayStart, i); + + displayStart = -1; // Reset. + displayBracket = false; + displayDollar = false; + i += 28; // The text length changed due to the . + len += 28; + changesDone = true; + } + } + + i++; + } + + return { + text: text, + changed: changesDone + }; + } +} diff --git a/src/addon/filter/multilang/providers/handler.ts b/src/addon/filter/multilang/providers/handler.ts index 93db110d0..8f809624a 100644 --- a/src/addon/filter/multilang/providers/handler.ts +++ b/src/addon/filter/multilang/providers/handler.ts @@ -47,11 +47,6 @@ export class AddonFilterMultilangHandler extends CoreFilterDefaultHandler { return this.sitesProvider.getSite(siteId).then((site) => { - // Don't apply this filter if Moodle is 3.7 or higher and the WS already filtered the content. - if (!this.shouldBeApplied(options, site)) { - return text; - } - return this.langProvider.getCurrentLanguage().then((language) => { // Match the current language. const anyLangRegEx = /<(?:lang|span)[^>]+lang="[a-zA-Z0-9_-]+"[^>]*>(.*?)<\/(?:lang|span)>/g; diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index 51d75825c..42b720139 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -298,9 +298,9 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr }; return this.filterHelper.getFiltersAndFormatText(message.text, 'system', 0, {clean: true, singleLine: true}).catch(() => { - return message.text; - }).then((formattedText) => { - data['text'] = formattedText; + return {text: message.text}; + }).then((result) => { + data['text'] = result.text; return data; }); diff --git a/src/components/site-picker/site-picker.ts b/src/components/site-picker/site-picker.ts index aa934bf57..dca537560 100644 --- a/src/components/site-picker/site-picker.ts +++ b/src/components/site-picker/site-picker.ts @@ -15,7 +15,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; +import { CoreFilterProvider } from '@core/filter/providers/filter'; /** * Component to display a site selector. It will display a select with the list of sites. If the selected site changes, @@ -35,8 +35,9 @@ export class CoreSitePickerComponent implements OnInit { selectedSite: string; sites: any[]; - constructor(private translate: TranslateService, private sitesProvider: CoreSitesProvider, - private filterHelper: CoreFilterHelperProvider) { + constructor(private translate: TranslateService, + private sitesProvider: CoreSitesProvider, + private filterProvider: CoreFilterProvider) { this.siteSelected = new EventEmitter(); } @@ -49,12 +50,12 @@ export class CoreSitePickerComponent implements OnInit { sites.forEach((site: any) => { // Format the site name. - promises.push(this.filterHelper.getFiltersAndFormatText(site.siteName, 'system', 0, - {clean: true, singleLine: true}, site.getId()).catch(() => { + promises.push(this.filterProvider.formatText(site.siteName, {clean: true, singleLine: true, filter: false}, [], + site.getId()).catch(() => { return site.siteName; - }).then((formatted) => { + }).then((siteName) => { site.fullNameAndSiteName = this.translate.instant('core.fullnameandsitename', - { fullname: site.fullName, sitename: formatted }); + { fullname: site.fullName, sitename: siteName }); })); }); diff --git a/src/core/contentlinks/pages/choose-site/choose-site.html b/src/core/contentlinks/pages/choose-site/choose-site.html index 5ade9add5..c6a401175 100644 --- a/src/core/contentlinks/pages/choose-site/choose-site.html +++ b/src/core/contentlinks/pages/choose-site/choose-site.html @@ -15,7 +15,7 @@ {{ 'core.pictureof' | translate:{$a: site.fullname} }}

{{site.fullName}}

-

+

{{site.siteUrl}}

diff --git a/src/core/course/components/module-completion/module-completion.ts b/src/core/course/components/module-completion/module-completion.ts index a2db476ad..08d979f40 100644 --- a/src/core/course/components/module-completion/module-completion.ts +++ b/src/core/course/components/module-completion/module-completion.ts @@ -139,7 +139,7 @@ export class CoreCourseModuleCompletionComponent implements OnChanges { if (moduleName) { this.filterHelper.getFiltersAndFormatText(moduleName, 'module', this.moduleId, - {clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId}).then((modName) => { + {clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId}).then((result) => { let promise; @@ -150,11 +150,11 @@ export class CoreCourseModuleCompletionComponent implements OnChanges { (profile) => { return { overrideuser: profile.fullname, - modname: modName + modname: result.text }; }); } else { - promise = Promise.resolve(modName); + promise = Promise.resolve(result.text); } return promise.then((translateParams) => { diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts index de9e1cdf2..de531d268 100644 --- a/src/core/course/pages/section/section.ts +++ b/src/core/course/pages/section/section.ts @@ -264,8 +264,8 @@ export class CoreCourseSectionPage implements OnDestroy { // Format the name of each section and check if it has content. this.sections = sections.map((section) => { this.filterHelper.getFiltersAndFormatText(section.name.trim(), 'course', this.course.id, - {clean: true, singleLine: true}).then((name) => { - section.formattedName = name; + {clean: true, singleLine: true}).then((result) => { + section.formattedName = result.text; }); section.hasContent = this.courseHelper.sectionHasContent(section); diff --git a/src/core/filter/providers/default-filter.ts b/src/core/filter/providers/default-filter.ts index d381e580f..bb9ff9d32 100644 --- a/src/core/filter/providers/default-filter.ts +++ b/src/core/filter/providers/default-filter.ts @@ -43,6 +43,21 @@ export class CoreFilterDefaultHandler implements CoreFilterHandler { return text; } + /** + * Handle HTML. This function is called after "filter", and it will receive an HTMLElement containing the text that was + * filtered. + * + * @param container The HTML container to handle. + * @param filter The filter. + * @param options Options passed to the filters. + * @param siteId Site ID. If not defined, current site. + * @return If async, promise resolved when done. + */ + handleHtml(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string) + : void | Promise { + // To be overridden. + } + /** * Whether or not the handler is enabled on a site level. * diff --git a/src/core/filter/providers/delegate.ts b/src/core/filter/providers/delegate.ts index dcea780cd..ef1e5852a 100644 --- a/src/core/filter/providers/delegate.ts +++ b/src/core/filter/providers/delegate.ts @@ -41,6 +41,19 @@ export interface CoreFilterHandler extends CoreDelegateHandler { */ filter(text: string, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string): string | Promise; + /** + * Handle HTML. This function is called after "filter", and it will receive an HTMLElement containing the text that was + * filtered. + * + * @param container The HTML container to handle. + * @param filter The filter. + * @param options Options passed to the filters. + * @param siteId Site ID. If not defined, current site. + * @return If async, promise resolved when done. + */ + handleHtml?(container: HTMLElement, filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, siteId?: string) + : void | Promise; + /** * Check if the filter should be applied in a certain site based on some filter options. * @@ -61,6 +74,7 @@ export class CoreFilterDelegate extends CoreDelegate { constructor(loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, protected defaultHandler: CoreFilterDefaultHandler) { + super('CoreFilterDelegate', loggerProvider, sitesProvider, eventsProvider); } @@ -79,19 +93,16 @@ export class CoreFilterDelegate extends CoreDelegate { // Wait for filters to be initialized. return this.handlersInitPromise.then(() => { + return this.sitesProvider.getSite(siteId); + }).then((site) => { + let promise: Promise = Promise.resolve(text); filters = filters || []; options = options || {}; filters.forEach((filter) => { - if (skipFilters && skipFilters.indexOf(filter.filter) != -1) { - // Skip this filter. - return; - } - - if (filter.localstate == -1 || (filter.localstate == 0 && filter.inheritedstate == -1)) { - // Filter is disabled, ignore it. + if (!this.isEnabledAndShouldApply(filter, options, site, skipFilters)) { return; } @@ -140,6 +151,77 @@ export class CoreFilterDelegate extends CoreDelegate { return filters; } + /** + * Let filters handle an HTML element. + * + * @param container The HTML container to handle. + * @param filters Filters to apply. + * @param options Options passed to the filters. + * @param skipFilters Names of filters that shouldn't be applied. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + handleHtml(container: HTMLElement, filters: CoreFilterFilter[], options?: any, skipFilters?: string[], siteId?: string) + : Promise { + + // Wait for filters to be initialized. + return this.handlersInitPromise.then(() => { + + return this.sitesProvider.getSite(siteId); + }).then((site) => { + + let promise: Promise = Promise.resolve(); + + filters = filters || []; + options = options || {}; + + filters.forEach((filter) => { + if (!this.isEnabledAndShouldApply(filter, options, site, skipFilters)) { + return; + } + + promise = promise.then(() => { + return Promise.resolve(this.executeFunctionOnEnabled(filter.filter, 'handleHtml', + [container, filter, options, siteId])).catch((error) => { + this.logger.error('Error handling HTML' + filter.filter, error); + }); + }); + }); + + return promise; + }); + } + + /** + * Check if a filter is enabled and should be applied. + * + * @param filters Filters to apply. + * @param options Options passed to the filters. + * @param site Site. + * @param skipFilters Names of filters that shouldn't be applied. + * @return Whether the filter is enabled and should be applied. + */ + isEnabledAndShouldApply(filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, site: CoreSite, + skipFilters?: string[]): boolean { + + if (filter.localstate == -1 || (filter.localstate == 0 && filter.inheritedstate == -1)) { + // Filter is disabled, ignore it. + return false; + } + + if (!this.shouldFilterBeApplied(filter, options, site)) { + // Filter shouldn't be applied. + return false; + } + + if (skipFilters && skipFilters.indexOf(filter.filter) != -1) { + // Skip this filter. + return false; + } + + return true; + } + /** * Check if at least 1 filter should be applied in a certain site and with certain options. * @@ -151,20 +233,11 @@ export class CoreFilterDelegate extends CoreDelegate { shouldBeApplied(filters: CoreFilterFilter[], options: CoreFilterFormatTextOptions, site?: CoreSite): Promise { // Wait for filters to be initialized. return this.handlersInitPromise.then(() => { - const promises = []; - let shouldBeApplied = false; - - filters.forEach((filter) => { - promises.push(this.shouldFilterBeApplied(filter, options, site).then((applied) => { - if (applied) { - shouldBeApplied = applied; - } - })); - }); - - return Promise.all(promises).then(() => { - return shouldBeApplied; - }); + for (let i = 0; i < filters.length; i++) { + if (this.shouldFilterBeApplied(filters[i], options, site)) { + return true; + } + } }); } @@ -174,15 +247,14 @@ export class CoreFilterDelegate extends CoreDelegate { * @param filter Filter to check. * @param options Options passed to the filters. * @param site Site. If not defined, current site. - * @return {Promise} Promise resolved with true: whether the filter should be applied. + * @return Whether the filter should be applied. */ - protected shouldFilterBeApplied(filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, site?: CoreSite) - : Promise { + protected shouldFilterBeApplied(filter: CoreFilterFilter, options: CoreFilterFormatTextOptions, site?: CoreSite): boolean { if (!this.hasHandler(filter.filter, true)) { - return Promise.resolve(false); + return false; } - return Promise.resolve(this.executeFunctionOnEnabled(filter.filter, 'shouldBeApplied', [options, site])); + return this.executeFunctionOnEnabled(filter.filter, 'shouldBeApplied', [options, site]); } } diff --git a/src/core/filter/providers/helper.ts b/src/core/filter/providers/helper.ts index 2b3a6704a..ee4767346 100644 --- a/src/core/filter/providers/helper.ts +++ b/src/core/filter/providers/helper.ts @@ -71,7 +71,7 @@ export class CoreFilterHelperProvider { */ getCourseModulesContexts(courseId: number, siteId?: string): Promise<{contextlevel: string, instanceid: number}[]> { - return this.courseProvider.getSections(courseId, false, true, {omitExpires: true}, siteId).then((sections) => { + return this.courseProvider.getSections(courseId, false, true, undefined, siteId).then((sections) => { const contexts: {contextlevel: string, instanceid: number}[] = []; sections.forEach((section) => { @@ -178,10 +178,12 @@ export class CoreFilterHelperProvider { * @return Promise resolved with the formatted text. */ getFiltersAndFormatText(text: string, contextLevel: string, instanceId: number, options?: CoreFilterFormatTextOptions, - siteId?: string): Promise { + siteId?: string): Promise<{text: string, filters: CoreFilterFilter[]}> { return this.getFilters(contextLevel, instanceId, options, siteId).then((filters) => { - return this.filterProvider.formatText(text, options, filters, siteId); + return this.filterProvider.formatText(text, options, filters, siteId).then((text) => { + return {text: text, filters: filters}; + }); }); } diff --git a/src/core/login/pages/sites/sites.html b/src/core/login/pages/sites/sites.html index 6b33ab936..d6e6ad2e4 100644 --- a/src/core/login/pages/sites/sites.html +++ b/src/core/login/pages/sites/sites.html @@ -19,7 +19,7 @@ {{ 'core.pictureof' | translate:{$a: site.fullname} }}

{{site.fullName}}

-

+

{{site.siteUrl}}

{{site.badge}}