From 90ec21333b2add03ecb121b1cd47ef3de73cb0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Jul 2024 08:58:35 +0200 Subject: [PATCH 1/4] MOBILE-4616 text: Migrate text utils to text --- .../components/myoverview/myoverview.ts | 4 +- .../recentlyaccesseditems.ts | 4 +- .../timeline/components/events/events.ts | 4 +- src/addons/blog/pages/index/index.ts | 4 +- src/addons/calendar/pages/event/event.ts | 7 +- src/addons/calendar/services/calendar.ts | 10 +- .../services/handlers/mathjaxloader.ts | 4 +- .../filter/mediaplugin/services/videojs.ts | 6 +- .../messages/pages/discussion/discussion.ts | 3 +- .../messages/pages/settings/settings.ts | 4 +- .../messages/services/messages-offline.ts | 4 +- src/addons/messages/services/messages-sync.ts | 10 +- .../components/submission/submission.ts | 6 +- .../feedback/comments/component/comments.ts | 4 +- .../feedback/comments/services/handler.ts | 7 +- .../mod/assign/services/assign-offline.ts | 8 +- src/addons/mod/assign/services/assign.ts | 3 +- .../onlinetext/component/onlinetext.ts | 6 +- .../submission/onlinetext/services/handler.ts | 8 +- .../bigbluebuttonbn/components/index/index.ts | 4 +- .../services/bigbluebuttonbn.ts | 4 +- .../mod/book/pages/contents/contents.ts | 4 +- src/addons/mod/book/services/book.ts | 7 +- .../mod/choice/services/choice-offline.ts | 4 +- .../components/search-modal/search-modal.ts | 4 +- .../fields/textarea/component/textarea.ts | 4 +- .../data/fields/textarea/services/handler.ts | 15 +- src/addons/mod/data/pages/edit/edit.ts | 4 +- src/addons/mod/data/services/data-helper.ts | 6 +- src/addons/mod/data/services/data-offline.ts | 6 +- src/addons/mod/data/services/data-sync.ts | 11 +- .../mod/feedback/components/index/index.ts | 6 +- .../mod/feedback/pages/attempt/attempt.ts | 4 +- .../mod/feedback/services/feedback-helper.ts | 7 +- .../mod/feedback/services/feedback-offline.ts | 4 +- src/addons/mod/forum/components/post/post.ts | 4 +- .../pages/new-discussion/new-discussion.ts | 4 +- .../mod/forum/services/forum-offline.ts | 4 +- .../mod/forum/services/handlers/module.ts | 4 +- .../mod/glossary/components/index/index.ts | 4 +- src/addons/mod/glossary/pages/edit/edit.ts | 10 +- .../mod/glossary/services/glossary-offline.ts | 6 +- .../mod/h5pactivity/components/index/index.ts | 4 +- .../h5pactivity/services/h5pactivity-sync.ts | 4 +- src/addons/mod/imscp/pages/view/view.ts | 4 +- src/addons/mod/imscp/services/imscp.ts | 4 +- .../mod/lesson/components/index/index.ts | 10 +- .../lesson/pages/user-retake/user-retake.ts | 6 +- .../mod/lesson/services/lesson-helper.ts | 4 +- .../mod/lesson/services/lesson-offline.ts | 6 +- src/addons/mod/lesson/services/lesson.ts | 20 +- src/addons/mod/lti/services/lti.ts | 6 +- src/addons/mod/page/components/index/index.ts | 4 +- src/addons/mod/page/services/page-helper.ts | 4 +- src/addons/mod/quiz/components/index/index.ts | 4 +- .../mod/quiz/services/handlers/prefetch.ts | 4 +- .../mod/resource/components/index/index.ts | 8 +- .../mod/resource/services/resource-helper.ts | 10 +- .../mod/scorm/services/scorm-offline.ts | 8 +- src/addons/mod/scorm/services/scorm.ts | 4 +- .../mod/survey/components/index/index.ts | 4 +- .../mod/survey/services/survey-offline.ts | 6 +- src/addons/mod/url/components/index/index.ts | 4 +- src/addons/mod/wiki/pages/edit/edit.ts | 11 +- .../assessment-strategy.ts | 5 +- .../workshop/pages/assessment/assessment.ts | 4 +- .../pages/edit-submission/edit-submission.ts | 7 +- .../workshop/pages/submission/submission.ts | 4 +- .../mod/workshop/services/workshop-helper.ts | 4 +- .../mod/workshop/services/workshop-offline.ts | 10 +- .../mod/workshop/services/workshop-sync.ts | 10 +- src/addons/mod/workshop/services/workshop.ts | 2 +- src/addons/notes/pages/list/list.ts | 6 +- src/addons/notes/services/notes-sync.ts | 4 +- .../notifications/pages/settings/settings.ts | 4 +- .../notifications/services/notifications.ts | 4 +- src/addons/privatefiles/pages/index/index.ts | 6 +- src/addons/qtype/ddwtos/classes/ddwtos.ts | 4 +- src/addons/qtype/essay/component/essay.ts | 4 +- .../qtype/essay/services/handlers/essay.ts | 11 +- .../social/services/handlers/social.ts | 4 +- .../text/services/handlers/text.ts | 4 +- .../textarea/services/handlers/textarea.ts | 4 +- src/core/classes/base-sync.ts | 7 +- src/core/classes/errors/error.ts | 4 +- src/core/classes/sites/authenticated-site.ts | 12 +- src/core/classes/sites/site.ts | 3 +- .../classes/sites/unauthenticated-site.ts | 79 +- .../components/attachments/attachments.ts | 8 +- src/core/components/file/file.ts | 4 +- src/core/components/local-file/local-file.ts | 4 +- .../components/mark-required/mark-required.ts | 4 +- src/core/components/message/message.ts | 3 +- src/core/components/mod-icon/mod-icon.ts | 4 +- .../send-message-form/send-message-form.ts | 4 +- src/core/core.module.ts | 3 + src/core/directives/external-content.ts | 4 +- src/core/directives/format-text.ts | 4 +- src/core/directives/link.ts | 3 +- .../block/classes/base-block-component.ts | 4 +- .../components/side-blocks/side-blocks.ts | 3 +- .../features/comments/pages/viewer/viewer.ts | 4 +- .../course/classes/main-resource-component.ts | 14 +- .../features/course/services/course-helper.ts | 4 +- .../features/course/services/log-helper.ts | 4 +- src/core/features/course/services/sync.ts | 4 +- .../rich-text-editor/rich-text-editor.ts | 3 +- .../emulator/services/file-transfer.ts | 2 +- .../services/fileuploader-helper.ts | 6 +- src/core/features/filter/services/filter.ts | 8 +- .../features/grades/services/grades-helper.ts | 12 +- .../features/h5p/classes/content-validator.ts | 20 +- src/core/features/h5p/classes/core.ts | 4 +- src/core/features/h5p/classes/file-storage.ts | 4 +- src/core/features/h5p/classes/framework.ts | 14 +- .../login/pages/email-signup/email-signup.ts | 10 +- src/core/features/login/pages/site/site.ts | 4 +- .../features/login/services/login-helper.ts | 17 +- .../features/mainmenu/services/mainmenu.ts | 4 +- .../services/pushnotifications.ts | 6 +- .../classes/base-question-component.ts | 8 +- .../question/services/question-helper.ts | 10 +- .../features/question/services/question.ts | 4 +- .../features/rating/services/rating-sync.ts | 4 +- .../components/report-detail/report-detail.ts | 4 +- .../settings/services/settings-helper.ts | 4 +- .../services/sharedfiles-helper.ts | 4 +- .../classes/call-ws-click-directive.ts | 4 +- .../siteplugins/services/siteplugins-init.ts | 6 +- .../siteplugins/services/siteplugins.ts | 6 +- src/core/features/tag/pages/search/search.ts | 4 +- src/core/features/user/services/user-sync.ts | 4 +- src/core/pipes/bytes-to-size.ts | 4 +- src/core/pipes/seconds-to-hms.ts | 6 +- src/core/services/analytics.ts | 4 +- src/core/services/error-helper.ts | 176 +++++ src/core/services/file-helper.ts | 155 ++++ src/core/services/file.ts | 14 +- src/core/services/filepool.ts | 16 +- src/core/services/local-notifications.ts | 4 +- src/core/services/navigator.ts | 8 +- src/core/services/sites.ts | 18 +- src/core/services/tests/utils/text.test.ts | 156 ---- src/core/services/urlschemes.ts | 8 +- src/core/services/utils/dom.ts | 35 +- src/core/services/utils/text.ts | 743 ++++-------------- src/core/services/utils/utils.ts | 4 +- src/core/services/ws.ts | 38 +- src/core/singletons/dom.ts | 32 + src/core/singletons/tests/text.test.ts | 81 ++ src/core/singletons/tests/url.test.ts | 53 +- src/core/singletons/text.ts | 607 +++++++++++++- src/core/singletons/url.ts | 60 +- src/testing/utils.ts | 5 +- 154 files changed, 1821 insertions(+), 1245 deletions(-) create mode 100644 src/core/services/error-helper.ts delete mode 100644 src/core/services/tests/utils/text.test.ts diff --git a/src/addons/block/myoverview/components/myoverview/myoverview.ts b/src/addons/block/myoverview/components/myoverview/myoverview.ts index f8fbc1614..57ada4ddb 100644 --- a/src/addons/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addons/block/myoverview/components/myoverview/myoverview.ts @@ -29,7 +29,7 @@ import { CoreBlockBaseComponent } from '@features/block/classes/base-block-compo import { CoreSite } from '@classes/sites/site'; import { CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion'; import { IonSearchbar } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; @@ -375,7 +375,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.filters.show.custom = config?.displaygroupingcustomfield?.value == '1' && !!config?.customfieldsexport?.value; this.filters.customFilters = this.filters.show.custom - ? CoreTextUtils.parseJSON(config?.customfieldsexport?.value || '[]', []) + ? CoreText.parseJSON(config?.customfieldsexport?.value || '[]', []) : []; // Check if any selector is shown and not disabled. diff --git a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts index c56027c1d..31d199720 100644 --- a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts +++ b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.ts @@ -19,7 +19,7 @@ import { AddonBlockRecentlyAccessedItems, AddonBlockRecentlyAccessedItemsItemCalculatedData, } from '../../services/recentlyaccesseditems'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreLoadings } from '@services/loadings'; import { CoreUtils } from '@services/utils/utils'; import { CoreSharedModule } from '@/core/shared.module'; @@ -92,7 +92,7 @@ export class AddonBlockRecentlyAccessedItemsComponent extends CoreBlockBaseCompo e.preventDefault(); e.stopPropagation(); - const url = CoreTextUtils.decodeHTMLEntities(item.viewurl); + const url = CoreText.decodeHTMLEntities(item.viewurl); const modal = await CoreLoadings.show(); try { diff --git a/src/addons/block/timeline/components/events/events.ts b/src/addons/block/timeline/components/events/events.ts index 5156464dc..141b8b917 100644 --- a/src/addons/block/timeline/components/events/events.ts +++ b/src/addons/block/timeline/components/events/events.ts @@ -15,7 +15,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreLoadings } from '@services/loadings'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; import { AddonBlockTimelineDayEvents } from '@addons/block/timeline/classes/section'; import { CoreSharedModule } from '@/core/shared.module'; @@ -64,7 +64,7 @@ export class AddonBlockTimelineEventsComponent implements OnInit { event.stopPropagation(); // Fix URL format. - url = CoreTextUtils.decodeHTMLEntities(url); + url = CoreText.decodeHTMLEntities(url); const modal = await CoreLoadings.show(); diff --git a/src/addons/blog/pages/index/index.ts b/src/addons/blog/pages/index/index.ts index 872477654..6a100b720 100644 --- a/src/addons/blog/pages/index/index.ts +++ b/src/addons/blog/pages/index/index.ts @@ -24,7 +24,7 @@ import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreFileHelper } from '@services/file-helper'; import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreArray } from '@singletons/array'; @@ -212,7 +212,7 @@ export class AddonBlogIndexPage implements OnInit, OnDestroy { entry.contextInstanceId = entry.userid; } - entry.summary = CoreTextUtils.replacePluginfileUrls(entry.summary, entry.summaryfiles || []); + entry.summary = CoreFileHelper.replacePluginfileUrls(entry.summary, entry.summaryfiles || []); entry.user = await CoreUtils.ignoreErrors(CoreUser.getProfile(entry.userid, entry.courseid, true)); }); diff --git a/src/addons/calendar/pages/event/event.ts b/src/addons/calendar/pages/event/event.ts index 67b82e7dc..d97f1d3dd 100644 --- a/src/addons/calendar/pages/event/event.ts +++ b/src/addons/calendar/pages/event/event.ts @@ -25,7 +25,7 @@ import { AddonCalendarSync, AddonCalendarSyncEvents, AddonCalendarSyncProvider } import { CoreNetwork } from '@services/network'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreSites } from '@services/sites'; import { CoreCourse } from '@features/course/services/course'; import { CoreTimeUtils } from '@services/utils/time'; @@ -45,6 +45,7 @@ import { CoreConfig } from '@services/config'; import { CoreToasts, ToastDuration } from '@services/toasts'; import { CorePopovers } from '@services/popovers'; import { CoreLoadings } from '@services/loadings'; +import { CoreUrl } from '@singletons/url'; /** * Page that displays a single calendar event. @@ -288,8 +289,8 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { if (this.event.location) { // Build a link to open the address in maps. - this.event.location = CoreTextUtils.decodeHTML(this.event.location); - this.event.encodedLocation = CoreTextUtils.buildAddressURL(this.event.location); + this.event.location = CoreText.decodeHTML(this.event.location); + this.event.encodedLocation = CoreUrl.buildAddressURL(this.event.location); } // Check if event was deleted in offine. diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index 71817798b..0492f66e4 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreSite } from '@classes/sites/site'; import { CoreNetwork } from '@services/network'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; @@ -330,7 +330,7 @@ export class AddonCalendarProvider { ): Promise { const getTimeHtml = (time: string, a11yLangKey: string): string => - `${time}`; + `${time}`; const getStartTimeHtml = (time: string): string => getTimeHtml(time, 'core.startingtime'); const getEndTimeHtml = (time: string): string => getTimeHtml(time, 'core.endingtime'); @@ -666,18 +666,18 @@ export class AddonCalendarProvider { eventConverted.iscategoryevent = originalEvent.eventtype == AddonCalendarEventType.CATEGORY; eventConverted.normalisedeventtype = this.getEventType(recordAsRecord); try { - eventConverted.category = CoreTextUtils.parseJSON(recordAsRecord.category || ''); + eventConverted.category = CoreText.parseJSON(recordAsRecord.category || ''); } catch { // Ignore errors. } try { - eventConverted.course = CoreTextUtils.parseJSON(recordAsRecord.course || ''); + eventConverted.course = CoreText.parseJSON(recordAsRecord.course || ''); } catch { // Ignore errors. } try { - eventConverted.subscription = CoreTextUtils.parseJSON(recordAsRecord.subscription || ''); + eventConverted.subscription = CoreText.parseJSON(recordAsRecord.subscription || ''); } catch { // Ignore errors. } diff --git a/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts b/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts index caa72b2d2..acf7683cb 100644 --- a/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts +++ b/src/addons/filter/mathjaxloader/services/handlers/mathjaxloader.ts @@ -18,7 +18,7 @@ import { CoreFilterDefaultHandler } from '@features/filter/services/handlers/def import { CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; import { CoreLang } from '@services/lang'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreEvents } from '@singletons/events'; import { CoreSite } from '@classes/sites/site'; @@ -189,7 +189,7 @@ export class AddonFilterMathJaxLoaderHandlerService extends CoreFilterDefaultHan * @returns The whole text with the span inserted around the defined substring. */ protected insertSpan(text: string, start: number, end: number): string { - return CoreTextUtils.substrReplace( + return CoreText.substrReplace( text, '' + text.substring(start, end + 1) + '', start, diff --git a/src/addons/filter/mediaplugin/services/videojs.ts b/src/addons/filter/mediaplugin/services/videojs.ts index a51932900..4d04a019c 100644 --- a/src/addons/filter/mediaplugin/services/videojs.ts +++ b/src/addons/filter/mediaplugin/services/videojs.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreExternalContentDirective } from '@directives/external-content'; import { CoreLang } from '@services/lang'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; import { makeSingleton } from '@singletons'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; @@ -64,7 +64,7 @@ export class AddonFilterMediaPluginVideoJSService { // Create player. const videojs = await this.getVideoJS(); const dataSetupString = element.getAttribute('data-setup') || element.getAttribute('data-setup-lazy') || '{}'; - const data = CoreTextUtils.parseJSON(dataSetupString, {}); + const data = CoreText.parseJSON(dataSetupString, {}); const player = videojs( element, { @@ -106,7 +106,7 @@ export class AddonFilterMediaPluginVideoJSService { } const dataSetupString = video.getAttribute('data-setup') || video.getAttribute('data-setup-lazy') || '{}'; - const data = CoreTextUtils.parseJSON(dataSetupString, {}); + const data = CoreText.parseJSON(dataSetupString, {}); const youtubeUrl = data.techOrder?.[0] == 'youtube' && CoreUrl.getYoutubeEmbedUrl(data.sources?.[0]?.src); if (!youtubeUrl) { diff --git a/src/addons/messages/pages/discussion/discussion.ts b/src/addons/messages/pages/discussion/discussion.ts index 60784c767..9f0f18050 100644 --- a/src/addons/messages/pages/discussion/discussion.ts +++ b/src/addons/messages/pages/discussion/discussion.ts @@ -32,7 +32,6 @@ import { AddonMessagesSync, AddonMessagesSyncProvider } from '../../services/mes import { CoreUser } from '@features/user/services/user'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreLogger } from '@singletons/logger'; import { CoreInfiniteLoadingComponent } from '@components/infinite-loading/infinite-loading'; import { Md5 } from 'ts-md5/dist/md5'; @@ -928,7 +927,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView */ copyMessage(message: AddonMessagesConversationMessageFormatted): void { const text = 'smallmessage' in message ? message.smallmessage || message.text || '' : message.text || ''; - CoreText.copyToClipboard(CoreTextUtils.decodeHTMLEntities(text)); + CoreText.copyToClipboard(CoreText.decodeHTMLEntities(text)); } /** diff --git a/src/addons/messages/pages/settings/settings.ts b/src/addons/messages/pages/settings/settings.ts index 21a2e9886..9fcb9ede7 100644 --- a/src/addons/messages/pages/settings/settings.ts +++ b/src/addons/messages/pages/settings/settings.ts @@ -27,7 +27,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreConstants } from '@/core/constants'; import { AddonNotificationsPreferencesNotificationProcessorState } from '@addons/notifications/services/notifications'; import { CorePlatform } from '@services/platform'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreLoadings } from '@services/loadings'; /** @@ -112,7 +112,7 @@ export class AddonMessagesSettingsPage implements OnInit, OnDestroy { this.warningMessage.set(undefined); } catch (error) { if (error.errorcode === 'nopermissions') { - this.warningMessage.set(CoreTextUtils.getErrorMessageFromError(error)); + this.warningMessage.set(CoreErrorHelper.getErrorMessageFromError(error)); return; } diff --git a/src/addons/messages/services/messages-offline.ts b/src/addons/messages/services/messages-offline.ts index ac4987ae7..0d1321f35 100644 --- a/src/addons/messages/services/messages-offline.ts +++ b/src/addons/messages/services/messages-offline.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreNetwork } from '@services/network'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { AddonMessagesOfflineConversationMessagesDBRecord, AddonMessagesOfflineMessagesDBRecord, @@ -227,7 +227,7 @@ export class AddonMessagesOfflineProvider { text: message.text, timecreated: message.timecreated, deviceoffline: message.deviceoffline, - conversation: message.conversation ? CoreTextUtils.parseJSON(message.conversation, undefined) : undefined, + conversation: message.conversation ? CoreText.parseJSON(message.conversation, undefined) : undefined, pending: true, useridfrom: userIdFrom, }; diff --git a/src/addons/messages/services/messages-sync.ts b/src/addons/messages/services/messages-sync.ts index 73b09de33..c4c415d4c 100644 --- a/src/addons/messages/services/messages-sync.ts +++ b/src/addons/messages/services/messages-sync.ts @@ -30,9 +30,9 @@ import { CoreNetwork } from '@services/network'; import { CoreConstants } from '@/core/constants'; import { CoreUser } from '@features/user/services/user'; import { CoreError } from '@classes/errors/error'; -import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; import { CoreWait } from '@singletons/wait'; +import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper'; /** * Service to sync messages. @@ -178,7 +178,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { if (!errors || errors.length <= 0) { @@ -356,7 +356,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { warnings.push(Translate.instant('addon.messages.warningconversationmessagenotsent', { conversation: conversationIdentifier, - error: CoreTextUtils.getErrorMessageFromError(error), + error: CoreErrorHelper.getErrorMessageFromError(error), })); }); } else if (userId) { @@ -373,7 +373,7 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { warnings.push(Translate.instant('addon.messages.warningmessagenotsent', { user: userIdentifier, - error: CoreTextUtils.getErrorMessageFromError(error), + error: CoreErrorHelper.getErrorMessageFromError(error), })); }); } diff --git a/src/addons/mod/assign/components/submission/submission.ts b/src/addons/mod/assign/components/submission/submission.ts index e5a0c1d42..f5fdd8df2 100644 --- a/src/addons/mod/assign/components/submission/submission.ts +++ b/src/addons/mod/assign/components/submission/submission.ts @@ -41,7 +41,7 @@ import { CoreMenuItem, CoreUtils } from '@services/utils/utils'; import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../../services/assign-helper'; import { CoreDomUtils } from '@services/utils/dom'; import { Translate } from '@singletons'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreCourse, CoreCourseModuleGradeInfo, CoreCourseModuleGradeOutcome } from '@features/course/services/course'; import { AddonModAssignOffline } from '../../services/assign-offline'; import { CoreUser, CoreUserProfile } from '@features/user/services/user'; @@ -1078,7 +1078,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can if (!grade.outcomeid && !grade.scaleid) { // Clean HTML tags, grade can contain an icon. - const gradeFormatted = CoreTextUtils.cleanTags(grade.gradeformatted || ''); + const gradeFormatted = CoreText.cleanTags(grade.gradeformatted || ''); // Not using outcomes or scale, get the numeric grade. if (this.grade.scale) { this.grade.gradebookGrade = CoreUtils.formatFloat( @@ -1099,7 +1099,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can gradeInfo.outcomes?.forEach((outcome) => { if (outcome.id == String(grade.outcomeid)) { // Clean HTML tags, grade can contain an icon. - outcome.selected = CoreTextUtils.cleanTags(grade.gradeformatted || ''); + outcome.selected = CoreText.cleanTags(grade.gradeformatted || ''); outcome.modified = grade.gradedategraded; if (outcome.options) { outcome.selectedId = CoreGradesHelper.getGradeValueFromLabel(outcome.options, outcome.selected); diff --git a/src/addons/mod/assign/feedback/comments/component/comments.ts b/src/addons/mod/assign/feedback/comments/component/comments.ts index e6f44ae02..0513d89ef 100644 --- a/src/addons/mod/assign/feedback/comments/component/comments.ts +++ b/src/addons/mod/assign/feedback/comments/component/comments.ts @@ -15,7 +15,7 @@ import { Component, OnInit, ElementRef } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { AddonModAssign } from '@addons/mod/assign/services/assign'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreFileHelper } from '@services/file-helper'; import { AddonModAssignFeedbackCommentsDraftData, AddonModAssignFeedbackCommentsHandler, @@ -159,7 +159,7 @@ export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedb replacePluginfileUrls(text: string): string { const files = this.plugin.fileareas && this.plugin.fileareas[0] && this.plugin.fileareas[0].files; - return CoreTextUtils.replacePluginfileUrls(text, files || []); + return CoreFileHelper.replacePluginfileUrls(text, files || []); } } diff --git a/src/addons/mod/assign/feedback/comments/services/handler.ts b/src/addons/mod/assign/feedback/comments/services/handler.ts index 6bb08860b..88c2ecb7d 100644 --- a/src/addons/mod/assign/feedback/comments/services/handler.ts +++ b/src/addons/mod/assign/feedback/comments/services/handler.ts @@ -24,11 +24,12 @@ import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offlin import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate'; import { Injectable, Type } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModAssignFeedbackCommentsComponent } from '../component/comments'; +import { CoreFileHelper } from '@services/file-helper'; /** * Handler for comments feedback plugin. @@ -56,7 +57,7 @@ export class AddonModAssignFeedbackCommentsHandlerService implements AddonModAss const files = plugin.fileareas && plugin.fileareas[0] ? plugin.fileareas[0].files : []; - return CoreTextUtils.restorePluginfileUrls(inputData.assignfeedbackcomments_editor, files || []); + return CoreFileHelper.restorePluginfileUrls(inputData.assignfeedbackcomments_editor, files || []); } /** @@ -180,7 +181,7 @@ export class AddonModAssignFeedbackCommentsHandlerService implements AddonModAss if (draft) { // Add some HTML to the text if needed. - draft.text = CoreTextUtils.formatHtmlLines(draft.text); + draft.text = CoreText.formatHtmlLines(draft.text); pluginData.assignfeedbackcomments_editor = draft; } diff --git a/src/addons/mod/assign/services/assign-offline.ts b/src/addons/mod/assign/services/assign-offline.ts index 305092943..7c411ca7b 100644 --- a/src/addons/mod/assign/services/assign-offline.ts +++ b/src/addons/mod/assign/services/assign-offline.ts @@ -17,7 +17,7 @@ import { CoreError } from '@classes/errors/error'; import { SQLiteDBRecordValues } from '@classes/sqlitedb'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { makeSingleton } from '@singletons'; import { CorePath } from '@singletons/path'; @@ -137,7 +137,7 @@ export class AddonModAssignOfflineProvider { assignid: submission.assignid, userid: submission.userid, courseid: submission.courseid, - plugindata: CoreTextUtils.parseJSON(submission.plugindata, {}), + plugindata: CoreText.parseJSON(submission.plugindata, {}), onlinetimemodified: submission.onlinetimemodified, timecreated: submission.timecreated, timemodified: submission.timemodified, @@ -195,8 +195,8 @@ export class AddonModAssignOfflineProvider { addattempt: submission.addattempt, workflowstate: submission.workflowstate, applytoall: submission.applytoall, - outcomes: CoreTextUtils.parseJSON(submission.outcomes, {}), - plugindata: CoreTextUtils.parseJSON(submission.plugindata, {}), + outcomes: CoreText.parseJSON(submission.outcomes, {}), + plugindata: CoreText.parseJSON(submission.plugindata, {}), timemodified: submission.timemodified, })); } diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index f44ccc25c..976ca8abe 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -19,7 +19,6 @@ import { CoreInterceptor } from '@classes/interceptor'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreGrades } from '@features/grades/services/grades'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; @@ -431,7 +430,7 @@ export class AddonModAssignProvider { }); if (!keepUrls && submissionPlugin.fileareas && submissionPlugin.fileareas[0]) { - text = CoreTextUtils.replacePluginfileUrls(text, submissionPlugin.fileareas[0].files || []); + text = CoreFileHelper.replacePluginfileUrls(text, submissionPlugin.fileareas[0].files || []); } return text; diff --git a/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts index 5b4390a98..47760672d 100644 --- a/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts +++ b/src/addons/mod/assign/submission/onlinetext/component/onlinetext.ts @@ -18,7 +18,7 @@ import { AddonModAssignOffline } from '@addons/mod/assign/services/assign-offlin import { Component, OnInit, ElementRef } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { AddonModAssignSubmissionOnlineTextPluginData } from '../services/handler'; import { ContextLevel } from '@/core/constants'; @@ -102,7 +102,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS // Calculate initial words. if (this.wordLimitEnabled) { - this.words = CoreTextUtils.countWords(this.text); + this.words = CoreText.countWords(this.text); } } finally { this.loaded = true; @@ -123,7 +123,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS // Wait before calculating, if the user keeps inputing we won't calculate. // This is to prevent slowing down devices, this calculation can be slow if the text is long. this.wordCountTimeout = window.setTimeout(() => { - this.words = CoreTextUtils.countWords(text); + this.words = CoreText.countWords(text); }, 1500); } } diff --git a/src/addons/mod/assign/submission/onlinetext/services/handler.ts b/src/addons/mod/assign/submission/onlinetext/services/handler.ts index 8ae7b7f78..ac88ded41 100644 --- a/src/addons/mod/assign/submission/onlinetext/services/handler.ts +++ b/src/addons/mod/assign/submission/onlinetext/services/handler.ts @@ -25,7 +25,7 @@ import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/sub import { Injectable, Type } from '@angular/core'; import { CoreError } from '@classes/errors/error'; import { CoreFileHelper } from '@services/file-helper'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -139,7 +139,7 @@ export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonMo const text = inputData.onlinetext_editor_text; const files = plugin.fileareas && plugin.fileareas[0] && plugin.fileareas[0].files || []; - return CoreTextUtils.restorePluginfileUrls(text, files || []); + return CoreFileHelper.restorePluginfileUrls(text, files || []); } /** @@ -198,7 +198,7 @@ export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonMo // Check word limit. const configs = AddonModAssignHelper.getPluginConfig(assign, 'assignsubmission', plugin.type); if (parseInt(configs.wordlimitenabled, 10)) { - const words = CoreTextUtils.countWords(text); + const words = CoreText.countWords(text); const wordlimit = parseInt(configs.wordlimit, 10); if (words > wordlimit) { const params = { $a: { count: words, limit: wordlimit } }; @@ -209,7 +209,7 @@ export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonMo } // Add some HTML to the text if needed. - text = CoreTextUtils.formatHtmlLines(text); + text = CoreText.formatHtmlLines(text); pluginData.onlinetext_editor = { text: text, diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts index 954fe5906..089ade130 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -21,7 +21,7 @@ import { CoreApp } from '@services/app'; import { CoreGroupInfo, CoreGroups } from '@services/groups'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; @@ -185,7 +185,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo }); return { - name: CoreTextUtils.cleanTags(String(recordingData.recording), { singleLine: true }), + name: CoreText.cleanTags(String(recordingData.recording), { singleLine: true }), playbackLabel: columns.playback.label, playbacks, details, diff --git a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts index a96f4e5d3..042cc9ce0 100644 --- a/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts +++ b/src/addons/mod/bigbluebuttonbn/services/bigbluebuttonbn.ts @@ -20,7 +20,7 @@ import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -247,7 +247,7 @@ export class AddonModBBBService { return { ...result.tabledata, - parsedData: CoreTextUtils.parseJSON(result.tabledata?.data, []), + parsedData: CoreText.parseJSON(result.tabledata?.data, []), }; } diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index 141d51dc6..29a8a5f43 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -25,7 +25,7 @@ import { CoreTag, CoreTagItem } from '@features/tag/services/tag'; import { CoreNetwork } from '@services/network'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { @@ -385,7 +385,7 @@ class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSo return newChapters; } catch (error) { - if (!CoreTextUtils.getErrorMessageFromError(error)) { + if (!CoreErrorHelper.getErrorMessageFromError(error)) { throw new CoreError(Translate.instant('addon.mod_book.errorchapter')); } diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index e8da3e010..097383876 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -22,12 +22,13 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreCourse, CoreCourseModuleContentFile } from '@features/course/services/course'; import { CoreUtils } from '@services/utils/utils'; import { CoreFilepool } from '@services/filepool'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFile } from '@services/file'; import { CoreError } from '@classes/errors/error'; import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; import { ADDON_MOD_BOOK_COMPONENT } from '../constants'; +import { CoreUrl } from '@singletons/url'; /** * Service that provides some features for books. @@ -177,7 +178,7 @@ export class AddonModBookProvider { key = content.filepath.replace('/' + chapter + '/', '') + content.filename; } - map[chapter].paths[CoreTextUtils.decodeURIComponent(key)] = content.fileurl; + map[chapter].paths[CoreUrl.decodeURIComponent(key)] = content.fileurl; }); return map; @@ -224,7 +225,7 @@ export class AddonModBookProvider { return []; } - return CoreTextUtils.parseJSON(contents[0].content, []); + return CoreText.parseJSON(contents[0].content, []); } /** diff --git a/src/addons/mod/choice/services/choice-offline.ts b/src/addons/mod/choice/services/choice-offline.ts index 137fa5325..85de0aafe 100644 --- a/src/addons/mod/choice/services/choice-offline.ts +++ b/src/addons/mod/choice/services/choice-offline.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { makeSingleton } from '@singletons'; import { AddonModChoiceResponsesDBRecord, RESPONSES_TABLE_NAME } from './database/choice'; @@ -101,7 +101,7 @@ export class AddonModChoiceOfflineProvider { protected parseResponse(entry: AddonModChoiceResponsesDBRecord): AddonModChoiceOfflineResponses { return { ...entry, - responses: CoreTextUtils.parseJSON(entry.responses, []), + responses: CoreText.parseJSON(entry.responses, []), }; } diff --git a/src/addons/mod/data/components/search-modal/search-modal.ts b/src/addons/mod/data/components/search-modal/search-modal.ts index 9f7cf1086..993563ea0 100644 --- a/src/addons/mod/data/components/search-modal/search-modal.ts +++ b/src/addons/mod/data/components/search-modal/search-modal.ts @@ -17,7 +17,7 @@ import { FormGroup, FormBuilder } from '@angular/forms'; import { CoreTag } from '@features/tag/services/tag'; import { CoreSites } from '@services/sites'; import { CoreFormFields, CoreForms } from '@singletons/form'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { ModalController } from '@singletons'; import { @@ -78,7 +78,7 @@ export class AddonModDataSearchModalComponent implements OnInit { this.search.advanced?.forEach((field) => { if (field !== undefined) { this.advancedIndexed[field.name] = field.value - ? CoreTextUtils.parseJSON(field.value, '') + ? CoreText.parseJSON(field.value, '') : ''; } }); diff --git a/src/addons/mod/data/fields/textarea/component/textarea.ts b/src/addons/mod/data/fields/textarea/component/textarea.ts index 13960a700..47bc4260b 100644 --- a/src/addons/mod/data/fields/textarea/component/textarea.ts +++ b/src/addons/mod/data/fields/textarea/component/textarea.ts @@ -15,7 +15,7 @@ import { Component } from '@angular/core'; import { AddonModDataFieldPluginBaseComponent } from '../../../classes/base-field-plugin-component'; import { AddonModDataEntryField } from '@addons/mod/data/services/data'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreFileHelper } from '@services/file-helper'; import { CoreWSFile } from '@services/ws'; import { ADDON_MOD_DATA_COMPONENT } from '@addons/mod/data/constants'; @@ -40,7 +40,7 @@ export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginB format(value?: Partial): string { const files: CoreWSFile[] = (value && value.files) || []; - return value ? CoreTextUtils.replacePluginfileUrls(value.content || '', files) : ''; + return value ? CoreFileHelper.replacePluginfileUrls(value.content || '', files) : ''; } /** diff --git a/src/addons/mod/data/fields/textarea/services/handler.ts b/src/addons/mod/data/fields/textarea/services/handler.ts index c749aa4ca..5b58cda67 100644 --- a/src/addons/mod/data/fields/textarea/services/handler.ts +++ b/src/addons/mod/data/fields/textarea/services/handler.ts @@ -15,13 +15,14 @@ import { AddonModDataEntryField, AddonModDataField, AddonModDataSubfieldData } from '@addons/mod/data/services/data'; import { Injectable, Type } from '@angular/core'; import { CoreFormFields } from '@singletons/form'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModDataFieldTextHandlerService } from '../../text/services/handler'; import { AddonModDataFieldTextareaComponent } from '../component/textarea'; -import { CoreFileEntry } from '@services/file-helper'; +import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; import type { AddonModDataFieldPluginBaseComponent } from '@addons/mod/data/classes/base-field-plugin-component'; +import { CoreDom } from '@singletons/dom'; /** * Handler for textarea data field plugin. @@ -50,12 +51,12 @@ export class AddonModDataFieldTextareaHandlerService extends AddonModDataFieldTe const fieldName = 'f_' + field.id; const files = this.getFieldEditFiles(field, inputData, originalFieldData); - let text = CoreTextUtils.restorePluginfileUrls(inputData[fieldName] || '', files); + let text = CoreFileHelper.restorePluginfileUrls(inputData[fieldName] || '', files); // Add some HTML to the text if needed. - text = CoreTextUtils.formatHtmlLines(text); + text = CoreText.formatHtmlLines(text); // WS does not properly check if HTML content is blank when the field is required. - if (CoreTextUtils.htmlIsBlank(text)) { + if (CoreDom.htmlIsBlank(text)) { text = ''; } @@ -102,7 +103,7 @@ export class AddonModDataFieldTextareaHandlerService extends AddonModDataFieldTe const value = inputData.find((value) => value.subfield == ''); - if (!value || CoreTextUtils.htmlIsBlank(value.value || '')) { + if (!value || CoreDom.htmlIsBlank(value.value || '')) { return Translate.instant('addon.mod_data.errormustsupplyvalue'); } @@ -115,7 +116,7 @@ export class AddonModDataFieldTextareaHandlerService extends AddonModDataFieldTe originalContent.content = offlineContent[''] || ''; if (originalContent.content.length > 0 && originalContent.files && originalContent.files.length > 0) { // Take the original files since we cannot edit them on the app. - originalContent.content = CoreTextUtils.replacePluginfileUrls( + originalContent.content = CoreFileHelper.replacePluginfileUrls( originalContent.content, originalContent.files, ); diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts index 5940aa0b2..610ad8eef 100644 --- a/src/addons/mod/data/pages/edit/edit.ts +++ b/src/addons/mod/data/pages/edit/edit.ts @@ -40,7 +40,7 @@ import { import { AddonModDataHelper } from '../../services/data-helper'; import { CoreDom } from '@singletons/dom'; import { AddonModDataEntryFieldInitialized } from '../../classes/base-field-plugin-component'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { ADDON_MOD_DATA_COMPONENT, ADDON_MOD_DATA_ENTRY_CHANGED, AddonModDataTemplateType } from '../../constants'; @@ -395,7 +395,7 @@ export class AddonModDataEditPage implements OnInit { if (updateEntryResult.generalnotifications?.length) { CoreDomUtils.showAlertWithOptions({ header: Translate.instant('core.notice'), - message: CoreTextUtils.buildMessage(updateEntryResult.generalnotifications), + message: CoreText.buildMessage(updateEntryResult.generalnotifications), buttons: [Translate.instant('core.ok')], }); } diff --git a/src/addons/mod/data/services/data-helper.ts b/src/addons/mod/data/services/data-helper.ts index 10dad51bc..a8d828af7 100644 --- a/src/addons/mod/data/services/data-helper.ts +++ b/src/addons/mod/data/services/data-helper.ts @@ -21,7 +21,7 @@ import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFormFields } from '@singletons/form'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -97,9 +97,9 @@ export class AddonModDataHelperProvider { if (offlineContent.subfield) { offlineContents[offlineContent.fieldid][offlineContent.subfield] = - CoreTextUtils.parseJSON(offlineContent.value, ''); + CoreText.parseJSON(offlineContent.value, ''); } else { - offlineContents[offlineContent.fieldid][''] = CoreTextUtils.parseJSON(offlineContent.value, ''); + offlineContents[offlineContent.fieldid][''] = CoreText.parseJSON(offlineContent.value, ''); } }); diff --git a/src/addons/mod/data/services/data-offline.ts b/src/addons/mod/data/services/data-offline.ts index c74d92633..69b363586 100644 --- a/src/addons/mod/data/services/data-offline.ts +++ b/src/addons/mod/data/services/data-offline.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { CorePath } from '@singletons/path'; @@ -90,7 +90,7 @@ export class AddonModDataOfflineProvider { const promises: Promise[] = []; entry.fields.forEach((field) => { - const value = CoreTextUtils.parseJSON(field.value, null); + const value = CoreText.parseJSON(field.value, null); if (!value || !value.offline) { return; @@ -234,7 +234,7 @@ export class AddonModDataOfflineProvider { */ protected parseRecord(record: AddonModDataEntryDBRecord): AddonModDataOfflineAction { return Object.assign(record, { - fields: CoreTextUtils.parseJSON(record.fields), + fields: CoreText.parseJSON(record.fields), }); } diff --git a/src/addons/mod/data/services/data-sync.ts b/src/addons/mod/data/services/data-sync.ts index 973597caa..26ba459d0 100644 --- a/src/addons/mod/data/services/data-sync.ts +++ b/src/addons/mod/data/services/data-sync.ts @@ -25,7 +25,7 @@ import { CoreNetwork } from '@services/network'; import { CoreFileEntry } from '@services/file-helper'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync, CoreSyncResult } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreUtils } from '@services/utils/utils'; import { Translate, makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -33,6 +33,7 @@ import { AddonModData, AddonModDataData } from './data'; import { AddonModDataHelper } from './data-helper'; import { AddonModDataOffline, AddonModDataOfflineAction } from './data-offline'; import { ADDON_MOD_DATA_AUTO_SYNCED, ADDON_MOD_DATA_COMPONENT, AddonModDataAction } from '../constants'; +import { CoreText } from '@singletons/text'; /** * Service to sync databases. @@ -326,7 +327,7 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider } catch (error) { if (error && CoreUtils.isWebServiceError(error)) { // The WebService has thrown an error, this means it cannot be performed. Discard. - entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error); + entryResult.discardError = CoreErrorHelper.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. throw error; @@ -345,7 +346,7 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider try { await Promise.all(editAction.fields.map(async (field) => { // Upload Files if asked. - const value = CoreTextUtils.parseJSON(field.value || '', null); + const value = CoreText.parseJSON(field.value || '', null); if (value && (value.online || value.offline)) { let files: CoreFileEntry[] = value.online || []; @@ -384,7 +385,7 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider } catch (error) { if (error && CoreUtils.isWebServiceError(error)) { // The WebService has thrown an error, this means it cannot be performed. Discard. - entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error); + entryResult.discardError = CoreErrorHelper.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. throw error; @@ -402,7 +403,7 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider } catch (error) { if (error && CoreUtils.isWebServiceError(error)) { // The WebService has thrown an error, this means it cannot be performed. Discard. - entryResult.discardError = CoreTextUtils.getErrorMessageFromError(error); + entryResult.discardError = CoreErrorHelper.getErrorMessageFromError(error); } else { // Couldn't connect to server, reject. throw error; diff --git a/src/addons/mod/feedback/components/index/index.ts b/src/addons/mod/feedback/components/index/index.ts index 957973d9e..7a24de452 100644 --- a/src/addons/mod/feedback/components/index/index.ts +++ b/src/addons/mod/feedback/components/index/index.ts @@ -20,7 +20,7 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { IonContent } from '@ionic/angular'; import { CoreGroupInfo, CoreGroups } from '@services/groups'; import { CoreNavigator } from '@services/navigator'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -312,7 +312,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity case 'info': item.data = item.data.map((dataItem) => { - const parsed = > CoreTextUtils.parseJSON(dataItem); + const parsed = > CoreText.parseJSON(dataItem); return parsed.show !== undefined ? parsed.show : false; }).filter((dataItem) => dataItem); // Filter false entries. @@ -325,7 +325,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity case 'multichoicerated': case 'multichoice': { const parsedData = []> item.data.map((dataItem) => { - const parsed = > CoreTextUtils.parseJSON(dataItem); + const parsed = > CoreText.parseJSON(dataItem); return parsed.answertext !== undefined ? parsed : false; }).filter((dataItem) => dataItem); // Filter false entries. diff --git a/src/addons/mod/feedback/pages/attempt/attempt.ts b/src/addons/mod/feedback/pages/attempt/attempt.ts index ec1bac755..08016f94f 100644 --- a/src/addons/mod/feedback/pages/attempt/attempt.ts +++ b/src/addons/mod/feedback/pages/attempt/attempt.ts @@ -18,7 +18,7 @@ import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/ import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreFileHelper } from '@services/file-helper'; import { AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source'; import { AddonModFeedback, @@ -140,7 +140,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy { const attemptItem = formItem; if (item.typ == 'label') { - attemptItem.submittedValue = CoreTextUtils.replacePluginfileUrls(item.presentation, item.itemfiles); + attemptItem.submittedValue = CoreFileHelper.replacePluginfileUrls(item.presentation, item.itemfiles); } else { for (const x in attempt.responses) { if (attempt.responses[x].id == item.id) { diff --git a/src/addons/mod/feedback/services/feedback-helper.ts b/src/addons/mod/feedback/services/feedback-helper.ts index b474ea4a6..80728c8d3 100644 --- a/src/addons/mod/feedback/services/feedback-helper.ts +++ b/src/addons/mod/feedback/services/feedback-helper.ts @@ -18,7 +18,7 @@ import { CoreUser } from '@features/user/services/user'; import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreFileHelper } from '@services/file-helper'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; @@ -41,6 +41,7 @@ import { ADDON_MOD_FEEDBACK_PAGE_NAME, } from '../constants'; import { CoreLoadings } from '@services/loadings'; +import { CoreText } from '@singletons/text'; const MODE_RESPONSETIME = 1; const MODE_COURSE = 2; @@ -253,7 +254,7 @@ export class AddonModFeedbackHelperProvider { */ protected getItemFormLabel(item: AddonModFeedbackItem): AddonModFeedbackFormBasicItem { item.name = ''; - item.presentation = CoreTextUtils.replacePluginfileUrls(item.presentation, item.itemfiles); + item.presentation = CoreFileHelper.replacePluginfileUrls(item.presentation, item.itemfiles); return Object.assign(item, { templateName: 'label', @@ -421,7 +422,7 @@ export class AddonModFeedbackHelperProvider { slottedLabel: false, }); - const data = CoreTextUtils.parseJSON(item.otherdata); + const data = CoreText.parseJSON(item.otherdata); if (data && data.length > 3) { formItem.captcha = { recaptchapublickey: data[3], diff --git a/src/addons/mod/feedback/services/feedback-offline.ts b/src/addons/mod/feedback/services/feedback-offline.ts index 2cfaa0a0d..7cfa363ec 100644 --- a/src/addons/mod/feedback/services/feedback-offline.ts +++ b/src/addons/mod/feedback/services/feedback-offline.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { makeSingleton } from '@singletons'; import { AddonModFeedbackResponseDBRecord, FEEDBACK_TABLE_NAME } from './database/feedback'; @@ -116,7 +116,7 @@ export class AddonModFeedbackOfflineProvider { */ protected parseResponse(record: AddonModFeedbackResponseDBRecord): AddonModFeedbackOfflineResponse { return Object.assign(record, { - responses: > CoreTextUtils.parseJSON(record.responses), + responses: > CoreText.parseJSON(record.responses), }); } diff --git a/src/addons/mod/forum/components/post/post.ts b/src/addons/mod/forum/components/post/post.ts index 0f2028c1b..9e10b3c35 100644 --- a/src/addons/mod/forum/components/post/post.ts +++ b/src/addons/mod/forum/components/post/post.ts @@ -41,7 +41,7 @@ import { Translate } from '@singletons'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; import { AddonModForumSync } from '../../services/forum-sync'; import { CoreSync } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { AddonModForumHelper } from '../../services/forum-helper'; import { AddonModForumOffline } from '../../services/forum-offline'; import { CoreUtils } from '@services/utils/utils'; @@ -370,7 +370,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges const modal = await CoreLoadings.show('core.sending', true); // Add some HTML to the message if needed. - message = CoreTextUtils.formatHtmlLines(message); + message = CoreText.formatHtmlLines(message); // Upload attachments first if any. let attachments; diff --git a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts index 963083e3c..2c0c3c18f 100644 --- a/src/addons/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addons/mod/forum/pages/new-discussion/new-discussion.ts @@ -34,7 +34,7 @@ import { AddonModForumDiscussionOptions, AddonModForumOffline } from '@addons/mo import { CoreUtils } from '@services/utils/utils'; import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CanLeave } from '@guards/can-leave'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreForms } from '@singletons/form'; @@ -553,7 +553,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea const modal = await CoreLoadings.show('core.sending', true); // Add some HTML to the message if needed. - message = CoreTextUtils.formatHtmlLines(message); + message = CoreText.formatHtmlLines(message); if (pin) { options.discussionpinned = true; diff --git a/src/addons/mod/forum/services/forum-offline.ts b/src/addons/mod/forum/services/forum-offline.ts index 3628192a4..1ab1116b1 100644 --- a/src/addons/mod/forum/services/forum-offline.ts +++ b/src/addons/mod/forum/services/forum-offline.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { makeSingleton } from '@singletons'; import { AddonModForumOfflineDiscussionDBRecord, @@ -399,7 +399,7 @@ export class AddonModForumOfflineProvider { R extends { options: string }, O extends Record = Record >(record: R): Omit & { options: O } { - record.options = CoreTextUtils.parseJSON(record.options); + record.options = CoreText.parseJSON(record.options); return record as unknown as Omit & { options: O }; } diff --git a/src/addons/mod/forum/services/handlers/module.ts b/src/addons/mod/forum/services/handlers/module.ts index 9335ce228..3572ef649 100644 --- a/src/addons/mod/forum/services/handlers/module.ts +++ b/src/addons/mod/forum/services/handlers/module.ts @@ -21,7 +21,7 @@ import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/ import { CoreConstants, ModPurpose } from '@/core/constants'; import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler'; import { CoreCourseModuleData } from '@features/course/services/course-helper'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUser } from '@features/user/services/user'; import { ADDON_MOD_FORUM_MARK_READ_EVENT, ADDON_MOD_FORUM_PAGE_NAME } from '../../constants'; @@ -57,7 +57,7 @@ export class AddonModForumModuleHandlerService extends CoreModuleHandlerBase imp const data = await super.getData(module, courseId); const customData = module.customdata ? - CoreTextUtils.parseJSON<{ trackingtype?: string | number } | ''>(module.customdata, {}) : {}; + CoreText.parseJSON<{ trackingtype?: string | number } | ''>(module.customdata, {}) : {}; const trackingType = typeof customData !== 'string' && customData.trackingtype !== undefined ? Number(customData.trackingtype) : undefined; diff --git a/src/addons/mod/glossary/components/index/index.ts b/src/addons/mod/glossary/components/index/index.ts index 28358bc66..ec6f8bd57 100644 --- a/src/addons/mod/glossary/components/index/index.ts +++ b/src/addons/mod/glossary/components/index/index.ts @@ -29,7 +29,7 @@ import { IonContent } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { @@ -312,7 +312,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity // Consider it is 'letter_all'. const getDivider = (entry) => { // Try to get the first letter without HTML tags. - const noTags = CoreTextUtils.cleanTags(entry.concept); + const noTags = CoreText.cleanTags(entry.concept); return (noTags || entry.concept).substring(0, 1).toUpperCase(); }; diff --git a/src/addons/mod/glossary/pages/edit/edit.ts b/src/addons/mod/glossary/pages/edit/edit.ts index e178775f1..950751961 100644 --- a/src/addons/mod/glossary/pages/edit/edit.ts +++ b/src/addons/mod/glossary/pages/edit/edit.ts @@ -24,7 +24,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -472,7 +472,7 @@ class AddonModGlossaryOfflineFormHandler extends AddonModGlossaryFormHandler { const originalData = this.page.originalData; const data = this.page.data; const options = this.getSaveOptions(glossary); - const definition = CoreTextUtils.formatHtmlLines(data.definition); + const definition = CoreText.formatHtmlLines(data.definition); if (!originalData) { return; @@ -561,7 +561,7 @@ class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { ): Promise { const data = this.page.data; const options = this.getSaveOptions(glossary); - const definition = CoreTextUtils.formatHtmlLines(data.definition); + const definition = CoreText.formatHtmlLines(data.definition); await this.checkDuplicates(glossary); await AddonModGlossaryOffline.addOfflineEntry( @@ -594,7 +594,7 @@ class AddonModGlossaryNewFormHandler extends AddonModGlossaryFormHandler { ): Promise { const data = this.page.data; const options = this.getSaveOptions(glossary); - const definition = CoreTextUtils.formatHtmlLines(data.definition); + const definition = CoreText.formatHtmlLines(data.definition); const entryId = await AddonModGlossary.addEntry( glossary.id, data.concept, @@ -674,7 +674,7 @@ class AddonModGlossaryOnlineFormHandler extends AddonModGlossaryFormHandler { const data = this.page.data; const options = this.getSaveOptions(glossary); - const definition = CoreTextUtils.formatHtmlLines(data.definition); + const definition = CoreText.formatHtmlLines(data.definition); // Upload attachments, if any. let attachmentsId: number | undefined = undefined; diff --git a/src/addons/mod/glossary/services/glossary-offline.ts b/src/addons/mod/glossary/services/glossary-offline.ts index a4807e6f9..96ee41b59 100644 --- a/src/addons/mod/glossary/services/glossary-offline.ts +++ b/src/addons/mod/glossary/services/glossary-offline.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CorePath } from '@singletons/path'; @@ -269,9 +269,9 @@ export class AddonModGlossaryOfflineProvider { */ protected parseRecord(record: AddonModGlossaryOfflineEntryDBRecord): AddonModGlossaryOfflineEntry { return Object.assign(record, { - options: > CoreTextUtils.parseJSON(record.options), + options: > CoreText.parseJSON(record.options), attachments: record.attachments ? - CoreTextUtils.parseJSON(record.attachments) : undefined, + CoreText.parseJSON(record.attachments) : undefined, }); } diff --git a/src/addons/mod/h5pactivity/components/index/index.ts b/src/addons/mod/h5pactivity/components/index/index.ts index 542a2b366..f202bacb8 100644 --- a/src/addons/mod/h5pactivity/components/index/index.ts +++ b/src/addons/mod/h5pactivity/components/index/index.ts @@ -44,7 +44,7 @@ import { AddonModH5PActivitySyncResult, } from '../../services/h5pactivity-sync'; import { CoreFileHelper } from '@services/file-helper'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { ADDON_MOD_H5PACTIVITY_AUTO_SYNCED, @@ -269,7 +269,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv return; } - const contentStateObj = CoreTextUtils.parseJSON<{h5p: string}>(contentState, { h5p: '{}' }); + const contentStateObj = CoreText.parseJSON<{h5p: string}>(contentState, { h5p: '{}' }); // The H5P state doesn't always use JSON, so an h5p property was added to jsonize it. this.contentState = contentStateObj.h5p ?? '{}'; diff --git a/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts b/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts index dc822ba76..8a2c3361b 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity-sync.ts @@ -31,7 +31,7 @@ import { AddonModH5PActivityData, } from './h5pactivity'; import { CoreXAPIStateDBRecord, CoreXAPIStatementDBRecord } from '@features/xapi/services/database/xapi'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreXAPIIRI } from '@features/xapi/classes/iri'; import { CoreXAPIItemAgent } from '@features/xapi/classes/item-agent'; import { CoreWSError } from '@classes/errors/wserror'; @@ -196,7 +196,7 @@ export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseP } catch (error) { if ( CoreUtils.isWebServiceError(error) || - CoreTextUtils.getErrorMessageFromError(error) === Translate.instant('core.course.modulenotfound') + CoreErrorHelper.getErrorMessageFromError(error) === Translate.instant('core.course.modulenotfound') ) { // Activity no longer accessible. Delete the data and finish the sync. await deleteOfflineData(); diff --git a/src/addons/mod/imscp/pages/view/view.ts b/src/addons/mod/imscp/pages/view/view.ts index 41d3b782c..354dabe01 100644 --- a/src/addons/mod/imscp/pages/view/view.ts +++ b/src/addons/mod/imscp/pages/view/view.ts @@ -23,7 +23,7 @@ import { CoreCourseModulePrefetchDelegate } from '@features/course/services/modu import { CoreNetwork } from '@services/network'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { AddonModImscp, AddonModImscpImscp, AddonModImscpTocItem } from '../../services/imscp'; @@ -124,7 +124,7 @@ export class AddonModImscpViewPage implements OnInit { } if (downloadResult?.failed) { - const error = CoreTextUtils.getErrorMessageFromError(downloadResult.error) || downloadResult.error; + const error = CoreErrorHelper.getErrorMessageFromError(downloadResult.error) || downloadResult.error; this.warning = Translate.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : ''); } else { this.warning = ''; diff --git a/src/addons/mod/imscp/services/imscp.ts b/src/addons/mod/imscp/services/imscp.ts index caa1cab77..9e2741d88 100644 --- a/src/addons/mod/imscp/services/imscp.ts +++ b/src/addons/mod/imscp/services/imscp.ts @@ -21,7 +21,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreNetwork } from '@services/network'; import { CoreFilepool } from '@services/filepool'; import { CoreSitesCommonWSOptions, CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -48,7 +48,7 @@ export class AddonModImscpProvider { return []; } - return CoreTextUtils.parseJSON(contents[0].content || ''); + return CoreText.parseJSON(contents[0].content || ''); } /** diff --git a/src/addons/mod/lesson/components/index/index.ts b/src/addons/mod/lesson/components/index/index.ts index d5ad92415..d89a3cc3f 100644 --- a/src/addons/mod/lesson/components/index/index.ts +++ b/src/addons/mod/lesson/components/index/index.ts @@ -24,7 +24,7 @@ import { CoreGroupInfo, CoreGroups } from '@services/groups'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreForms } from '@singletons/form'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { AddonModLessonRetakeFinishedInSyncDBRecord } from '../../services/database/lesson'; @@ -550,20 +550,20 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo if (formattedData.lessonscored) { if (formattedData.numofattempts && formattedData.avescore != null) { - formattedData.avescore = CoreTextUtils.roundToDecimals(formattedData.avescore, 2); + formattedData.avescore = CoreText.roundToDecimals(formattedData.avescore, 2); } if (formattedData.highscore != null) { - formattedData.highscore = CoreTextUtils.roundToDecimals(formattedData.highscore, 2); + formattedData.highscore = CoreText.roundToDecimals(formattedData.highscore, 2); } if (formattedData.lowscore != null) { - formattedData.lowscore = CoreTextUtils.roundToDecimals(formattedData.lowscore, 2); + formattedData.lowscore = CoreText.roundToDecimals(formattedData.lowscore, 2); } } if (formattedData.students) { // Get the user data for each student returned. await CoreUtils.allPromises(formattedData.students.map(async (student) => { - student.bestgrade = CoreTextUtils.roundToDecimals(student.bestgrade, 2); + student.bestgrade = CoreText.roundToDecimals(student.bestgrade, 2); const user = await CoreUtils.ignoreErrors(CoreUser.getProfile(student.id, this.courseId, true)); if (user) { diff --git a/src/addons/mod/lesson/pages/user-retake/user-retake.ts b/src/addons/mod/lesson/pages/user-retake/user-retake.ts index ba3cc1028..3ecb605a4 100644 --- a/src/addons/mod/lesson/pages/user-retake/user-retake.ts +++ b/src/addons/mod/lesson/pages/user-retake/user-retake.ts @@ -19,7 +19,7 @@ import { CoreUser } from '@features/user/services/user'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { @@ -145,7 +145,7 @@ export class AddonModLessonUserRetakePage implements OnInit { throw new CoreError(Translate.instant('addon.mod_lesson.cannotfindattempt')); } - student.bestgrade = CoreTextUtils.roundToDecimals(student.bestgrade, 2); + student.bestgrade = CoreText.roundToDecimals(student.bestgrade, 2); student.attempts.forEach((retake) => { if (!this.selectedRetake && this.retakeNumber == retake.try) { // The retake specified as parameter exists. Use it. @@ -222,7 +222,7 @@ export class AddonModLessonUserRetakePage implements OnInit { if (formattedData.userstats.gradeinfo) { // Completed. - formattedData.userstats.grade = CoreTextUtils.roundToDecimals(formattedData.userstats.grade, 2); + formattedData.userstats.grade = CoreText.roundToDecimals(formattedData.userstats.grade, 2); this.timeTakenReadable = CoreTime.formatTime(formattedData.userstats.timetotake); } diff --git a/src/addons/mod/lesson/services/lesson-helper.ts b/src/addons/mod/lesson/services/lesson-helper.ts index 9398f2b74..d54343733 100644 --- a/src/addons/mod/lesson/services/lesson-helper.ts +++ b/src/addons/mod/lesson/services/lesson-helper.ts @@ -17,7 +17,7 @@ import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFormFields } from '@singletons/form'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { makeSingleton, Translate } from '@singletons'; import { @@ -567,7 +567,7 @@ export class AddonModLessonHelperProvider { // Add some HTML to the answer if needed. if (textarea) { - data[textarea.name] = CoreTextUtils.formatHtmlLines( data[textarea.name] || ''); + data[textarea.name] = CoreText.formatHtmlLines( data[textarea.name] || ''); } } else if (question.template == 'multichoice' && ( question).multi) { // Only send the options with value set to true. diff --git a/src/addons/mod/lesson/services/lesson-offline.ts b/src/addons/mod/lesson/services/lesson-offline.ts index 5e50397db..daf5b656c 100644 --- a/src/addons/mod/lesson/services/lesson-offline.ts +++ b/src/addons/mod/lesson/services/lesson-offline.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreFormFields } from '@singletons/form'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -454,8 +454,8 @@ export class AddonModLessonOfflineProvider { protected parsePageAttempt(attempt: AddonModLessonPageAttemptDBRecord): AddonModLessonPageAttemptRecord { return { ...attempt, - data: attempt.data ? CoreTextUtils.parseJSON(attempt.data) : null, - useranswer: attempt.useranswer ? CoreTextUtils.parseJSON(attempt.useranswer) : null, + data: attempt.data ? CoreText.parseJSON(attempt.data) : null, + useranswer: attempt.useranswer ? CoreText.parseJSON(attempt.useranswer) : null, }; } diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts index 1fc068f10..a6f2df32c 100644 --- a/src/addons/mod/lesson/services/lesson.ts +++ b/src/addons/mod/lesson/services/lesson.ts @@ -19,7 +19,7 @@ import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; @@ -90,7 +90,7 @@ export class AddonModLessonProvider { className: string, ): string { // Add a table row containing the answer. - feedback += '' + (answerFormat ? answer : CoreTextUtils.cleanTags(answer)) + + feedback += '' + (answerFormat ? answer : CoreText.cleanTags(answer)) + ''; // If the response exists, add a table row containing the response. If not, add en empty row. @@ -301,7 +301,7 @@ export class AddonModLessonProvider { } // Progress calculation as a percent. - return CoreTextUtils.roundToDecimals(viewedPagesIds.length / Object.keys(validPages).length, 2) * 100; + return CoreText.roundToDecimals(viewedPagesIds.length / Object.keys(validPages).length, 2) * 100; } /** @@ -482,7 +482,7 @@ export class AddonModLessonProvider { return; } - value = CoreTextUtils.decodeHTML(value); + value = CoreText.decodeHTML(value); userResponse.push(value); if (answers[id] !== undefined) { @@ -747,7 +747,7 @@ export class AddonModLessonProvider { } } else { expectedAnswer = expectedAnswer.replace('*', '#####'); - expectedAnswer = CoreTextUtils.escapeForRegex(expectedAnswer); + expectedAnswer = CoreText.escapeForRegex(expectedAnswer); expectedAnswer = expectedAnswer.replace('#####', '.*'); } @@ -830,7 +830,7 @@ export class AddonModLessonProvider { this.checkOtherAnswers(lesson, pageData, result); result.userresponse = studentAnswer; - result.studentanswer = CoreTextUtils.s(studentAnswer); // Clean student answer as it goes to output. + result.studentanswer = CoreText.s(studentAnswer); // Clean student answer as it goes to output. } /** @@ -981,7 +981,7 @@ export class AddonModLessonProvider { response.data.forEach((entry) => { if (entry.value && typeof entry.value == 'string' && entry.value !== '1') { // It's a JSON encoded object. Try to decode it. - entry.value = CoreTextUtils.parseJSON(entry.value); + entry.value = CoreText.parseJSON(entry.value); } map[entry.name] = entry; @@ -1108,7 +1108,7 @@ export class AddonModLessonProvider { } if (lesson.grade !== undefined && lesson.grade !== CoreGradeType.NONE) { - entryData.grade = CoreTextUtils.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1); + entryData.grade = CoreText.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1); entryData.total = lesson.grade; this.addResultValueEolPage(result, 'yourcurrentgradeisoutof', entryData, true); } @@ -1904,7 +1904,7 @@ export class AddonModLessonProvider { if (lesson.grade !== undefined && lesson.grade !== CoreGradeType.NONE) { this.addMessage(messages, 'addon.mod_lesson.yourcurrentgradeisoutof', { $a: { - grade: CoreTextUtils.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1), + grade: CoreText.roundToDecimals(gradeInfo.grade * lesson.grade / 100, 1), total: lesson.grade, } }); } @@ -2920,7 +2920,7 @@ export class AddonModLessonProvider { } if (result.total) { // Not zero. - result.grade = CoreTextUtils.roundToDecimals(result.earned * 100 / result.total, 5); + result.grade = CoreText.roundToDecimals(result.earned * 100 / result.total, 5); } return result; diff --git a/src/addons/mod/lti/services/lti.ts b/src/addons/mod/lti/services/lti.ts index f88d4c3b6..2e9df9064 100644 --- a/src/addons/mod/lti/services/lti.ts +++ b/src/addons/mod/lti/services/lti.ts @@ -21,7 +21,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFile } from '@services/file'; import { CorePlatform } from '@services/platform'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; @@ -64,9 +64,9 @@ export class AddonModLtiProvider { if (p.name == 'ext_submit') { text += ' \n'; + text += ' value="' + CoreText.escapeHTML(p.value) + '"/>\n'; }); text += '\n'; diff --git a/src/addons/mod/page/components/index/index.ts b/src/addons/mod/page/components/index/index.ts index 5e78527f1..ad368ca1f 100644 --- a/src/addons/mod/page/components/index/index.ts +++ b/src/addons/mod/page/components/index/index.ts @@ -15,7 +15,7 @@ import { Component, OnInit, Optional } from '@angular/core'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { AddonModPagePage, AddonModPage } from '../../services/page'; import { AddonModPageHelper } from '../../services/page-helper'; @@ -89,7 +89,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp // Check if description and timemodified should be displayed. if (this.page.displayoptions) { const options: Record = - CoreTextUtils.unserialize(this.page.displayoptions) || {}; + CoreText.unserialize(this.page.displayoptions) || {}; this.displayDescription = options.printintro === undefined || CoreUtils.isTrueOrOne(options.printintro); diff --git a/src/addons/mod/page/services/page-helper.ts b/src/addons/mod/page/services/page-helper.ts index 406c71bcd..bb07c18c0 100644 --- a/src/addons/mod/page/services/page-helper.ts +++ b/src/addons/mod/page/services/page-helper.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrl } from '@singletons/url'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; import { CoreFilepool } from '@services/filepool'; @@ -54,7 +54,7 @@ export class AddonModPageHelperProvider { // Add the folders without the leading slash. key = content.filepath.substring(1) + key; } - paths[CoreTextUtils.decodeURIComponent(key)] = url; + paths[CoreUrl.decodeURIComponent(key)] = url; } }); diff --git a/src/addons/mod/quiz/components/index/index.ts b/src/addons/mod/quiz/components/index/index.ts index 0e2d76bd3..3d6208f26 100644 --- a/src/addons/mod/quiz/components/index/index.ts +++ b/src/addons/mod/quiz/components/index/index.ts @@ -22,7 +22,7 @@ import { CoreQuestionBehaviourDelegate } from '@features/question/services/behav import { IonContent } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -192,7 +192,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp if (warnings?.length) { // Show warnings and delete them so they aren't shown again. - CoreDomUtils.showErrorModal(CoreTextUtils.buildMessage(warnings)); + CoreDomUtils.showErrorModal(CoreText.buildMessage(warnings)); await AddonModQuizSync.setSyncWarnings(quiz.id, []); } diff --git a/src/addons/mod/quiz/services/handlers/prefetch.ts b/src/addons/mod/quiz/services/handlers/prefetch.ts index 9f30d9406..14fd3ad8e 100644 --- a/src/addons/mod/quiz/services/handlers/prefetch.ts +++ b/src/addons/mod/quiz/services/handlers/prefetch.ts @@ -23,7 +23,7 @@ import { CoreCourses } from '@features/courses/services/courses'; import { CoreQuestionHelper } from '@features/question/services/question-helper'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; @@ -334,7 +334,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet if (canStart && (!attempt || AddonModQuiz.isAttemptCompleted(attempt.state))) { // Check if the user can attempt the quiz. if (attemptAccessInfo.preventnewattemptreasons.length) { - throw new CoreError(CoreTextUtils.buildMessage(attemptAccessInfo.preventnewattemptreasons)); + throw new CoreError(CoreText.buildMessage(attemptAccessInfo.preventnewattemptreasons)); } startAttempt = true; diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts index 46e30f194..27f536692 100644 --- a/src/addons/mod/resource/components/index/index.ts +++ b/src/addons/mod/resource/components/index/index.ts @@ -24,7 +24,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils, OpenFileAction } from '@services/utils/utils'; import { NgZone, Translate } from '@singletons'; import { Subscription } from 'rxjs'; @@ -118,7 +118,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource const resource = await AddonModResource.getResourceData(this.courseId, this.module.id); this.description = resource.intro || ''; const options: AddonModResourceCustomData = - resource.displayoptions ? CoreTextUtils.unserialize(resource.displayoptions) : {}; + resource.displayoptions ? CoreText.unserialize(resource.displayoptions) : {}; this.displayDescription = options.printintro === undefined || !!options.printintro; this.dataRetrieved.emit(resource); @@ -172,11 +172,11 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource if ('contentsinfo' in this.module && this.module.contentsinfo) { mimetype = this.module.contentsinfo.mimetypes[0]; - this.readableSize = CoreTextUtils.bytesToSize(this.module.contentsinfo.filessize, 1); + this.readableSize = CoreText.bytesToSize(this.module.contentsinfo.filessize, 1); this.timemodified = this.module.contentsinfo.lastmodified * 1000; } else { mimetype = await CoreUtils.getMimeTypeFromUrl(CoreFileHelper.getFileUrl(contents[0])); - this.readableSize = CoreTextUtils.bytesToSize(contents[0].filesize, 1); + this.readableSize = CoreText.bytesToSize(contents[0].filesize, 1); this.timemodified = contents[0].timemodified * 1000; } diff --git a/src/addons/mod/resource/services/resource-helper.ts b/src/addons/mod/resource/services/resource-helper.ts index 66afb0a74..0f9fc20b6 100644 --- a/src/addons/mod/resource/services/resource-helper.ts +++ b/src/addons/mod/resource/services/resource-helper.ts @@ -29,7 +29,7 @@ import { makeSingleton, Translate } from '@singletons'; import { CorePath } from '@singletons/path'; import { AddonModResource, AddonModResourceCustomData } from './resource'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { ADDON_MOD_RESOURCE_COMPONENT } from '../constants'; import { CoreLoadings } from '@services/loadings'; @@ -235,15 +235,15 @@ export class AddonModResourceHelperProvider { */ protected async getModuleOptions(module: CoreCourseModuleData, courseId: number): Promise { if (module.customdata !== undefined) { - const customData: { displayoptions: string } | string = CoreTextUtils.parseJSON(module.customdata); + const customData: { displayoptions: string } | string = CoreText.parseJSON(module.customdata); const displayOptions = typeof customData === 'object' ? customData.displayoptions : customData; - return CoreTextUtils.unserialize(displayOptions); + return CoreText.unserialize(displayOptions); } // Get the resource data. Legacy version (from 3.5 to 3.6.6) const info = await AddonModResource.getResourceData(courseId, module.id); - const options: AddonModResourceCustomData = CoreTextUtils.unserialize(info.displayoptions); + const options: AddonModResourceCustomData = CoreText.unserialize(info.displayoptions); if (!module.contents?.[0] || options.filedetails !== undefined) { // Contents attribute should be loaded at this point and it's needed to get mainFile. @@ -303,7 +303,7 @@ export class AddonModResourceHelperProvider { const extra: string[] = []; if (options.showsize && details.size) { - extra.push(CoreTextUtils.bytesToSize(details.size, 1)); + extra.push(CoreText.bytesToSize(details.size, 1)); } if (options.showtype) { diff --git a/src/addons/mod/scorm/services/scorm-offline.ts b/src/addons/mod/scorm/services/scorm-offline.ts index 4fd951ce9..9ba1aa1f5 100644 --- a/src/addons/mod/scorm/services/scorm-offline.ts +++ b/src/addons/mod/scorm/services/scorm-offline.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreUser } from '@features/user/services/user'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -297,7 +297,7 @@ export class AddonModScormOfflineProvider { case 'cmi.core.score.raw': case 'cmi.score.raw': - formatted.score_raw = CoreTextUtils.roundToDecimals(Number(value), 2); // Round to 2 decimals max. + formatted.score_raw = CoreText.roundToDecimals(Number(value), 2); // Round to 2 decimals max. break; case 'cmi.core.session_time': @@ -813,7 +813,7 @@ export class AddonModScormOfflineProvider { protected parseAttempt(attempt: AddonModScormAttemptDBRecord): AddonModScormOfflineAttempt { return { ...attempt, - snapshot: attempt.snapshot ? CoreTextUtils.parseJSON(attempt.snapshot) : null, + snapshot: attempt.snapshot ? CoreText.parseJSON(attempt.snapshot) : null, }; } @@ -826,7 +826,7 @@ export class AddonModScormOfflineProvider { protected parseTracks(tracks: AddonModScormTrackDBRecord[]): AddonModScormOfflineTrack[] { return tracks.map((track) => ({ ...track, - value: track.value ? CoreTextUtils.parseJSON(track.value) : null, + value: track.value ? CoreText.parseJSON(track.value) : null, })); } diff --git a/src/addons/mod/scorm/services/scorm.ts b/src/addons/mod/scorm/services/scorm.ts index f0025d62e..8cc9af793 100644 --- a/src/addons/mod/scorm/services/scorm.ts +++ b/src/addons/mod/scorm/services/scorm.ts @@ -21,7 +21,7 @@ import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; @@ -400,7 +400,7 @@ export class AddonModScormProvider { if (scorm.grademethod !== AddonModScormGradingMethod.GRADESCOES && scorm.maxgrade) { grade = (grade / scorm.maxgrade) * 100; - return Translate.instant('core.percentagenumber', { $a: CoreTextUtils.roundToDecimals(grade, 2) }); + return Translate.instant('core.percentagenumber', { $a: CoreText.roundToDecimals(grade, 2) }); } return String(grade); diff --git a/src/addons/mod/survey/components/index/index.ts b/src/addons/mod/survey/components/index/index.ts index c1adf5b53..c87fba5f6 100644 --- a/src/addons/mod/survey/components/index/index.ts +++ b/src/addons/mod/survey/components/index/index.ts @@ -20,7 +20,7 @@ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents import { IonContent } from '@ionic/angular'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { getPrefetchHandlerInstance } from '../../services/handlers/prefetch'; @@ -156,7 +156,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo if (question.multiArray && !question.multiArray.length && question.parent === 0 && question.type > 0) { // Options shown in a select. Remove all HTML. - question.optionsArray = question.optionsArray?.map((option) => CoreTextUtils.cleanTags(option)); + question.optionsArray = question.optionsArray?.map((option) => CoreText.cleanTags(option)); } }); } diff --git a/src/addons/mod/survey/services/survey-offline.ts b/src/addons/mod/survey/services/survey-offline.ts index e049d3abc..82cc63e4c 100644 --- a/src/addons/mod/survey/services/survey-offline.ts +++ b/src/addons/mod/survey/services/survey-offline.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { makeSingleton } from '@singletons'; import { AddonModSurveyAnswersDBRecord, SURVEY_TABLE } from './database/survey'; import { AddonModSurveySubmitAnswerData } from './survey'; @@ -51,7 +51,7 @@ export class AddonModSurveyOfflineProvider { const entries = await site.getDb().getAllRecords(SURVEY_TABLE); return entries.map((entry) => Object.assign(entry, { - answers: CoreTextUtils.parseJSON(entry.answers), + answers: CoreText.parseJSON(entry.answers), })); } @@ -91,7 +91,7 @@ export class AddonModSurveyOfflineProvider { ); return Object.assign(entry, { - answers: CoreTextUtils.parseJSON(entry.answers), + answers: CoreText.parseJSON(entry.answers), }); } diff --git a/src/addons/mod/url/components/index/index.ts b/src/addons/mod/url/components/index/index.ts index b6f70314c..9c2cc0463 100644 --- a/src/addons/mod/url/components/index/index.ts +++ b/src/addons/mod/url/components/index/index.ts @@ -19,7 +19,7 @@ import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/ import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { CoreCourse } from '@features/course/services/course'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { AddonModUrl, AddonModUrlDisplayOptions, AddonModUrlUrl } from '../../services/url'; import { AddonModUrlHelper } from '../../services/url-helper'; import { ADDON_MOD_URL_COMPONENT } from '../../constants'; @@ -90,7 +90,7 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo this.dataRetrieved.emit(url); if (url.displayoptions) { - const unserialized = CoreTextUtils.unserialize(url.displayoptions); + const unserialized = CoreText.unserialize(url.displayoptions); this.displayDescription = unserialized.printintro === undefined || !!unserialized.printintro; } diff --git a/src/addons/mod/wiki/pages/edit/edit.ts b/src/addons/mod/wiki/pages/edit/edit.ts index e6a7ca15a..c9d11962f 100644 --- a/src/addons/mod/wiki/pages/edit/edit.ts +++ b/src/addons/mod/wiki/pages/edit/edit.ts @@ -21,7 +21,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { Translate } from '@singletons'; @@ -33,6 +33,7 @@ import { AddonModWikiSync } from '../../services/wiki-sync'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { ADDON_MOD_WIKI_COMPONENT, ADDON_MOD_WIKI_PAGE_CREATED_EVENT, ADDON_MOD_WIKI_RENEW_LOCK_TIME } from '../../constants'; import { CoreLoadings } from '@services/loadings'; +import { CoreFileHelper } from '@services/file-helper'; /** * Page that allows adding or editing a wiki page. @@ -94,7 +95,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { this.userId = CoreNavigator.getRouteNumberParam('userId'); const pageTitle = CoreNavigator.getRouteParam('pageTitle'); - this.originalTitle = pageTitle ? CoreTextUtils.cleanTags(pageTitle.replace(/\+/g, ' '), { singleLine: true }) : ''; + this.originalTitle = pageTitle ? CoreText.cleanTags(pageTitle.replace(/\+/g, ' '), { singleLine: true }) : ''; this.canEditTitle = !this.originalTitle; this.title = this.originalTitle ? @@ -182,7 +183,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { const editContents = await AddonModWiki.getPageForEditing(this.pageId, this.section); // Get the original page contents, treating file URLs if needed. - const content = CoreTextUtils.replacePluginfileUrls(editContents.content || '', this.subwikiFiles); + const content = CoreFileHelper.replacePluginfileUrls(editContents.content || '', this.subwikiFiles); this.contentControl.setValue(content); this.originalContent = content; @@ -365,8 +366,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { const modal = await CoreLoadings.show('core.sending', true); - text = CoreTextUtils.restorePluginfileUrls(text, this.subwikiFiles); - text = CoreTextUtils.formatHtmlLines(text); + text = CoreFileHelper.restorePluginfileUrls(text, this.subwikiFiles); + text = CoreText.formatHtmlLines(text); try { if (this.editing && this.pageId) { diff --git a/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts b/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts index 9f8344d89..2ae834566 100644 --- a/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts +++ b/src/addons/mod/workshop/components/assessment-strategy/assessment-strategy.ts @@ -22,7 +22,6 @@ import { CoreFileSession } from '@services/file-session'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -260,7 +259,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe } // Compare feedback text. - const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []); + const text = CoreFileHelper.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []); if (this.originalData.text != text) { return true; } @@ -334,7 +333,7 @@ export class AddonModWorkshopAssessmentStrategyComponent implements OnInit, OnDe ); } - const text = CoreTextUtils.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []); + const text = CoreFileHelper.restorePluginfileUrls(this.feedbackText, this.data.assessment?.feedbackcontentfiles || []); let assessmentData: CoreFormFields; try { diff --git a/src/addons/mod/workshop/pages/assessment/assessment.ts b/src/addons/mod/workshop/pages/assessment/assessment.ts index eb1601a86..53f591a08 100644 --- a/src/addons/mod/workshop/pages/assessment/assessment.ts +++ b/src/addons/mod/workshop/pages/assessment/assessment.ts @@ -22,7 +22,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreForms } from '@singletons/form'; @@ -378,7 +378,7 @@ export class AddonModWorkshopAssessmentPage implements OnInit, OnDestroy, CanLea const grade = inputData.grade >= 0 ? String(inputData.grade) : ''; // Add some HTML to the message if needed. - const text = CoreTextUtils.formatHtmlLines(inputData.text); + const text = CoreText.formatHtmlLines(inputData.text); try { // Try to send it to server. diff --git a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts index 27ebfea2e..8e884b84d 100644 --- a/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addons/mod/workshop/pages/edit-submission/edit-submission.ts @@ -24,7 +24,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -45,6 +45,7 @@ import { AddonModWorkshopSubmissionType, } from '@addons/mod/workshop/constants'; import { CoreLoadings } from '@services/loadings'; +import { CoreDom } from '@singletons/dom'; /** * Page that displays the workshop edit submission. @@ -343,7 +344,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca throw new CoreError(Translate.instant('addon.mod_workshop.submissionrequiredtitle')); } - const noText = CoreTextUtils.htmlIsBlank(inputData.content); + const noText = CoreDom.htmlIsBlank(inputData.content); const noFiles = !inputData.attachmentfiles.length; if ((this.textRequired && noText) || (this.fileRequired && noFiles) || (noText && noFiles)) { @@ -359,7 +360,7 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy, Ca // Add some HTML to the message if needed. if (this.textAvailable) { - inputData.content = CoreTextUtils.formatHtmlLines(inputData.content); + inputData.content = CoreText.formatHtmlLines(inputData.content); } // Upload attachments first if any. diff --git a/src/addons/mod/workshop/pages/submission/submission.ts b/src/addons/mod/workshop/pages/submission/submission.ts index 295fc7903..d7977bab1 100644 --- a/src/addons/mod/workshop/pages/submission/submission.ts +++ b/src/addons/mod/workshop/pages/submission/submission.ts @@ -25,7 +25,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreForms } from '@singletons/form'; @@ -521,7 +521,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea inputData.grade = Number(inputData.grade) >= 0 ? inputData.grade : ''; // Add some HTML to the message if needed. - inputData.text = CoreTextUtils.formatHtmlLines(inputData.text); + inputData.text = CoreText.formatHtmlLines(inputData.text); // Try to send it to server. try { diff --git a/src/addons/mod/workshop/services/workshop-helper.ts b/src/addons/mod/workshop/services/workshop-helper.ts index 7dec857eb..54091eeed 100644 --- a/src/addons/mod/workshop/services/workshop-helper.ts +++ b/src/addons/mod/workshop/services/workshop-helper.ts @@ -19,7 +19,7 @@ import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; import { CoreFile } from '@services/file'; import { CoreFileEntry } from '@services/file-helper'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils, CoreTextFormat } from '@services/utils/text'; +import { CoreText, CoreTextFormat } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate } from '@singletons'; import { CoreFormFields } from '@singletons/form'; @@ -564,7 +564,7 @@ export class AddonModWorkshopHelperProvider { return '0'; } - value = CoreTextUtils.roundToDecimals(max * value / 100, decimals); + value = CoreText.roundToDecimals(max * value / 100, decimals); return CoreUtils.formatFloat(value); } diff --git a/src/addons/mod/workshop/services/workshop-offline.ts b/src/addons/mod/workshop/services/workshop-offline.ts index c88c06e68..1b84c1f66 100644 --- a/src/addons/mod/workshop/services/workshop-offline.ts +++ b/src/addons/mod/workshop/services/workshop-offline.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { makeSingleton } from '@singletons'; import { CoreFormFields } from '@singletons/form'; @@ -239,7 +239,7 @@ export class AddonModWorkshopOfflineProvider { protected parseSubmissionRecord(record: AddonModWorkshopSubmissionDBRecord): AddonModWorkshopOfflineSubmission { return { ...record, - attachmentsid: CoreTextUtils.parseJSON(record.attachmentsid), + attachmentsid: CoreText.parseJSON(record.attachmentsid), }; } @@ -355,7 +355,7 @@ export class AddonModWorkshopOfflineProvider { protected parseAssessmentRecord(record: AddonModWorkshopAssessmentDBRecord): AddonModWorkshopOfflineAssessment { return { ...record, - inputdata: CoreTextUtils.parseJSON(record.inputdata), + inputdata: CoreText.parseJSON(record.inputdata), }; } @@ -486,7 +486,7 @@ export class AddonModWorkshopOfflineProvider { return { ...record, published: Boolean(record.published), - gradeover: CoreTextUtils.parseJSON(record.gradeover), + gradeover: CoreText.parseJSON(record.gradeover), }; } @@ -616,7 +616,7 @@ export class AddonModWorkshopOfflineProvider { ): AddonModWorkshopOfflineEvaluateAssessment { return { ...record, - gradinggradeover: CoreTextUtils.parseJSON(record.gradinggradeover), + gradinggradeover: CoreText.parseJSON(record.gradinggradeover), }; } diff --git a/src/addons/mod/workshop/services/workshop-sync.ts b/src/addons/mod/workshop/services/workshop-sync.ts index a666badd4..645a3fa38 100644 --- a/src/addons/mod/workshop/services/workshop-sync.ts +++ b/src/addons/mod/workshop/services/workshop-sync.ts @@ -21,7 +21,7 @@ import { CoreNetwork } from '@services/network'; import { CoreFileEntry } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreSync, CoreSyncResult } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreUtils } from '@services/utils/utils'; import { Translate, makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -373,7 +373,7 @@ export class AddonModWorkshopSyncProvider extends CoreSyncBaseProvider { - note.content = CoreTextUtils.decodeHTML(note.content); + note.content = CoreText.decodeHTML(note.content); }); await AddonNotes.setOfflineDeletedNotes(notesList, this.courseId); @@ -301,7 +301,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy { * @param warnings the warnings */ protected showSyncWarnings(warnings: string[]): void { - const message = CoreTextUtils.buildMessage(warnings); + const message = CoreText.buildMessage(warnings); if (message) { CoreDomUtils.showAlert(undefined, message); diff --git a/src/addons/notes/services/notes-sync.ts b/src/addons/notes/services/notes-sync.ts index bfe3aecd2..c60a6f431 100644 --- a/src/addons/notes/services/notes-sync.ts +++ b/src/addons/notes/services/notes-sync.ts @@ -26,7 +26,7 @@ import { AddonNotes, AddonNotesCreateNoteData } from './notes'; import { AddonNotesOffline } from './notes-offline'; import { CoreArray } from '@singletons/array'; import { CoreAnyError } from '@classes/errors/error'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; /** * Service to sync notes. @@ -235,7 +235,7 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider Translate.instant('addon.notes.warningnotenotsent', { course: 'fullname' in course ? course.fullname : courseId, // @deprecated since 4.3. - error: CoreTextUtils.getErrorMessageFromError(error), + error: CoreErrorHelper.getErrorMessageFromError(error), })); } diff --git a/src/addons/notifications/pages/settings/settings.ts b/src/addons/notifications/pages/settings/settings.ts index 6be1b7032..c6702431d 100644 --- a/src/addons/notifications/pages/settings/settings.ts +++ b/src/addons/notifications/pages/settings/settings.ts @@ -39,7 +39,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { Translate } from '@singletons'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreLoadings } from '@services/loadings'; /** @@ -122,7 +122,7 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy { this.logView(); } catch (error) { if (error.errorcode === 'nopermissions') { - this.warningMessage.set(CoreTextUtils.getErrorMessageFromError(error)); + this.warningMessage.set(CoreErrorHelper.getErrorMessageFromError(error)); return; } diff --git a/src/addons/notifications/services/notifications.ts b/src/addons/notifications/services/notifications.ts index 49ed2a791..542b24e7b 100644 --- a/src/addons/notifications/services/notifications.ts +++ b/src/addons/notifications/services/notifications.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreWSExternalWarning } from '@services/ws'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUser, USER_NOREPLY_USER } from '@features/user/services/user'; import { CoreSite } from '@classes/sites/site'; @@ -120,7 +120,7 @@ export class AddonNotificationsProvider { notification.read = notification.timeread > 0; if (typeof notification.customdata === 'string') { - notification.customdata = CoreTextUtils.parseJSON>(notification.customdata, {}); + notification.customdata = CoreText.parseJSON>(notification.customdata, {}); } // Try to set courseid the notification belongs to. diff --git a/src/addons/privatefiles/pages/index/index.ts b/src/addons/privatefiles/pages/index/index.ts index a241b1474..0dc3cc720 100644 --- a/src/addons/privatefiles/pages/index/index.ts +++ b/src/addons/privatefiles/pages/index/index.ts @@ -18,7 +18,7 @@ import { Md5 } from 'ts-md5/dist/md5'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { @@ -214,8 +214,8 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { // Get the info to calculate the available size. this.filesInfo = await AddonPrivateFiles.getPrivateFilesInfo(); - this.spaceUsed = CoreTextUtils.bytesToSize(this.filesInfo.filesizewithoutreferences, 1); - this.userQuotaReadable = CoreTextUtils.bytesToSize(this.userQuota, 1); + this.spaceUsed = CoreText.bytesToSize(this.filesInfo.filesizewithoutreferences, 1); + this.userQuotaReadable = CoreText.bytesToSize(this.userQuota, 1); } else { // User quota isn't useful, delete it. delete this.userQuota; diff --git a/src/addons/qtype/ddwtos/classes/ddwtos.ts b/src/addons/qtype/ddwtos/classes/ddwtos.ts index e34999889..ab7fb7641 100644 --- a/src/addons/qtype/ddwtos/classes/ddwtos.ts +++ b/src/addons/qtype/ddwtos/classes/ddwtos.ts @@ -13,7 +13,7 @@ // limitations under the License. import { CoreFormatTextDirective } from '@directives/format-text'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CoreCoordinates, CoreDom } from '@singletons/dom'; import { CoreEventObserver } from '@singletons/events'; @@ -490,7 +490,7 @@ export class AddonQtypeDdwtosQuestion { } groupItems.forEach((item) => { - item.innerHTML = CoreTextUtils.decodeHTML(item.innerHTML); + item.innerHTML = CoreText.decodeHTML(item.innerHTML); }); // Wait to render in order to calculate size. diff --git a/src/addons/qtype/essay/component/essay.ts b/src/addons/qtype/essay/component/essay.ts index 197389dbd..8859c2264 100644 --- a/src/addons/qtype/essay/component/essay.ts +++ b/src/addons/qtype/essay/component/essay.ts @@ -19,7 +19,7 @@ import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { AddonModQuizEssayQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component'; import { CoreQuestionHelper } from '@features/question/services/question-helper'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreFileSession } from '@services/file-session'; import { CoreQuestion } from '@features/question/services/question'; import { CoreFileEntry } from '@services/file-helper'; @@ -71,7 +71,7 @@ export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent maxWords) { return Translate.instant('addon.qtype_essay.maxwordlimitboundary', { $a: { limit: maxWords, count: count } }); } else if (count < minWords) { @@ -386,7 +387,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { return; } - const attachmentsData: CoreFileUploaderStoreFilesResult = CoreTextUtils.parseJSON( + const attachmentsData: CoreFileUploaderStoreFilesResult = CoreText.parseJSON( answers.attachments_offline, { online: [], @@ -438,7 +439,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { // Restore draftfile URLs. const site = await CoreSites.getSite(siteId); - answers[textarea.name] = CoreTextUtils.restoreDraftfileUrls( + answers[textarea.name] = CoreFileHelper.restoreDraftfileUrls( site.getURL(), answers[textarea.name], question.html, @@ -459,7 +460,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { if (!isPlainText) { // Add some HTML to the text if needed. - answers[textarea.name] = CoreTextUtils.formatHtmlLines( answers[textarea.name] || ''); + answers[textarea.name] = CoreText.formatHtmlLines( answers[textarea.name] || ''); } } diff --git a/src/addons/userprofilefield/social/services/handlers/social.ts b/src/addons/userprofilefield/social/services/handlers/social.ts index ca7335ef5..73f837bfe 100644 --- a/src/addons/userprofilefield/social/services/handlers/social.ts +++ b/src/addons/userprofilefield/social/services/handlers/social.ts @@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate'; import { AddonUserProfileFieldSocialComponent } from '../../component/social'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; import { CoreUserProfileField } from '@features/user/services/user'; import { makeSingleton } from '@singletons'; @@ -60,7 +60,7 @@ export class AddonUserProfileFieldSocialHandlerService implements CoreUserProfil return { type: 'social', name: name, - value: CoreTextUtils.cleanTags( formValues[name]), + value: CoreText.cleanTags( formValues[name]), }; } diff --git a/src/addons/userprofilefield/text/services/handlers/text.ts b/src/addons/userprofilefield/text/services/handlers/text.ts index 72ff5c7a1..15e94f1d7 100644 --- a/src/addons/userprofilefield/text/services/handlers/text.ts +++ b/src/addons/userprofilefield/text/services/handlers/text.ts @@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate'; import { AddonUserProfileFieldTextComponent } from '../../component/text'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; import { CoreUserProfileField } from '@features/user/services/user'; import { makeSingleton } from '@singletons'; @@ -60,7 +60,7 @@ export class AddonUserProfileFieldTextHandlerService implements CoreUserProfileF return { type: 'text', name: name, - value: CoreTextUtils.cleanTags( formValues[name]), + value: CoreText.cleanTags( formValues[name]), }; } diff --git a/src/addons/userprofilefield/textarea/services/handlers/textarea.ts b/src/addons/userprofilefield/textarea/services/handlers/textarea.ts index e0bb9d578..98a1fb2e7 100644 --- a/src/addons/userprofilefield/textarea/services/handlers/textarea.ts +++ b/src/addons/userprofilefield/textarea/services/handlers/textarea.ts @@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate'; import { AddonUserProfileFieldTextareaComponent } from '../../component/textarea'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { AuthEmailSignupProfileField } from '@features/login/services/login-helper'; import { CoreUserProfileField } from '@features/user/services/user'; import { makeSingleton } from '@singletons'; @@ -60,7 +60,7 @@ export class AddonUserProfileFieldTextareaHandlerService implements CoreUserProf if (formValues[name]) { let text = formValues[name] || ''; // Add some HTML to the message in case the user edited with textarea. - text = CoreTextUtils.formatHtmlLines(text); + text = CoreText.formatHtmlLines(text); return { type: 'textarea', diff --git a/src/core/classes/base-sync.ts b/src/core/classes/base-sync.ts index d25a0797b..587151c81 100644 --- a/src/core/classes/base-sync.ts +++ b/src/core/classes/base-sync.ts @@ -15,11 +15,12 @@ import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreAnyError, CoreError } from '@classes/errors/error'; +import { CoreErrorHelper } from '@services/error-helper'; /** * Blocked sync error. @@ -120,7 +121,7 @@ export class CoreSyncBaseProvider { return Translate.instant('core.warningofflinedatadeleted', { component: this.componentTranslate, name: name, - error: CoreTextUtils.getErrorMessageFromError(error), + error: CoreErrorHelper.getErrorMessageFromError(error), }); } @@ -195,7 +196,7 @@ export class CoreSyncBaseProvider { try { const entry = await CoreSync.getSyncRecord(this.component, id, siteId); - return CoreTextUtils.parseJSON(entry.warnings, []); + return CoreText.parseJSON(entry.warnings, []); } catch { return []; } diff --git a/src/core/classes/errors/error.ts b/src/core/classes/errors/error.ts index e046ea3d1..cccd985ba 100644 --- a/src/core/classes/errors/error.ts +++ b/src/core/classes/errors/error.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreTextErrorObject } from '@services/utils/text'; +import { CoreErrorObject } from '@services/error-helper'; /** * Base Error class. @@ -34,4 +34,4 @@ export class CoreError extends Error { } -export type CoreAnyError = string | CoreError | CoreTextErrorObject | null | undefined; +export type CoreAnyError = string | CoreError | CoreErrorObject | null | undefined; diff --git a/src/core/classes/sites/authenticated-site.ts b/src/core/classes/sites/authenticated-site.ts index a40896b9e..f396bbd77 100644 --- a/src/core/classes/sites/authenticated-site.ts +++ b/src/core/classes/sites/authenticated-site.ts @@ -22,7 +22,7 @@ import { CoreWSTypeExpected, } from '@services/ws'; import { CoreToasts, ToastDuration } from '@services/toasts'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; @@ -418,7 +418,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { splitRequest: preSets.splitRequest, }; - if (wsPreSets.cleanUnicode && CoreTextUtils.hasUnicodeData(data)) { + if (wsPreSets.cleanUnicode && CoreText.hasUnicodeData(data)) { // Data will be cleaned, notify the user. CoreToasts.show({ message: 'core.unicodenotsupported', @@ -676,7 +676,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { error.message = Translate.instant('core.policy.sitepolicynotagreederror'); throw new CoreSilentError(error); - } else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.hasUnicodeData(data)) { + } else if (error.errorcode === 'dmlwriteexception' && CoreText.hasUnicodeData(data)) { if (!this.cleanUnicode) { // Try again cleaning unicode. this.cleanUnicode = true; @@ -961,7 +961,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { // Request not executed, enqueue again. this.enqueueRequest(request); } else if (response.error) { - const rejectReason = CoreTextUtils.parseJSON(response.exception || '') as Error | undefined; + const rejectReason = CoreText.parseJSON(response.exception || '') as Error | undefined; request.deferred.reject(rejectReason); CoreErrorLogs.addErrorLog({ method: request.method, @@ -971,7 +971,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { data: request.data, }); } else { - let responseData = response.data ? CoreTextUtils.parseJSON(response.data) : {}; + let responseData = response.data ? CoreText.parseJSON(response.data) : {}; // Match the behaviour of CoreWSProvider.call when no response is expected. const responseExpected = wsPresets.responseExpected === undefined || wsPresets.responseExpected; if (!responseExpected && (responseData == null || responseData === '')) { @@ -1097,7 +1097,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { } return { - response: CoreTextUtils.parseJSON(entry.data, {}), + response: CoreText.parseJSON(entry.data, {}), expirationIgnored: forceCache, expirationTime, }; diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index 5cd6d6d5d..58eb7ad71 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -25,7 +25,6 @@ import { CoreWSUploadFileResult, } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils'; @@ -166,7 +165,7 @@ export class CoreSite extends CoreAuthenticatedSite { */ setConfig(config: CoreSiteConfig): void { if (config) { - config.tool_mobile_disabledfeatures = CoreTextUtils.treatDisabledFeatures(config.tool_mobile_disabledfeatures); + config.tool_mobile_disabledfeatures = this.treatDisabledFeatures(config.tool_mobile_disabledfeatures); } this.config = config; diff --git a/src/core/classes/sites/unauthenticated-site.ts b/src/core/classes/sites/unauthenticated-site.ts index 3d4a5e3fa..8ce6df346 100644 --- a/src/core/classes/sites/unauthenticated-site.ts +++ b/src/core/classes/sites/unauthenticated-site.ts @@ -16,7 +16,7 @@ import { CoreConstants } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSitesReadingStrategy } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreWS, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws'; import { CorePath } from '@singletons/path'; @@ -30,6 +30,55 @@ export class CoreUnauthenticatedSite { protected publicConfig?: CoreSitePublicConfigResponse; + // List of regular expressions to convert the old nomenclature to new nomenclature for disabled features. + protected static readonly DISABLED_FEATURES_COMPAT_REGEXPS: { old: RegExp; new: string }[] = [ + { old: /\$mmLoginEmailSignup/g, new: 'CoreLoginEmailSignup' }, + { old: /\$mmSideMenuDelegate/g, new: 'CoreMainMenuDelegate' }, + { old: /\$mmCoursesDelegate/g, new: 'CoreCourseOptionsDelegate' }, + { old: /\$mmUserDelegate/g, new: 'CoreUserDelegate' }, + { old: /\$mmCourseDelegate/g, new: 'CoreCourseModuleDelegate' }, + { old: /_mmCourses/g, new: '_CoreCourses' }, + { old: /_mmaFrontpage/g, new: '_CoreSiteHome' }, + { old: /_mmaGrades/g, new: '_CoreGrades' }, + { old: /_mmaCompetency/g, new: '_AddonCompetency' }, + { old: /_mmaNotifications/g, new: '_AddonNotifications' }, + { old: /_mmaMessages/g, new: '_AddonMessages' }, + { old: /_mmaCalendar/g, new: '_AddonCalendar' }, + { old: /_mmaFiles/g, new: '_AddonPrivateFiles' }, + { old: /_mmaParticipants/g, new: '_CoreUserParticipants' }, + { old: /_mmaCourseCompletion/g, new: '_AddonCourseCompletion' }, + { old: /_mmaNotes/g, new: '_AddonNotes' }, + { old: /_mmaBadges/g, new: '_AddonBadges' }, + { old: /files_privatefiles/g, new: 'AddonPrivateFilesPrivateFiles' }, + { old: /files_sitefiles/g, new: 'AddonPrivateFilesSiteFiles' }, + { old: /files_upload/g, new: 'AddonPrivateFilesUpload' }, + { old: /_mmaModAssign/g, new: '_AddonModAssign' }, + { old: /_mmaModBigbluebuttonbn/g, new: '_AddonModBBB' }, + { old: /_mmaModBook/g, new: '_AddonModBook' }, + { old: /_mmaModChat/g, new: '_AddonModChat' }, + { old: /_mmaModChoice/g, new: '_AddonModChoice' }, + { old: /_mmaModData/g, new: '_AddonModData' }, + { old: /_mmaModFeedback/g, new: '_AddonModFeedback' }, + { old: /_mmaModFolder/g, new: '_AddonModFolder' }, + { old: /_mmaModForum/g, new: '_AddonModForum' }, + { old: /_mmaModGlossary/g, new: '_AddonModGlossary' }, + { old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity' }, + { old: /_mmaModImscp/g, new: '_AddonModImscp' }, + { old: /_mmaModLabel/g, new: '_AddonModLabel' }, + { old: /_mmaModLesson/g, new: '_AddonModLesson' }, + { old: /_mmaModLti/g, new: '_AddonModLti' }, + { old: /_mmaModPage/g, new: '_AddonModPage' }, + { old: /_mmaModQuiz/g, new: '_AddonModQuiz' }, + { old: /_mmaModResource/g, new: '_AddonModResource' }, + { old: /_mmaModScorm/g, new: '_AddonModScorm' }, + { old: /_mmaModSurvey/g, new: '_AddonModSurvey' }, + { old: /_mmaModUrl/g, new: '_AddonModUrl' }, + { old: /_mmaModWiki/g, new: '_AddonModWiki' }, + { old: /_mmaModWorkshop/g, new: '_AddonModWorkshop' }, + { old: /remoteAddOn_/g, new: 'sitePlugin_' }, + { old: /AddonNotes:addNote/g, new: 'AddonNotes:notes' }, + ]; + /** * Create a site. * @@ -160,10 +209,10 @@ export class CoreUnauthenticatedSite { return false; } - const siteUrl = CoreTextUtils.addEndingSlash( + const siteUrl = CoreText.addEndingSlash( CoreUrl.removeUrlParts(this.siteUrl, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), ); - url = CoreTextUtils.addEndingSlash(CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])); + url = CoreText.addEndingSlash(CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])); return url.indexOf(siteUrl) == 0; } @@ -207,7 +256,7 @@ export class CoreUnauthenticatedSite { */ setPublicConfig(publicConfig: CoreSitePublicConfigResponse): void { publicConfig.tool_mobile_disabledfeatures = - CoreTextUtils.treatDisabledFeatures(publicConfig.tool_mobile_disabledfeatures ?? ''); + this.treatDisabledFeatures(publicConfig.tool_mobile_disabledfeatures ?? ''); this.publicConfig = publicConfig; } @@ -330,7 +379,7 @@ export class CoreUnauthenticatedSite { return false; } - const regEx = new RegExp('(,|^)' + CoreTextUtils.escapeForRegex(name) + '(,|$)', 'g'); + const regEx = new RegExp('(,|^)' + CoreText.escapeForRegex(name) + '(,|$)', 'g'); return !!disabledFeatures.match(regEx); } @@ -344,6 +393,26 @@ export class CoreUnauthenticatedSite { return this.publicConfig?.tool_mobile_disabledfeatures; } + /** + * Treat the list of disabled features, replacing old nomenclature with the new one. + * + * @param features List of disabled features. + * @returns Treated list. + */ + protected treatDisabledFeatures(features: string): string { + if (!features) { + return ''; + } + + for (let i = 0; i < CoreUnauthenticatedSite.DISABLED_FEATURES_COMPAT_REGEXPS.length; i++) { + const entry = CoreUnauthenticatedSite.DISABLED_FEATURES_COMPAT_REGEXPS[i]; + + features = features.replace(entry.old, entry.new); + } + + return features; + } + } /** diff --git a/src/core/components/attachments/attachments.ts b/src/core/components/attachments/attachments.ts index c844e8206..54895fa55 100644 --- a/src/core/components/attachments/attachments.ts +++ b/src/core/components/attachments/attachments.ts @@ -17,7 +17,7 @@ import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; import { CoreFileUploader, CoreFileUploaderTypeList } from '@features/fileuploader/services/fileuploader'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { Translate } from '@singletons'; import { CoreNetwork } from '@services/network'; import { CoreDomUtils } from '@services/utils/dom'; @@ -74,7 +74,7 @@ export class CoreAttachmentsComponent implements OnInit { if (this.maxSize === 0) { await this.getMaxSizeOfArea(); } else if (this.maxSize > 0) { - this.maxSizeReadable = CoreTextUtils.bytesToSize(this.maxSize, 2); + this.maxSizeReadable = CoreText.bytesToSize(this.maxSize, 2); } else if (this.maxSize === -1) { this.maxSizeReadable = Translate.instant('core.unlimited'); } else { @@ -110,7 +110,7 @@ export class CoreAttachmentsComponent implements OnInit { if (course?.maxbytes) { this.maxSize = course.maxbytes; - this.maxSizeReadable = CoreTextUtils.bytesToSize(this.maxSize, 2); + this.maxSizeReadable = CoreText.bytesToSize(this.maxSize, 2); return; } @@ -122,7 +122,7 @@ export class CoreAttachmentsComponent implements OnInit { if (siteInfo?.usermaxuploadfilesize) { this.maxSize = siteInfo.usermaxuploadfilesize; - this.maxSizeReadable = CoreTextUtils.bytesToSize(this.maxSize, 2); + this.maxSizeReadable = CoreText.bytesToSize(this.maxSize, 2); } else { this.maxSizeReadable = Translate.instant('core.unknown'); } diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index e42544666..574a138e2 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -22,7 +22,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreUrl } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { DownloadStatus } from '@/core/constants'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreWSFile } from '@services/ws'; @@ -90,7 +90,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.openButtonLabel = this.defaultIsOpenWithPicker ? 'core.openfile' : 'core.openwith'; if (this.showSize && this.fileSize && this.fileSize >= 0) { - this.fileSizeReadable = CoreTextUtils.bytesToSize(this.fileSize, 2); + this.fileSizeReadable = CoreText.bytesToSize(this.fileSize, 2); } this.showTime = this.showTime && this.timemodified > 0; diff --git a/src/core/components/local-file/local-file.ts b/src/core/components/local-file/local-file.ts index 2c9c70e97..89cc829fe 100644 --- a/src/core/components/local-file/local-file.ts +++ b/src/core/components/local-file/local-file.ts @@ -21,7 +21,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils, CoreUtilsOpenFileOptions, OpenFileAction } from '@services/utils/utils'; import { CoreForms } from '@singletons/form'; @@ -78,7 +78,7 @@ export class CoreLocalFileComponent implements OnInit { // Get the size and timemodified. const metadata = await CoreFile.getMetadata(this.file); if (metadata.size >= 0) { - this.size = CoreTextUtils.bytesToSize(metadata.size, 2); + this.size = CoreText.bytesToSize(metadata.size, 2); } this.timemodified = CoreTimeUtils.userDate(metadata.modificationTime.getTime(), 'core.strftimedatetimeshort'); diff --git a/src/core/components/mark-required/mark-required.ts b/src/core/components/mark-required/mark-required.ts index e6ed33d3e..da40c2940 100644 --- a/src/core/components/mark-required/mark-required.ts +++ b/src/core/components/mark-required/mark-required.ts @@ -15,7 +15,7 @@ import { toBoolean } from '@/core/transforms/boolean'; import { Component, Input, AfterViewInit, ElementRef } from '@angular/core'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { Translate } from '@singletons'; /** @@ -53,7 +53,7 @@ export class CoreMarkRequiredComponent implements AfterViewInit { if (this.coreMarkRequired) { // Add the "required" to the aria-label. const ariaLabel = this.hostElement.getAttribute('aria-label') || - CoreTextUtils.cleanTags(this.hostElement.innerHTML, { singleLine: true }); + CoreText.cleanTags(this.hostElement.innerHTML, { singleLine: true }); if (ariaLabel) { this.hostElement.setAttribute('aria-label', ariaLabel + '. ' + this.requiredLabel); } diff --git a/src/core/components/message/message.ts b/src/core/components/message/message.ts index b8982f723..99b4d9b2e 100644 --- a/src/core/components/message/message.ts +++ b/src/core/components/message/message.ts @@ -17,7 +17,6 @@ import { Component, EventEmitter, HostBinding, Input, OnInit, Output } from '@an import { CoreAnimations } from '@components/animations'; import { CoreSites } from '@services/sites'; import { CoreText } from '@singletons/text'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreUserWithAvatar } from '@components/user-avatar/user-avatar'; import { toBoolean } from '@/core/transforms/boolean'; @@ -103,7 +102,7 @@ export class CoreMessageComponent implements OnInit { * Copy message to clipboard. */ copyMessage(): void { - CoreText.copyToClipboard(CoreTextUtils.decodeHTMLEntities(this.text)); + CoreText.copyToClipboard(CoreText.decodeHTMLEntities(this.text)); } } diff --git a/src/core/components/mod-icon/mod-icon.ts b/src/core/components/mod-icon/mod-icon.ts index ab8727dfe..3c10b5eb2 100644 --- a/src/core/components/mod-icon/mod-icon.ts +++ b/src/core/components/mod-icon/mod-icon.ts @@ -28,7 +28,7 @@ import { import { CoreCourse } from '@features/course/services/course'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; const assetsPath = 'assets/img/'; @@ -150,7 +150,7 @@ export class CoreModIconComponent implements OnInit, OnChanges { return; } - this.iconUrl.update(value => CoreTextUtils.decodeHTMLEntities(value)); + this.iconUrl.update(value => CoreText.decodeHTMLEntities(value)); if (this.brandedClass !== undefined) { return; } diff --git a/src/core/components/send-message-form/send-message-form.ts b/src/core/components/send-message-form/send-message-form.ts index 8f0a4be5b..63e0cde37 100644 --- a/src/core/components/send-message-form/send-message-form.ts +++ b/src/core/components/send-message-form/send-message-form.ts @@ -16,7 +16,7 @@ import { Component, Input, Output, EventEmitter, ViewChild, ElementRef } from '@ import { CoreConfig } from '@services/config'; import { CoreEvents } from '@singletons/events'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreConstants } from '@/core/constants'; import { CoreForms } from '@singletons/form'; import { CorePlatform } from '@services/platform'; @@ -88,7 +88,7 @@ export class CoreSendMessageFormComponent { CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId()); - value = CoreTextUtils.replaceNewLines(value, '
'); + value = CoreText.replaceNewLines(value, '
'); this.onSubmit.emit(value); } diff --git a/src/core/core.module.ts b/src/core/core.module.ts index 234a8f7c3..554e088fc 100644 --- a/src/core/core.module.ts +++ b/src/core/core.module.ts @@ -34,6 +34,7 @@ export async function getCoreServices(): Promise[]> { const { CoreCustomURLSchemesProvider } = await import('@services/urlschemes'); const { CoreDbProvider } = await import('@services/db'); const { CoreDomUtilsProvider } = await import('@services/utils/dom'); + const { CoreErrorHelperService } = await import('@services/error-helper'); const { CoreToastsService } = await import('@services/toasts'); const { CoreFileHelperProvider } = await import('@services/file-helper'); const { CoreFilepoolProvider } = await import('@services/filepool'); @@ -51,6 +52,7 @@ export async function getCoreServices(): Promise[]> { const { CoreScreenService } = await import('@services/screen'); const { CoreSitesProvider } = await import('@services/sites'); const { CoreSyncProvider } = await import('@services/sync'); + // eslint-disable-next-line deprecation/deprecation const { CoreTextUtilsProvider } = await import('@services/utils/text'); const { CoreTimeUtilsProvider } = await import('@services/utils/time'); const { CoreUpdateManagerProvider } = await import('@services/update-manager'); @@ -68,6 +70,7 @@ export async function getCoreServices(): Promise[]> { CoreCustomURLSchemesProvider, CoreDbProvider, CoreDomUtilsProvider, + CoreErrorHelperService, CoreFileHelperProvider, CoreFilepoolProvider, CoreFileProvider, diff --git a/src/core/directives/external-content.ts b/src/core/directives/external-content.ts index 94c02458b..4ed81c8a4 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -39,7 +39,7 @@ import { AsyncDirective } from '@classes/async-directive'; import { CoreDirectivesRegistry } from '@singletons/directives-registry'; import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreArray } from '@singletons/array'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { FileEntry } from '@awesome-cordova-plugins/file/ngx'; @@ -301,7 +301,7 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O const finalUrl = await CoreFilepool.getSrcByUrl(siteId, url, this.component, this.componentId, 0, true, true); this.logger.debug('Using URL ' + finalUrl + ' for ' + url + ' in inline styles'); - inlineStyles = inlineStyles.replace(new RegExp(CoreTextUtils.escapeForRegex(url), 'gi'), finalUrl); + inlineStyles = inlineStyles.replace(new RegExp(CoreText.escapeForRegex(url), 'gi'), finalUrl); }); try { diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 9c4f4691b..f762b426e 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -31,7 +31,7 @@ import { import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreIframeUtils, CoreIframeUtilsProvider } from '@services/utils/iframe'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreSite } from '@classes/sites/site'; import { NgZone, Translate } from '@singletons'; @@ -291,7 +291,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec button.innerHTML = ``; button.addEventListener('click', (e: Event) => { - const imgSrc = CoreTextUtils.escapeHTML(img.getAttribute('data-original-src') || img.getAttribute('src')); + const imgSrc = CoreText.escapeHTML(img.getAttribute('data-original-src') || img.getAttribute('src')); e.preventDefault(); e.stopPropagation(); diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index ec463b819..3bb1f30d3 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -20,7 +20,6 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrl } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreCustomURLSchemes } from '@services/urlschemes'; @@ -96,7 +95,7 @@ export class CoreLinkDirective implements OnInit { const openIn = this.element.getAttribute('data-open-in'); if (this.capture) { - const treated = await CoreContentLinksHelper.handleLink(CoreTextUtils.decodeURI(href), undefined, true, true); + const treated = await CoreContentLinksHelper.handleLink(CoreUrl.decodeURI(href), undefined, true, true); if (!treated) { this.navigate(href, openIn); diff --git a/src/core/features/block/classes/base-block-component.ts b/src/core/features/block/classes/base-block-component.ts index cfb6402c0..86f74c90b 100644 --- a/src/core/features/block/classes/base-block-component.ts +++ b/src/core/features/block/classes/base-block-component.ts @@ -16,7 +16,7 @@ import { OnInit, Input, Component, Optional, Inject, OnChanges, SimpleChanges } import { CoreLogger } from '@singletons/logger'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreCourseBlock } from '../../course/services/course'; import { Params } from '@angular/router'; import { ContextLevel } from '@/core/constants'; @@ -75,7 +75,7 @@ export abstract class CoreBlockBaseComponent implements OnInit, OnChanges, ICore } this.block.configs.forEach((config) => { - config.value = CoreTextUtils.parseJSON(config.value); + config.value = CoreText.parseJSON(config.value); }); this.block.configsRecord = CoreUtils.arrayToObject(this.block.configs, 'name'); diff --git a/src/core/features/block/components/side-blocks/side-blocks.ts b/src/core/features/block/components/side-blocks/side-blocks.ts index 23eee8649..92b20b1b8 100644 --- a/src/core/features/block/components/side-blocks/side-blocks.ts +++ b/src/core/features/block/components/side-blocks/side-blocks.ts @@ -20,7 +20,6 @@ import { CoreBlockHelper } from '../../services/block-helper'; import { CoreBlockComponent } from '../block/block'; import { CoreUtils } from '@services/utils/utils'; import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreDom } from '@singletons/dom'; import { ContextLevel } from '@/core/constants'; import { CoreWait } from '@singletons/wait'; @@ -110,7 +109,7 @@ export class CoreBlockSideBlocksComponent implements OnInit { } this.blocks = this.blocks.filter(block => - block.name !== 'html' || (block.contents && !CoreTextUtils.htmlIsBlank(block.contents.content))); + block.name !== 'html' || (block.contents && !CoreDom.htmlIsBlank(block.contents.content))); } /** diff --git a/src/core/features/comments/pages/viewer/viewer.ts b/src/core/features/comments/pages/viewer/viewer.ts index 01a1322b8..5879bbb9d 100644 --- a/src/core/features/comments/pages/viewer/viewer.ts +++ b/src/core/features/comments/pages/viewer/viewer.ts @@ -33,7 +33,7 @@ import { NgZone, Translate } from '@singletons'; import { CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUser } from '@features/user/services/user'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreError } from '@classes/errors/error'; import { CoreCommentsOffline } from '@features/comments/services/comments-offline'; import { CoreCommentsDBRecord } from '@features/comments/services/database/comments'; @@ -274,7 +274,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { * @param warnings the warnings */ private showSyncWarnings(warnings: string[]): void { - const message = CoreTextUtils.buildMessage(warnings); + const message = CoreText.buildMessage(warnings); if (message) { CoreDomUtils.showAlert(undefined, message); } diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index 6434c503b..6e67b5d1e 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -19,7 +19,6 @@ import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { Translate } from '@singletons'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -35,13 +34,14 @@ import { CoreUrl } from '@singletons/url'; import { CoreTime } from '@singletons/time'; import { CoreText } from '@singletons/text'; import { CoreModals } from '@services/modals'; +import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper'; /** * Result of a resource download. */ export type CoreCourseResourceDownloadResult = { failed?: boolean; // Whether the download has failed. - error?: string | CoreTextErrorObject; // The error in case it failed. + error?: string | CoreErrorObject; // The error in case it failed. }; /** @@ -220,7 +220,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * @returns Whether the error is a "module not found" error. */ protected isNotFoundError(error: CoreAnyError): boolean { - return CoreTextUtils.getErrorMessageFromError(error) === Translate.instant('core.course.modulenotfound'); + return CoreErrorHelper.getErrorMessageFromError(error) === Translate.instant('core.course.modulenotfound'); } /** @@ -255,14 +255,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * @param multiLine Whether to put each message in a different paragraph or in a single line. * @returns Error text message. */ - protected getErrorDownloadingSomeFilesMessage(error: string | CoreTextErrorObject, multiLine?: boolean): string { + protected getErrorDownloadingSomeFilesMessage(error: string | CoreErrorObject, multiLine?: boolean): string { if (multiLine) { - return CoreTextUtils.buildSeveralParagraphsMessage([ + return CoreErrorHelper.buildSeveralParagraphsMessage([ Translate.instant('core.errordownloadingsomefiles'), error, ]); } else { - error = CoreTextUtils.getErrorMessageFromError(error) || ''; + error = CoreErrorHelper.getErrorMessageFromError(error) || ''; return Translate.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : ''); } @@ -273,7 +273,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * * @param error The specific error. */ - protected showErrorDownloadingSomeFiles(error: string | CoreTextErrorObject): void { + protected showErrorDownloadingSomeFiles(error: string | CoreErrorObject): void { CoreDomUtils.showErrorModal(this.getErrorDownloadingSomeFilesMessage(error, true)); } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index df9b29f42..11ae811e7 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -60,7 +60,7 @@ import { CoreNetwork } from '@services/network'; import { CoreSite } from '@classes/sites/site'; import { CoreFile } from '@services/file'; import { CoreUrl } from '@singletons/url'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { CoreNetworkError } from '@classes/errors/network-error'; @@ -1342,7 +1342,7 @@ export class CoreCourseHelperProvider { // Treat stored size. const size = results[0]; - const sizeReadable = CoreTextUtils.bytesToSize(results[0], 2); + const sizeReadable = CoreText.bytesToSize(results[0], 2); // Treat module status. const status = results[1]; diff --git a/src/core/features/course/services/log-helper.ts b/src/core/features/course/services/log-helper.ts index 5a41c4e97..41deadb21 100644 --- a/src/core/features/course/services/log-helper.ts +++ b/src/core/features/course/services/log-helper.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreNetwork } from '@services/network'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; @@ -338,7 +338,7 @@ export class CoreCourseLogHelperProvider { */ protected async syncLogs(logs: CoreCourseActivityLogDBRecord[], siteId: string): Promise { await Promise.all(logs.map(async (log) => { - const data = CoreTextUtils.parseJSON>(log.data || '{}', {}); + const data = CoreText.parseJSON>(log.data || '{}', {}); try { await this.logOnline(log.ws, data, siteId); diff --git a/src/core/features/course/services/sync.ts b/src/core/features/course/services/sync.ts index c8ff8fe15..e38e3ad56 100644 --- a/src/core/features/course/services/sync.ts +++ b/src/core/features/course/services/sync.ts @@ -19,7 +19,7 @@ import { CoreSyncBaseProvider } from '@classes/base-sync'; import { CoreSites } from '@services/sites'; import { CoreNetwork } from '@services/network'; import { CoreUtils } from '@services/utils/utils'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreCourseOffline } from './course-offline'; import { CoreCourse } from './course'; import { CoreCourseLogHelper } from './log-helper'; @@ -227,7 +227,7 @@ export class CoreCourseSyncProvider extends CoreSyncBaseProvider= 300) { - // Request failed. Try to get the respnse. + // Request failed. Try to get the response. const body = xhr.response ? await this.blobToText(xhr.response) : ''; const error = new FileTransferErrorMock(-1, source, target, xhr.status, body, ''); diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index 19a2e8008..c79a55413 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -23,7 +23,7 @@ import { CoreNetwork } from '@services/network'; import { CoreFile, CoreFileProvider, CoreFileProgressEvent } from '@services/file'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton, Translate, Camera, ActionSheetController } from '@singletons'; import { CoreLogger } from '@singletons/logger'; @@ -140,7 +140,7 @@ export class CoreFileUploaderHelperProvider { if (size < 0) { return CoreDomUtils.showConfirm(Translate.instant('core.fileuploader.confirmuploadunknownsize')); } else if (size >= wifiThreshold || (CoreNetwork.isNetworkAccessLimited() && size >= limitedThreshold)) { - const readableSize = CoreTextUtils.bytesToSize(size, 2); + const readableSize = CoreText.bytesToSize(size, 2); return CoreDomUtils.showConfirm( Translate.instant('core.fileuploader.confirmuploadfile', { size: readableSize }), @@ -247,7 +247,7 @@ export class CoreFileUploaderHelperProvider { return new CoreError(Translate.instant('core.fileuploader.maxbytesfile', { $a: { file: fileName, - size: CoreTextUtils.bytesToSize(maxSize, 2), + size: CoreText.bytesToSize(maxSize, 2), }, })); } diff --git a/src/core/features/filter/services/filter.ts b/src/core/features/filter/services/filter.ts index c64e70c7f..e85e46edd 100644 --- a/src/core/features/filter/services/filter.ts +++ b/src/core/features/filter/services/filter.ts @@ -18,7 +18,7 @@ import { CoreNetwork } from '@services/network'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSite } from '@classes/sites/site'; import { CoreWSExternalWarning } from '@services/ws'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreFilterDelegate } from './filter-delegate'; import { makeSingleton } from '@singletons'; import { CoreEvents, CoreEventSiteData } from '@singletons/events'; @@ -253,15 +253,15 @@ export class CoreFilterProvider { } if (options.clean) { - text = CoreTextUtils.cleanTags(text, { singleLine: options.singleLine }); + text = CoreText.cleanTags(text, { singleLine: options.singleLine }); } if (options.shortenLength && options.shortenLength > 0) { - text = CoreTextUtils.shortenText(text, options.shortenLength); + text = CoreText.shortenText(text, options.shortenLength); } if (options.highlight) { - text = CoreTextUtils.highlightText(text, options.highlight); + text = CoreText.highlightText(text, options.highlight); } return text; diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 1c05c1571..3b76d57f1 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -33,7 +33,7 @@ import { CoreGradesTableLeaderColumn, CoreGradesTableRow, } from '@features/grades/services/grades'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; import { CoreMenuItem, CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; @@ -111,7 +111,7 @@ export class CoreGradesHelperProvider { } content = content.replace(/<\/span>/gi, '\n'); - content = CoreTextUtils.cleanTags(content, { trim: true }); + content = CoreText.cleanTags(content, { trim: true }); name = 'gradeitem'; } else if (name === 'grade') { // Add the pass/fail class if present. @@ -119,7 +119,7 @@ export class CoreGradesHelperProvider { (column.class.includes('gradefail') ? 'text-danger' : ''); if (content.includes('action-menu')) { - content = CoreTextUtils.processHTML(content, (element) => { + content = CoreText.processHTML(content, (element) => { element.querySelector('.action-menu')?.parentElement?.remove(); }); } @@ -127,14 +127,14 @@ export class CoreGradesHelperProvider { if (content.includes('fa-check')) { row.gradeIcon = 'fas-check'; row.gradeIconAlt = Translate.instant('core.grades.pass'); - content = CoreTextUtils.cleanTags(content); + content = CoreText.cleanTags(content); } else if (content.includes('fa-times') || content.includes('fa-xmark')) { row.gradeIcon = 'fas-xmark'; row.gradeIconAlt = Translate.instant('core.grades.fail'); - content = CoreTextUtils.cleanTags(content); + content = CoreText.cleanTags(content); } } else { - content = CoreTextUtils.replaceNewLines(content, '
'); + content = CoreText.replaceNewLines(content, '
'); } if (row.itemtype !== 'category') { diff --git a/src/core/features/h5p/classes/content-validator.ts b/src/core/features/h5p/classes/content-validator.ts index df57b88d1..e7b923a94 100644 --- a/src/core/features/h5p/classes/content-validator.ts +++ b/src/core/features/h5p/classes/content-validator.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreH5P } from '@features/h5p/services/h5p'; import { Translate } from '@singletons'; @@ -165,7 +165,7 @@ export class CoreH5PContentValidator { text = this.filterXss(text, tags, stylePatterns); } else { // Filter text to plain text. - text = CoreTextUtils.escapeHTML(text, false); + text = CoreText.escapeHTML(text, false); } // Check if string is within allowed length. @@ -271,7 +271,7 @@ export class CoreH5PContentValidator { if (strict && !optional && !options[value]) { delete select[key]; } else { - select[key] = CoreTextUtils.escapeHTML(value, false); + select[key] = CoreText.escapeHTML(value, false); } } } else { @@ -283,7 +283,7 @@ export class CoreH5PContentValidator { if (strict && !optional && !options[select]) { select = ( semantics.options![0]).value || ''; } - select = CoreTextUtils.escapeHTML(select, false); + select = CoreText.escapeHTML(select, false); } return select; @@ -363,9 +363,9 @@ export class CoreH5PContentValidator { } // Make sure path and mime does not have any special chars - file.path = CoreTextUtils.escapeHTML(file.path, false); + file.path = CoreText.escapeHTML(file.path, false); if (file.mime) { - file.mime = CoreTextUtils.escapeHTML(file.mime, false); + file.mime = CoreText.escapeHTML(file.mime, false); } // Remove attributes that should not exist, they may contain JSON escape code. @@ -386,7 +386,7 @@ export class CoreH5PContentValidator { } if (file.codecs) { - file.codecs = CoreTextUtils.escapeHTML(file.codecs, false); + file.codecs = CoreText.escapeHTML(file.codecs, false); } if (typeof file.bitrate == 'string') { @@ -399,7 +399,7 @@ export class CoreH5PContentValidator { } else { this.filterParams(file.quality, ['level', 'label']); file.quality.level = Number(file.quality.level); - file.quality.label = CoreTextUtils.escapeHTML(file.quality.label, false); + file.quality.label = CoreText.escapeHTML(file.quality.label, false); } } @@ -845,10 +845,10 @@ export class CoreH5PContentValidator { filterXssBadProtocol(str: string, decode: boolean = true): string { // Get the plain text representation of the attribute value (i.e. its meaning). if (decode) { - str = CoreTextUtils.decodeHTMLEntities(str); + str = CoreText.decodeHTMLEntities(str); } - return CoreTextUtils.escapeHTML(this.stripDangerousProtocols(str), false); + return CoreText.escapeHTML(this.stripDangerousProtocols(str), false); } /** diff --git a/src/core/features/h5p/classes/core.ts b/src/core/features/h5p/classes/core.ts index 45132b185..bae63e9fc 100644 --- a/src/core/features/h5p/classes/core.ts +++ b/src/core/features/h5p/classes/core.ts @@ -15,7 +15,7 @@ import { Md5 } from 'ts-md5/dist/md5'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreH5P } from '@features/h5p/services/h5p'; import { CoreH5PFileStorage } from './file-storage'; @@ -270,7 +270,7 @@ export class CoreH5PCore { const params = { library: CoreH5PCore.libraryToString(content.library), - params: CoreTextUtils.parseJSON(content.params, false), + params: CoreText.parseJSON(content.params, false), }; if (!params.params) { diff --git a/src/core/features/h5p/classes/file-storage.ts b/src/core/features/h5p/classes/file-storage.ts index 1be91e3c1..9b949b889 100644 --- a/src/core/features/h5p/classes/file-storage.ts +++ b/src/core/features/h5p/classes/file-storage.ts @@ -16,7 +16,7 @@ import { CoreFile } from '@services/file'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CorePath } from '@singletons/path'; import { @@ -118,7 +118,7 @@ export class CoreH5PFileStorage { const assetPathFolder = CoreFile.getFileAndDirectoryFromPath(assetPath).directory; fileContent = fileContent.replace( - new RegExp(CoreTextUtils.escapeForRegex(match), 'g'), + new RegExp(CoreText.escapeForRegex(match), 'g'), 'url("' + CorePath.changeRelativePath(assetPathFolder, url, newFolder) + '")', ); }); diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts index f3d70b2bc..e2b3c6afb 100644 --- a/src/core/features/h5p/classes/framework.ts +++ b/src/core/features/h5p/classes/framework.ts @@ -13,7 +13,7 @@ // limitations under the License. import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreH5P } from '@features/h5p/services/h5p'; import { CoreH5PCore, @@ -559,7 +559,7 @@ export class CoreH5PFramework { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const params = CoreTextUtils.parseJSON(contentData.jsoncontent); + const params = CoreText.parseJSON(contentData.jsoncontent); if (!params.metadata) { params.metadata = {}; } @@ -695,7 +695,7 @@ export class CoreH5PFramework { */ parseLibAddonData(library: LibraryAddonDBData): CoreH5PLibraryAddonData { const parsedLib = library; - parsedLib.addTo = CoreTextUtils.parseJSON(library.addTo, null); + parsedLib.addTo = CoreText.parseJSON(library.addTo, null); return parsedLib; } @@ -708,9 +708,9 @@ export class CoreH5PFramework { */ protected parseLibDBData(library: CoreH5PLibraryDBRecord): CoreH5PLibraryParsedDBRecord { return Object.assign(library, { - semantics: library.semantics ? CoreTextUtils.parseJSON(library.semantics, null) : null, - addto: library.addto ? CoreTextUtils.parseJSON(library.addto, null) : null, - metadatasettings: library.metadatasettings ? CoreTextUtils.parseJSON(library.metadatasettings, null) : null, + semantics: library.semantics ? CoreText.parseJSON(library.semantics, null) : null, + addto: library.addto ? CoreText.parseJSON(library.addto, null) : null, + metadatasettings: library.metadatasettings ? CoreText.parseJSON(library.metadatasettings, null) : null, }); } @@ -922,7 +922,7 @@ export class CoreH5PFramework { // Add title to 'params' to be able to add it to metadata later. if (typeof content.title === 'string') { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const params = CoreTextUtils.parseJSON(content.params || '{}'); + const params = CoreText.parseJSON(content.params || '{}'); params.title = content.title; content.params = JSON.stringify(params); } diff --git a/src/core/features/login/pages/email-signup/email-signup.ts b/src/core/features/login/pages/email-signup/email-signup.ts index 6202eff90..35d1b1433 100644 --- a/src/core/features/login/pages/email-signup/email-signup.ts +++ b/src/core/features/login/pages/email-signup/email-signup.ts @@ -16,7 +16,7 @@ import { Component, ViewChild, ElementRef, OnInit, ChangeDetectorRef } from '@an import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreCountry, CoreUtils } from '@services/utils/utils'; import { CoreWS, CoreWSExternalWarning } from '@services/ws'; import { Translate } from '@singletons'; @@ -312,10 +312,10 @@ export class CoreLoginEmailSignupPage implements OnInit { const params: SignupUserWSParams = { username: this.signupForm.value.username.trim().toLowerCase(), password: this.signupForm.value.password, - firstname: CoreTextUtils.cleanTags(this.signupForm.value.firstname), - lastname: CoreTextUtils.cleanTags(this.signupForm.value.lastname), + firstname: CoreText.cleanTags(this.signupForm.value.firstname), + lastname: CoreText.cleanTags(this.signupForm.value.lastname), email: this.signupForm.value.email.trim(), - city: CoreTextUtils.cleanTags(this.signupForm.value.city), + city: CoreText.cleanTags(this.signupForm.value.city), country: this.signupForm.value.country, }; @@ -380,7 +380,7 @@ export class CoreLoginEmailSignupPage implements OnInit { * @returns Escaped mail. */ escapeMail(text: string): string { - return CoreTextUtils.escapeForRegex(text); + return CoreText.escapeForRegex(text); } /** diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 92ce70fa3..586c8761d 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -31,7 +31,7 @@ import { Translate } from '@singletons'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreNavigator } from '@services/navigator'; import { CoreCustomURLSchemes, CoreCustomURLSchemesHandleError } from '@services/urlschemes'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreForms } from '@singletons/form'; import { AlertButton } from '@ionic/core'; import { CoreSiteError, CoreSiteErrorDebug } from '@classes/errors/siteerror'; @@ -620,7 +620,7 @@ export class CoreLoginSitePage implements OnInit { } // Now display the error. - error.error = CoreTextUtils.addTextToError( + error.error = CoreErrorHelper.addTextToError( error.error, '

' + Translate.instant('core.login.youcanstillconnectwithcredentials'), ); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index d3d9b416b..f1d37e681 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -22,7 +22,7 @@ import { CoreEvents, CoreEventSessionExpiredData, CoreEventSiteData } from '@sin import { CoreSites, CoreLoginSiteInfo, CoreSiteBasicInfo } from '@services/sites'; import { CoreWS, CoreWSExternalWarning } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -60,6 +60,7 @@ import { LazyRoutesModule } from '@/app/app-routing.module'; import { CoreSiteError, CoreSiteErrorDebug } from '@classes/errors/siteerror'; import { CoreQRScan } from '@services/qrscan'; import { CoreLoadings } from '@services/loadings'; +import { CoreErrorHelper } from '@services/error-helper'; /** * Helper provider that provides some common features regarding authentication. @@ -235,7 +236,7 @@ export class CoreLoginHelperProvider { * * @param config Site public config. * @returns Disabled features. - * @deprecated since 4.4. No longer needed. + * @deprecated since 4.4. Shoudn't be used since disabled features are not treated by this function anymore. */ getDisabledFeatures(config?: CoreSitePublicConfigResponse): string { const disabledFeatures = config?.tool_mobile_disabledfeatures; @@ -243,7 +244,7 @@ export class CoreLoginHelperProvider { return ''; } - return CoreTextUtils.treatDisabledFeatures(disabledFeatures); + return disabledFeatures; } /** @@ -1161,16 +1162,16 @@ export class CoreLoginHelperProvider { switch (errorCode) { case 'forcepasswordchangenotice': - this.openChangePassword(siteUrl, CoreTextUtils.getErrorMessageFromError(error) ?? ''); + this.openChangePassword(siteUrl, CoreErrorHelper.getErrorMessageFromError(error) ?? ''); break; case 'usernotconfirmed': this.showNotConfirmedModal(siteUrl, undefined, username, password); break; case 'connecttomoodleapp': - this.showMoodleAppNoticeModal(CoreTextUtils.getErrorMessageFromError(error) ?? ''); + this.showMoodleAppNoticeModal(CoreErrorHelper.getErrorMessageFromError(error) ?? ''); break; case 'connecttoworkplaceapp': - this.showWorkplaceNoticeModal(CoreTextUtils.getErrorMessageFromError(error) ?? ''); + this.showWorkplaceNoticeModal(CoreErrorHelper.getErrorMessageFromError(error) ?? ''); break; case 'invalidlogin': this.showInvalidLoginModal(error); @@ -1193,7 +1194,7 @@ export class CoreLoginHelperProvider { const serializedData = await CoreConfig.get(CoreConstants.LOGIN_LAUNCH_DATA); - const data = CoreTextUtils.parseJSON(serializedData, null); + const data = CoreText.parseJSON(serializedData, null); if (data === null) { throw new CoreError('No launch data stored.'); } @@ -1571,7 +1572,7 @@ export class CoreLoginHelperProvider { protected async getPasswordResets(): Promise> { const passwordResetsJson = await CoreConfig.get(CoreLoginHelperProvider.PASSWORD_RESETS_CONFIG_KEY, '{}'); - return CoreTextUtils.parseJSON>(passwordResetsJson, {}); + return CoreText.parseJSON>(passwordResetsJson, {}); } } diff --git a/src/core/features/mainmenu/services/mainmenu.ts b/src/core/features/mainmenu/services/mainmenu.ts index d48451d56..58ee9df22 100644 --- a/src/core/features/mainmenu/services/mainmenu.ts +++ b/src/core/features/mainmenu/services/mainmenu.ts @@ -19,7 +19,7 @@ import { CoreSites } from '@services/sites'; import { CoreConstants } from '@/core/constants'; import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from './mainmenu-delegate'; import { Device, makeSingleton } from '@singletons'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreScreen } from '@services/screen'; import { CorePlatform } from '@services/platform'; @@ -211,7 +211,7 @@ export class CoreMainMenuProvider { .filter(item => typeof item.label === 'string' || currentLang in item.label || fallbackLang in item.label) .map(item => ({ ...item, - url: CoreTextUtils.replaceArguments(item.url, replacements, 'uri'), + url: CoreText.replaceArguments(item.url, replacements, 'uri'), label: typeof item.label === 'string' ? item.label : item.label[currentLang] ?? item.label[fallbackLang], diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index e27c7746a..f8bb9cfb3 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -21,7 +21,7 @@ import { CoreSites } from '@services/sites'; import { CorePushNotificationsDelegate } from './push-delegate'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreUtils } from '@services/utils/utils'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreConfig } from '@services/config'; import { CoreConstants } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; @@ -461,7 +461,7 @@ export class CorePushNotificationsProvider { title: notification.title, message: notification.message, customdata: typeof rawData.customdata == 'string' ? - CoreTextUtils.parseJSON>(rawData.customdata, {}) : rawData.customdata, + CoreText.parseJSON>(rawData.customdata, {}) : rawData.customdata, }); let site: CoreSite | undefined; @@ -862,7 +862,7 @@ export class CorePushNotificationsProvider { result.siteid, result.siteurl, result.token, - { info: CoreTextUtils.parseJSON(result.info, null) || undefined }, + { info: CoreText.parseJSON(result.info, null) || undefined }, ); await this.unregisterDeviceOnMoodle(tmpSite); diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index 78fc86068..f54cd05dd 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -17,7 +17,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; import { CoreWSFile } from '@services/ws'; import { CoreIonicColorNames } from '@singletons/colors'; @@ -319,10 +319,10 @@ export class CoreQuestionBaseComponent('input[type="hidden"][name*=answerformat]'); - let content = CoreTextUtils.decodeHTML(textarea.innerHTML || ''); + let content = CoreText.decodeHTML(textarea.innerHTML || ''); if (question.hasDraftFiles && question.responsefileareas) { - content = CoreTextUtils.replaceDraftfileUrls( + content = CoreFileHelper.replaceDraftfileUrls( CoreSites.getRequiredCurrentSite().getURL(), content, CoreQuestionHelper.getResponseFileAreaFiles(question, 'answer'), @@ -590,7 +590,7 @@ export class CoreQuestionBaseComponent${Translate.instant('addon.mod_page.errorwhileloadingthepage')}

diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index 25bc2400a..de0febbf4 100644 --- a/src/core/features/settings/services/settings-helper.ts +++ b/src/core/features/settings/services/settings-helper.ts @@ -29,7 +29,7 @@ import { CoreCourse } from '@features/course/services/course'; import { makeSingleton, Translate } from '@singletons'; import { CoreError } from '@classes/errors/error'; import { Observable, Subject } from 'rxjs'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreNavigator } from '@services/navigator'; /** @@ -263,7 +263,7 @@ export class CoreSettingsHelperProvider { try { await syncPromise; } catch (error) { - throw CoreTextUtils.addTitleToError(error, Translate.instant('core.settings.sitesyncfailed')); + throw CoreErrorHelper.addTitleToError(error, Translate.instant('core.settings.sitesyncfailed')); } finally { delete this.syncPromises[siteId]; } diff --git a/src/core/features/sharedfiles/services/sharedfiles-helper.ts b/src/core/features/sharedfiles/services/sharedfiles-helper.ts index 0c8613bcb..325bfd63a 100644 --- a/src/core/features/sharedfiles/services/sharedfiles-helper.ts +++ b/src/core/features/sharedfiles/services/sharedfiles-helper.ts @@ -106,9 +106,9 @@ export class CoreSharedFilesHelperProvider { const result = await alert.onDidDismiss(); - if (result.role == 'rename') { + if (result.role === 'rename') { return newName; - } else if (result.role == 'replace') { + } else if (result.role === 'replace') { return originalName; } else { // Canceled. diff --git a/src/core/features/siteplugins/classes/call-ws-click-directive.ts b/src/core/features/siteplugins/classes/call-ws-click-directive.ts index 6525b4366..bb656d9c3 100644 --- a/src/core/features/siteplugins/classes/call-ws-click-directive.ts +++ b/src/core/features/siteplugins/classes/call-ws-click-directive.ts @@ -15,7 +15,7 @@ import { Input, OnInit, ElementRef, Directive } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { Translate } from '@singletons'; import { CoreSitePluginsPluginContentComponent } from '../components/plugin-content/plugin-content'; import { CoreSitePluginsCallWSBaseDirective } from './call-ws-directive'; @@ -77,7 +77,7 @@ export class CoreSitePluginsCallWSOnClickBaseDirective extends CoreSitePluginsCa CoreDomUtils.showErrorModalDefault( error, Translate.instant('core.serverconnection', { - details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error', + details: CoreErrorHelper.getErrorMessageFromError(error) ?? 'Unknown error', }), ); } diff --git a/src/core/features/siteplugins/services/siteplugins-init.ts b/src/core/features/siteplugins/services/siteplugins-init.ts index e85dd2902..9df028152 100644 --- a/src/core/features/siteplugins/services/siteplugins-init.ts +++ b/src/core/features/siteplugins/services/siteplugins-init.ts @@ -37,7 +37,7 @@ import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profi import { CoreFilepool } from '@services/filepool'; import { CoreLang } from '@services/lang'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWS } from '@services/ws'; import { CoreEvents } from '@singletons/events'; @@ -344,7 +344,7 @@ export class CoreSitePluginsInitService { this.logger.debug('Load site plugin:', plugin); if (!plugin.parsedHandlers && plugin.handlers) { - plugin.parsedHandlers = CoreTextUtils.parseJSON( + plugin.parsedHandlers = CoreText.parseJSON( plugin.handlers, null, error => this.logger.error('Error parsing site plugin handlers', error), @@ -352,7 +352,7 @@ export class CoreSitePluginsInitService { } if (!plugin.parsedLang && plugin.lang) { - plugin.parsedLang = CoreTextUtils.parseJSON( + plugin.parsedLang = CoreText.parseJSON( plugin.lang, null, error => this.logger.error('Error parsing site plugin lang', error), diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 30deff990..0a6fb8e4b 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -22,7 +22,7 @@ import { CoreApp } from '@services/app'; import { CoreFilepool } from '@services/filepool'; import { CoreLang, CoreLangFormat } from '@services/lang'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; @@ -238,7 +238,7 @@ export class CoreSitePluginsProvider { const value = otherData[name]; if (typeof value == 'string' && (value[0] == '{' || value[0] == '[')) { - otherData[name] = CoreTextUtils.parseJSON(value); + otherData[name] = CoreText.parseJSON(value); } } } @@ -466,7 +466,7 @@ export class CoreSitePluginsProvider { // Site plugin not disabled. Check if it has handlers. if (!plugin.parsedHandlers) { - plugin.parsedHandlers = CoreTextUtils.parseJSON( + plugin.parsedHandlers = CoreText.parseJSON( plugin.handlers, null, error => this.logger.error('Error parsing site plugin handlers', error), diff --git a/src/core/features/tag/pages/search/search.ts b/src/core/features/tag/pages/search/search.ts index eeeed8eef..6cc3147de 100644 --- a/src/core/features/tag/pages/search/search.ts +++ b/src/core/features/tag/pages/search/search.ts @@ -16,7 +16,7 @@ import { Component, OnInit } from '@angular/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreUrl } from '@singletons/url'; import { CoreTagCloud, CoreTagCollection, CoreTagCloudTag, CoreTag } from '@features/tag/services/tag'; import { Translate } from '@singletons'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; @@ -120,7 +120,7 @@ export class CoreTagSearchPage implements OnInit { * Go to tag index page. */ openTag(tag: CoreTagCloudTag): void { - const url = CoreTextUtils.decodeURI(tag.viewurl); + const url = CoreUrl.decodeURI(tag.viewurl); CoreContentLinksHelper.handleLink(url); } diff --git a/src/core/features/user/services/user-sync.ts b/src/core/features/user/services/user-sync.ts index 31f6d0d9f..f67422292 100644 --- a/src/core/features/user/services/user-sync.ts +++ b/src/core/features/user/services/user-sync.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreErrorHelper } from '@services/error-helper'; import { CoreUtils } from '@services/utils/utils'; import { CoreSyncBaseProvider } from '@classes/base-sync'; import { makeSingleton } from '@singletons'; @@ -91,7 +91,7 @@ export class CoreUserSyncProvider extends CoreSyncBaseProvider { await CoreUser.setUserPreference(preference.name, preference.value, siteId); } catch (error) { if (CoreUtils.isWebServiceError(error)) { - const warning = CoreTextUtils.getErrorMessageFromError(error); + const warning = CoreErrorHelper.getErrorMessageFromError(error); if (warning) { warnings.push(warning); } diff --git a/src/core/pipes/bytes-to-size.ts b/src/core/pipes/bytes-to-size.ts index 19b24b9d7..2ef3e2bf5 100644 --- a/src/core/pipes/bytes-to-size.ts +++ b/src/core/pipes/bytes-to-size.ts @@ -15,7 +15,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { CoreLogger } from '@singletons/logger'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; /** * Pipe to turn a number in bytes to a human readable size (e.g. 5,25 MB). @@ -49,7 +49,7 @@ export class CoreBytesToSizePipe implements PipeTransform { value = numberValue; } - return CoreTextUtils.bytesToSize(value); + return CoreText.bytesToSize(value); } } diff --git a/src/core/pipes/seconds-to-hms.ts b/src/core/pipes/seconds-to-hms.ts index 9afc7c7ac..c4c284491 100644 --- a/src/core/pipes/seconds-to-hms.ts +++ b/src/core/pipes/seconds-to-hms.ts @@ -14,7 +14,7 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreLogger } from '@singletons/logger'; import { CoreConstants } from '@/core/constants'; @@ -63,8 +63,8 @@ export class CoreSecondsToHMSPipe implements PipeTransform { seconds -= minutes * CoreConstants.SECONDS_MINUTE; return showHours - ? CoreTextUtils.twoDigits(hours) + ':' + CoreTextUtils.twoDigits(minutes) + ':' + CoreTextUtils.twoDigits(seconds) - : CoreTextUtils.twoDigits(minutes) + ':' + CoreTextUtils.twoDigits(seconds); + ? CoreText.twoDigits(hours) + ':' + CoreText.twoDigits(minutes) + ':' + CoreText.twoDigits(seconds) + : CoreText.twoDigits(minutes) + ':' + CoreText.twoDigits(seconds); } } diff --git a/src/core/services/analytics.ts b/src/core/services/analytics.ts index 91b6520e5..09be8156a 100644 --- a/src/core/services/analytics.ts +++ b/src/core/services/analytics.ts @@ -20,7 +20,7 @@ import { CoreEvents } from '@singletons/events'; import { CoreSites } from './sites'; import { CoreConfig, CoreConfigProvider } from './config'; import { CoreConstants } from '../constants'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl } from '@singletons/url'; /** @@ -103,7 +103,7 @@ export class CoreAnalyticsService extends CoreDelegate { }; if (treatedEvent.type === CoreAnalyticsEventType.VIEW_ITEM || treatedEvent.type === CoreAnalyticsEventType.VIEW_ITEM_LIST) { - treatedEvent.name = CoreTextUtils.cleanTags(treatedEvent.name); + treatedEvent.name = CoreText.cleanTags(treatedEvent.name); } if ('url' in treatedEvent && treatedEvent.url) { diff --git a/src/core/services/error-helper.ts b/src/core/services/error-helper.ts new file mode 100644 index 000000000..daadb2382 --- /dev/null +++ b/src/core/services/error-helper.ts @@ -0,0 +1,176 @@ +// (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 { CoreAnyError, CoreError } from '@classes/errors/error'; +import { makeSingleton, Translate } from '@singletons'; +import { AlertButton } from '@ionic/angular'; + +/** + * Provider to provide some helper functions regarding files and packages. + */ +@Injectable({ providedIn: 'root' }) +export class CoreErrorHelperService { + + /** + * Add some text to an error message. + * + * @param error Error message or object. + * @param text Text to add. + * @returns Modified error. + */ + addTextToError(error: string | CoreError | CoreErrorObject | undefined | null, text: string): string | CoreErrorObject { + if (typeof error === 'string') { + return error + text; + } + + if (error instanceof CoreError) { + error.message += text; + + return error; + } + + if (!error) { + return text; + } + + if (typeof error.message === 'string') { + error.message += text; + } else if (typeof error.error === 'string') { + error.error += text; + } else if (typeof error.content === 'string') { + error.content += text; + } else if (typeof error.body === 'string') { + error.body += text; + } + + return error; + } + + /** + * Add some title to an error message. + * + * @param error Error message or object. + * @param title Title to add. + * @returns Modified error. + */ + addTitleToError(error: string | CoreError | CoreErrorObject | undefined | null, title: string): CoreErrorObject { + let improvedError: CoreErrorObject = {}; + + if (typeof error === 'string') { + improvedError.message = error; + } else if (error && 'message' in error) { + improvedError = error; + } + + improvedError.title = improvedError.title || title; + + return improvedError; + } + + /** + * Build a message with several paragraphs. + * + * @param paragraphs List of paragraphs. + * @returns Built message. + */ + buildSeveralParagraphsMessage(paragraphs: (string | CoreErrorObject)[]): string { + // Filter invalid messages, and convert them to messages in case they're errors. + const messages: string[] = []; + + paragraphs.forEach(paragraph => { + // If it's an error, get its message. + const message = this.getErrorMessageFromError(paragraph); + + if (paragraph && message) { + messages.push(message); + } + }); + + if (messages.length < 2) { + return messages[0] || ''; + } + + let builtMessage = messages[0]; + + for (let i = 1; i < messages.length; i++) { + builtMessage = Translate.instant('core.twoparagraphs', { p1: builtMessage, p2: messages[i] }); + } + + return builtMessage; + } + + /** + * Get the error message from an error object. + * + * @param error Error. + * @returns Error message, undefined if not found. + */ + getErrorMessageFromError(error?: CoreAnyError): string | undefined { + if (typeof error === 'string') { + return error; + } + + if (error instanceof CoreError) { + return error.message; + } + + if (!error) { + return undefined; + } + + if (error.message || error.error || error.content) { + return error.message || error.error || error.content; + } + + if (error.body) { + return this.getErrorMessageFromHTML(error.body); + } + + return undefined; + } + + /** + * Get the error message from an HTML error page. + * + * @param body HTML content. + * @returns Error message or empty string if not found. + */ + getErrorMessageFromHTML(body: string): string { + // THe parser does not throw errors and scripts are not executed. + const parser = new DOMParser(); + const doc = parser.parseFromString(body, 'text/html'); + + // Errors are rendered using the "errorbox" and "errormessage" classes since Moodle 2.0. + const element = doc.body.querySelector('.errorbox .errormessage'); + + return element?.innerText.trim() ?? ''; + } + +} +export const CoreErrorHelper = makeSingleton(CoreErrorHelperService); + +/** + * Different type of errors the app can treat. + */ +export type CoreErrorObject = { + message?: string; + error?: string; + content?: string; + body?: string; + debuginfo?: string; + backtrace?: string; + title?: string; + buttons?: AlertButton[]; +}; diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 91163f2ad..7089e27fe 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -31,6 +31,8 @@ import { CoreConfig } from './config'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CorePlatform } from './platform'; +import { CorePath } from '@singletons/path'; +import { CoreText } from '@singletons/text'; /** * Provider to provide some helper functions regarding files and packages. @@ -496,6 +498,159 @@ export class CoreFileHelperProvider { return path.split('\\').pop()?.split('/').pop(); } + /** + * Get the pluginfile URL to replace @@PLUGINFILE@@ wildcards. + * + * @param files Files to extract the URL from. They need to have the URL in a 'url' or 'fileurl' attribute. + * @returns Pluginfile URL, undefined if no files found. + */ + getTextPluginfileUrl(files: CoreWSFile[]): string | undefined { + if (files?.length) { + const url = this.getFileUrl(files[0]); + + // Remove text after last slash (encoded or not). + return url?.substring(0, Math.max(url.lastIndexOf('/'), url.lastIndexOf('%2F'))); + } + + return; + } + + /** + * Replace draftfile URLs with the equivalent pluginfile URL. + * + * @param siteUrl URL of the site. + * @param text Text to treat, including draftfile URLs. + * @param files List of files of the area, using pluginfile URLs. + * @returns Treated text and map with the replacements. + */ + replaceDraftfileUrls( + siteUrl: string, + text: string, + files: CoreWSFile[], + ): { text: string; replaceMap?: {[url: string]: string} } { + + if (!text || !files || !files.length) { + return { text }; + } + + const draftfileUrl = CorePath.concatenatePaths(siteUrl, 'draftfile.php'); + const matches = text.match(new RegExp(CoreText.escapeForRegex(draftfileUrl) + '[^\'" ]+', 'ig')); + + if (!matches || !matches.length) { + return { text }; + } + + // Index the pluginfile URLs by file name. + const pluginfileMap: {[name: string]: string} = {}; + files.forEach((file) => { + if (!file.filename) { + return; + } + pluginfileMap[file.filename] = CoreFileHelper.getFileUrl(file); + }); + + // Replace each draftfile with the corresponding pluginfile URL. + const replaceMap: {[url: string]: string} = {}; + matches.forEach((url) => { + if (replaceMap[url]) { + // URL already treated, same file embedded more than once. + return; + } + + // Get the filename from the URL. + let filename = url.substring(url.lastIndexOf('/') + 1); + if (filename.indexOf('?') != -1) { + filename = filename.substring(0, filename.indexOf('?')); + } + + if (pluginfileMap[filename]) { + replaceMap[url] = pluginfileMap[filename]; + text = text.replace(new RegExp(CoreText.escapeForRegex(url), 'g'), pluginfileMap[filename]); + } + }); + + return { + text, + replaceMap, + }; + } + + /** + * Replace @@PLUGINFILE@@ wildcards with the real URL in a text. + * + * @param text to treat. + * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. + * @returns Treated text. + */ + replacePluginfileUrls(text: string, files: CoreWSFile[]): string { + if (text && typeof text === 'string') { + const fileURL = this.getTextPluginfileUrl(files); + if (fileURL) { + return text.replace(/@@PLUGINFILE@@/g, fileURL); + } + } + + return text; + } + + /** + * Restore original draftfile URLs. + * + * @param siteUrl Site URL. + * @param treatedText Treated text with replacements. + * @param originalText Original text. + * @param files List of files to search and replace. + * @returns Treated text. + */ + restoreDraftfileUrls(siteUrl: string, treatedText: string, originalText: string, files: CoreWSFile[]): string { + if (!treatedText || !files || !files.length) { + return treatedText; + } + + const draftfileUrl = CorePath.concatenatePaths(siteUrl, 'draftfile.php'); + const draftfileUrlRegexPrefix = CoreText.escapeForRegex(draftfileUrl) + '/[^/]+/[^/]+/[^/]+/[^/]+/'; + + files.forEach((file) => { + if (!file.filename) { + return; + } + + // Search the draftfile URL in the original text. + const matches = originalText.match( + new RegExp(draftfileUrlRegexPrefix + CoreText.escapeForRegex(file.filename) + '[^\'" ]*', 'i'), + ); + + if (!matches || !matches[0]) { + return; // Original URL not found, skip. + } + + treatedText = treatedText.replace( + new RegExp(CoreText.escapeForRegex(CoreFileHelper.getFileUrl(file)), 'g'), + matches[0], + ); + }); + + return treatedText; + } + + /** + * Replace pluginfile URLs with @@PLUGINFILE@@ wildcards. + * + * @param text Text to treat. + * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. + * @returns Treated text. + */ + restorePluginfileUrls(text: string, files: CoreWSFile[]): string { + if (text && typeof text == 'string') { + const fileURL = this.getTextPluginfileUrl(files); + if (fileURL) { + return text.replace(new RegExp(CoreText.escapeForRegex(fileURL), 'g'), '@@PLUGINFILE@@'); + } + } + + return text; + } + } export const CoreFileHelper = makeSingleton(CoreFileHelperProvider); diff --git a/src/core/services/file.ts b/src/core/services/file.ts index 57be3a3e3..07c11679e 100644 --- a/src/core/services/file.ts +++ b/src/core/services/file.ts @@ -17,7 +17,6 @@ import { Injectable } from '@angular/core'; import { FileEntry, DirectoryEntry, Entry, Metadata, IFile } from '@awesome-cordova-plugins/file/ngx'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; @@ -29,6 +28,7 @@ import { CoreText } from '@singletons/text'; import { CorePlatform } from '@services/platform'; import { CorePath } from '@singletons/path'; import { Zip } from '@features/native/plugins'; +import { CoreUrl } from '@singletons/url'; /** * Progress event used when writing a file data into a file. @@ -439,7 +439,7 @@ export class CoreFileProvider { * @returns The file name normalized. */ normalizeFileName(filename: string): string { - filename = CoreTextUtils.decodeURIComponent(filename); + filename = CoreUrl.decodeURIComponent(filename); return filename; } @@ -481,7 +481,7 @@ export class CoreFileProvider { return File.readAsArrayBuffer(folder, path); case CoreFileFormat.FORMATJSON: return File.readAsText(folder, path).then((text) => { - const parsed = CoreTextUtils.parseJSON(text, null); + const parsed = CoreText.parseJSON(text, null); if (parsed == null && text != null) { throw new CoreError('Error parsing JSON file: ' + path); @@ -512,7 +512,7 @@ export class CoreFileProvider { if (event.target?.result !== undefined && event.target.result !== null) { if (format == CoreFileFormat.FORMATJSON) { // Convert to object. - const parsed = CoreTextUtils.parseJSON( event.target.result, null); + const parsed = CoreText.parseJSON( event.target.result, null); if (parsed == null) { reject('Error parsing JSON file.'); @@ -1081,8 +1081,8 @@ export class CoreFileProvider { let extension = CoreMimetypeUtils.getFileExtension(fileName) || defaultExt; // Clean the file name. - fileNameWithoutExtension = CoreTextUtils.removeSpecialCharactersForFiles( - CoreTextUtils.decodeURIComponent(fileNameWithoutExtension), + fileNameWithoutExtension = CoreText.removeSpecialCharactersForFiles( + CoreUrl.decodeURIComponent(fileNameWithoutExtension), ); // Index the files by name. @@ -1100,7 +1100,7 @@ export class CoreFileProvider { return this.calculateUniqueName(files, fileNameWithoutExtension + extension); } catch (error) { // Folder doesn't exist, name is unique. Clean it and return it. - return CoreTextUtils.removeSpecialCharactersForFiles(CoreTextUtils.decodeURIComponent(fileName)); + return CoreText.removeSpecialCharactersForFiles(CoreUrl.decodeURIComponent(fileName)); } } diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 7ec4147ca..863c1523a 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -24,7 +24,7 @@ import { CoreSites } from '@services/sites'; import { CoreWS, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils, CoreUtilsOpenFileOptions } from '@services/utils/utils'; @@ -1339,7 +1339,7 @@ export class CoreFilepoolProvider { url = this.removeRevisionFromUrl(url); // Decode URL. - url = CoreTextUtils.decodeHTML(CoreTextUtils.decodeURIComponent(url)); + url = CoreText.decodeHTML(CoreUrl.decodeURIComponent(url)); if (url.indexOf('/webservice/pluginfile') !== -1) { // Remove attributes that do not matter. @@ -1376,7 +1376,7 @@ export class CoreFilepoolProvider { url = url.replace(/\/tokenpluginfile\.php\/[^/]+\//, '/webservice/pluginfile.php/'); // Decode URL. - url = CoreTextUtils.decodeHTML(CoreTextUtils.decodeURIComponent(url)); + url = CoreText.decodeHTML(CoreUrl.decodeURIComponent(url)); if (url.indexOf('/webservice/pluginfile') !== -1) { // Remove attributes that do not matter. @@ -2205,7 +2205,7 @@ export class CoreFilepoolProvider { filename += '_' + hashes.join('_'); } - return CoreTextUtils.removeSpecialCharactersForFiles(filename); + return CoreText.removeSpecialCharactersForFiles(filename); } /** @@ -2299,7 +2299,7 @@ export class CoreFilepoolProvider { return { ...entry, - linksUnserialized: CoreTextUtils.parseJSON(entry.links, []), + linksUnserialized: CoreText.parseJSON(entry.links, []), }; } @@ -2653,7 +2653,7 @@ export class CoreFilepoolProvider { return this.processQueueItem({ ...item, - linksUnserialized: CoreTextUtils.parseJSON(item.links, []), + linksUnserialized: CoreText.parseJSON(item.links, []), }); } catch (err) { throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; @@ -3095,7 +3095,7 @@ export class CoreFilepoolProvider { fileUrl = CoreFile.convertFileSrc(fileUrl); if (fileUrl !== url) { - cssCode = cssCode.replace(new RegExp(CoreTextUtils.escapeForRegex(url), 'g'), fileUrl); + cssCode = cssCode.replace(new RegExp(CoreText.escapeForRegex(url), 'g'), fileUrl); updated = true; } } catch (error) { @@ -3103,7 +3103,7 @@ export class CoreFilepoolProvider { // If the URL is relative, store the absolute URL. if (absoluteUrl !== url) { - cssCode = cssCode.replace(new RegExp(CoreTextUtils.escapeForRegex(url), 'g'), absoluteUrl); + cssCode = cssCode.replace(new RegExp(CoreText.escapeForRegex(url), 'g'), absoluteUrl); updated = true; } } diff --git a/src/core/services/local-notifications.ts b/src/core/services/local-notifications.ts index f297b21ce..77608781e 100644 --- a/src/core/services/local-notifications.ts +++ b/src/core/services/local-notifications.ts @@ -19,7 +19,7 @@ import { ILocalNotification } from '@awesome-cordova-plugins/local-notifications import { CoreApp } from '@services/app'; import { CoreConfig } from '@services/config'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreError } from '@classes/errors/error'; import { CoreConstants } from '@/core/constants'; @@ -522,7 +522,7 @@ export class CoreLocalNotificationsProvider { if (!data) { return {}; } else if (typeof data == 'string') { - return CoreTextUtils.parseJSON(data, {}); + return CoreText.parseJSON(data, {}); } else { return data; } diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 407121ce9..caf6e3642 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -23,7 +23,7 @@ import { CoreObject } from '@singletons/object'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { makeSingleton, NavController, Router } from '@singletons'; import { CoreScreen } from './screen'; import { CoreError } from '@classes/errors/error'; @@ -85,7 +85,7 @@ export class CoreNavigatorService { * @returns Whether the active route is using the given path. */ isCurrent(path: string): boolean { - return CoreTextUtils.matchesGlob(this.getCurrentPath(), path); + return CoreText.matchesGlob(this.getCurrentPath(), path); } /** @@ -325,7 +325,7 @@ export class CoreNavigatorService { // Try to retrieve the param from local storage in browser. const storageParam = localStorage.getItem(value); if (storageParam) { - storedParam = CoreTextUtils.parseJSON(storageParam); + storedParam = CoreText.parseJSON(storageParam); } } @@ -763,7 +763,7 @@ export class CoreNavigatorService { */ getRelativePathToParent(parentPath: string): string { // Add an ending slash to avoid collisions with other routes (e.g. /foo and /foobar). - parentPath = CoreTextUtils.addEndingSlash(parentPath); + parentPath = CoreText.addEndingSlash(parentPath); const path = this.getCurrentPath(); const parentRouteIndex = path.indexOf(parentPath); diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index 8433311e9..0cbb0b27d 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -20,7 +20,6 @@ import { CoreApp, CoreStoreConfig } from '@services/app'; import { CoreEvents } from '@singletons/events'; import { CoreWS } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; @@ -67,6 +66,7 @@ import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; import { firstValueFrom } from 'rxjs'; import { CoreHTMLClasses } from '@singletons/html-classes'; import { CoreSiteErrorDebug } from '@classes/errors/siteerror'; +import { CoreErrorHelper } from './error-helper'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; @@ -315,9 +315,9 @@ export class CoreSitesProvider { } // Site doesn't exist. Return the error message. - if (CoreTextUtils.getErrorMessageFromError(error)) { + if (CoreErrorHelper.getErrorMessageFromError(error)) { throw error; - } else if (CoreTextUtils.getErrorMessageFromError(secondError)) { + } else if (CoreErrorHelper.getErrorMessageFromError(secondError)) { throw secondError; } else { throw new CoreError(Translate.instant('core.sitenotfoundhelp')); @@ -361,7 +361,7 @@ export class CoreSitesProvider { } // App didn't receive a WS response, probably cannot connect. Prioritize first error if it's valid. - if (CoreTextUtils.getErrorMessageFromError(error)) { + if (CoreErrorHelper.getErrorMessageFromError(error)) { throw error; } else { throw secondError; @@ -1256,8 +1256,8 @@ export class CoreSitesProvider { * @returns Site. */ makeSiteFromSiteListEntry(entry: SiteDBEntry): CoreSite { - const info = entry.info ? CoreTextUtils.parseJSON(entry.info) : undefined; - const config = entry.config ? CoreTextUtils.parseJSON(entry.config) : undefined; + const info = entry.info ? CoreText.parseJSON(entry.info) : undefined; + const config = entry.config ? CoreText.parseJSON(entry.config) : undefined; const site = CoreSitesFactory.makeSite( entry.id, @@ -1339,7 +1339,7 @@ export class CoreSitesProvider { await Promise.all(sites.map(async (site) => { if (!ids || ids.indexOf(site.id) > -1) { - const siteInfo = site.info ? CoreTextUtils.parseJSON(site.info) : undefined; + const siteInfo = site.info ? CoreText.parseJSON(site.info) : undefined; const siteInstance = CoreSitesFactory.makeSite(site.id, site.siteUrl, site.token, { info: siteInfo }); const siteName = await siteInstance.getSiteName(); @@ -1377,8 +1377,8 @@ export class CoreSitesProvider { // Sort sites by site name, url and then fullname. sites.sort((a, b) => { // First compare by site name. - let textA = CoreTextUtils.cleanTags(a.siteName).toLowerCase().trim(); - let textB = CoreTextUtils.cleanTags(b.siteName).toLowerCase().trim(); + let textA = CoreText.cleanTags(a.siteName).toLowerCase().trim(); + let textB = CoreText.cleanTags(b.siteName).toLowerCase().trim(); let compare = textA.localeCompare(textB); if (compare !== 0) { diff --git a/src/core/services/tests/utils/text.test.ts b/src/core/services/tests/utils/text.test.ts deleted file mode 100644 index 793546903..000000000 --- a/src/core/services/tests/utils/text.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -// (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 { CoreTextUtilsProvider } from '@services/utils/text'; -import { DomSanitizer } from '@singletons'; - -import { mockSingleton } from '@/testing/utils'; -import { CorePlatform } from '@services/platform'; - -describe('CoreTextUtilsProvider', () => { - - const config = { platform: 'android' }; - let textUtils: CoreTextUtilsProvider; - - beforeEach(() => { - mockSingleton(CorePlatform, [], { isAndroid: () => config.platform === 'android' }); - mockSingleton(DomSanitizer, [], { bypassSecurityTrustUrl: url => url }); - - textUtils = new CoreTextUtilsProvider(); - }); - - it('adds ending slashes', () => { - const originalUrl = 'https://moodle.org'; - const url = textUtils.addEndingSlash(originalUrl); - - expect(url).toEqual('https://moodle.org/'); - }); - - it('doesn\'t add duplicated ending slashes', () => { - const originalUrl = 'https://moodle.org/'; - const url = textUtils.addEndingSlash(originalUrl); - - expect(url).toEqual('https://moodle.org/'); - }); - - it('builds address URL for Android platforms', () => { - // Arrange - const address = 'Moodle Spain HQ'; - - config.platform = 'android'; - - // Act - const url = textUtils.buildAddressURL(address); - - // Assert - expect(url).toEqual('geo:0,0?q=Moodle%20Spain%20HQ'); - - expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); - expect(CorePlatform.isAndroid).toHaveBeenCalled(); - }); - - it('builds address URL for non-Android platforms', () => { - // Arrange - const address = 'Moodle Spain HQ'; - - config.platform = 'ios'; - - // Act - const url = textUtils.buildAddressURL(address); - - // Assert - expect(url).toEqual('http://maps.google.com?q=Moodle%20Spain%20HQ'); - - expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); - expect(CorePlatform.isAndroid).toHaveBeenCalled(); - }); - - it('doesn\'t build address if it\'s already a URL', () => { - const address = 'https://moodle.org'; - - const url = textUtils.buildAddressURL(address); - - expect(url).toEqual(address); - - expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); - }); - - it('matches glob patterns', () => { - expect(textUtils.matchesGlob('/foo/bar', '/foo/bar')).toBe(true); - expect(textUtils.matchesGlob('/foo/bar', '/foo/bar/')).toBe(false); - expect(textUtils.matchesGlob('/foo', '/foo/*')).toBe(false); - expect(textUtils.matchesGlob('/foo/', '/foo/*')).toBe(true); - expect(textUtils.matchesGlob('/foo/bar', '/foo/*')).toBe(true); - expect(textUtils.matchesGlob('/foo/bar/', '/foo/*')).toBe(false); - expect(textUtils.matchesGlob('/foo/bar/baz', '/foo/*')).toBe(false); - expect(textUtils.matchesGlob('/foo/bar/baz', '/foo/**')).toBe(true); - expect(textUtils.matchesGlob('/foo/bar/baz/', '/foo/**')).toBe(true); - expect(textUtils.matchesGlob('/foo/bar/baz', '**/baz')).toBe(true); - expect(textUtils.matchesGlob('/foo/bar/baz', '**/bar')).toBe(false); - expect(textUtils.matchesGlob('/foo/bar/baz', '/foo/ba?/ba?')).toBe(true); - }); - - it('replaces arguments', () => { - // Arrange - const url = 'http://campus.edu?device={{device}}&version={{version}}'; - const replacements = { - device: 'iPhone or iPad', - version: '1.2.3', - }; - - // Act - const replaced = textUtils.replaceArguments(url, replacements, 'uri'); - - // Assert - expect(replaced).toEqual('http://campus.edu?device=iPhone%20or%20iPad&version=1.2.3'); - }); - - it('counts words', () => { - expect(textUtils.countWords('')).toEqual(0); - expect(textUtils.countWords('one two three four')).toEqual(4); - expect(textUtils.countWords('a\'b')).toEqual(1); - expect(textUtils.countWords('1+1=2')).toEqual(1); - expect(textUtils.countWords(' one-sided ')).toEqual(1); - expect(textUtils.countWords('one two')).toEqual(2); - expect(textUtils.countWords('email@example.com')).toEqual(1); - expect(textUtils.countWords('first\\part second/part')).toEqual(2); - expect(textUtils.countWords('

one two

three four

')).toEqual(4); - expect(textUtils.countWords('

one two
three four

')).toEqual(4); - expect(textUtils.countWords('

one two
three four

')).toEqual(4); - expect(textUtils.countWords(' one ... three ')).toEqual(3); - expect(textUtils.countWords('just...one')).toEqual(1); - expect(textUtils.countWords(' one & three ')).toEqual(3); - expect(textUtils.countWords('just&one')).toEqual(1); - expect(textUtils.countWords('em—dash')).toEqual(2); - expect(textUtils.countWords('en–dash')).toEqual(2); - expect(textUtils.countWords('1³ £2 €3.45 $6,789')).toEqual(4); - expect(textUtils.countWords('ブルース カンベッル')).toEqual(2); - expect(textUtils.countWords('

one two

three four

')).toEqual(4); - expect(textUtils.countWords('

one two


three four

')).toEqual(4); - expect(textUtils.countWords('

one

  • two
  • three

four.

')).toEqual(4); - expect(textUtils.countWords('

emphasis.

')).toEqual(1); - expect(textUtils.countWords('

emphasis.

')).toEqual(1); - expect(textUtils.countWords('

emphasis.

')).toEqual(1); - expect(textUtils.countWords('

emphasis.

')).toEqual(1); - expect(textUtils.countWords('one\ntwo')).toEqual(2); - expect(textUtils.countWords('one\rtwo')).toEqual(2); - expect(textUtils.countWords('one\ttwo')).toEqual(2); - expect(textUtils.countWords('one\vtwo')).toEqual(2); - expect(textUtils.countWords('one\ftwo')).toEqual(2); - expect(textUtils.countWords('SO42-')).toEqual(1); - expect(textUtils.countWords('4+4=8 i.e. O(1) a,b,c,d I’m black&blue_really')).toEqual(6); - expect(textUtils.countWords('ab')).toEqual(1); - }); - -}); diff --git a/src/core/services/urlschemes.ts b/src/core/services/urlschemes.ts index 530c303c7..74a83f90b 100644 --- a/src/core/services/urlschemes.ts +++ b/src/core/services/urlschemes.ts @@ -27,7 +27,7 @@ import { CoreApp } from './app'; import { CoreNavigator, CoreRedirectPayload } from './navigator'; import { CoreSiteCheckResponse, CoreSites } from './sites'; import { CoreDomUtils } from './utils/dom'; -import { CoreTextErrorObject, CoreTextUtils } from './utils/text'; +import { CoreErrorHelper, CoreErrorObject } from './error-helper'; import { CoreUrl } from '@singletons/url'; import { CoreUtils } from './utils/utils'; import { CoreLoadings } from './loadings'; @@ -102,7 +102,7 @@ export class CoreCustomURLSchemesProvider { } this.lastUrls[url] = Date.now(); - url = CoreTextUtils.decodeURIComponent(url); + url = CoreUrl.decodeURIComponent(url); // Wait for app to be ready. await ApplicationInit.donePromise; @@ -539,8 +539,8 @@ export class CoreCustomURLSchemesHandleError extends CoreError { * @param error The error message or object. * @param data Data obtained from the URL (if any). */ - constructor(public error: string | CoreError | CoreTextErrorObject | null, public data?: CoreCustomURLSchemesParams) { - super(CoreTextUtils.getErrorMessageFromError(error)); + constructor(public error: string | CoreError | CoreErrorObject | null, public data?: CoreCustomURLSchemesParams) { + super(CoreErrorHelper.getErrorMessageFromError(error)); } } diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index f939451db..b677d06b5 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -20,7 +20,7 @@ import { Md5 } from 'ts-md5'; import { CoreConfig } from '@services/config'; import { CoreFile } from '@services/file'; import { CoreWSExternalWarning } from '@services/ws'; -import { CoreTextUtils, CoreTextErrorObject } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; @@ -54,6 +54,7 @@ import { CoreModals, OpenModalOptions } from '@services/modals'; import { CorePopovers, OpenPopoverOptions } from '@services/popovers'; import { CoreViewer } from '@features/viewer/services/viewer'; import { CoreLoadings } from '@services/loadings'; +import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -109,7 +110,7 @@ export class CoreDomUtilsProvider { limitedThreshold?: number, alwaysConfirm?: boolean, ): Promise { - const readableSize = CoreTextUtils.bytesToSize(size.size, 2); + const readableSize = CoreText.bytesToSize(size.size, 2); const getAvailableBytes = async (): Promise => { const availableBytes = await CoreFile.calculateFreeSpace(); @@ -131,7 +132,7 @@ export class CoreDomUtilsProvider { if (availableBytes === null) { return ''; } else { - const availableSize = CoreTextUtils.bytesToSize(availableBytes, 2); + const availableSize = CoreText.bytesToSize(availableBytes, 2); if (CorePlatform.isAndroid() && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) { throw new CoreError( @@ -409,7 +410,7 @@ export class CoreDomUtilsProvider { * @param error Error object. * @returns True if the message error is a network error, false otherwise. */ - protected isNetworkError(message: string, error?: CoreError | CoreTextErrorObject | string): boolean { + protected isNetworkError(message: string, error?: CoreError | CoreErrorObject | string): boolean { return message == Translate.instant('core.networkerrormsg') || message == Translate.instant('core.fileuploader.errormustbeonlinetoupload') || error instanceof CoreNetworkError; @@ -423,7 +424,7 @@ export class CoreDomUtilsProvider { */ protected isSiteUnavailableError(message: string): boolean { let siteUnavailableMessage = Translate.instant('core.siteunavailablehelp', { site: 'SITEURLPLACEHOLDER' }); - siteUnavailableMessage = CoreTextUtils.escapeForRegex(siteUnavailableMessage); + siteUnavailableMessage = CoreText.escapeForRegex(siteUnavailableMessage); siteUnavailableMessage = siteUnavailableMessage.replace('SITEURLPLACEHOLDER', '.*'); return new RegExp(siteUnavailableMessage).test(message); @@ -436,7 +437,7 @@ export class CoreDomUtilsProvider { * @param needsTranslate Whether the error needs to be translated. * @returns Error message, null if no error should be displayed. */ - getErrorMessage(error: CoreError | CoreTextErrorObject | string, needsTranslate?: boolean): string | null { + getErrorMessage(error: CoreError | CoreErrorObject | string, needsTranslate?: boolean): string | null { if (typeof error != 'string' && !error) { return null; } @@ -448,11 +449,11 @@ export class CoreDomUtilsProvider { if (this.debugDisplay) { // Get the debug info. Escape the HTML so it is displayed as it is in the view. if ('debuginfo' in error && error.debuginfo) { - extraInfo = '

' + CoreTextUtils.escapeHTML(error.debuginfo, false); + extraInfo = '

' + CoreText.escapeHTML(error.debuginfo, false); } if ('backtrace' in error && error.backtrace) { - extraInfo += '

' + CoreTextUtils.replaceNewLines( - CoreTextUtils.escapeHTML(error.backtrace, false), + extraInfo += '

' + CoreText.replaceNewLines( + CoreText.escapeHTML(error.backtrace, false), '
', ); } @@ -467,7 +468,7 @@ export class CoreDomUtilsProvider { } // We received an object instead of a string. Search for common properties. - errorMessage = CoreTextUtils.getErrorMessageFromError(error); + errorMessage = CoreErrorHelper.getErrorMessageFromError(error); CoreErrorLogs.addErrorLog({ message: JSON.stringify(error), type: errorMessage || '', time: new Date().getTime() }); if (!errorMessage) { // No common properties found, just stringify it. @@ -484,7 +485,7 @@ export class CoreDomUtilsProvider { errorMessage = error; } - let message = CoreTextUtils.decodeHTML(needsTranslate ? Translate.instant(errorMessage) : errorMessage); + let message = CoreText.decodeHTML(needsTranslate ? Translate.instant(errorMessage) : errorMessage); if (extraInfo) { message += extraInfo; @@ -705,7 +706,7 @@ export class CoreDomUtilsProvider { const currentSrc = media.getAttribute('src'); const newSrc = currentSrc ? paths[CoreUrl.removeUrlParts( - CoreTextUtils.decodeURIComponent(currentSrc), + CoreUrl.decodeURIComponent(currentSrc), [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], )] : undefined; @@ -717,7 +718,7 @@ export class CoreDomUtilsProvider { // Treat video posters. const currentPoster = media.getAttribute('poster'); if (media.tagName == 'VIDEO' && currentPoster) { - const newPoster = paths[CoreTextUtils.decodeURIComponent(currentPoster)]; + const newPoster = paths[CoreUrl.decodeURIComponent(currentPoster)]; if (newPoster !== undefined) { media.setAttribute('poster', newPoster); } @@ -730,7 +731,7 @@ export class CoreDomUtilsProvider { const currentHref = anchor.getAttribute('href'); const newHref = currentHref ? paths[CoreUrl.removeUrlParts( - CoreTextUtils.decodeURIComponent(currentHref), + CoreUrl.decodeURIComponent(currentHref), [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], )] : undefined; @@ -838,7 +839,7 @@ export class CoreDomUtilsProvider { ? options.message : options.message?.value || ''; - const hasHTMLTags = CoreTextUtils.hasHTMLTags(message); + const hasHTMLTags = CoreText.hasHTMLTags(message); if (hasHTMLTags && !CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) { // Treat multilang. @@ -1022,7 +1023,7 @@ export class CoreDomUtilsProvider { * @returns Promise resolved with the alert modal. */ async showErrorModal( - error: CoreError | CoreTextErrorObject | string, + error: CoreError | CoreErrorObject | string, needsTranslate?: boolean, autocloseTime?: number, ): Promise { @@ -1117,7 +1118,7 @@ export class CoreDomUtilsProvider { let errorMessage = error || undefined; if (error && typeof error != 'string') { - errorMessage = CoreTextUtils.getErrorMessageFromError(error); + errorMessage = CoreErrorHelper.getErrorMessageFromError(error); } return this.showErrorModal( diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 834394e74..78a3b5386 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -16,105 +16,33 @@ import { Injectable } from '@angular/core'; import { SafeUrl } from '@angular/platform-browser'; import { CoreAnyError, CoreError } from '@classes/errors/error'; -import { DomSanitizer, makeSingleton, Translate } from '@singletons'; +import { makeSingleton } from '@singletons'; import { CoreWSFile } from '@services/ws'; -import { Locutus } from '@singletons/locutus'; import { CoreFileHelper } from '@services/file-helper'; import { CoreUrl } from '@singletons/url'; -import { AlertButton } from '@ionic/angular'; -import { CorePath } from '@singletons/path'; -import { CorePlatform } from '@services/platform'; import { CoreDom } from '@singletons/dom'; import { CoreText } from '@singletons/text'; import { CoreViewer, CoreViewerTextOptions } from '@features/viewer/services/viewer'; +import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper'; /** - * Different type of errors the app can treat. - */ -export type CoreTextErrorObject = { - message?: string; - error?: string; - content?: string; - body?: string; - debuginfo?: string; - backtrace?: string; - title?: string; - buttons?: AlertButton[]; -}; - -/* * "Utils" service with helper functions for text. -*/ + * + * @deprecated since 4.5. Some of the functions have been moved to CoreText but not all of them, check function deprecation message. + */ @Injectable({ providedIn: 'root' }) export class CoreTextUtilsProvider { - // List of regular expressions to convert the old nomenclature to new nomenclature for disabled features. - protected readonly DISABLED_FEATURES_COMPAT_REGEXPS: { old: RegExp; new: string }[] = [ - { old: /\$mmLoginEmailSignup/g, new: 'CoreLoginEmailSignup' }, - { old: /\$mmSideMenuDelegate/g, new: 'CoreMainMenuDelegate' }, - { old: /\$mmCoursesDelegate/g, new: 'CoreCourseOptionsDelegate' }, - { old: /\$mmUserDelegate/g, new: 'CoreUserDelegate' }, - { old: /\$mmCourseDelegate/g, new: 'CoreCourseModuleDelegate' }, - { old: /_mmCourses/g, new: '_CoreCourses' }, - { old: /_mmaFrontpage/g, new: '_CoreSiteHome' }, - { old: /_mmaGrades/g, new: '_CoreGrades' }, - { old: /_mmaCompetency/g, new: '_AddonCompetency' }, - { old: /_mmaNotifications/g, new: '_AddonNotifications' }, - { old: /_mmaMessages/g, new: '_AddonMessages' }, - { old: /_mmaCalendar/g, new: '_AddonCalendar' }, - { old: /_mmaFiles/g, new: '_AddonPrivateFiles' }, - { old: /_mmaParticipants/g, new: '_CoreUserParticipants' }, - { old: /_mmaCourseCompletion/g, new: '_AddonCourseCompletion' }, - { old: /_mmaNotes/g, new: '_AddonNotes' }, - { old: /_mmaBadges/g, new: '_AddonBadges' }, - { old: /files_privatefiles/g, new: 'AddonPrivateFilesPrivateFiles' }, - { old: /files_sitefiles/g, new: 'AddonPrivateFilesSiteFiles' }, - { old: /files_upload/g, new: 'AddonPrivateFilesUpload' }, - { old: /_mmaModAssign/g, new: '_AddonModAssign' }, - { old: /_mmaModBigbluebuttonbn/g, new: '_AddonModBBB' }, - { old: /_mmaModBook/g, new: '_AddonModBook' }, - { old: /_mmaModChat/g, new: '_AddonModChat' }, - { old: /_mmaModChoice/g, new: '_AddonModChoice' }, - { old: /_mmaModData/g, new: '_AddonModData' }, - { old: /_mmaModFeedback/g, new: '_AddonModFeedback' }, - { old: /_mmaModFolder/g, new: '_AddonModFolder' }, - { old: /_mmaModForum/g, new: '_AddonModForum' }, - { old: /_mmaModGlossary/g, new: '_AddonModGlossary' }, - { old: /_mmaModH5pactivity/g, new: '_AddonModH5PActivity' }, - { old: /_mmaModImscp/g, new: '_AddonModImscp' }, - { old: /_mmaModLabel/g, new: '_AddonModLabel' }, - { old: /_mmaModLesson/g, new: '_AddonModLesson' }, - { old: /_mmaModLti/g, new: '_AddonModLti' }, - { old: /_mmaModPage/g, new: '_AddonModPage' }, - { old: /_mmaModQuiz/g, new: '_AddonModQuiz' }, - { old: /_mmaModResource/g, new: '_AddonModResource' }, - { old: /_mmaModScorm/g, new: '_AddonModScorm' }, - { old: /_mmaModSurvey/g, new: '_AddonModSurvey' }, - { old: /_mmaModUrl/g, new: '_AddonModUrl' }, - { old: /_mmaModWiki/g, new: '_AddonModWiki' }, - { old: /_mmaModWorkshop/g, new: '_AddonModWorkshop' }, - { old: /remoteAddOn_/g, new: 'sitePlugin_' }, - { old: /AddonNotes:addNote/g, new: 'AddonNotes:notes' }, - ]; - - protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element. - /** * Add ending slash from a path or URL. * * @param text Text to treat. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreText.addEndingSlash instead. */ addEndingSlash(text: string): string { - if (!text) { - return ''; - } - - if (text.slice(-1) != '/') { - return text + '/'; - } - - return text; + return CoreText.addEndingSlash(text); } /** @@ -123,33 +51,14 @@ export class CoreTextUtilsProvider { * @param error Error message or object. * @param text Text to add. * @returns Modified error. + * + * @deprecated since 4.5. Use CoreErrorHelper.addTextToError instead. */ - addTextToError(error: string | CoreError | CoreTextErrorObject | undefined | null, text: string): string | CoreTextErrorObject { - if (typeof error == 'string') { - return error + text; - } - - if (error instanceof CoreError) { - error.message += text; - - return error; - } - - if (!error) { - return text; - } - - if (typeof error.message == 'string') { - error.message += text; - } else if (typeof error.error == 'string') { - error.error += text; - } else if (typeof error.content == 'string') { - error.content += text; - } else if (typeof error.body == 'string') { - error.body += text; - } - - return error; + addTextToError( + error: string | CoreError | CoreErrorObject | undefined | null, + text: string, + ): string | CoreErrorObject { + return CoreErrorHelper.addTextToError(error, text); } /** @@ -158,19 +67,11 @@ export class CoreTextUtilsProvider { * @param error Error message or object. * @param title Title to add. * @returns Modified error. + * + * @deprecated since 4.5. Use CoreErrorHelper.addTitleToError instead. */ - addTitleToError(error: string | CoreError | CoreTextErrorObject | undefined | null, title: string): CoreTextErrorObject { - let improvedError: CoreTextErrorObject = {}; - - if (typeof error === 'string') { - improvedError.message = error; - } else if (error && 'message' in error) { - improvedError = error; - } - - improvedError.title = improvedError.title || title; - - return improvedError; + addTitleToError(error: string | CoreError | CoreErrorObject | undefined | null, title: string): CoreErrorObject { + return CoreErrorHelper.addTitleToError(error, title); } /** @@ -178,16 +79,11 @@ export class CoreTextUtilsProvider { * * @param address The address. * @returns URL to view the address. + * + * @deprecated since 4.5. Use CoreUrl.buildAddressURL instead. */ buildAddressURL(address: string): SafeUrl { - const parsedUrl = CoreUrl.parse(address); - if (parsedUrl?.protocol) { - // It's already a URL, don't convert it. - return DomSanitizer.bypassSecurityTrustUrl(address); - } - - return DomSanitizer.bypassSecurityTrustUrl((CorePlatform.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') + - encodeURIComponent(address)); + return CoreUrl.buildAddressURL(address); } /** @@ -195,17 +91,11 @@ export class CoreTextUtilsProvider { * * @param messages Messages to show. * @returns Message with all the messages. + * + * @deprecated since 4.5. Use CoreText.buildMessage instead. */ buildMessage(messages: string[]): string { - let result = ''; - - messages.forEach((message) => { - if (message) { - result += `

${message}

`; - } - }); - - return result; + return CoreText.buildMessage(messages); } /** @@ -213,31 +103,11 @@ export class CoreTextUtilsProvider { * * @param paragraphs List of paragraphs. * @returns Built message. + * + * @deprecated since 4.5. Use CoreErrorHelper.buildSeveralParagraphsMessage instead. */ - buildSeveralParagraphsMessage(paragraphs: (string | CoreTextErrorObject)[]): string { - // Filter invalid messages, and convert them to messages in case they're errors. - const messages: string[] = []; - - paragraphs.forEach(paragraph => { - // If it's an error, get its message. - const message = this.getErrorMessageFromError(paragraph); - - if (paragraph && message) { - messages.push(message); - } - }); - - if (messages.length < 2) { - return messages[0] || ''; - } - - let builtMessage = messages[0]; - - for (let i = 1; i < messages.length; i++) { - builtMessage = Translate.instant('core.twoparagraphs', { p1: builtMessage, p2: messages[i] }); - } - - return builtMessage; + buildSeveralParagraphsMessage(paragraphs: (string | CoreErrorObject)[]): string { + return CoreErrorHelper.buildSeveralParagraphsMessage(paragraphs); } /** @@ -246,30 +116,11 @@ export class CoreTextUtilsProvider { * @param bytes Number of bytes to convert. * @param precision Number of digits after the decimal separator. * @returns Size in human readable format. + * + * @deprecated since 4.5. Use CoreText.bytesToSize instead. */ bytesToSize(bytes: number, precision: number = 2): string { - if (bytes === undefined || bytes === null || bytes < 0) { - return Translate.instant('core.notapplicable'); - } - - if (precision < 0) { - precision = 2; - } - - const keys = ['core.sizeb', 'core.sizekb', 'core.sizemb', 'core.sizegb', 'core.sizetb']; - const units = Translate.instant(keys); - let pos = 0; - - if (bytes >= 1024) { - while (bytes >= 1024) { - pos++; - bytes = bytes / 1024; - } - // Round to "precision" decimals if needed. - bytes = Number(Math.round(parseFloat(bytes + 'e+' + precision)) + 'e-' + precision); - } - - return Translate.instant('core.humanreadablesize', { size: bytes, unit: units[keys[pos]] }); + return CoreText.bytesToSize(bytes, precision); } /** @@ -278,13 +129,11 @@ export class CoreTextUtilsProvider { * @param text HTML string. * @param process Method to process the HTML. * @returns Processed HTML string. + * + * @deprecated since 4.5. Use CoreText.processHTML instead. */ processHTML(text: string, process: (element: HTMLElement) => unknown): string { - const element = this.convertToElement(text); - - process(element); - - return element.innerHTML; + return CoreText.processHTML(text, process); } /** @@ -295,36 +144,11 @@ export class CoreTextUtilsProvider { * @param options.singleLine True if new lines should be removed (all the text in a single line). * @param options.trim True if text should be trimmed. * @returns Clean text. + * + * @deprecated since 4.5. Use CoreText.cleanTags instead. */ cleanTags(text: string | undefined, options: { singleLine?: boolean; trim?: boolean } = {}): string { - if (!text) { - return ''; - } - - // First, we use a regexpr. - text = text.replace(/(<([^>]+)>)/ig, ''); - // Then, we rely on the browser. We need to wrap the text to be sure is HTML. - text = this.convertToElement(text).textContent || ''; - // Trim text - text = options.trim ? text.trim() : text; - // Recover or remove new lines. - text = this.replaceNewLines(text, options.singleLine ? ' ' : '
'); - - return text; - } - - /** - * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body. - * This function is the same as in DomUtils, but we cannot use that one because of circular dependencies. - * - * @param html Text to convert. - * @returns Element. - */ - protected convertToElement(html: string): HTMLElement { - // Add a div to hold the content, that's the element that will be returned. - this.template.innerHTML = '
' + html + '
'; - - return this.template.content.children[0]; + return CoreText.cleanTags(text, options); } /** @@ -333,37 +157,11 @@ export class CoreTextUtilsProvider { * * @param text Text to count. * @returns Number of words. + * + * @deprecated since 4.5. Use CoreText.countWords instead. */ countWords(text?: string | null): number { - if (!text || typeof text != 'string') { - return 0; - } - - // Before stripping tags, add a space after the close tag of anything that is not obviously inline. - // Also, br is a special case because it definitely delimits a word, but has no close tag. - text = text.replace(/(<\/(?!a>|b>|del>|em>|i>|ins>|s>|small>|span>|strong>|sub>|sup>|u>)\w+>|
|)/ig, '$1 '); - - // Now remove HTML tags. - text = text.replace(/(<([^>]+)>)/ig, ''); - // Decode HTML entities. - text = this.decodeHTMLEntities(text); - - // Now, the word count is the number of blocks of characters separated - // by any sort of space. That seems to be the definition used by all other systems. - // To be precise about what is considered to separate words: - // * Anything that Unicode considers a 'Separator' - // * Anything that Unicode considers a 'Control character' - // * An em- or en- dash. - let words: string[]; - try { - words = text.split(/[\p{Z}\p{Cc}—–]+/u); - } catch { - // Unicode-aware flag not supported. - words = text.split(/\s+/); - } - - // Filter empty words. - return words.filter(word => word).length; + return CoreText.countWords(text); } /** @@ -371,21 +169,11 @@ export class CoreTextUtilsProvider { * * @param text Text to decode. * @returns Decoded text. + * + * @deprecated since 4.5. Use CoreText.decodeHTML instead. */ decodeHTML(text: string | number): string { - if (text === undefined || text === null || (typeof text == 'number' && isNaN(text))) { - return ''; - } else if (typeof text != 'string') { - return '' + text; - } - - return text - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, '\'') - .replace(/ /g, ' '); + return CoreText.decodeHTML(text); } /** @@ -393,13 +181,11 @@ export class CoreTextUtilsProvider { * * @param text Text to decode. * @returns Decoded text. + * + * @deprecated since 4.5. Use CoreText.decodeHTMLEntities instead. */ decodeHTMLEntities(text: string): string { - if (text) { - text = this.convertToElement(text).textContent || ''; - } - - return text; + return CoreText.decodeHTMLEntities(text); } /** @@ -407,15 +193,11 @@ export class CoreTextUtilsProvider { * * @param uri URI to decode. * @returns Decoded URI, or original URI if an exception is thrown. + * + * @deprecated since 4.5. Use CoreUrl.decodeURI instead. */ decodeURI(uri: string): string { - try { - return decodeURI(uri); - } catch (ex) { - // Error, use the original URI. - } - - return uri; + return CoreUrl.decodeURI(uri); } /** @@ -423,15 +205,11 @@ export class CoreTextUtilsProvider { * * @param uri URI to decode. * @returns Decoded URI, or original URI if an exception is thrown. + * + * @deprecated since 4.5. Use CoreUrl.decodeURIComponent instead. */ decodeURIComponent(uri: string): string { - try { - return decodeURIComponent(uri); - } catch (ex) { - // Error, use the original URI. - } - - return uri; + return CoreUrl.decodeURIComponent(uri); } /** @@ -439,13 +217,11 @@ export class CoreTextUtilsProvider { * * @param text Text to escape. * @returns Escaped text. + * + * @deprecated since 4.5. Use CoreText.escapeForRegex instead. */ escapeForRegex(text: string): string { - if (!text || typeof text != 'string') { - return ''; - } - - return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + return CoreText.escapeForRegex(text); } /** @@ -454,25 +230,11 @@ export class CoreTextUtilsProvider { * @param text Text to escape. * @param doubleEncode If false, it will not convert existing html entities. Defaults to true. * @returns Escaped text. + * + * @deprecated since 4.5. Use CoreText.escapeHTML instead. */ - escapeHTML(text?: string | number | null, doubleEncode: boolean = true): string { - if (text === undefined || text === null || (typeof text == 'number' && isNaN(text))) { - return ''; - } else if (typeof text != 'string') { - return '' + text; - } - - if (doubleEncode) { - text = text.replace(/&/g, '&'); - } else { - text = text.replace(/&(?!amp;)(?!lt;)(?!gt;)(?!quot;)(?!#039;)/g, '&'); - } - - return text - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + escapeHTML(text?: string | number | null, doubleEncode = true): string { + return CoreText.escapeHTML(text, doubleEncode); } /** @@ -480,20 +242,11 @@ export class CoreTextUtilsProvider { * * @param text Text to format. * @returns Formatted text. + * + * @deprecated since 4.5. Use CoreText.formatHtmlLines instead. */ formatHtmlLines(text: string): string { - const hasHTMLTags = this.hasHTMLTags(text); - if (text.indexOf('

') == -1) { - // Wrap the text in

tags. - text = '

' + text + '

'; - } - - if (!hasHTMLTags) { - // The text doesn't have HTML, replace new lines for
. - return this.replaceNewLines(text, '
'); - } - - return text; + return CoreText.formatHtmlLines(text); } /** @@ -501,46 +254,11 @@ export class CoreTextUtilsProvider { * * @param error Error. * @returns Error message, undefined if not found. + * + * @deprecated since 4.5. Use CoreErrorHelper.getErrorMessageFromError instead. */ getErrorMessageFromError(error?: CoreAnyError): string | undefined { - if (typeof error === 'string') { - return error; - } - - if (error instanceof CoreError) { - return error.message; - } - - if (!error) { - return undefined; - } - - if (error.message || error.error || error.content) { - return error.message || error.error || error.content; - } - - if (error.body) { - return this.getErrorMessageFromHTML(error.body); - } - - return undefined; - } - - /** - * Get the error message from an HTML error page. - * - * @param body HTML content. - * @returns Error message or empty string if not found. - */ - getErrorMessageFromHTML(body: string): string { - // THe parser does not throw errors and scripts are not executed. - const parser = new DOMParser(); - const doc = parser.parseFromString(body, 'text/html'); - - // Errors are rendered using the "errorbox" and "errormessage" classes since Moodle 2.0. - const element = doc.body.querySelector('.errorbox .errormessage'); - - return element?.innerText.trim() ?? ''; + return CoreErrorHelper.getErrorMessageFromError(error); } /** @@ -548,11 +266,11 @@ export class CoreTextUtilsProvider { * * @param html HTML text. * @returns Body HTML. + * + * @deprecated since 4.5. Use CoreDom.getHTMLBodyContent instead. */ getHTMLBodyContent(html: string): string { - const matches = html.match(/([\s\S]*)<\/body>/im); - - return matches?.[1] ?? html; + return CoreDom.getHTMLBodyContent(html); } /** @@ -560,16 +278,11 @@ export class CoreTextUtilsProvider { * * @param files Files to extract the URL from. They need to have the URL in a 'url' or 'fileurl' attribute. * @returns Pluginfile URL, undefined if no files found. + * + * @deprecated since 4.5. Use CoreFileHelper.getTextPluginfileUrl instead. */ getTextPluginfileUrl(files: CoreWSFile[]): string | undefined { - if (files?.length) { - const url = CoreFileHelper.getFileUrl(files[0]); - - // Remove text after last slash (encoded or not). - return url?.substring(0, Math.max(url.lastIndexOf('/'), url.lastIndexOf('%2F'))); - } - - return undefined; + return CoreFileHelper.getTextPluginfileUrl(files); } /** @@ -577,9 +290,11 @@ export class CoreTextUtilsProvider { * * @param text Text to check. * @returns Whether it has HTML tags. + * + * @deprecated since 4.5. Use CoreText.hasHTMLTags instead. */ hasHTMLTags(text: string): boolean { - return /<[a-z][\s\S]*>/i.test(text); + return CoreText.hasHTMLTags(text); } /** @@ -588,17 +303,11 @@ export class CoreTextUtilsProvider { * @param text Full text. * @param searchText Text to search and highlight. * @returns Highlighted text. + * + * @deprecated since 4.5. Use CoreText.highlightText instead. */ highlightText(text: string, searchText: string): string { - if (!text || typeof text != 'string') { - return ''; - } else if (!searchText) { - return text; - } - - const regex = new RegExp('(' + searchText + ')', 'gi'); - - return text.replace(regex, '$1'); + return CoreText.highlightText(text, searchText); } /** @@ -606,15 +315,11 @@ export class CoreTextUtilsProvider { * * @param content HTML content. * @returns True if the string does not contain actual content: text, images, etc. + * + * @deprecated since 4.5. Use CoreDom.htmlIsBlank instead. */ htmlIsBlank(content: string): boolean { - if (!content) { - return true; - } - - this.template.innerHTML = content; - - return !CoreDom.elementHasContent(this.template.content); + return CoreDom.htmlIsBlank(content); } /** @@ -623,15 +328,11 @@ export class CoreTextUtilsProvider { * * @param text Text to check. * @returns True if has Unicode chars, false otherwise. + * + * @deprecated since 4.5. Use CoreText.hasUnicode instead. */ hasUnicode(text: string): boolean { - for (let x = 0; x < text.length; x++) { - if (text.charCodeAt(x) > 55295) { - return true; - } - } - - return false; + return CoreText.hasUnicode(text); } /** @@ -639,23 +340,11 @@ export class CoreTextUtilsProvider { * * @param data Object to be checked. * @returns If the data has any long Unicode char on it. + * + * @deprecated since 4.5. Use CoreText.hasUnicodeData instead. */ hasUnicodeData(data: Record): boolean { - for (const el in data) { - if (typeof data[el] == 'object') { - if (this.hasUnicodeData(data[el] as Record)) { - return true; - } - - continue; - } - - if (typeof data[el] == 'string' && this.hasUnicode(data[el] as string)) { - return true; - } - } - - return false; + return CoreText.hasUnicodeData(data); } /** @@ -664,21 +353,11 @@ export class CoreTextUtilsProvider { * @param text Text to match against. * @param pattern Glob pattern. * @returns Whether the pattern matches. + * + * @deprecated since 4.5. Use CoreText.matchesGlob instead. */ matchesGlob(text: string, pattern: string): boolean { - pattern = pattern - .replace(/\*\*/g, '%RECURSIVE_MATCH%') - .replace(/\*/g, '%LOCAL_MATCH%') - .replace(/\?/g, '%CHARACTER_MATCH%'); - - pattern = this.escapeForRegex(pattern); - - pattern = pattern - .replace(/%RECURSIVE_MATCH%/g, '.*') - .replace(/%LOCAL_MATCH%/g, '[^/]*') - .replace(/%CHARACTER_MATCH%/g, '[^/]'); - - return new RegExp(`^${pattern}$`).test(text); + return CoreText.matchesGlob(text, pattern); } /** @@ -688,23 +367,11 @@ export class CoreTextUtilsProvider { * @param defaultValue Default value to return if the parse fails. Defaults to the original value. * @param logErrorFn An error to call with the exception to log the error. If not supplied, no error. * @returns JSON parsed as object or what it gets. + * + * @deprecated since 4.5. Use CoreText.parseJSON instead. */ parseJSON(json: string, defaultValue?: T, logErrorFn?: (error?: Error) => void): T { - try { - return JSON.parse(json); - } catch (error) { - // Error, log the error if needed. - if (logErrorFn) { - logErrorFn(error); - } - } - - // Error parsing, return the default value or the original value. - if (defaultValue !== undefined) { - return defaultValue; - } - - throw new CoreError('JSON cannot be parsed and not default value has been provided') ; + return CoreText.parseJSON(json, defaultValue, logErrorFn); } /** @@ -712,13 +379,11 @@ export class CoreTextUtilsProvider { * * @param text Text to treat. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreText.removeSpecialCharactersForFiles instead. */ removeSpecialCharactersForFiles(text: string): string { - if (!text || typeof text != 'string') { - return ''; - } - - return text.replace(/[#:/?\\]+/g, '_'); + return CoreText.removeSpecialCharactersForFiles(text); } /** @@ -728,19 +393,11 @@ export class CoreTextUtilsProvider { * @param replacements Argument values. * @param encoding Encoding to use in values. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreText.replaceArguments instead. */ replaceArguments(text: string, replacements: Record = {}, encoding?: 'uri'): string { - let match: RegExpMatchArray | null = null; - - while ((match = text.match(/\{\{([^}]+)\}\}/))) { - const argument = match[1].trim(); - const value = replacements[argument] ?? ''; - const encodedValue = encoding ? encodeURIComponent(value) : value; - - text = text.replace(`{{${argument}}}`, encodedValue); - } - - return text; + return CoreText.replaceArguments(text, replacements, encoding); } /** @@ -749,13 +406,11 @@ export class CoreTextUtilsProvider { * @param text The text to be treated. * @param newValue Text to use instead of new lines. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreText.replaceNewLines instead. */ replaceNewLines(text: string, newValue: string): string { - if (!text || typeof text != 'string') { - return ''; - } - - return text.replace(/(?:\r\n|\r|\n)/g, newValue); + return CoreText.replaceNewLines(text, newValue); } /** @@ -765,57 +420,15 @@ export class CoreTextUtilsProvider { * @param text Text to treat, including draftfile URLs. * @param files List of files of the area, using pluginfile URLs. * @returns Treated text and map with the replacements. + * + * @deprecated since 4.5. Use CoreFileHelper.replaceDraftfileUrls instead. */ replaceDraftfileUrls( siteUrl: string, text: string, files: CoreWSFile[], ): { text: string; replaceMap?: {[url: string]: string} } { - - if (!text || !files || !files.length) { - return { text }; - } - - const draftfileUrl = CorePath.concatenatePaths(siteUrl, 'draftfile.php'); - const matches = text.match(new RegExp(this.escapeForRegex(draftfileUrl) + '[^\'" ]+', 'ig')); - - if (!matches || !matches.length) { - return { text }; - } - - // Index the pluginfile URLs by file name. - const pluginfileMap: {[name: string]: string} = {}; - files.forEach((file) => { - if (!file.filename) { - return; - } - pluginfileMap[file.filename] = CoreFileHelper.getFileUrl(file); - }); - - // Replace each draftfile with the corresponding pluginfile URL. - const replaceMap: {[url: string]: string} = {}; - matches.forEach((url) => { - if (replaceMap[url]) { - // URL already treated, same file embedded more than once. - return; - } - - // Get the filename from the URL. - let filename = url.substring(url.lastIndexOf('/') + 1); - if (filename.indexOf('?') != -1) { - filename = filename.substring(0, filename.indexOf('?')); - } - - if (pluginfileMap[filename]) { - replaceMap[url] = pluginfileMap[filename]; - text = text.replace(new RegExp(this.escapeForRegex(url), 'g'), pluginfileMap[filename]); - } - }); - - return { - text, - replaceMap, - }; + return CoreFileHelper.replaceDraftfileUrls(siteUrl, text, files); } /** @@ -824,16 +437,11 @@ export class CoreTextUtilsProvider { * @param text to treat. * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreFileHelper.replacePluginfileUrls instead. */ replacePluginfileUrls(text: string, files: CoreWSFile[]): string { - if (text && typeof text == 'string') { - const fileURL = this.getTextPluginfileUrl(files); - if (fileURL) { - return text.replace(/@@PLUGINFILE@@/g, fileURL); - } - } - - return text; + return CoreFileHelper.replacePluginfileUrls(text, files); } /** @@ -844,33 +452,11 @@ export class CoreTextUtilsProvider { * @param originalText Original text. * @param files List of files to search and replace. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreFileHelper.restoreDraftfileUrls instead. */ restoreDraftfileUrls(siteUrl: string, treatedText: string, originalText: string, files: CoreWSFile[]): string { - if (!treatedText || !files || !files.length) { - return treatedText; - } - - const draftfileUrl = CorePath.concatenatePaths(siteUrl, 'draftfile.php'); - const draftfileUrlRegexPrefix = this.escapeForRegex(draftfileUrl) + '/[^/]+/[^/]+/[^/]+/[^/]+/'; - - files.forEach((file) => { - if (!file.filename) { - return; - } - - // Search the draftfile URL in the original text. - const matches = originalText.match( - new RegExp(draftfileUrlRegexPrefix + this.escapeForRegex(file.filename) + '[^\'" ]*', 'i'), - ); - - if (!matches || !matches[0]) { - return; // Original URL not found, skip. - } - - treatedText = treatedText.replace(new RegExp(this.escapeForRegex(CoreFileHelper.getFileUrl(file)), 'g'), matches[0]); - }); - - return treatedText; + return CoreFileHelper.restoreDraftfileUrls(siteUrl, treatedText, originalText, files); } /** @@ -879,16 +465,11 @@ export class CoreTextUtilsProvider { * @param text Text to treat. * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreFileHelper.restorePluginfileUrls instead. */ restorePluginfileUrls(text: string, files: CoreWSFile[]): string { - if (text && typeof text == 'string') { - const fileURL = this.getTextPluginfileUrl(files); - if (fileURL) { - return text.replace(new RegExp(this.escapeForRegex(fileURL), 'g'), '@@PLUGINFILE@@'); - } - } - - return text; + return CoreFileHelper.restorePluginfileUrls(text, files); } /** @@ -900,11 +481,11 @@ export class CoreTextUtilsProvider { * @param num Number to round. * @param decimals Number of decimals. By default, 2. * @returns Rounded number. + * + * @deprecated since 4.5. Use CoreText.roundToDecimals instead. */ roundToDecimals(num: number, decimals: number = 2): number { - const multiplier = Math.pow(10, decimals); - - return Math.round(num * multiplier) / multiplier; + return CoreText.roundToDecimals(num, decimals); } /** @@ -915,13 +496,11 @@ export class CoreTextUtilsProvider { * * @param text Text to treat. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreText.s instead. */ s(text: string): string { - if (!text) { - return ''; - } - - return this.escapeHTML(text).replace(/&#(\d+|x[0-9a-f]+);/i, '&#$1;'); + return CoreText.s(text); } /** @@ -930,20 +509,11 @@ export class CoreTextUtilsProvider { * @param text The text to be shortened. * @param length The desired length. * @returns Shortened text. + * + * @deprecated since 4.5. Use CoreText.shortenText instead. */ shortenText(text: string, length: number): string { - if (text.length > length) { - text = text.substring(0, length); - - // Now, truncate at the last word boundary (if exists). - const lastWordPos = text.lastIndexOf(' '); - if (lastWordPos > 0) { - text = text.substring(0, lastWordPos); - } - text += '…'; - } - - return text; + return CoreText.shortenText(text, length); } /** @@ -952,16 +522,11 @@ export class CoreTextUtilsProvider { * * @param text Text to check. * @returns Without the Unicode chars. + * + * @deprecated since 4.5. Use CoreText.stripUnicode instead. */ stripUnicode(text: string): string { - let stripped = ''; - for (let x = 0; x < text.length; x++) { - if (text.charCodeAt(x) <= 55295) { - stripped += text.charAt(x); - } - } - - return stripped; + return CoreText.stripUnicode(text); } /** @@ -973,9 +538,11 @@ export class CoreTextUtilsProvider { * @param length Length of the portion of string which is to be replaced. If negative, it represents the number of characters * from the end of string at which to stop replacing. If not provided, replace until the end of the string. * @returns Treated string. + * + * @deprecated since 4.5. Use CoreText.substrReplace instead. */ substrReplace(str: string, replace: string, start: number, length?: number): string { - return Locutus.substrReplace(str, replace, start, length); + return CoreText.substrReplace(str, replace, start, length); } /** @@ -983,18 +550,10 @@ export class CoreTextUtilsProvider { * * @param features List of disabled features. * @returns Treated list. + * + * @deprecated since 4.5. Shoudn't be used since disabled features are not treated by this function anymore. */ treatDisabledFeatures(features: string): string { - if (!features) { - return ''; - } - - for (let i = 0; i < this.DISABLED_FEATURES_COMPAT_REGEXPS.length; i++) { - const entry = this.DISABLED_FEATURES_COMPAT_REGEXPS[i]; - - features = features.replace(entry.old, entry.new); - } - return features; } @@ -1004,12 +563,11 @@ export class CoreTextUtilsProvider { * @param text Text to treat. * @param character Character to remove. * @returns Treated text. + * + * @deprecated since 4.5. Use CoreText.trimCharacter instead. */ trimCharacter(text: string, character: string): string { - const escaped = this.escapeForRegex(character); - const regExp = new RegExp(`^${escaped}+|${escaped}+$`, 'g'); - - return text.replace(regExp, ''); + return CoreText.trimCharacter(text, character); } /** @@ -1017,13 +575,11 @@ export class CoreTextUtilsProvider { * * @param num Number to convert. * @returns Number with leading zeros. + * + * @deprecated since 4.5. Use CoreText.twoDigits instead. */ twoDigits(num: string | number): string { - if (Number(num) < 10) { - return '0' + num; - } else { - return '' + num; // Convert to string for coherence. - } + return CoreText.twoDigits(num); } /** @@ -1042,9 +598,11 @@ export class CoreTextUtilsProvider { * * @param data String to unserialize. * @returns Unserialized data. + * + * @deprecated since 4.5. Use CoreText.unserialize instead. */ unserialize(data: string): T { - return Locutus.unserialize(data); + return CoreText.unserialize(data); } /** @@ -1061,24 +619,5 @@ export class CoreTextUtilsProvider { } } +// eslint-disable-next-line deprecation/deprecation export const CoreTextUtils = makeSingleton(CoreTextUtilsProvider); - -/** - * Options for viewText. - * - * @deprecated since 4.5. Use CoreViewerTextOptions instead. - */ -export type CoreTextUtilsViewTextOptions = CoreViewerTextOptions; - -/** - * Define text formatting types. - */ -export enum CoreTextFormat { - FORMAT_MOODLE = 0, // Does all sorts of transformations and filtering. - FORMAT_HTML = 1, // Plain HTML (with some tags stripped). Use it by default. - FORMAT_PLAIN = 2, // Plain text (even tags are printed in full). - // FORMAT_WIKI is deprecated since 2005... - FORMAT_MARKDOWN = 4, // Markdown-formatted text http://daringfireball.net/projects/markdown/ -} - -export const defaultTextFormat = CoreTextFormat.FORMAT_HTML; diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index 3bb3ba823..05fe91cec 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -20,7 +20,6 @@ import { CoreFile } from '@services/file'; import { CoreLang, CoreLangFormat } from '@services/lang'; import { CoreWS } from '@services/ws'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextUtils } from '@services/utils/text'; import { makeSingleton, InAppBrowser, FileOpener, WebIntent, Translate, NgZone } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreFileEntry } from '@services/file-helper'; @@ -38,6 +37,7 @@ import { CoreArray } from '@singletons/array'; import { CoreText } from '@singletons/text'; import { CoreWait, CoreWaitOptions } from '@singletons/wait'; import { CoreQRScan } from '@services/qrscan'; +import { CoreErrorHelper } from '@services/error-helper'; export type TreeNode = T & { children: TreeNode[] }; @@ -65,7 +65,7 @@ export class CoreUtilsProvider { * @returns New error message. */ addDataNotDownloadedError(error: Error | string, defaultError?: string): string { - const errorMessage = CoreTextUtils.getErrorMessageFromError(error) || defaultError || ''; + const errorMessage = CoreErrorHelper.getErrorMessageFromError(error) || defaultError || ''; if (this.isWebServiceError(error)) { return errorMessage; diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index 004da122c..3b9ce5954 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -24,7 +24,7 @@ import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http import { CoreNetwork } from '@services/network'; import { CoreFile, CoreFileFormat } from '@services/file'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; -import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreConstants } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; import { CoreInterceptor } from '@classes/interceptor'; @@ -43,6 +43,8 @@ import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest import { CoreSites } from '@services/sites'; import { CoreLang, CoreLangFormat } from './lang'; import { CoreErrorLogs } from '@singletons/error-logs'; +import { CoreErrorHelper, CoreErrorObject } from './error-helper'; +import { CoreDom } from '@singletons/dom'; /** * This service allows performing WS calls and download/upload files. @@ -174,9 +176,9 @@ export class CoreWSProvider { if (value == null) { return null; } - } else if (typeof value == 'string') { + } else if (typeof value === 'string') { if (stripUnicode) { - const stripped = CoreTextUtils.stripUnicode(value); + const stripped = CoreText.stripUnicode(value); if (stripped != value && stripped.trim().length == 0) { return null; } @@ -532,45 +534,45 @@ export class CoreWSProvider { options.debug = { code: 'invalidcertificate', details: Translate.instant('core.certificaterror', { - details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Invalid certificate', + details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Invalid certificate', }), }; break; case NativeHttp.ErrorCode.SERVER_NOT_FOUND: options.debug = { code: 'servernotfound', - details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Server could not be found', + details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Server could not be found', }; break; case NativeHttp.ErrorCode.TIMEOUT: options.debug = { code: 'requesttimeout', - details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request timed out', + details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Request timed out', }; break; case NativeHttp.ErrorCode.UNSUPPORTED_URL: options.debug = { code: 'unsupportedurl', - details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Url not supported', + details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Url not supported', }; break; case NativeHttp.ErrorCode.NOT_CONNECTED: options.debug = { code: 'connectionerror', - details: CoreTextUtils.getErrorMessageFromError(data.error) + details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Connection error, is network available?', }; break; case NativeHttp.ErrorCode.ABORTED: options.debug = { code: 'requestaborted', - details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request aborted', + details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Request aborted', }; break; case NativeHttp.ErrorCode.POST_PROCESSING_FAILED: options.debug = { code: 'requestprocessingfailed', - details: CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Request processing failed', + details: CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Request processing failed', }; break; } @@ -587,7 +589,7 @@ export class CoreWSProvider { }; break; default: { - const details = CoreTextUtils.getErrorMessageFromError(data.error) ?? 'Unknown error'; + const details = CoreErrorHelper.getErrorMessageFromError(data.error) ?? 'Unknown error'; options.debug = { code: 'serverconnectionajax', @@ -836,7 +838,7 @@ export class CoreWSProvider { debug: { code: 'invalidcertificate', details: Translate.instant('core.certificaterror', { - details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error', + details: CoreErrorHelper.getErrorMessageFromError(error) ?? 'Unknown error', }), }, }); @@ -845,7 +847,7 @@ export class CoreWSProvider { } throw new CoreError(Translate.instant('core.serverconnection', { - details: CoreTextUtils.getErrorMessageFromError(error) ?? 'Unknown error', + details: CoreErrorHelper.getErrorMessageFromError(error) ?? 'Unknown error', })); }).catch(err => { CoreErrorLogs.addErrorLog({ @@ -965,7 +967,7 @@ export class CoreWSProvider { } // Treat response. - data = CoreTextUtils.parseJSON(data); + data = CoreText.parseJSON(data); // Some moodle web services return null. // If the responseExpected value is set then so long as no data is returned, we create a blank object. @@ -1057,7 +1059,7 @@ export class CoreWSProvider { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - const data = CoreTextUtils.parseJSON( + const data = CoreText.parseJSON( success.response, null, error => this.logger.error('Error parsing response from upload', success.response, error), @@ -1119,12 +1121,12 @@ export class CoreWSProvider { * @param status Status code (if any). * @returns CoreHttpError. */ - protected createHttpError(error: CoreTextErrorObject, status: number): CoreHttpError { - const message = CoreTextUtils.buildSeveralParagraphsMessage([ + protected createHttpError(error: CoreErrorObject, status: number): CoreHttpError { + const message = CoreErrorHelper.buildSeveralParagraphsMessage([ CoreSites.isLoggedIn() ? Translate.instant('core.siteunavailablehelp', { site: CoreSites.getCurrentSite()?.siteUrl }) : Translate.instant('core.sitenotfoundhelp'), - CoreTextUtils.getHTMLBodyContent(CoreTextUtils.getErrorMessageFromError(error) || ''), + CoreDom.getHTMLBodyContent(CoreErrorHelper.getErrorMessageFromError(error) || ''), ]); return new CoreHttpError(message, status); diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index 8f962cc87..a8050115e 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -18,6 +18,9 @@ import { CoreEventObserver } from '@singletons/events'; import { CorePlatform } from '@services/platform'; import { CoreWait } from './wait'; +// A template element to convert HTML to element. +export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template'); + /** * Singleton with helper functions for dom. */ @@ -74,6 +77,19 @@ export class CoreDom { ).length > 0; } + /** + * Given some HTML code, return the HTML code inside tags. If there are no body tags, return the whole HTML. + * + * @param html HTML text. + * @returns Body HTML. + */ + static getHTMLBodyContent(html: string): string { + const doc = new DOMParser().parseFromString(html, 'text/html'); + const bodyContent = doc.body.innerHTML; + + return bodyContent ?? html; + } + /** * Retrieve the position of a element relative to another element. * @@ -93,6 +109,22 @@ export class CoreDom { }; } + /** + * Check if HTML content is blank. + * + * @param content HTML content. + * @returns True if the string does not contain actual content: text, images, etc. + */ + static htmlIsBlank(content: string): boolean { + if (!content) { + return true; + } + + CoreTemplateElement.innerHTML = content; + + return !CoreDom.elementHasContent(CoreTemplateElement.content); + } + /** * Check whether an element has been added to the DOM. * diff --git a/src/core/singletons/tests/text.test.ts b/src/core/singletons/tests/text.test.ts index b50f0db7d..f66221be5 100644 --- a/src/core/singletons/tests/text.test.ts +++ b/src/core/singletons/tests/text.test.ts @@ -16,6 +16,20 @@ import { CoreText } from '@singletons/text'; describe('CoreText singleton', () => { + it('adds ending slashes', () => { + const originalUrl = 'https://moodle.org'; + const url = CoreText.addEndingSlash(originalUrl); + + expect(url).toEqual('https://moodle.org/'); + }); + + it('doesn\'t add duplicated ending slashes', () => { + const originalUrl = 'https://moodle.org/'; + const url = CoreText.addEndingSlash(originalUrl); + + expect(url).toEqual('https://moodle.org/'); + }); + it('adds a starting slash if needed', () => { expect(CoreText.addStartingSlash('')).toEqual('/'); expect(CoreText.addStartingSlash('foo')).toEqual('/foo'); @@ -36,4 +50,71 @@ describe('CoreText singleton', () => { expect(CoreText.removeStartingSlash('//foo')).toEqual('/foo'); }); + it('matches glob patterns', () => { + expect(CoreText.matchesGlob('/foo/bar', '/foo/bar')).toBe(true); + expect(CoreText.matchesGlob('/foo/bar', '/foo/bar/')).toBe(false); + expect(CoreText.matchesGlob('/foo', '/foo/*')).toBe(false); + expect(CoreText.matchesGlob('/foo/', '/foo/*')).toBe(true); + expect(CoreText.matchesGlob('/foo/bar', '/foo/*')).toBe(true); + expect(CoreText.matchesGlob('/foo/bar/', '/foo/*')).toBe(false); + expect(CoreText.matchesGlob('/foo/bar/baz', '/foo/*')).toBe(false); + expect(CoreText.matchesGlob('/foo/bar/baz', '/foo/**')).toBe(true); + expect(CoreText.matchesGlob('/foo/bar/baz/', '/foo/**')).toBe(true); + expect(CoreText.matchesGlob('/foo/bar/baz', '**/baz')).toBe(true); + expect(CoreText.matchesGlob('/foo/bar/baz', '**/bar')).toBe(false); + expect(CoreText.matchesGlob('/foo/bar/baz', '/foo/ba?/ba?')).toBe(true); + }); + + it('replaces arguments', () => { + // Arrange + const url = 'http://campus.edu?device={{device}}&version={{version}}'; + const replacements = { + device: 'iPhone or iPad', + version: '1.2.3', + }; + + // Act + const replaced = CoreText.replaceArguments(url, replacements, 'uri'); + + // Assert + expect(replaced).toEqual('http://campus.edu?device=iPhone%20or%20iPad&version=1.2.3'); + }); + + it('counts words', () => { + expect(CoreText.countWords('')).toEqual(0); + expect(CoreText.countWords('one two three four')).toEqual(4); + expect(CoreText.countWords('a\'b')).toEqual(1); + expect(CoreText.countWords('1+1=2')).toEqual(1); + expect(CoreText.countWords(' one-sided ')).toEqual(1); + expect(CoreText.countWords('one two')).toEqual(2); + expect(CoreText.countWords('email@example.com')).toEqual(1); + expect(CoreText.countWords('first\\part second/part')).toEqual(2); + expect(CoreText.countWords('

one two

three four

')).toEqual(4); + expect(CoreText.countWords('

one two
three four

')).toEqual(4); + expect(CoreText.countWords('

one two
three four

')).toEqual(4); + expect(CoreText.countWords(' one ... three ')).toEqual(3); + expect(CoreText.countWords('just...one')).toEqual(1); + expect(CoreText.countWords(' one & three ')).toEqual(3); + expect(CoreText.countWords('just&one')).toEqual(1); + expect(CoreText.countWords('em—dash')).toEqual(2); + expect(CoreText.countWords('en–dash')).toEqual(2); + expect(CoreText.countWords('1³ £2 €3.45 $6,789')).toEqual(4); + expect(CoreText.countWords('ブルース カンベッル')).toEqual(2); + expect(CoreText.countWords('

one two

three four

')).toEqual(4); + expect(CoreText.countWords('

one two


three four

')).toEqual(4); + expect(CoreText.countWords('

one

  • two
  • three

four.

')).toEqual(4); + expect(CoreText.countWords('

emphasis.

')).toEqual(1); + expect(CoreText.countWords('

emphasis.

')).toEqual(1); + expect(CoreText.countWords('

emphasis.

')).toEqual(1); + expect(CoreText.countWords('

emphasis.

')).toEqual(1); + expect(CoreText.countWords('one\ntwo')).toEqual(2); + expect(CoreText.countWords('one\rtwo')).toEqual(2); + expect(CoreText.countWords('one\ttwo')).toEqual(2); + expect(CoreText.countWords('one\vtwo')).toEqual(2); + expect(CoreText.countWords('one\ftwo')).toEqual(2); + expect(CoreText.countWords('SO42-')).toEqual(1); + expect(CoreText.countWords('4+4=8 i.e. O(1) a,b,c,d I’m black&blue_really')).toEqual(6); + expect(CoreText.countWords('ab')).toEqual(1); + }); + }); diff --git a/src/core/singletons/tests/url.test.ts b/src/core/singletons/tests/url.test.ts index 0e4980e05..7903de55f 100644 --- a/src/core/singletons/tests/url.test.ts +++ b/src/core/singletons/tests/url.test.ts @@ -12,12 +12,63 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { mock } from '@/testing/utils'; +import { mock, mockSingleton } from '@/testing/utils'; import { CoreSite } from '@classes/sites/site'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; +import { CorePlatform } from '@services/platform'; +import { DomSanitizer } from '@singletons'; describe('CoreUrl singleton', () => { + const config = { platform: 'android' }; + + beforeEach(() => { + mockSingleton(CorePlatform, [], { isAndroid: () => config.platform === 'android' }); + mockSingleton(DomSanitizer, [], { bypassSecurityTrustUrl: url => url }); + }); + + it('builds address URL for Android platforms', () => { + // Arrange + const address = 'Moodle Spain HQ'; + + config.platform = 'android'; + + // Act + const url = CoreUrl.buildAddressURL(address); + + // Assert + expect(url).toEqual('geo:0,0?q=Moodle%20Spain%20HQ'); + + expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); + expect(CorePlatform.isAndroid).toHaveBeenCalled(); + }); + + it('builds address URL for non-Android platforms', () => { + // Arrange + const address = 'Moodle Spain HQ'; + + config.platform = 'ios'; + + // Act + const url = CoreUrl.buildAddressURL(address); + + // Assert + expect(url).toEqual('http://maps.google.com?q=Moodle%20Spain%20HQ'); + + expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); + expect(CorePlatform.isAndroid).toHaveBeenCalled(); + }); + + it('doesn\'t build address if it\'s already a URL', () => { + const address = 'https://moodle.org'; + + const url = CoreUrl.buildAddressURL(address); + + expect(url).toEqual(address); + + expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); + }); + it('adds www if missing', () => { const originalUrl = 'https://moodle.org'; const url = CoreUrl.addOrRemoveWWW(originalUrl); diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts index 0a60c5a48..58c066655 100644 --- a/src/core/singletons/text.ts +++ b/src/core/singletons/text.ts @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Clipboard } from '@singletons'; +import { Clipboard, Translate } from '@singletons'; import { CoreToasts } from '@services/toasts'; +import { Locutus } from './locutus'; +import { CoreError } from '@classes/errors/error'; +import { CoreTemplateElement } from './dom'; /** * Singleton with helper functions for text manipulation. @@ -25,6 +28,24 @@ export class CoreText { // Nothing to do. } + /** + * Add ending slash from a path or URL. + * + * @param text Text to treat. + * @returns Treated text. + */ + static addEndingSlash(text: string): string { + if (!text) { + return ''; + } + + if (text.slice(-1) != '/') { + return text + '/'; + } + + return text; + } + /** * Add starting slash to a string if needed. * @@ -39,6 +60,405 @@ export class CoreText { return '/' + text; } + /** + * Given a list of sentences, build a message with all of them wrapped in

. + * + * @param messages Messages to show. + * @returns Message with all the messages. + */ + static buildMessage(messages: string[]): string { + let result = ''; + + messages.forEach((message) => { + if (message) { + result += `

${message}

`; + } + }); + + return result; + } + + /** + * Convert size in bytes into human readable format + * + * @param bytes Number of bytes to convert. + * @param precision Number of digits after the decimal separator. + * @returns Size in human readable format. + */ + static bytesToSize(bytes: number, precision: number = 2): string { + if (bytes === undefined || bytes === null || bytes < 0) { + return Translate.instant('core.notapplicable'); + } + + if (precision < 0) { + precision = 2; + } + + const keys = ['core.sizeb', 'core.sizekb', 'core.sizemb', 'core.sizegb', 'core.sizetb']; + const units = Translate.instant(keys); + let pos = 0; + + if (bytes >= 1024) { + while (bytes >= 1024) { + pos++; + bytes = bytes / 1024; + } + // Round to "precision" decimals if needed. + bytes = Number(Math.round(parseFloat(bytes + 'e+' + precision)) + 'e-' + precision); + } + + return Translate.instant('core.humanreadablesize', { size: bytes, unit: units[keys[pos]] }); + } + + /** + * Copies a text to clipboard and shows a toast message. + * + * @param text Text to be copied + */ + static async copyToClipboard(text: string): Promise { + try { + await Clipboard.copy(text); + } catch { + // Use HTML Copy command. + const virtualInput = document.createElement('textarea'); + virtualInput.innerHTML = text; + virtualInput.select(); + virtualInput.setSelectionRange(0, 99999); + document.execCommand('copy'); // eslint-disable-line deprecation/deprecation + } + + // Show toast using ionicLoading. + CoreToasts.show({ + message: 'core.copiedtoclipboard', + translateMessage: true, + }); + } + + /** + * Count words in a text. + * This function is based on Moodle's count_words. + * + * @param text Text to count. + * @returns Number of words. + */ + static countWords(text?: string | null): number { + if (!text || typeof text != 'string') { + return 0; + } + + // Before stripping tags, add a space after the close tag of anything that is not obviously inline. + // Also, br is a special case because it definitely delimits a word, but has no close tag. + text = text.replace(/(<\/(?!a>|b>|del>|em>|i>|ins>|s>|small>|span>|strong>|sub>|sup>|u>)\w+>|
|)/ig, '$1 '); + + // Now remove HTML tags. + text = text.replace(/(<([^>]+)>)/ig, ''); + // Decode HTML entities. + text = CoreText.decodeHTMLEntities(text); + + // Now, the word count is the number of blocks of characters separated + // by any sort of space. That seems to be the definition used by all other systems. + // To be precise about what is considered to separate words: + // * Anything that Unicode considers a 'Separator' + // * Anything that Unicode considers a 'Control character' + // * An em- or en- dash. + let words: string[]; + try { + words = text.split(/[\p{Z}\p{Cc}—–]+/u); + } catch { + // Unicode-aware flag not supported. + words = text.split(/\s+/); + } + + // Filter empty words. + return words.filter(word => word).length; + } + + /** + * Clean HTML tags. + * + * @param text The text to be cleaned. + * @param options Processing options. + * @param options.singleLine True if new lines should be removed (all the text in a single line). + * @param options.trim True if text should be trimmed. + * @returns Clean text. + */ + static cleanTags(text: string | undefined, options: { singleLine?: boolean; trim?: boolean } = {}): string { + if (!text) { + return ''; + } + + // First, we use a regexpr. + text = text.replace(/(<([^>]+)>)/ig, ''); + // Then, we rely on the browser. We need to wrap the text to be sure is HTML. + text = CoreText.convertToElement(text).textContent || ''; + // Trim text + text = options.trim ? text.trim() : text; + // Recover or remove new lines. + text = CoreText.replaceNewLines(text, options.singleLine ? ' ' : '
'); + + return text; + } + + /** + * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body. + * This function is the same as in DomUtils, but we cannot use that one because of circular dependencies. + * + * @param html Text to convert. + * @returns Element. + */ + protected static convertToElement(html: string): HTMLElement { + // Add a div to hold the content, that's the element that will be returned. + CoreTemplateElement.innerHTML = '
' + html + '
'; + + return CoreTemplateElement.content.children[0]; + } + + /** + * Decode an escaped HTML text. This implementation is based on PHP's htmlspecialchars_decode. + * + * @param text Text to decode. + * @returns Decoded text. + */ + static decodeHTML(text: string | number): string { + if (text === undefined || text === null || (typeof text === 'number' && isNaN(text))) { + return ''; + } else if (typeof text != 'string') { + return '' + text; + } + + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '\'') + .replace(/ /g, ' '); + } + + /** + * Decode HTML entities in a text. Equivalent to PHP html_entity_decode. + * + * @param text Text to decode. + * @returns Decoded text. + */ + static decodeHTMLEntities(text: string): string { + if (text) { + text = CoreText.convertToElement(text).textContent || ''; + } + + return text; + } + + /** + * Escapes some characters in a string to be used as a regular expression. + * + * @param text Text to escape. + * @returns Escaped text. + */ + static escapeForRegex(text: string): string { + if (!text || typeof text !== 'string') { + return ''; + } + + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + } + + /** + * Escape an HTML text. This implementation is based on PHP's htmlspecialchars. + * + * @param text Text to escape. + * @param doubleEncode If false, it will not convert existing html entities. Defaults to true. + * @returns Escaped text. + */ + static escapeHTML(text?: string | number | null, doubleEncode = true): string { + if (text === undefined || text === null || (typeof text === 'number' && isNaN(text))) { + return ''; + } else if (typeof text !== 'string') { + return '' + text; + } + + if (doubleEncode) { + text = text.replace(/&/g, '&'); + } else { + text = text.replace(/&(?!amp;)(?!lt;)(?!gt;)(?!quot;)(?!#039;)/g, '&'); + } + + return text + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + /** + * Formats a text, in HTML replacing new lines by correct html new lines. + * + * @param text Text to format. + * @returns Formatted text. + */ + static formatHtmlLines(text: string): string { + const hasHTMLTags = CoreText.hasHTMLTags(text); + if (text.indexOf('

') == -1) { + // Wrap the text in

tags. + text = '

' + text + '

'; + } + + if (!hasHTMLTags) { + // The text doesn't have HTML, replace new lines for
. + return CoreText.replaceNewLines(text, '
'); + } + + return text; + } + + /** + * Check if a text contains HTML tags. + * + * @param text Text to check. + * @returns Whether it has HTML tags. + */ + static hasHTMLTags(text: string): boolean { + return /<[a-z][\s\S]*>/i.test(text); + } + + /** + * Check if a text contains Unicode long chars. + * Using as threshold Hex value D800 + * + * @param text Text to check. + * @returns True if has Unicode chars, false otherwise. + */ + static hasUnicode(text: string): boolean { + for (let x = 0; x < text.length; x++) { + if (text.charCodeAt(x) > 55295) { + return true; + } + } + + return false; + } + + /** + * Check if an object has any long Unicode char. + * + * @param data Object to be checked. + * @returns If the data has any long Unicode char on it. + */ + static hasUnicodeData(data: Record): boolean { + for (const el in data) { + if (typeof data[el] === 'object') { + if (CoreText.hasUnicodeData(data[el] as Record)) { + return true; + } + + continue; + } + + if (typeof data[el] === 'string' && CoreText.hasUnicode(data[el] as string)) { + return true; + } + } + + return false; + } + + /** + * Highlight all occurrences of a certain text inside another text. It will add some HTML code to highlight it. + * + * @param text Full text. + * @param searchText Text to search and highlight. + * @returns Highlighted text. + */ + static highlightText(text: string, searchText: string): string { + if (!text || typeof text !== 'string') { + return ''; + } else if (!searchText) { + return text; + } + + const regex = new RegExp('(' + searchText + ')', 'gi'); + + return text.replace(regex, '$1'); + } + + /** + * Check whether the given text matches a glob pattern. + * + * @param text Text to match against. + * @param pattern Glob pattern. + * @returns Whether the pattern matches. + */ + static matchesGlob(text: string, pattern: string): boolean { + pattern = pattern + .replace(/\*\*/g, '%RECURSIVE_MATCH%') + .replace(/\*/g, '%LOCAL_MATCH%') + .replace(/\?/g, '%CHARACTER_MATCH%'); + + pattern = CoreText.escapeForRegex(pattern); + + pattern = pattern + .replace(/%RECURSIVE_MATCH%/g, '.*') + .replace(/%LOCAL_MATCH%/g, '[^/]*') + .replace(/%CHARACTER_MATCH%/g, '[^/]'); + + return new RegExp(`^${pattern}$`).test(text); + } + + /** + * Same as Javascript's JSON.parse, but it will handle errors. + * + * @param json JSON text. + * @param defaultValue Default value to return if the parse fails. Defaults to the original value. + * @param logErrorFn An error to call with the exception to log the error. If not supplied, no error. + * @returns JSON parsed as object or what it gets. + */ + static parseJSON(json: string, defaultValue?: T, logErrorFn?: (error?: Error) => void): T { + try { + return JSON.parse(json); + } catch (error) { + // Error, log the error if needed. + if (logErrorFn) { + logErrorFn(error); + } + } + + // Error parsing, return the default value or the original value. + if (defaultValue !== undefined) { + return defaultValue; + } + + throw new CoreError('JSON cannot be parsed and not default value has been provided'); + } + + /** + * Process HTML string. + * + * @param text HTML string. + * @param process Method to process the HTML. + * @returns Processed HTML string. + */ + static processHTML(text: string, process: (element: HTMLElement) => unknown): string { + const element = CoreText.convertToElement(text); + + process(element); + + return element.innerHTML; + } + + /** + * Replace all characters that cause problems with files in Android and iOS. + * + * @param text Text to treat. + * @returns Treated text. + */ + static removeSpecialCharactersForFiles(text: string): string { + if (!text || typeof text !== 'string') { + return ''; + } + + return text.replace(/[#:/?\\]+/g, '_'); + } + /** * Remove ending slash from a path or URL. * @@ -72,27 +492,155 @@ export class CoreText { } /** - * Copies a text to clipboard and shows a toast message. + * Replace {{ARGUMENT}} arguments in the text. * - * @param text Text to be copied + * @param text Text to treat. + * @param replacements Argument values. + * @param encoding Encoding to use in values. + * @returns Treated text. */ - static async copyToClipboard(text: string): Promise { - try { - await Clipboard.copy(text); - } catch { - // Use HTML Copy command. - const virtualInput = document.createElement('textarea'); - virtualInput.innerHTML = text; - virtualInput.select(); - virtualInput.setSelectionRange(0, 99999); - document.execCommand('copy'); // eslint-disable-line deprecation/deprecation + static replaceArguments(text: string, replacements: Record = {}, encoding?: 'uri'): string { + let match: RegExpMatchArray | null = null; + + while ((match = text.match(/\{\{([^}]+)\}\}/))) { + const argument = match[1].trim(); + const value = replacements[argument] ?? ''; + const encodedValue = encoding ? encodeURIComponent(value) : value; + + text = text.replace(`{{${argument}}}`, encodedValue); } - // Show toast using ionicLoading. - CoreToasts.show({ - message: 'core.copiedtoclipboard', - translateMessage: true, - }); + return text; + } + + /** + * Replace all the new lines on a certain text. + * + * @param text The text to be treated. + * @param newValue Text to use instead of new lines. + * @returns Treated text. + */ + static replaceNewLines(text: string, newValue: string): string { + if (!text || typeof text !== 'string') { + return ''; + } + + return text.replace(/(?:\r\n|\r|\n)/g, newValue); + } + + /** + * Rounds a number to use a certain amout of decimals or less. + * Difference between this function and float's toFixed: + * 7.toFixed(2) -> 7.00 + * roundToDecimals(7, 2) -> 7 + * + * @param num Number to round. + * @param decimals Number of decimals. By default, 2. + * @returns Rounded number. + */ + static roundToDecimals(num: number, decimals: number = 2): number { + const multiplier = Math.pow(10, decimals); + + return Math.round(num * multiplier) / multiplier; + } + + /** + * Add quotes to HTML characters. + * + * Returns text with HTML characters (like "<", ">", etc.) properly quoted. + * Based on Moodle's s() function. + * + * @param text Text to treat. + * @returns Treated text. + */ + static s(text: string): string { + if (!text) { + return ''; + } + + return CoreText.escapeHTML(text).replace(/&#(\d+|x[0-9a-f]+);/i, '&#$1;'); + } + + /** + * Shortens a text to length and adds an ellipsis. + * + * @param text The text to be shortened. + * @param length The desired length. + * @returns Shortened text. + */ + static shortenText(text: string, length: number): string { + if (text.length > length) { + text = text.substring(0, length); + + // Now, truncate at the last word boundary (if exists). + const lastWordPos = text.lastIndexOf(' '); + if (lastWordPos > 0) { + text = text.substring(0, lastWordPos); + } + text += '…'; + } + + return text; + } + + /** + * Strip Unicode long char of a given text. + * Using as threshold Hex value D800 + * + * @param text Text to check. + * @returns Without the Unicode chars. + */ + static stripUnicode(text: string): string { + let stripped = ''; + for (let x = 0; x < text.length; x++) { + if (text.charCodeAt(x) <= 55295) { + stripped += text.charAt(x); + } + } + + return stripped; + } + + /** + * Replace text within a portion of a string. Equivalent to PHP's substr_replace. + * + * @param str The string to treat. + * @param replace The value to put inside the string. + * @param start The index where to start putting the new string. If negative, it will count from the end of the string. + * @param length Length of the portion of string which is to be replaced. If negative, it represents the number of characters + * from the end of string at which to stop replacing. If not provided, replace until the end of the string. + * @returns Treated string. + */ + static substrReplace(str: string, replace: string, start: number, length?: number): string { + return Locutus.substrReplace(str, replace, start, length); + } + + /** + * Remove all ocurrences of a certain character from the start and end of a string. + * + * @param text Text to treat. + * @param character Character to remove. + * @returns Treated text. + */ + static trimCharacter(text: string, character: string): string { + const escaped = CoreText.escapeForRegex(character); + const regExp = new RegExp(`^${escaped}+|${escaped}+$`, 'g'); + + return text.replace(regExp, ''); + } + + /** + * If a number has only 1 digit, add a leading zero to it. + * + * @param num Number to convert. + * @returns Number with leading zeros. + */ + static twoDigits(num: string | number): string { + if (Number(num) < 10) { + return '0' + num; + } else { + return '' + num; // Convert to string for coherence. + } } /** @@ -105,4 +653,27 @@ export class CoreText { return text.charAt(0).toUpperCase() + text.slice(1); } + /** + * Unserialize Array from PHP. + * + * @param data String to unserialize. + * @returns Unserialized data. + */ + static unserialize(data: string): T { + return Locutus.unserialize(data); + } + } + +/** + * Define text formatting types. + */ +export enum CoreTextFormat { + FORMAT_MOODLE = 0, // Does all sorts of transformations and filtering. + FORMAT_HTML = 1, // Plain HTML (with some tags stripped). Use it by default. + FORMAT_PLAIN = 2, // Plain text (even tags are printed in full). + // FORMAT_WIKI is deprecated since 2005... + FORMAT_MARKDOWN = 4, // Markdown-formatted text http://daringfireball.net/projects/markdown/ +} + +export const defaultTextFormat = CoreTextFormat.FORMAT_HTML; diff --git a/src/core/singletons/url.ts b/src/core/singletons/url.ts index e9080b1e3..dafbe1c3b 100644 --- a/src/core/singletons/url.ts +++ b/src/core/singletons/url.ts @@ -17,10 +17,11 @@ import { CorePath } from './path'; import { CoreText } from './text'; import { CorePlatform } from '@services/platform'; -import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '../constants'; import { CoreMedia } from './media'; import { CoreLang, CoreLangFormat } from '@services/lang'; +import { DomSanitizer } from '@singletons'; +import { SafeUrl } from '@angular/platform-browser'; /** * Parts contained within a url. @@ -91,6 +92,23 @@ export class CoreUrl { // Nothing to do. } + /** + * Given an address as a string, return a URL to open the address in maps. + * + * @param address The address. + * @returns URL to view the address. + */ + static buildAddressURL(address: string): SafeUrl { + const parsedUrl = CoreUrl.parse(address); + if (parsedUrl?.protocol) { + // It's already a URL, don't convert it. + return DomSanitizer.bypassSecurityTrustUrl(address); + } + + return DomSanitizer.bypassSecurityTrustUrl((CorePlatform.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') + + encodeURIComponent(address)); + } + /** * Parse parts of a url, using an implicit protocol if it is missing from the url. * @@ -455,6 +473,38 @@ export class CoreUrl { !CoreMedia.sourceUsesJavascriptPlayer({ src: url }); } + /** + * Same as Javascript's decodeURI, but if an exception is thrown it will return the original URI. + * + * @param uri URI to decode. + * @returns Decoded URI, or original URI if an exception is thrown. + */ + static decodeURI(uri: string): string { + try { + return decodeURI(uri); + } catch { + // Error, use the original URI. + } + + return uri; + } + + /** + * Same as Javascript's decodeURIComponent, but if an exception is thrown it will return the original URI. + * + * @param uri URI to decode. + * @returns Decoded URI, or original URI if an exception is thrown. + */ + static decodeURIComponent(uri: string): string { + try { + return decodeURIComponent(uri); + } catch { + // Error, use the original URI. + } + + return uri; + } + /** * Extracts the parameters from a URL and stores them in an object. * @@ -479,7 +529,7 @@ export class CoreUrl { } urlAndHash[0].replace(regex, (match: string, key: string, value: string): string => { - params[key] = value !== undefined ? CoreTextUtils.decodeURIComponent(value) : ''; + params[key] = value !== undefined ? CoreUrl.decodeURIComponent(value) : ''; if (subParams) { params[key] = params[key].replace(subParamsPlaceholder, subParams); @@ -525,7 +575,7 @@ export class CoreUrl { } // Check if is a valid URL (contains the pluginfile endpoint) and belongs to the site. - if (!CoreUrl.isPluginFileUrl(url) || url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { + if (!CoreUrl.isPluginFileUrl(url) || url.indexOf(CoreText.addEndingSlash(siteUrl)) !== 0) { return url; } @@ -584,7 +634,7 @@ export class CoreUrl { let videoId = ''; const params: CoreUrlParams = {}; - url = CoreTextUtils.decodeHTML(url); + url = CoreText.decodeHTML(url); // Get the video ID. let match = url.match(/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/); @@ -925,7 +975,7 @@ export class CoreUrl { url = url.replace(/&/g, '&'); // It site URL is supplied, check if the URL belongs to the site. - if (siteUrl && url.indexOf(CoreTextUtils.addEndingSlash(siteUrl)) !== 0) { + if (siteUrl && url.indexOf(CoreText.addEndingSlash(siteUrl)) !== 0) { return url; } diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 1cbe0ea39..633f6f6b2 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -20,7 +20,7 @@ import { sep } from 'path'; import { CORE_SITE_SCHEMAS } from '@services/sites'; import { ApplicationInit, CoreSingletonProxy, Translate } from '@singletons'; -import { CoreTextUtilsProvider } from '@services/utils/text'; +import { CoreText } from '@singletons/text'; import { CoreExternalContentDirectiveStub } from './stubs/directives/core-external-content'; import { CoreNetwork } from '@services/network'; @@ -44,7 +44,6 @@ abstract class WrapperComponent { type ServiceInjectionToken = AbstractType | Type | string; let testBedInitialized = false; -const textUtils = new CoreTextUtilsProvider(); const DEFAULT_SERVICE_SINGLETON_MOCKS: [CoreSingletonProxy, unknown][] = [ [Translate, mock({ instant: key => key, @@ -479,7 +478,7 @@ export async function renderWrapperComponent( ): Promise> { const inputAttributes = Object .entries(inputs) - .map(([name, value]) => `[${name}]="${textUtils.escapeHTML(JSON.stringify(value)).replace(/\//g, '\\/')}"`) + .map(([name, value]) => `[${name}]="${CoreText.escapeHTML(JSON.stringify(value)).replace(/\//g, '\\/')}"`) .join(' '); return renderTemplate(component, `<${tag} ${inputAttributes}>`, config); From 323ccc8c760ad87bdbe2e87cf315b5f45aa2285f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Jul 2024 15:37:32 +0200 Subject: [PATCH 2/4] MOBILE-4616 chore: Isolate convertHTMLToHTMLElement --- .../services/handlers/displayh5p.ts | 13 +++++---- .../services/handlers/mediaplugin.ts | 9 +++--- .../bigbluebuttonbn/components/index/index.ts | 5 ++-- .../mod/folder/services/handlers/module.ts | 4 +-- .../mod/lesson/services/lesson-helper.ts | 15 +++++----- src/addons/mod/lesson/services/lesson.ts | 4 +-- src/addons/mod/quiz/services/quiz-helper.ts | 3 +- src/addons/mod/quiz/services/quiz.ts | 4 +-- .../services/handlers/calculated.ts | 4 +-- .../qtype/essay/services/handlers/essay.ts | 12 ++++---- .../components/show-password/show-password.ts | 4 +-- .../services/handlers/course-tag-area.ts | 4 +-- .../features/grades/services/grades-helper.ts | 3 +- .../classes/base-question-component.ts | 5 ++-- .../question/services/question-helper.ts | 21 +++++++------- src/core/features/tag/services/tag-helper.ts | 4 +-- .../user/services/handlers/tag-area.ts | 4 +-- src/core/services/filepool.ts | 3 +- src/core/services/utils/dom.ts | 24 +++++++-------- src/core/singletons/dom.ts | 4 +-- src/core/singletons/text.ts | 22 +++----------- src/core/utils/create-html-element.ts | 29 +++++++++++++++++++ 22 files changed, 109 insertions(+), 91 deletions(-) create mode 100644 src/core/utils/create-html-element.ts diff --git a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts index e71a429fa..0ef26c726 100644 --- a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts +++ b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts @@ -20,6 +20,7 @@ import { makeSingleton } from '@singletons'; import { CoreH5PPlayerComponent } from '@features/h5p/components/h5p-player/h5p-player'; import { CoreUrl } from '@singletons/url'; import { CoreH5PHelper } from '@features/h5p/classes/helper'; +import { CoreTemplateElement } from '@/core/utils/create-html-element'; /** * Handler to support the Display H5P filter. @@ -30,17 +31,15 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle name = 'AddonFilterDisplayH5PHandler'; filterName = 'displayh5p'; - protected template = document.createElement('template'); // A template element to convert HTML to element. - /** * @inheritdoc */ filter( text: string, ): string | Promise { - this.template.innerHTML = text; + CoreTemplateElement.innerHTML = text; - const h5pIframes = Array.from(this.template.content.querySelectorAll('iframe.h5p-iframe')); + const h5pIframes = Array.from(CoreTemplateElement.content.querySelectorAll('iframe.h5p-iframe')); // Replace all iframes with an empty div that will be treated in handleHtml. h5pIframes.forEach((iframe) => { @@ -53,7 +52,9 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle }); // Handle H5P iframes embedded using the embed HTML code. - const embeddedH5PIframes = Array.from(this.template.content.querySelectorAll('iframe.h5p-player')); + const embeddedH5PIframes = Array.from( + CoreTemplateElement.content.querySelectorAll('iframe.h5p-player'), + ); embeddedH5PIframes.forEach((iframe) => { // Add the preventredirect param to allow authenticating if auto-login fails. @@ -70,7 +71,7 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle } }); - return this.template.innerHTML; + return CoreTemplateElement.innerHTML; } /** diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts index 5c5cea576..6d111dca4 100644 --- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts +++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreTemplateElement } from '@/core/utils/create-html-element'; import { AddonFilterMediaPluginVideoJS } from '@addons/filter/mediaplugin/services/videojs'; import { Injectable } from '@angular/core'; @@ -28,21 +29,19 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl name = 'AddonFilterMediaPluginHandler'; filterName = 'mediaplugin'; - protected template = document.createElement('template'); // A template element to convert HTML to element. - /** * @inheritdoc */ filter(text: string): string | Promise { - this.template.innerHTML = text; + CoreTemplateElement.innerHTML = text; - const videos = Array.from(this.template.content.querySelectorAll('video')); + const videos = Array.from(CoreTemplateElement.content.querySelectorAll('video')); videos.forEach((video) => { AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video); }); - return this.template.innerHTML; + return CoreTemplateElement.innerHTML; } /** diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts index 089ade130..29614ff71 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -33,6 +33,7 @@ import { } from '../../services/bigbluebuttonbn'; import { ADDON_MOD_BBB_COMPONENT } from '../../constants'; import { CoreLoadings } from '@services/loadings'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; /** * Component that displays a Big Blue Button activity. @@ -147,7 +148,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo this.recordings = recordingsTable.parsedData.map(recordingData => { const details: RecordingDetail[] = []; - const playbacksEl = CoreDomUtils.convertToElement(String(recordingData.playback)); + const playbacksEl = convertHTMLToHTMLElement(String(recordingData.playback)); const playbacks: RecordingPlayback[] = Array.from(playbacksEl.querySelectorAll('a')).map(playbackAnchor => ({ name: playbackAnchor.textContent ?? '', url: playbackAnchor.href, @@ -164,7 +165,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo value = CoreTimeUtils.userDate(Number(value), 'core.strftimedaydate'); } else if (columnData.allowHTML && typeof value === 'string') { // If the HTML is empty, don't display it. - const valueElement = CoreDomUtils.convertToElement(value); + const valueElement = convertHTMLToHTMLElement(value); if (!valueElement.querySelector('img') && (valueElement.textContent ?? '').trim() === '') { return; } diff --git a/src/addons/mod/folder/services/handlers/module.ts b/src/addons/mod/folder/services/handlers/module.ts index aefa39df7..4470e98f5 100644 --- a/src/addons/mod/folder/services/handlers/module.ts +++ b/src/addons/mod/folder/services/handlers/module.ts @@ -18,7 +18,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand import { CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreNavigator } from '@services/navigator'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { makeSingleton } from '@singletons'; import { ADDON_MOD_FOLDER_PAGE_NAME } from '../../constants'; @@ -58,7 +58,7 @@ export class AddonModFolderModuleHandlerService extends CoreModuleHandlerBase im if (module.description) { // Module description can contain the folder contents if it's inline, remove it. - const descriptionElement = CoreDomUtils.convertToElement(module.description); + const descriptionElement = convertHTMLToHTMLElement(module.description); Array.from(descriptionElement.querySelectorAll('.foldertree, .folderbuttons, .tertiary-navigation')) .forEach(element => element.remove()); diff --git a/src/addons/mod/lesson/services/lesson-helper.ts b/src/addons/mod/lesson/services/lesson-helper.ts index d54343733..25e014a1a 100644 --- a/src/addons/mod/lesson/services/lesson-helper.ts +++ b/src/addons/mod/lesson/services/lesson-helper.ts @@ -28,6 +28,7 @@ import { import { CoreTime } from '@singletons/time'; import { CoreUtils } from '@services/utils/utils'; import { AddonModLessonPageSubtype } from '../constants'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; /** * Helper service that provides some features for quiz. @@ -46,7 +47,7 @@ export class AddonModLessonHelperProvider { * @returns Formatted data. */ formatActivityLink(activityLink: string): AddonModLessonActivityLink { - const element = CoreDomUtils.convertToElement(activityLink); + const element = convertHTMLToHTMLElement(activityLink); const anchor = element.querySelector('a'); if (!anchor) { @@ -76,7 +77,7 @@ export class AddonModLessonHelperProvider { buttonText: '', content: '', }; - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); // Search the input button. const button = element.querySelector('input[type="button"]'); @@ -100,7 +101,7 @@ export class AddonModLessonHelperProvider { */ getPageButtonsFromHtml(html: string): AddonModLessonPageButton[] { const buttons: AddonModLessonPageButton[] = []; - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); // Get the container of the buttons if it exists. let buttonsContainer = element.querySelector('.branchbuttoncontainer'); @@ -152,7 +153,7 @@ export class AddonModLessonHelperProvider { */ getPageContentsFromPageData(data: AddonModLessonGetPageDataWSResponse): string { // Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered. - const element = CoreDomUtils.convertToElement(data.pagecontent || ''); + const element = convertHTMLToHTMLElement(data.pagecontent || ''); const contents = element.querySelector('.contents'); if (contents) { @@ -178,7 +179,7 @@ export class AddonModLessonHelperProvider { * @returns Question data. */ getQuestionFromPageData(questionForm: FormGroup, pageData: AddonModLessonGetPageDataWSResponse): AddonModLessonQuestion { - const element = CoreDomUtils.convertToElement(pageData.pagecontent || ''); + const element = convertHTMLToHTMLElement(pageData.pagecontent || ''); // Get the container of the question answers if it exists. const fieldContainer = element.querySelector('.fcontainer'); @@ -463,7 +464,7 @@ export class AddonModLessonHelperProvider { * @returns Object with the data to render the answer. If the answer doesn't require any parsing, return a string with the HTML. */ getQuestionPageAnswerDataFromHtml(html: string): AddonModLessonAnswerData { - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); // Check if it has a checkbox. let input = element.querySelector('input[type="checkbox"][name*="answer"]'); @@ -588,7 +589,7 @@ export class AddonModLessonHelperProvider { * @returns Feedback without the question text. */ removeQuestionFromFeedback(html: string): string { - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); // Remove the question text. CoreDomUtils.removeElement(element, '.generalbox:not(.feedback):not(.correctanswer)'); diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts index a6f2df32c..dd07e4ae8 100644 --- a/src/addons/mod/lesson/services/lesson.ts +++ b/src/addons/mod/lesson/services/lesson.ts @@ -18,7 +18,7 @@ import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; @@ -164,7 +164,7 @@ export class AddonModLessonProvider { if (page.answerdata && !this.answerPageIsQuestion(page)) { // It isn't a question page, but it can be an end of branch, etc. Check if the first answer has a button. if (page.answerdata.answers && page.answerdata.answers[0]) { - const element = CoreDomUtils.convertToElement(page.answerdata.answers[0][0]); + const element = convertHTMLToHTMLElement(page.answerdata.answers[0][0]); return !!element.querySelector('input[type="button"]'); } diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts index 4026938fb..1113eb95a 100644 --- a/src/addons/mod/quiz/services/quiz-helper.ts +++ b/src/addons/mod/quiz/services/quiz-helper.ts @@ -42,6 +42,7 @@ import { CoreGroups } from '@services/groups'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreModals } from '@services/modals'; import { CoreLoadings } from '@services/loadings'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; /** * Helper service that provides some features for quiz. @@ -301,7 +302,7 @@ export class AddonModQuizHelperProvider { * @returns Question's mark. */ getQuestionMarkFromHtml(html: string): string | undefined { - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); return CoreDomUtils.getContentsOfElement(element, '.grade'); } diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index 07604d170..80cdc81ee 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -29,7 +29,7 @@ import { } from '@features/question/services/question'; import { CoreQuestionDelegate } from '@features/question/services/question-delegate'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; @@ -1588,7 +1588,7 @@ export class AddonModQuizProvider { * @returns Whether it's blocked. */ isQuestionBlocked(question: CoreQuestionQuestionParsed): boolean { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); return !!element.querySelector('.mod_quiz-blocked_question_warning'); } diff --git a/src/addons/qtype/calculated/services/handlers/calculated.ts b/src/addons/qtype/calculated/services/handlers/calculated.ts index 58f6ac24a..5c42adc04 100644 --- a/src/addons/qtype/calculated/services/handlers/calculated.ts +++ b/src/addons/qtype/calculated/services/handlers/calculated.ts @@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question'; import { CoreQuestionHandler } from '@features/question/services/question-delegate'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonQtypeCalculatedComponent } from '../../component/calculated'; @@ -53,7 +53,7 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler { */ hasSeparateUnitField(question: CoreQuestionQuestionParsed): boolean { if (!question.parsedSettings) { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]')); } diff --git a/src/addons/qtype/essay/services/handlers/essay.ts b/src/addons/qtype/essay/services/handlers/essay.ts index 33d05bd09..517317035 100644 --- a/src/addons/qtype/essay/services/handlers/essay.ts +++ b/src/addons/qtype/essay/services/handlers/essay.ts @@ -22,7 +22,7 @@ import { CoreQuestionHandler } from '@features/question/services/question-delega import { CoreQuestionHelper } from '@features/question/services/question-helper'; import { CoreFileSession } from '@services/file-session'; import { CoreSites } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; @@ -90,7 +90,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { }; } - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); return { text: !!element.querySelector('textarea[name*=_answer]'), @@ -116,7 +116,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { * @inheritdoc */ getPreventSubmitMessage(question: CoreQuestionQuestionParsed): string | undefined { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); const uploadFilesSupported = question.responsefileareas !== undefined; if (!uploadFilesSupported && element.querySelector('div[id*=filemanager]')) { @@ -293,7 +293,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { siteId?: string, ): Promise { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]'); // Search the textarea to get its name. @@ -375,7 +375,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { siteId?: string, ): Promise { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]'); if (attachmentsInput) { @@ -454,7 +454,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { isPlainText = question.parsedSettings.responseformat == 'monospaced' || question.parsedSettings.responseformat == 'plain'; } else { - const questionEl = CoreDomUtils.convertToElement(question.html); + const questionEl = convertHTMLToHTMLElement(question.html); isPlainText = !!questionEl.querySelector('.qtype_essay_monospaced') || !!questionEl.querySelector('.qtype_essay_plain'); } diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts index 88bdcc81f..ad01b6273 100644 --- a/src/core/components/show-password/show-password.ts +++ b/src/core/components/show-password/show-password.ts @@ -14,7 +14,7 @@ import { Component, AfterViewInit, Input, ContentChild, ViewEncapsulation } from '@angular/core'; import { IonInput } from '@ionic/angular'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { CoreUtils } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; @@ -84,7 +84,7 @@ export class CoreShowPasswordComponent implements AfterViewInit { return; } - const toggle = CoreDomUtils.convertToElement(''); + const toggle = convertHTMLToHTMLElement(''); input.parentElement?.appendChild(toggle.children[0]); } diff --git a/src/core/features/course/services/handlers/course-tag-area.ts b/src/core/features/course/services/handlers/course-tag-area.ts index 9c52f9b9a..93cb1dc5c 100644 --- a/src/core/features/course/services/handlers/course-tag-area.ts +++ b/src/core/features/course/services/handlers/course-tag-area.ts @@ -14,7 +14,7 @@ import { Injectable, Type } from '@angular/core'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; import { CoreCourseTagAreaComponent } from '../../components/tag-area/tag-area'; import { makeSingleton } from '@singletons'; @@ -45,7 +45,7 @@ export class CoreCourseTagAreaHandlerService implements CoreTagAreaHandler { */ parseContent(content: string): CoreCouseTagItems[] { const items: CoreCouseTagItems[] = []; - const element = CoreDomUtils.convertToElement(content); + const element = convertHTMLToHTMLElement(content); Array.from(element.querySelectorAll('div.coursebox')).forEach((coursebox) => { const courseId = parseInt(coursebox.getAttribute('data-courseid') || '', 10); diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index 3b76d57f1..be49fb3fd 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -44,6 +44,7 @@ import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseAccess } from '@features/course/services/course-options-delegate'; import { CoreLoadings } from '@services/loadings'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; export const GRADES_PAGE_NAME = 'grades'; export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades'; @@ -573,7 +574,7 @@ export class CoreGradesHelperProvider { const modname = module?.[1]; if (modname !== undefined) { - const modicon = CoreDomUtils.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined; + const modicon = convertHTMLToHTMLElement(text).querySelector('img')?.getAttribute('src') ?? undefined; row.itemtype = 'mod'; row.itemmodule = modname; diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index f54cd05dd..34085904c 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -25,6 +25,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper'; import { ContextLevel } from '@/core/constants'; import { toBoolean } from '@/core/transforms/boolean'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; /** * Base class for components to render a question. @@ -87,7 +88,7 @@ export class CoreQuestionBaseComponent(contentSelector); diff --git a/src/core/features/question/services/question-helper.ts b/src/core/features/question/services/question-helper.ts index ca4e4cbb7..be8afac1e 100644 --- a/src/core/features/question/services/question-helper.ts +++ b/src/core/features/question/services/question-helper.ts @@ -31,6 +31,7 @@ import { CoreUrl } from '@singletons/url'; import { ContextLevel } from '@/core/constants'; import { CoreIonicColorNames } from '@singletons/colors'; import { CoreViewer } from '@features/viewer/services/viewer'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; /** * Service with some common functions to handle questions. @@ -131,7 +132,7 @@ export class CoreQuestionHelperProvider { selector = selector || '.im-controls [type="submit"]'; - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); // Search the buttons. const buttons = Array.from(element.querySelectorAll(selector)); @@ -150,7 +151,7 @@ export class CoreQuestionHelperProvider { * @returns Wether the certainty is found. */ extractQbehaviourCBM(question: CoreQuestionQuestion): boolean { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); const labels = Array.from(element.querySelectorAll('.im-controls .certaintychoices label[for*="certainty"]')); question.behaviourCertaintyOptions = []; @@ -217,7 +218,7 @@ export class CoreQuestionHelperProvider { * @returns Whether the seen input is found. */ extractQbehaviourSeenInput(question: CoreQuestionQuestion): boolean { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); // Search the "seen" input. const seenInput = element.querySelector('input[type="hidden"][name*=seen]'); @@ -273,7 +274,7 @@ export class CoreQuestionHelperProvider { * @param attrName Name of the attribute to store the HTML in. */ protected extractQuestionLastElementNotInContent(question: CoreQuestionQuestion, selector: string, attrName: string): void { - const element = CoreDomUtils.convertToElement(question.html); + const element = convertHTMLToHTMLElement(question.html); const matches = Array.from(element.querySelectorAll(selector)); // Get the last element and check it's not in the question contents. @@ -358,7 +359,7 @@ export class CoreQuestionHelperProvider { * @returns Object where the keys are the names. */ getAllInputNamesFromHtml(html: string): Record { - const element = CoreDomUtils.convertToElement('
' + html + '
'); + const element = convertHTMLToHTMLElement('
' + html + '
'); const form = element.children[0]; const answers: Record = {}; @@ -424,7 +425,7 @@ export class CoreQuestionHelperProvider { * @returns Attachments. */ getQuestionAttachmentsFromHtml(html: string): CoreWSFile[] { - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); // Remove the filemanager (area to attach files to a question). CoreDomUtils.removeElement(element, 'div[id*=filemanager]'); @@ -461,7 +462,7 @@ export class CoreQuestionHelperProvider { } // Search the input holding the sequencecheck. - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); const input = element.querySelector('input[name*=sequencecheck]'); if (!input || input.name === undefined || input.value === undefined) { @@ -531,7 +532,7 @@ export class CoreQuestionHelperProvider { * @returns Validation error message if present. */ getValidationErrorFromHtml(html: string): string | undefined { - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); return CoreDomUtils.getContentsOfElement(element, '.validationerror'); } @@ -583,7 +584,7 @@ export class CoreQuestionHelperProvider { * @param question Question. */ loadLocalAnswersInHtml(question: CoreQuestionQuestion): void { - const element = CoreDomUtils.convertToElement('
' + question.html + '
'); + const element = convertHTMLToHTMLElement('
' + question.html + '
'); const form = element.children[0]; // Search all input elements. @@ -758,7 +759,7 @@ export class CoreQuestionHelperProvider { * @returns Whether the button is found. */ protected searchBehaviourButton(question: CoreQuestionQuestion, htmlProperty: string, selector: string): boolean { - const element = CoreDomUtils.convertToElement(question[htmlProperty]); + const element = convertHTMLToHTMLElement(question[htmlProperty]); const button = element.querySelector(selector); if (!button) { diff --git a/src/core/features/tag/services/tag-helper.ts b/src/core/features/tag/services/tag-helper.ts index becd52a95..da1d45ddf 100644 --- a/src/core/features/tag/services/tag-helper.ts +++ b/src/core/features/tag/services/tag-helper.ts @@ -14,7 +14,7 @@ import { makeSingleton } from '@singletons'; import { Injectable } from '@angular/core'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; /** * Service with helper functions for tags. @@ -30,7 +30,7 @@ export class CoreTagHelperProvider { */ parseFeedContent(content: string): CoreTagFeedElement[] { const items: CoreTagFeedElement[] = []; - const element = CoreDomUtils.convertToElement(content); + const element = convertHTMLToHTMLElement(content); Array.from(element.querySelectorAll('ul.tag_feed > li')).forEach((itemElement) => { const item: CoreTagFeedElement = { details: [] }; diff --git a/src/core/features/user/services/handlers/tag-area.ts b/src/core/features/user/services/handlers/tag-area.ts index 37111a061..9fae87eaf 100644 --- a/src/core/features/user/services/handlers/tag-area.ts +++ b/src/core/features/user/services/handlers/tag-area.ts @@ -14,7 +14,7 @@ import { Injectable, Type } from '@angular/core'; -import { CoreDomUtils } from '@services/utils/dom'; +import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; import { CoreUserTagAreaComponent } from '@features/user/components/tag-area/tag-area'; import { CoreTagFeedElement } from '@features/tag/services/tag-helper'; @@ -47,7 +47,7 @@ export class CoreUserTagAreaHandlerService implements CoreTagAreaHandler { */ parseContent(content: string): CoreUserTagFeedElement[] { const items: CoreUserTagFeedElement[] = []; - const element = CoreDomUtils.convertToElement(content); + const element = convertHTMLToHTMLElement(content); Array.from(element.querySelectorAll('div.user-box')).forEach((userbox: HTMLElement) => { const avatarLink = userbox.querySelector('a:first-child'); diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 863c1523a..77201627a 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -58,6 +58,7 @@ import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CorePath } from '@singletons/path'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreAnalytics, CoreAnalyticsEventType } from './analytics'; +import { convertHTMLToHTMLElement } from '../utils/create-html-element'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -1147,7 +1148,7 @@ export class CoreFilepoolProvider { extractDownloadableFilesFromHtml(html: string): string[] { let urls: string[] = []; - const element = CoreDomUtils.convertToElement(html); + const element = convertHTMLToHTMLElement(html); const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); for (let i = 0; i < elements.length; i++) { diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index b677d06b5..2f677a978 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -55,6 +55,7 @@ import { CorePopovers, OpenPopoverOptions } from '@services/popovers'; import { CoreViewer } from '@features/viewer/services/viewer'; import { CoreLoadings } from '@services/loadings'; import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper'; +import { convertHTMLToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -68,8 +69,6 @@ export class CoreDomUtilsProvider { protected readonly INPUT_SUPPORT_KEYBOARD: string[] = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password', 'search', 'tel', 'text', 'time', 'url', 'week']; - protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element. - protected matchesFunctionName?: string; // Name of the "matches" function to use when simulating a closest call. protected debugDisplay = false; // Whether to display debug messages. Store it in a variable to make it synchronous. protected displayedAlerts: Record = {}; // To prevent duplicated alerts. @@ -196,12 +195,11 @@ export class CoreDomUtilsProvider { * * @param html Text to convert. * @returns Element. + * + * @deprecated since 4.5. Use convertToElement directly instead. */ convertToElement(html: string): HTMLElement { - // Add a div to hold the content, that's the element that will be returned. - this.template.innerHTML = '
' + html + '
'; - - return this.template.content.children[0]; + return convertHTMLToHTMLElement(html); } /** @@ -263,7 +261,7 @@ export class CoreDomUtilsProvider { * @returns Fixed HTML text. */ fixHtml(html: string): string { - this.template.innerHTML = html; + CoreTemplateElement.innerHTML = html; // eslint-disable-next-line no-control-regex const attrNameRegExp = /[^\x00-\x20\x7F-\x9F"'>/=]+/; @@ -278,9 +276,9 @@ export class CoreDomUtilsProvider { Array.from(element.children).forEach(fixElement); }; - Array.from(this.template.content.children).forEach(fixElement); + Array.from(CoreTemplateElement.content.children).forEach(fixElement); - return this.template.innerHTML; + return CoreTemplateElement.innerHTML; } /** @@ -389,7 +387,7 @@ export class CoreDomUtilsProvider { * @returns Attribute value. */ getHTMLElementAttribute(html: string, attribute: string): string | null { - return this.convertToElement(html).children[0].getAttribute(attribute); + return convertHTMLToHTMLElement(html).children[0].getAttribute(attribute); } /** @@ -650,7 +648,7 @@ export class CoreDomUtilsProvider { * @returns HTML without the element. */ removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string { - const element = this.convertToElement(html); + const element = convertHTMLToHTMLElement(html); if (removeAll) { const selected = element.querySelectorAll(selector); @@ -698,7 +696,7 @@ export class CoreDomUtilsProvider { paths: {[url: string]: string}, anchorFn?: (anchor: HTMLElement, href: string) => void, ): string { - const element = this.convertToElement(html); + const element = convertHTMLToHTMLElement(html); // Treat elements with src (img, audio, video, ...). const media = Array.from(element.querySelectorAll('img, video, audio, source, track, iframe, embed')); @@ -1401,7 +1399,7 @@ export class CoreDomUtilsProvider { * @returns Same text converted to HTMLCollection. */ toDom(text: string): HTMLCollection { - const element = this.convertToElement(text); + const element = convertHTMLToHTMLElement(text); return element.children; } diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index a8050115e..e35035472 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -17,9 +17,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver } from '@singletons/events'; import { CorePlatform } from '@services/platform'; import { CoreWait } from './wait'; - -// A template element to convert HTML to element. -export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template'); +import { CoreTemplateElement } from '../utils/create-html-element'; /** * Singleton with helper functions for dom. diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts index 58c066655..9520127a5 100644 --- a/src/core/singletons/text.ts +++ b/src/core/singletons/text.ts @@ -16,7 +16,7 @@ import { Clipboard, Translate } from '@singletons'; import { CoreToasts } from '@services/toasts'; import { Locutus } from './locutus'; import { CoreError } from '@classes/errors/error'; -import { CoreTemplateElement } from './dom'; +import { convertHTMLToHTMLElement } from '../utils/create-html-element'; /** * Singleton with helper functions for text manipulation. @@ -190,7 +190,7 @@ export class CoreText { // First, we use a regexpr. text = text.replace(/(<([^>]+)>)/ig, ''); // Then, we rely on the browser. We need to wrap the text to be sure is HTML. - text = CoreText.convertToElement(text).textContent || ''; + text = convertHTMLToHTMLElement(text).textContent || ''; // Trim text text = options.trim ? text.trim() : text; // Recover or remove new lines. @@ -199,20 +199,6 @@ export class CoreText { return text; } - /** - * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body. - * This function is the same as in DomUtils, but we cannot use that one because of circular dependencies. - * - * @param html Text to convert. - * @returns Element. - */ - protected static convertToElement(html: string): HTMLElement { - // Add a div to hold the content, that's the element that will be returned. - CoreTemplateElement.innerHTML = '
' + html + '
'; - - return CoreTemplateElement.content.children[0]; - } - /** * Decode an escaped HTML text. This implementation is based on PHP's htmlspecialchars_decode. * @@ -243,7 +229,7 @@ export class CoreText { */ static decodeHTMLEntities(text: string): string { if (text) { - text = CoreText.convertToElement(text).textContent || ''; + text = convertHTMLToHTMLElement(text).textContent || ''; } return text; @@ -438,7 +424,7 @@ export class CoreText { * @returns Processed HTML string. */ static processHTML(text: string, process: (element: HTMLElement) => unknown): string { - const element = CoreText.convertToElement(text); + const element = convertHTMLToHTMLElement(text); process(element); diff --git a/src/core/utils/create-html-element.ts b/src/core/utils/create-html-element.ts new file mode 100644 index 000000000..2ce0b29cf --- /dev/null +++ b/src/core/utils/create-html-element.ts @@ -0,0 +1,29 @@ +// (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. + +// A template element to convert HTML to element. +export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template'); + +/** + * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body. + * + * @param html Text to convert. + * @returns Element. + */ +export function convertHTMLToHTMLElement(html: string): HTMLElement { + // Add a div to hold the content, that's the element that will be returned. + CoreTemplateElement.innerHTML = '
' + html + '
'; + + return CoreTemplateElement.content.children[0]; +} From b0c494ee51008cc379273c0d1e7cc93e2de8578d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 19 Jul 2024 15:54:15 +0200 Subject: [PATCH 3/4] MOBILE-4616 chore: Move html mode classes to CoreHTMLClasses --- .github/workflows/testing.yml | 2 +- .../bigbluebuttonbn/components/index/index.ts | 6 +- .../mod/folder/services/handlers/module.ts | 4 +- .../mod/lesson/services/lesson-helper.ts | 16 ++-- src/addons/mod/lesson/services/lesson.ts | 4 +- src/addons/mod/quiz/services/quiz-helper.ts | 4 +- src/addons/mod/quiz/services/quiz.ts | 4 +- .../services/handlers/calculated.ts | 4 +- .../qtype/essay/services/handlers/essay.ts | 12 +-- .../components/show-password/show-password.ts | 4 +- .../services/handlers/course-tag-area.ts | 4 +- .../features/grades/services/grades-helper.ts | 4 +- .../classes/base-question-component.ts | 6 +- .../question/services/question-helper.ts | 22 +++--- .../settings/services/settings-helper.ts | 5 +- src/core/features/tag/services/tag-helper.ts | 4 +- .../user/services/handlers/tag-area.ts | 4 +- src/core/services/filepool.ts | 4 +- src/core/services/network.ts | 14 ++-- src/core/services/utils/dom.ts | 25 ++++--- src/core/singletons/html-classes.ts | 74 ++++++++++++++----- src/core/singletons/text.ts | 8 +- src/core/utils/create-html-element.ts | 2 +- 23 files changed, 141 insertions(+), 95 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d6c708a28..7f710b31b 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -69,7 +69,7 @@ jobs: cat circular-dependencies lines=$(cat circular-dependencies | wc -l) echo "Total circular dependencies: $lines" - test $lines -eq 131 + test $lines -eq 130 - name: JavaScript code compatibility run: | npx check-es-compat www/*.js --polyfills="\{Array,String,TypedArray\}.prototype.at,Object.hasOwn" diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.ts b/src/addons/mod/bigbluebuttonbn/components/index/index.ts index 29614ff71..2c3e5d53f 100644 --- a/src/addons/mod/bigbluebuttonbn/components/index/index.ts +++ b/src/addons/mod/bigbluebuttonbn/components/index/index.ts @@ -33,7 +33,7 @@ import { } from '../../services/bigbluebuttonbn'; import { ADDON_MOD_BBB_COMPONENT } from '../../constants'; import { CoreLoadings } from '@services/loadings'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; /** * Component that displays a Big Blue Button activity. @@ -148,7 +148,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo this.recordings = recordingsTable.parsedData.map(recordingData => { const details: RecordingDetail[] = []; - const playbacksEl = convertHTMLToHTMLElement(String(recordingData.playback)); + const playbacksEl = convertTextToHTMLElement(String(recordingData.playback)); const playbacks: RecordingPlayback[] = Array.from(playbacksEl.querySelectorAll('a')).map(playbackAnchor => ({ name: playbackAnchor.textContent ?? '', url: playbackAnchor.href, @@ -165,7 +165,7 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo value = CoreTimeUtils.userDate(Number(value), 'core.strftimedaydate'); } else if (columnData.allowHTML && typeof value === 'string') { // If the HTML is empty, don't display it. - const valueElement = convertHTMLToHTMLElement(value); + const valueElement = convertTextToHTMLElement(value); if (!valueElement.querySelector('img') && (valueElement.textContent ?? '').trim() === '') { return; } diff --git a/src/addons/mod/folder/services/handlers/module.ts b/src/addons/mod/folder/services/handlers/module.ts index 4470e98f5..d0603a298 100644 --- a/src/addons/mod/folder/services/handlers/module.ts +++ b/src/addons/mod/folder/services/handlers/module.ts @@ -18,7 +18,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand import { CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreNavigator } from '@services/navigator'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { makeSingleton } from '@singletons'; import { ADDON_MOD_FOLDER_PAGE_NAME } from '../../constants'; @@ -58,7 +58,7 @@ export class AddonModFolderModuleHandlerService extends CoreModuleHandlerBase im if (module.description) { // Module description can contain the folder contents if it's inline, remove it. - const descriptionElement = convertHTMLToHTMLElement(module.description); + const descriptionElement = convertTextToHTMLElement(module.description); Array.from(descriptionElement.querySelectorAll('.foldertree, .folderbuttons, .tertiary-navigation')) .forEach(element => element.remove()); diff --git a/src/addons/mod/lesson/services/lesson-helper.ts b/src/addons/mod/lesson/services/lesson-helper.ts index 25e014a1a..1bd5c7397 100644 --- a/src/addons/mod/lesson/services/lesson-helper.ts +++ b/src/addons/mod/lesson/services/lesson-helper.ts @@ -28,7 +28,7 @@ import { import { CoreTime } from '@singletons/time'; import { CoreUtils } from '@services/utils/utils'; import { AddonModLessonPageSubtype } from '../constants'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; /** * Helper service that provides some features for quiz. @@ -47,7 +47,7 @@ export class AddonModLessonHelperProvider { * @returns Formatted data. */ formatActivityLink(activityLink: string): AddonModLessonActivityLink { - const element = convertHTMLToHTMLElement(activityLink); + const element = convertTextToHTMLElement(activityLink); const anchor = element.querySelector('a'); if (!anchor) { @@ -77,7 +77,7 @@ export class AddonModLessonHelperProvider { buttonText: '', content: '', }; - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); // Search the input button. const button = element.querySelector('input[type="button"]'); @@ -101,7 +101,7 @@ export class AddonModLessonHelperProvider { */ getPageButtonsFromHtml(html: string): AddonModLessonPageButton[] { const buttons: AddonModLessonPageButton[] = []; - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); // Get the container of the buttons if it exists. let buttonsContainer = element.querySelector('.branchbuttoncontainer'); @@ -153,7 +153,7 @@ export class AddonModLessonHelperProvider { */ getPageContentsFromPageData(data: AddonModLessonGetPageDataWSResponse): string { // Search the page contents inside the whole page HTML. Use data.pagecontent because it's filtered. - const element = convertHTMLToHTMLElement(data.pagecontent || ''); + const element = convertTextToHTMLElement(data.pagecontent || ''); const contents = element.querySelector('.contents'); if (contents) { @@ -179,7 +179,7 @@ export class AddonModLessonHelperProvider { * @returns Question data. */ getQuestionFromPageData(questionForm: FormGroup, pageData: AddonModLessonGetPageDataWSResponse): AddonModLessonQuestion { - const element = convertHTMLToHTMLElement(pageData.pagecontent || ''); + const element = convertTextToHTMLElement(pageData.pagecontent || ''); // Get the container of the question answers if it exists. const fieldContainer = element.querySelector('.fcontainer'); @@ -464,7 +464,7 @@ export class AddonModLessonHelperProvider { * @returns Object with the data to render the answer. If the answer doesn't require any parsing, return a string with the HTML. */ getQuestionPageAnswerDataFromHtml(html: string): AddonModLessonAnswerData { - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); // Check if it has a checkbox. let input = element.querySelector('input[type="checkbox"][name*="answer"]'); @@ -589,7 +589,7 @@ export class AddonModLessonHelperProvider { * @returns Feedback without the question text. */ removeQuestionFromFeedback(html: string): string { - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); // Remove the question text. CoreDomUtils.removeElement(element, '.generalbox:not(.feedback):not(.correctanswer)'); diff --git a/src/addons/mod/lesson/services/lesson.ts b/src/addons/mod/lesson/services/lesson.ts index dd07e4ae8..b9dcd4105 100644 --- a/src/addons/mod/lesson/services/lesson.ts +++ b/src/addons/mod/lesson/services/lesson.ts @@ -18,7 +18,7 @@ import { CoreSite } from '@classes/sites/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; @@ -164,7 +164,7 @@ export class AddonModLessonProvider { if (page.answerdata && !this.answerPageIsQuestion(page)) { // It isn't a question page, but it can be an end of branch, etc. Check if the first answer has a button. if (page.answerdata.answers && page.answerdata.answers[0]) { - const element = convertHTMLToHTMLElement(page.answerdata.answers[0][0]); + const element = convertTextToHTMLElement(page.answerdata.answers[0][0]); return !!element.querySelector('input[type="button"]'); } diff --git a/src/addons/mod/quiz/services/quiz-helper.ts b/src/addons/mod/quiz/services/quiz-helper.ts index 1113eb95a..d1461a886 100644 --- a/src/addons/mod/quiz/services/quiz-helper.ts +++ b/src/addons/mod/quiz/services/quiz-helper.ts @@ -42,7 +42,7 @@ import { CoreGroups } from '@services/groups'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreModals } from '@services/modals'; import { CoreLoadings } from '@services/loadings'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; /** * Helper service that provides some features for quiz. @@ -302,7 +302,7 @@ export class AddonModQuizHelperProvider { * @returns Question's mark. */ getQuestionMarkFromHtml(html: string): string | undefined { - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); return CoreDomUtils.getContentsOfElement(element, '.grade'); } diff --git a/src/addons/mod/quiz/services/quiz.ts b/src/addons/mod/quiz/services/quiz.ts index 80cdc81ee..7384b276e 100644 --- a/src/addons/mod/quiz/services/quiz.ts +++ b/src/addons/mod/quiz/services/quiz.ts @@ -29,7 +29,7 @@ import { } from '@features/question/services/question'; import { CoreQuestionDelegate } from '@features/question/services/question-delegate'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; @@ -1588,7 +1588,7 @@ export class AddonModQuizProvider { * @returns Whether it's blocked. */ isQuestionBlocked(question: CoreQuestionQuestionParsed): boolean { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); return !!element.querySelector('.mod_quiz-blocked_question_warning'); } diff --git a/src/addons/qtype/calculated/services/handlers/calculated.ts b/src/addons/qtype/calculated/services/handlers/calculated.ts index 5c42adc04..5bc610356 100644 --- a/src/addons/qtype/calculated/services/handlers/calculated.ts +++ b/src/addons/qtype/calculated/services/handlers/calculated.ts @@ -16,7 +16,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question'; import { CoreQuestionHandler } from '@features/question/services/question-delegate'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonQtypeCalculatedComponent } from '../../component/calculated'; @@ -53,7 +53,7 @@ export class AddonQtypeCalculatedHandlerService implements CoreQuestionHandler { */ hasSeparateUnitField(question: CoreQuestionQuestionParsed): boolean { if (!question.parsedSettings) { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); return !!(element.querySelector('select[name*=unit]') || element.querySelector('input[type="radio"]')); } diff --git a/src/addons/qtype/essay/services/handlers/essay.ts b/src/addons/qtype/essay/services/handlers/essay.ts index 517317035..cc8166546 100644 --- a/src/addons/qtype/essay/services/handlers/essay.ts +++ b/src/addons/qtype/essay/services/handlers/essay.ts @@ -22,7 +22,7 @@ import { CoreQuestionHandler } from '@features/question/services/question-delega import { CoreQuestionHelper } from '@features/question/services/question-helper'; import { CoreFileSession } from '@services/file-session'; import { CoreSites } from '@services/sites'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreText } from '@singletons/text'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; @@ -90,7 +90,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { }; } - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); return { text: !!element.querySelector('textarea[name*=_answer]'), @@ -116,7 +116,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { * @inheritdoc */ getPreventSubmitMessage(question: CoreQuestionQuestionParsed): string | undefined { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); const uploadFilesSupported = question.responsefileareas !== undefined; if (!uploadFilesSupported && element.querySelector('div[id*=filemanager]')) { @@ -293,7 +293,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { siteId?: string, ): Promise { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]'); // Search the textarea to get its name. @@ -375,7 +375,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { siteId?: string, ): Promise { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); const attachmentsInput = element.querySelector('.attachments input[name*=_attachments]'); if (attachmentsInput) { @@ -454,7 +454,7 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { isPlainText = question.parsedSettings.responseformat == 'monospaced' || question.parsedSettings.responseformat == 'plain'; } else { - const questionEl = convertHTMLToHTMLElement(question.html); + const questionEl = convertTextToHTMLElement(question.html); isPlainText = !!questionEl.querySelector('.qtype_essay_monospaced') || !!questionEl.querySelector('.qtype_essay_plain'); } diff --git a/src/core/components/show-password/show-password.ts b/src/core/components/show-password/show-password.ts index ad01b6273..12ca321ca 100644 --- a/src/core/components/show-password/show-password.ts +++ b/src/core/components/show-password/show-password.ts @@ -14,7 +14,7 @@ import { Component, AfterViewInit, Input, ContentChild, ViewEncapsulation } from '@angular/core'; import { IonInput } from '@ionic/angular'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreUtils } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; @@ -84,7 +84,7 @@ export class CoreShowPasswordComponent implements AfterViewInit { return; } - const toggle = convertHTMLToHTMLElement(''); + const toggle = convertTextToHTMLElement(''); input.parentElement?.appendChild(toggle.children[0]); } diff --git a/src/core/features/course/services/handlers/course-tag-area.ts b/src/core/features/course/services/handlers/course-tag-area.ts index 93cb1dc5c..e6b1de384 100644 --- a/src/core/features/course/services/handlers/course-tag-area.ts +++ b/src/core/features/course/services/handlers/course-tag-area.ts @@ -14,7 +14,7 @@ import { Injectable, Type } from '@angular/core'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; import { CoreCourseTagAreaComponent } from '../../components/tag-area/tag-area'; import { makeSingleton } from '@singletons'; @@ -45,7 +45,7 @@ export class CoreCourseTagAreaHandlerService implements CoreTagAreaHandler { */ parseContent(content: string): CoreCouseTagItems[] { const items: CoreCouseTagItems[] = []; - const element = convertHTMLToHTMLElement(content); + const element = convertTextToHTMLElement(content); Array.from(element.querySelectorAll('div.coursebox')).forEach((coursebox) => { const courseId = parseInt(coursebox.getAttribute('data-courseid') || '', 10); diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts index be49fb3fd..e7a6b6632 100644 --- a/src/core/features/grades/services/grades-helper.ts +++ b/src/core/features/grades/services/grades-helper.ts @@ -44,7 +44,7 @@ import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseAccess } from '@features/course/services/course-options-delegate'; import { CoreLoadings } from '@services/loadings'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; export const GRADES_PAGE_NAME = 'grades'; export const GRADES_PARTICIPANTS_PAGE_NAME = 'participant-grades'; @@ -574,7 +574,7 @@ export class CoreGradesHelperProvider { const modname = module?.[1]; if (modname !== undefined) { - const modicon = convertHTMLToHTMLElement(text).querySelector('img')?.getAttribute('src') ?? undefined; + const modicon = convertTextToHTMLElement(text).querySelector('img')?.getAttribute('src') ?? undefined; row.itemtype = 'mod'; row.itemmodule = modname; diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index 34085904c..2c7c13cd5 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -25,7 +25,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper'; import { ContextLevel } from '@/core/constants'; import { toBoolean } from '@/core/transforms/boolean'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; /** * Base class for components to render a question. @@ -88,7 +88,7 @@ export class CoreQuestionBaseComponent(contentSelector); diff --git a/src/core/features/question/services/question-helper.ts b/src/core/features/question/services/question-helper.ts index be8afac1e..736d3a625 100644 --- a/src/core/features/question/services/question-helper.ts +++ b/src/core/features/question/services/question-helper.ts @@ -31,7 +31,7 @@ import { CoreUrl } from '@singletons/url'; import { ContextLevel } from '@/core/constants'; import { CoreIonicColorNames } from '@singletons/colors'; import { CoreViewer } from '@features/viewer/services/viewer'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; /** * Service with some common functions to handle questions. @@ -132,7 +132,7 @@ export class CoreQuestionHelperProvider { selector = selector || '.im-controls [type="submit"]'; - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); // Search the buttons. const buttons = Array.from(element.querySelectorAll(selector)); @@ -151,7 +151,7 @@ export class CoreQuestionHelperProvider { * @returns Wether the certainty is found. */ extractQbehaviourCBM(question: CoreQuestionQuestion): boolean { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); const labels = Array.from(element.querySelectorAll('.im-controls .certaintychoices label[for*="certainty"]')); question.behaviourCertaintyOptions = []; @@ -218,7 +218,7 @@ export class CoreQuestionHelperProvider { * @returns Whether the seen input is found. */ extractQbehaviourSeenInput(question: CoreQuestionQuestion): boolean { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); // Search the "seen" input. const seenInput = element.querySelector('input[type="hidden"][name*=seen]'); @@ -274,7 +274,7 @@ export class CoreQuestionHelperProvider { * @param attrName Name of the attribute to store the HTML in. */ protected extractQuestionLastElementNotInContent(question: CoreQuestionQuestion, selector: string, attrName: string): void { - const element = convertHTMLToHTMLElement(question.html); + const element = convertTextToHTMLElement(question.html); const matches = Array.from(element.querySelectorAll(selector)); // Get the last element and check it's not in the question contents. @@ -359,7 +359,7 @@ export class CoreQuestionHelperProvider { * @returns Object where the keys are the names. */ getAllInputNamesFromHtml(html: string): Record { - const element = convertHTMLToHTMLElement('
' + html + '
'); + const element = convertTextToHTMLElement('
' + html + '
'); const form = element.children[0]; const answers: Record = {}; @@ -425,7 +425,7 @@ export class CoreQuestionHelperProvider { * @returns Attachments. */ getQuestionAttachmentsFromHtml(html: string): CoreWSFile[] { - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); // Remove the filemanager (area to attach files to a question). CoreDomUtils.removeElement(element, 'div[id*=filemanager]'); @@ -462,7 +462,7 @@ export class CoreQuestionHelperProvider { } // Search the input holding the sequencecheck. - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); const input = element.querySelector('input[name*=sequencecheck]'); if (!input || input.name === undefined || input.value === undefined) { @@ -532,7 +532,7 @@ export class CoreQuestionHelperProvider { * @returns Validation error message if present. */ getValidationErrorFromHtml(html: string): string | undefined { - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); return CoreDomUtils.getContentsOfElement(element, '.validationerror'); } @@ -584,7 +584,7 @@ export class CoreQuestionHelperProvider { * @param question Question. */ loadLocalAnswersInHtml(question: CoreQuestionQuestion): void { - const element = convertHTMLToHTMLElement('
' + question.html + '
'); + const element = convertTextToHTMLElement('
' + question.html + '
'); const form = element.children[0]; // Search all input elements. @@ -759,7 +759,7 @@ export class CoreQuestionHelperProvider { * @returns Whether the button is found. */ protected searchBehaviourButton(question: CoreQuestionQuestion, htmlProperty: string, selector: string): boolean { - const element = convertHTMLToHTMLElement(question[htmlProperty]); + const element = convertTextToHTMLElement(question[htmlProperty]); const button = element.querySelector(selector); if (!button) { diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index de0febbf4..0936acaca 100644 --- a/src/core/features/settings/services/settings-helper.ts +++ b/src/core/features/settings/services/settings-helper.ts @@ -31,6 +31,7 @@ import { CoreError } from '@classes/errors/error'; import { Observable, Subject } from 'rxjs'; import { CoreErrorHelper } from '@services/error-helper'; import { CoreNavigator } from '@services/navigator'; +import { CoreHTMLClasses } from '@singletons/html-classes'; /** * Object with space usage and cache entries that can be erased. @@ -433,10 +434,10 @@ export class CoreSettingsHelperProvider { * @param enable True to enable dark mode, false to disable. */ protected toggleDarkMode(enable: boolean = false): void { - const isDark = CoreDomUtils.hasModeClass('dark'); + const isDark = CoreHTMLClasses.hasModeClass('dark'); if (isDark !== enable) { - CoreDomUtils.toggleModeClass('dark', enable); + CoreHTMLClasses.toggleModeClass('dark', enable); this.darkModeObservable.next(enable); CoreApp.setSystemUIColors(); diff --git a/src/core/features/tag/services/tag-helper.ts b/src/core/features/tag/services/tag-helper.ts index da1d45ddf..448dcfd59 100644 --- a/src/core/features/tag/services/tag-helper.ts +++ b/src/core/features/tag/services/tag-helper.ts @@ -14,7 +14,7 @@ import { makeSingleton } from '@singletons'; import { Injectable } from '@angular/core'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; /** * Service with helper functions for tags. @@ -30,7 +30,7 @@ export class CoreTagHelperProvider { */ parseFeedContent(content: string): CoreTagFeedElement[] { const items: CoreTagFeedElement[] = []; - const element = convertHTMLToHTMLElement(content); + const element = convertTextToHTMLElement(content); Array.from(element.querySelectorAll('ul.tag_feed > li')).forEach((itemElement) => { const item: CoreTagFeedElement = { details: [] }; diff --git a/src/core/features/user/services/handlers/tag-area.ts b/src/core/features/user/services/handlers/tag-area.ts index 9fae87eaf..4e5c6761f 100644 --- a/src/core/features/user/services/handlers/tag-area.ts +++ b/src/core/features/user/services/handlers/tag-area.ts @@ -14,7 +14,7 @@ import { Injectable, Type } from '@angular/core'; -import { convertHTMLToHTMLElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate'; import { CoreUserTagAreaComponent } from '@features/user/components/tag-area/tag-area'; import { CoreTagFeedElement } from '@features/tag/services/tag-helper'; @@ -47,7 +47,7 @@ export class CoreUserTagAreaHandlerService implements CoreTagAreaHandler { */ parseContent(content: string): CoreUserTagFeedElement[] { const items: CoreUserTagFeedElement[] = []; - const element = convertHTMLToHTMLElement(content); + const element = convertTextToHTMLElement(content); Array.from(element.querySelectorAll('div.user-box')).forEach((userbox: HTMLElement) => { const avatarLink = userbox.querySelector('a:first-child'); diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 77201627a..e0417317f 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -58,7 +58,7 @@ import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { CorePath } from '@singletons/path'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreAnalytics, CoreAnalyticsEventType } from './analytics'; -import { convertHTMLToHTMLElement } from '../utils/create-html-element'; +import { convertTextToHTMLElement } from '../utils/create-html-element'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -1148,7 +1148,7 @@ export class CoreFilepoolProvider { extractDownloadableFilesFromHtml(html: string): string[] { let urls: string[] = []; - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track')); for (let i = 0; i < elements.length; i++) { diff --git a/src/core/services/network.ts b/src/core/services/network.ts index 3c8cc18f9..35597f4f5 100644 --- a/src/core/services/network.ts +++ b/src/core/services/network.ts @@ -17,7 +17,7 @@ import { CorePlatform } from '@services/platform'; import { Network } from '@awesome-cordova-plugins/network/ngx'; import { NgZone, makeSingleton } from '@singletons'; import { Observable, Subject, merge } from 'rxjs'; -import { CoreDomUtils } from './utils/dom'; +import { CoreHTMLClasses } from '@singletons/html-classes'; export enum CoreNetworkConnection { UNKNOWN = 'unknown', @@ -109,24 +109,24 @@ export class CoreNetworkService extends Network { NgZone.run(() => { const isOnline = this.isOnline(); - const hadOfflineMessage = CoreDomUtils.hasModeClass('core-offline'); + const hadOfflineMessage = CoreHTMLClasses.hasModeClass('core-offline'); - CoreDomUtils.toggleModeClass('core-offline', !isOnline); + CoreHTMLClasses.toggleModeClass('core-offline', !isOnline); if (isOnline && hadOfflineMessage) { - CoreDomUtils.toggleModeClass('core-online', true); + CoreHTMLClasses.toggleModeClass('core-online', true); setTimeout(() => { - CoreDomUtils.toggleModeClass('core-online', false); + CoreHTMLClasses.toggleModeClass('core-online', false); }, 3000); } else if (!isOnline) { - CoreDomUtils.toggleModeClass('core-online', false); + CoreHTMLClasses.toggleModeClass('core-online', false); } }); }); const isOnline = this.isOnline(); - CoreDomUtils.toggleModeClass('core-offline', !isOnline); + CoreHTMLClasses.toggleModeClass('core-offline', !isOnline); } /** diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 2f677a978..f074d20c8 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -55,7 +55,8 @@ import { CorePopovers, OpenPopoverOptions } from '@services/popovers'; import { CoreViewer } from '@features/viewer/services/viewer'; import { CoreLoadings } from '@services/loadings'; import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper'; -import { convertHTMLToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element'; +import { CoreHTMLClasses } from '@singletons/html-classes'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -199,7 +200,7 @@ export class CoreDomUtilsProvider { * @deprecated since 4.5. Use convertToElement directly instead. */ convertToElement(html: string): HTMLElement { - return convertHTMLToHTMLElement(html); + return convertTextToHTMLElement(html); } /** @@ -387,7 +388,7 @@ export class CoreDomUtilsProvider { * @returns Attribute value. */ getHTMLElementAttribute(html: string, attribute: string): string | null { - return convertHTMLToHTMLElement(html).children[0].getAttribute(attribute); + return convertTextToHTMLElement(html).children[0].getAttribute(attribute); } /** @@ -648,7 +649,7 @@ export class CoreDomUtilsProvider { * @returns HTML without the element. */ removeElementFromHtml(html: string, selector: string, removeAll?: boolean): string { - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); if (removeAll) { const selected = element.querySelectorAll(selector); @@ -696,7 +697,7 @@ export class CoreDomUtilsProvider { paths: {[url: string]: string}, anchorFn?: (anchor: HTMLElement, href: string) => void, ): string { - const element = convertHTMLToHTMLElement(html); + const element = convertTextToHTMLElement(html); // Treat elements with src (img, audio, video, ...). const media = Array.from(element.querySelectorAll('img, video, audio, source, track, iframe, embed')); @@ -1399,7 +1400,7 @@ export class CoreDomUtilsProvider { * @returns Same text converted to HTMLCollection. */ toDom(text: string): HTMLCollection { - const element = convertHTMLToHTMLElement(text); + const element = convertTextToHTMLElement(text); return element.children; } @@ -1610,18 +1611,22 @@ export class CoreDomUtilsProvider { * * @param className Class name. * @returns Whether the CSS class is set. + * + * @deprecated since 4.5. Use CoreHTMLClasses.hasModeClass instead. */ hasModeClass(className: string): boolean { - return document.documentElement.classList.contains(className); + return CoreHTMLClasses.hasModeClass(className); } /** * Get active mode CSS classes. * * @returns Mode classes. + * + * @deprecated since 4.5. Use CoreHTMLClasses.getModeClasses instead. */ getModeClasses(): string[] { - return Array.from(document.documentElement.classList); + return CoreHTMLClasses.getModeClasses(); } /** @@ -1629,12 +1634,14 @@ export class CoreDomUtilsProvider { * * @param className Class name. * @param enable Whether to add or remove the class. + * + * @deprecated since 4.5. Use CoreHTMLClasses.toggleModeClass instead. */ toggleModeClass( className: string, enable = false, ): void { - document.documentElement.classList.toggle(className, enable); + CoreHTMLClasses.toggleModeClass(className, enable); } } diff --git a/src/core/singletons/html-classes.ts b/src/core/singletons/html-classes.ts index ff74fda5b..9803f1e19 100644 --- a/src/core/singletons/html-classes.ts +++ b/src/core/singletons/html-classes.ts @@ -14,29 +14,31 @@ import { CoreSiteInfo, CoreSiteInfoResponse } from '@classes/sites/unauthenticated-site'; import { CoreSites } from '@services/sites'; -import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrl } from './url'; import { CoreConstants } from '../constants'; import { ScrollDetail } from '@ionic/angular'; import { CoreDom } from './dom'; -const MOODLE_SITE_URL_PREFIX = 'url-'; -const MOODLE_VERSION_PREFIX = 'version-'; -const MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; -const MOODLE_SITE_THEME_PREFIX = 'theme-site-'; - /** * Singleton with helper functions to manage HTML classes. */ export class CoreHTMLClasses { + protected static readonly MOODLE_SITE_URL_PREFIX = 'url-'; + protected static readonly MOODLE_VERSION_PREFIX = 'version-'; + protected static readonly MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; + protected static readonly MOODLE_SITE_THEME_PREFIX = 'theme-site-'; + /** * Initialize HTML classes. */ static initialize(): void { - CoreDomUtils.toggleModeClass('ionic8', true); - CoreDomUtils.toggleModeClass('development', CoreConstants.BUILD.isDevelopment); - CoreHTMLClasses.addVersionClass(MOODLEAPP_VERSION_PREFIX, CoreConstants.CONFIG.versionname.replace('-dev', '')); + CoreHTMLClasses.toggleModeClass('ionic8', true); + CoreHTMLClasses.toggleModeClass('development', CoreConstants.BUILD.isDevelopment); + CoreHTMLClasses.addVersionClass( + CoreHTMLClasses.MOODLEAPP_VERSION_PREFIX, + CoreConstants.CONFIG.versionname.replace('-dev', ''), + ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const win = window; @@ -72,9 +74,9 @@ export class CoreHTMLClasses { parts[1] = parts[1] || '0'; parts[2] = parts[2] || '0'; - CoreDomUtils.toggleModeClass(prefix + parts[0], true); - CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1], true); - CoreDomUtils.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true); + CoreHTMLClasses.toggleModeClass(prefix + parts[0], true); + CoreHTMLClasses.toggleModeClass(prefix + parts[0] + '-' + parts[1], true); + CoreHTMLClasses.toggleModeClass(prefix + parts[0] + '-' + parts[1] + '-' + parts[2], true); } /** @@ -83,12 +85,12 @@ export class CoreHTMLClasses { * @param prefixes Prefixes of the class mode to be removed. */ protected static removeModeClasses(prefixes: string[]): void { - for (const modeClass of CoreDomUtils.getModeClasses()) { + for (const modeClass of CoreHTMLClasses.getModeClasses()) { if (!prefixes.some((prefix) => modeClass.startsWith(prefix))) { continue; } - CoreDomUtils.toggleModeClass(modeClass, false); + CoreHTMLClasses.toggleModeClass(modeClass, false); } } @@ -101,11 +103,11 @@ export class CoreHTMLClasses { // Add version classes to html tag. this.removeSiteClasses(); - this.addVersionClass(MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(siteInfo.release || '')); + this.addVersionClass(CoreHTMLClasses.MOODLE_VERSION_PREFIX, CoreSites.getReleaseNumber(siteInfo.release || '')); this.addSiteUrlClass(siteInfo.siteurl); if (siteInfo.theme) { - CoreDomUtils.toggleModeClass(MOODLE_SITE_THEME_PREFIX + siteInfo.theme, true); + CoreHTMLClasses.toggleModeClass(CoreHTMLClasses.MOODLE_SITE_THEME_PREFIX + siteInfo.theme, true); } } @@ -115,7 +117,11 @@ export class CoreHTMLClasses { static removeSiteClasses(): void { // Remove version classes from html tag. this.removeModeClasses( - [MOODLE_VERSION_PREFIX, MOODLE_SITE_URL_PREFIX, MOODLE_SITE_THEME_PREFIX], + [ + CoreHTMLClasses.MOODLE_VERSION_PREFIX, + CoreHTMLClasses.MOODLE_SITE_URL_PREFIX, + CoreHTMLClasses.MOODLE_SITE_THEME_PREFIX, + ], ); } @@ -157,7 +163,39 @@ export class CoreHTMLClasses { static addSiteUrlClass(siteUrl: string): void { const className = this.urlToClassName(siteUrl); - CoreDomUtils.toggleModeClass(MOODLE_SITE_URL_PREFIX + className, true); + CoreHTMLClasses.toggleModeClass(CoreHTMLClasses.MOODLE_SITE_URL_PREFIX + className, true); + } + + /** + * Check whether a CSS class indicating an app mode is set. + * + * @param className Class name. + * @returns Whether the CSS class is set. + */ + static hasModeClass(className: string): boolean { + return document.documentElement.classList.contains(className); + } + + /** + * Get active mode CSS classes. + * + * @returns Mode classes. + */ + static getModeClasses(): string[] { + return Array.from(document.documentElement.classList); + } + + /** + * Toggle a CSS class in the root element used to indicate app modes. + * + * @param className Class name. + * @param enable Whether to add or remove the class. + */ + static toggleModeClass( + className: string, + enable = false, + ): void { + document.documentElement.classList.toggle(className, enable); } } diff --git a/src/core/singletons/text.ts b/src/core/singletons/text.ts index 9520127a5..482ce044a 100644 --- a/src/core/singletons/text.ts +++ b/src/core/singletons/text.ts @@ -16,7 +16,7 @@ import { Clipboard, Translate } from '@singletons'; import { CoreToasts } from '@services/toasts'; import { Locutus } from './locutus'; import { CoreError } from '@classes/errors/error'; -import { convertHTMLToHTMLElement } from '../utils/create-html-element'; +import { convertTextToHTMLElement } from '../utils/create-html-element'; /** * Singleton with helper functions for text manipulation. @@ -190,7 +190,7 @@ export class CoreText { // First, we use a regexpr. text = text.replace(/(<([^>]+)>)/ig, ''); // Then, we rely on the browser. We need to wrap the text to be sure is HTML. - text = convertHTMLToHTMLElement(text).textContent || ''; + text = convertTextToHTMLElement(text).textContent || ''; // Trim text text = options.trim ? text.trim() : text; // Recover or remove new lines. @@ -229,7 +229,7 @@ export class CoreText { */ static decodeHTMLEntities(text: string): string { if (text) { - text = convertHTMLToHTMLElement(text).textContent || ''; + text = convertTextToHTMLElement(text).textContent || ''; } return text; @@ -424,7 +424,7 @@ export class CoreText { * @returns Processed HTML string. */ static processHTML(text: string, process: (element: HTMLElement) => unknown): string { - const element = convertHTMLToHTMLElement(text); + const element = convertTextToHTMLElement(text); process(element); diff --git a/src/core/utils/create-html-element.ts b/src/core/utils/create-html-element.ts index 2ce0b29cf..60f98e5c2 100644 --- a/src/core/utils/create-html-element.ts +++ b/src/core/utils/create-html-element.ts @@ -21,7 +21,7 @@ export const CoreTemplateElement: HTMLTemplateElement = document.createElement(' * @param html Text to convert. * @returns Element. */ -export function convertHTMLToHTMLElement(html: string): HTMLElement { +export function convertTextToHTMLElement(html: string): HTMLElement { // Add a div to hold the content, that's the element that will be returned. CoreTemplateElement.innerHTML = '
' + html + '
'; From 00951b22d5377354b77a13215c687c85f9ae2cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 14 Aug 2024 14:29:51 +0200 Subject: [PATCH 4/4] MOBILE-4616 chore: Always use convertTextToHTMLElement to convert HTML --- .../services/handlers/displayh5p.ts | 60 +++++++++---------- .../services/handlers/mediaplugin.ts | 14 ++--- src/core/services/utils/dom.ts | 31 +++++----- src/core/singletons/dom.ts | 6 +- src/core/utils/create-html-element.ts | 13 ++-- 5 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts index 0ef26c726..8e4dbcb47 100644 --- a/src/addons/filter/displayh5p/services/handlers/displayh5p.ts +++ b/src/addons/filter/displayh5p/services/handlers/displayh5p.ts @@ -20,7 +20,7 @@ import { makeSingleton } from '@singletons'; import { CoreH5PPlayerComponent } from '@features/h5p/components/h5p-player/h5p-player'; import { CoreUrl } from '@singletons/url'; import { CoreH5PHelper } from '@features/h5p/classes/helper'; -import { CoreTemplateElement } from '@/core/utils/create-html-element'; +import { CoreText } from '@singletons/text'; /** * Handler to support the Display H5P filter. @@ -37,41 +37,39 @@ export class AddonFilterDisplayH5PHandlerService extends CoreFilterDefaultHandle filter( text: string, ): string | Promise { - CoreTemplateElement.innerHTML = text; + return CoreText.processHTML(text, (element) => { + const h5pIframes = Array.from(element.querySelectorAll('iframe.h5p-iframe')); - const h5pIframes = Array.from(CoreTemplateElement.content.querySelectorAll('iframe.h5p-iframe')); + // Replace all iframes with an empty div that will be treated in handleHtml. + h5pIframes.forEach((iframe) => { + const placeholder = document.createElement('div'); - // Replace all iframes with an empty div that will be treated in handleHtml. - h5pIframes.forEach((iframe) => { - const placeholder = document.createElement('div'); + placeholder.classList.add('core-h5p-tmp-placeholder'); + placeholder.setAttribute('data-player-src', iframe.src); - placeholder.classList.add('core-h5p-tmp-placeholder'); - placeholder.setAttribute('data-player-src', iframe.src); + iframe.parentElement?.replaceChild(placeholder, iframe); + }); - iframe.parentElement?.replaceChild(placeholder, iframe); + // Handle H5P iframes embedded using the embed HTML code. + const embeddedH5PIframes = Array.from( + element.querySelectorAll('iframe.h5p-player'), + ); + + embeddedH5PIframes.forEach((iframe) => { + // Add the preventredirect param to allow authenticating if auto-login fails. + iframe.src = CoreUrl.addParamsToUrl(iframe.src, { preventredirect: false }); + + // Add resizer script so the H5P has the right height. + CoreH5PHelper.addResizerScript(); + + // If the iframe has a small height, add some minimum initial height so it's seen if auto-login fails. + const styleHeight = Number(iframe.style.height); + const height = Number(iframe.getAttribute('height')); + if ((!height || height < 400) && (!styleHeight || styleHeight < 400)) { + iframe.style.height = '400px'; + } + }); }); - - // Handle H5P iframes embedded using the embed HTML code. - const embeddedH5PIframes = Array.from( - CoreTemplateElement.content.querySelectorAll('iframe.h5p-player'), - ); - - embeddedH5PIframes.forEach((iframe) => { - // Add the preventredirect param to allow authenticating if auto-login fails. - iframe.src = CoreUrl.addParamsToUrl(iframe.src, { preventredirect: false }); - - // Add resizer script so the H5P has the right height. - CoreH5PHelper.addResizerScript(); - - // If the iframe has a small height, add some minimum initial height so it's seen if auto-login fails. - const styleHeight = Number(iframe.style.height); - const height = Number(iframe.getAttribute('height')); - if ((!height || height < 400) && (!styleHeight || styleHeight < 400)) { - iframe.style.height = '400px'; - } - }); - - return CoreTemplateElement.innerHTML; } /** diff --git a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts index 6d111dca4..363cc109c 100644 --- a/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts +++ b/src/addons/filter/mediaplugin/services/handlers/mediaplugin.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { CoreTemplateElement } from '@/core/utils/create-html-element'; +import { CoreText } from '@singletons/text'; import { AddonFilterMediaPluginVideoJS } from '@addons/filter/mediaplugin/services/videojs'; import { Injectable } from '@angular/core'; @@ -33,15 +33,13 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl * @inheritdoc */ filter(text: string): string | Promise { - CoreTemplateElement.innerHTML = text; + return CoreText.processHTML(text, (element) => { + const videos = Array.from(element.querySelectorAll('video')); - const videos = Array.from(CoreTemplateElement.content.querySelectorAll('video')); - - videos.forEach((video) => { - AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video); + videos.forEach((video) => { + AddonFilterMediaPluginVideoJS.treatYoutubeVideos(video); + }); }); - - return CoreTemplateElement.innerHTML; } /** diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index f074d20c8..924ef2b8c 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -55,7 +55,7 @@ import { CorePopovers, OpenPopoverOptions } from '@services/popovers'; import { CoreViewer } from '@features/viewer/services/viewer'; import { CoreLoadings } from '@services/loadings'; import { CoreErrorHelper, CoreErrorObject } from '@services/error-helper'; -import { convertTextToHTMLElement, CoreTemplateElement } from '@/core/utils/create-html-element'; +import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; import { CoreHTMLClasses } from '@singletons/html-classes'; /* @@ -197,7 +197,7 @@ export class CoreDomUtilsProvider { * @param html Text to convert. * @returns Element. * - * @deprecated since 4.5. Use convertToElement directly instead. + * @deprecated since 4.5. Use convertTextToHTMLElement directly instead. */ convertToElement(html: string): HTMLElement { return convertTextToHTMLElement(html); @@ -262,24 +262,23 @@ export class CoreDomUtilsProvider { * @returns Fixed HTML text. */ fixHtml(html: string): string { - CoreTemplateElement.innerHTML = html; + return CoreText.processHTML(html, (element) => { + // eslint-disable-next-line no-control-regex + const attrNameRegExp = /[^\x00-\x20\x7F-\x9F"'>/=]+/; + const fixElement = (element: Element): void => { + // Remove attributes with an invalid name. + Array.from(element.attributes).forEach((attr) => { + if (!attrNameRegExp.test(attr.name)) { + element.removeAttributeNode(attr); + } + }); - // eslint-disable-next-line no-control-regex - const attrNameRegExp = /[^\x00-\x20\x7F-\x9F"'>/=]+/; - const fixElement = (element: Element): void => { - // Remove attributes with an invalid name. - Array.from(element.attributes).forEach((attr) => { - if (!attrNameRegExp.test(attr.name)) { - element.removeAttributeNode(attr); - } - }); + Array.from(element.children).forEach(fixElement); + }; Array.from(element.children).forEach(fixElement); - }; + }); - Array.from(CoreTemplateElement.content.children).forEach(fixElement); - - return CoreTemplateElement.innerHTML; } /** diff --git a/src/core/singletons/dom.ts b/src/core/singletons/dom.ts index e35035472..c35b87ed6 100644 --- a/src/core/singletons/dom.ts +++ b/src/core/singletons/dom.ts @@ -17,7 +17,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreEventObserver } from '@singletons/events'; import { CorePlatform } from '@services/platform'; import { CoreWait } from './wait'; -import { CoreTemplateElement } from '../utils/create-html-element'; +import { convertTextToHTMLElement } from '../utils/create-html-element'; /** * Singleton with helper functions for dom. @@ -118,9 +118,9 @@ export class CoreDom { return true; } - CoreTemplateElement.innerHTML = content; + const element = convertTextToHTMLElement(content); - return !CoreDom.elementHasContent(CoreTemplateElement.content); + return !CoreDom.elementHasContent(element); } /** diff --git a/src/core/utils/create-html-element.ts b/src/core/utils/create-html-element.ts index 60f98e5c2..076d6bbc4 100644 --- a/src/core/utils/create-html-element.ts +++ b/src/core/utils/create-html-element.ts @@ -12,18 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -// A template element to convert HTML to element. -export const CoreTemplateElement: HTMLTemplateElement = document.createElement('template'); - /** - * Convert some HTML as text into an HTMLElement. This HTML is put inside a div or a body. + * Convert some HTML as text into an HTMLElement. This HTML is put inside a div. * * @param html Text to convert. * @returns Element. */ export function convertTextToHTMLElement(html: string): HTMLElement { - // Add a div to hold the content, that's the element that will be returned. - CoreTemplateElement.innerHTML = '
' + html + '
'; + const element = document.createElement('template'); - return CoreTemplateElement.content.children[0]; + // Add a div to hold the content, that's the element that will be returned. + element.innerHTML = '
' + html + '
'; + + return element.content.children[0]; }