' + 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}>${tag}>`, config);