From 2489cc7c16c343708c96472d01ccd314d38162c5 Mon Sep 17 00:00:00 2001 From: Muhammad Arslan Abdul Rauf Date: Fri, 7 Nov 2025 16:13:50 +0500 Subject: [PATCH 1/3] fix: contentAnalyzer added to determine TinyMCE Editor type --- .../introducing-section/contentTypeUtils.js | 43 +++++++++++++++++++ .../introducing-section/index.jsx | 3 ++ 2 files changed, 46 insertions(+) create mode 100644 src/schedule-and-details/introducing-section/contentTypeUtils.js diff --git a/src/schedule-and-details/introducing-section/contentTypeUtils.js b/src/schedule-and-details/introducing-section/contentTypeUtils.js new file mode 100644 index 0000000000..900789d44c --- /dev/null +++ b/src/schedule-and-details/introducing-section/contentTypeUtils.js @@ -0,0 +1,43 @@ +/** + * Utility functions for detecting content type and + * determining appropriate editor type for TinyMCE editor + */ + +/** + * Detects if content contains HTML tags + * @param {string} content - The content to analyze + * @returns {boolean} - True if content contains HTML tags + */ +export const containsHtml = (content) => { + if (!content || typeof content !== 'string') { + return false; + } + + // Check for common HTML patterns + const htmlPatterns = [ + /<\/?[a-z][\s\S]*>/i, // HTML tags + /&[a-z]+;/i, // HTML entities + /&#\d+;/, // Numeric entities + ]; + + return htmlPatterns.some(pattern => pattern.test(content)); +}; + +/** + * Determines the appropriate editor type based on content analysis + * @param {string} content - The content to analyze + * @returns {string} - The recommended editor type ('text' or 'html') + */ +export const determineEditorType = (content) => { + if (!content || typeof content !== 'string') { + return 'text'; + } + + // If content contains HTML, use html editor for better HTML editing + if (containsHtml(content)) { + return 'html'; + } + + // For plain text content, use text editor + return 'text'; +}; diff --git a/src/schedule-and-details/introducing-section/index.jsx b/src/schedule-and-details/introducing-section/index.jsx index 32e8e21c55..71e7bdccd7 100644 --- a/src/schedule-and-details/introducing-section/index.jsx +++ b/src/schedule-and-details/introducing-section/index.jsx @@ -11,6 +11,7 @@ import { WysiwygEditor } from '../../generic/WysiwygEditor'; import SectionSubHeader from '../../generic/section-sub-header'; import IntroductionVideo from './introduction-video'; import ExtendedCourseDetails from './extended-course-details'; +import { determineEditorType } from './contentTypeUtils'; import messages from './messages'; const IntroducingSection = ({ @@ -112,6 +113,7 @@ const IntroducingSection = ({ {intl.formatMessage(messages.courseOverviewLabel)} onChange(value, 'overview')} /> {overviewHelpText} @@ -121,6 +123,7 @@ const IntroducingSection = ({ {intl.formatMessage(messages.courseAboutSidebarLabel)} onChange(value, 'aboutSidebarHtml')} /> {aboutSidebarHelpText} From 18f0db9e7d12ea999ab9eb5a55e05f31dff91863 Mon Sep 17 00:00:00 2001 From: Muhammad Arslan Abdul Rauf Date: Fri, 7 Nov 2025 21:35:00 +0500 Subject: [PATCH 2/3] test: contentTypeUtil tests added --- .../contentTypeUtils.test.js | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 src/schedule-and-details/introducing-section/contentTypeUtils.test.js diff --git a/src/schedule-and-details/introducing-section/contentTypeUtils.test.js b/src/schedule-and-details/introducing-section/contentTypeUtils.test.js new file mode 100644 index 0000000000..38d3200557 --- /dev/null +++ b/src/schedule-and-details/introducing-section/contentTypeUtils.test.js @@ -0,0 +1,167 @@ +/** + * Tests for content type detection utilities + */ +import { containsHtml, determineEditorType } from './contentTypeUtils'; + +describe('contentTypeUtils', () => { + describe('containsHtml', () => { + describe('should return false for non-HTML content', () => { + test.each([ + ['empty string', ''], + ['null', null], + ['undefined', undefined], + ['number', 123], + ['plain text', 'This is just plain text'], + ['text with special characters', 'Text with @#$%^&*()'], + ['text with quotes', 'Text with "quotes" and \'apostrophes\''], + ['text with newlines', 'Line 1\nLine 2\nLine 3'], + ['text with angle brackets but not HTML', '5 < 10 and 10 > 5'], + ['mathematical expressions', 'x = y < z > w'], + ])('for %s', (description, input) => { + expect(containsHtml(input)).toBe(false); + }); + }); + + describe('should return true for HTML content', () => { + test.each([ + // Basic HTML tags + ['simple paragraph', '

Hello world

'], + ['heading tag', '

Title

'], + ['div element', '
Content
'], + ['span element', 'Text'], + ['self-closing tag', '
'], + ['self-closing without space', '
'], + + // HTML tags with attributes + ['tag with class', '

Text

'], + ['tag with id', '
Text
'], + ['tag with multiple attributes', 'Link'], + + // Mixed case HTML + ['uppercase tag', '

Paragraph

'], + ['mixed case tag', '
Content
'], + + // HTML with content + ['nested tags', '

Nested paragraph

'], + ['multiple tags', '

Title

Paragraph

'], + ['formatting tags', 'Text with bold and italic'], + + // Complex HTML structures + ['list structure', '
  • Item 1
  • Item 2
'], + ['table structure', '
Cell
'], + ['form elements', ''], + ['image tag', 'Image'], + + // HTML entities + ['named entities', 'Price: $100 & free shipping'], + ['more entities', 'Copyright © 2024 – All rights reserved'], + ['quotes entity', 'He said "Hello" to me'], + ['numeric entities', 'Special char: € ©'], + + // HTML with text content + ['HTML in mixed content', 'Introduction:

This is the main content.

End.'], + ['multiple entities', 'A & B < C > D'], + + // Edge cases + ['unclosed tag', '

Unclosed paragraph'], + ['tag with newlines', '

\nMultiline\ncontent\n

'], + ])('for %s', (description, input) => { + expect(containsHtml(input)).toBe(true); + }); + }); + }); + + describe('determineEditorType', () => { + describe('should return "text" for non-HTML content', () => { + test.each([ + ['empty string', ''], + ['null', null], + ['undefined', undefined], + ['number', 123], + ['plain text', 'This is just plain text content'], + ['long plain text', 'Lorem ipsum '.repeat(100)], + ['text with special chars', 'Email: test@example.com, Phone: (555) 123-4567'], + ['mathematical content', '2 + 2 = 4, x < y, a > b'], + ['code-like content', 'function() { return value < threshold; }'], + ])('for %s', (description, input) => { + expect(determineEditorType(input)).toBe('text'); + }); + }); + + describe('should return "html" for HTML content', () => { + test.each([ + // Simple HTML + ['basic paragraph', '

Simple paragraph

'], + ['heading', '

Section Title

'], + ['formatted text', 'Text with bold formatting'], + + // Complex HTML structures + ['nested elements', '

Title

Content

'], + ['lists', '
  • First item
  • Second item
'], + ['tables', '
Cell 1Cell 2
'], + ['links and images', 'Image'], + + // Course content examples + ['course overview', '

Course Overview

This course covers...

  • Topic 1
'], + ['about sidebar', ''], + + // HTML entities + ['content with entities', 'Price: $100 & includes shipping – 50% off!'], + ['mixed content', 'Introduction

Main content with © symbol

conclusion'], + ])('for %s', (description, input) => { + expect(determineEditorType(input)).toBe('html'); + }); + }); + + describe('integration scenarios', () => { + test('should handle real course overview content', () => { + const courseOverview = ` +
+

Introduction to Computer Science

+

This course provides a comprehensive introduction to computer science concepts.

+

What You'll Learn:

+
    +
  • Programming fundamentals
  • +
  • Data structures and algorithms
  • +
  • Software engineering practices
  • +
+

Prerequisites: Basic mathematics knowledge

+

Duration: 12 weeks

+
+ `; + + expect(containsHtml(courseOverview)).toBe(true); + expect(determineEditorType(courseOverview)).toBe('html'); + }); + + test('should handle sidebar HTML content', () => { + const sidebarHtml = ` +
+

Course Information

+

Instructor: Dr. Smith

+

Credits: 3

+

Format: Online & In-person

+ Download Syllabus +
+ `; + + expect(containsHtml(sidebarHtml)).toBe(true); + expect(determineEditorType(sidebarHtml)).toBe('html'); + }); + + test('should handle plain text course descriptions', () => { + const plainDescription = 'A beginner-friendly course covering the basics of programming. No prior experience required.'; + + expect(containsHtml(plainDescription)).toBe(false); + expect(determineEditorType(plainDescription)).toBe('text'); + }); + + test('should handle empty or minimal content', () => { + expect(determineEditorType('')).toBe('text'); + expect(determineEditorType(' ')).toBe('text'); + expect(determineEditorType(null)).toBe('text'); + expect(determineEditorType(undefined)).toBe('text'); + }); + }); + }); +}); From 94364410a6e94e4da2b6e899663b5d027e3cd82d Mon Sep 17 00:00:00 2001 From: Muhammad Arslan Abdul Rauf Date: Mon, 10 Nov 2025 15:21:07 +0500 Subject: [PATCH 3/3] fix: used ts instead of js --- ...Utils.test.js => contentTypeUtils.test.ts} | 89 ++++++++++++++++--- ...ontentTypeUtils.js => contentTypeUtils.ts} | 23 ++--- 2 files changed, 91 insertions(+), 21 deletions(-) rename src/schedule-and-details/introducing-section/{contentTypeUtils.test.js => contentTypeUtils.test.ts} (69%) rename src/schedule-and-details/introducing-section/{contentTypeUtils.js => contentTypeUtils.ts} (53%) diff --git a/src/schedule-and-details/introducing-section/contentTypeUtils.test.js b/src/schedule-and-details/introducing-section/contentTypeUtils.test.ts similarity index 69% rename from src/schedule-and-details/introducing-section/contentTypeUtils.test.js rename to src/schedule-and-details/introducing-section/contentTypeUtils.test.ts index 38d3200557..c818a1827f 100644 --- a/src/schedule-and-details/introducing-section/contentTypeUtils.test.js +++ b/src/schedule-and-details/introducing-section/contentTypeUtils.test.ts @@ -1,7 +1,7 @@ /** * Tests for content type detection utilities */ -import { containsHtml, determineEditorType } from './contentTypeUtils'; +import { containsHtml, determineEditorType, type EditorType } from './contentTypeUtils'; describe('contentTypeUtils', () => { describe('containsHtml', () => { @@ -9,15 +9,13 @@ describe('contentTypeUtils', () => { test.each([ ['empty string', ''], ['null', null], - ['undefined', undefined], - ['number', 123], ['plain text', 'This is just plain text'], ['text with special characters', 'Text with @#$%^&*()'], ['text with quotes', 'Text with "quotes" and \'apostrophes\''], ['text with newlines', 'Line 1\nLine 2\nLine 3'], ['text with angle brackets but not HTML', '5 < 10 and 10 > 5'], ['mathematical expressions', 'x = y < z > w'], - ])('for %s', (description, input) => { + ] as const)('for %s', (_description: string, input: string | null) => { expect(containsHtml(input)).toBe(false); }); }); @@ -65,10 +63,24 @@ describe('contentTypeUtils', () => { // Edge cases ['unclosed tag', '

Unclosed paragraph'], ['tag with newlines', '

\nMultiline\ncontent\n

'], - ])('for %s', (description, input) => { + ] as const)('for %s', (_description: string, input: string) => { expect(containsHtml(input)).toBe(true); }); }); + + describe('edge cases', () => { + test('should handle very long content', () => { + const longText = 'a'.repeat(10000); + expect(containsHtml(longText)).toBe(false); + + const longHtml = `

${longText}

`; + expect(containsHtml(longHtml)).toBe(true); + }); + + test('should handle content with only whitespace', () => { + expect(containsHtml(' \n\t ')).toBe(false); + }); + }); }); describe('determineEditorType', () => { @@ -76,14 +88,12 @@ describe('contentTypeUtils', () => { test.each([ ['empty string', ''], ['null', null], - ['undefined', undefined], - ['number', 123], ['plain text', 'This is just plain text content'], ['long plain text', 'Lorem ipsum '.repeat(100)], ['text with special chars', 'Email: test@example.com, Phone: (555) 123-4567'], ['mathematical content', '2 + 2 = 4, x < y, a > b'], ['code-like content', 'function() { return value < threshold; }'], - ])('for %s', (description, input) => { + ] as const)('for %s', (_description: string, input: string | null) => { expect(determineEditorType(input)).toBe('text'); }); }); @@ -108,7 +118,7 @@ describe('contentTypeUtils', () => { // HTML entities ['content with entities', 'Price: $100 & includes shipping – 50% off!'], ['mixed content', 'Introduction

Main content with © symbol

conclusion'], - ])('for %s', (description, input) => { + ] as const)('for %s', (_description: string, input: string) => { expect(determineEditorType(input)).toBe('html'); }); }); @@ -159,8 +169,65 @@ describe('contentTypeUtils', () => { test('should handle empty or minimal content', () => { expect(determineEditorType('')).toBe('text'); expect(determineEditorType(' ')).toBe('text'); - expect(determineEditorType(null)).toBe('text'); - expect(determineEditorType(undefined)).toBe('text'); + expect(determineEditorType(null as any)).toBe('text'); + expect(determineEditorType(undefined as any)).toBe('text'); + }); + }); + + describe('type safety', () => { + test('should return correct EditorType', () => { + const result: EditorType = determineEditorType('

Test

'); + expect(result).toBe('html'); + + const result2: EditorType = determineEditorType('Plain text'); + expect(result2).toBe('text'); + }); + }); + + describe('performance considerations', () => { + test('should handle very large content efficiently', () => { + const largeContent = 'This is a large text content. '.repeat(1000); + const start = Date.now(); + const result = determineEditorType(largeContent); + const end = Date.now(); + + expect(result).toBe('text'); + expect(end - start).toBeLessThan(100); // Should complete in under 100ms + }); + + test('should handle large HTML content efficiently', () => { + const largeHtml = `
${'

Paragraph content.

'.repeat(1000)}
`; + const start = Date.now(); + const result = determineEditorType(largeHtml); + const end = Date.now(); + + expect(result).toBe('html'); + expect(end - start).toBeLessThan(100); // Should complete in under 100ms + }); + }); + }); + + describe('function integration', () => { + test('containsHtml and determineEditorType should be consistent', () => { + const testCases: Array = [ + 'Plain text', + '

HTML content

', + 'Text with & entities', + '', + null, + undefined, + '

Complex

HTML

', + ]; + + testCases.forEach((content) => { + const hasHtml = containsHtml(content as any); + const editorType = determineEditorType(content as any); + + if (hasHtml) { + expect(editorType).toBe('html'); + } else { + expect(editorType).toBe('text'); + } }); }); }); diff --git a/src/schedule-and-details/introducing-section/contentTypeUtils.js b/src/schedule-and-details/introducing-section/contentTypeUtils.ts similarity index 53% rename from src/schedule-and-details/introducing-section/contentTypeUtils.js rename to src/schedule-and-details/introducing-section/contentTypeUtils.ts index 900789d44c..31ada1498c 100644 --- a/src/schedule-and-details/introducing-section/contentTypeUtils.js +++ b/src/schedule-and-details/introducing-section/contentTypeUtils.ts @@ -3,33 +3,36 @@ * determining appropriate editor type for TinyMCE editor */ +// Define the supported editor types +export type EditorType = 'text' | 'html'; + /** * Detects if content contains HTML tags - * @param {string} content - The content to analyze - * @returns {boolean} - True if content contains HTML tags + * @param content - The content to analyze + * @returns True if content contains HTML tags */ -export const containsHtml = (content) => { - if (!content || typeof content !== 'string') { +export const containsHtml = (content: string | null): boolean => { + if (!content) { return false; } // Check for common HTML patterns - const htmlPatterns = [ + const htmlPatterns: RegExp[] = [ /<\/?[a-z][\s\S]*>/i, // HTML tags /&[a-z]+;/i, // HTML entities /&#\d+;/, // Numeric entities ]; - return htmlPatterns.some(pattern => pattern.test(content)); + return htmlPatterns.some((pattern) => pattern.test(content)); }; /** * Determines the appropriate editor type based on content analysis - * @param {string} content - The content to analyze - * @returns {string} - The recommended editor type ('text' or 'html') + * @param content - The content to analyze + * @returns The recommended editor type ('text' or 'html') */ -export const determineEditorType = (content) => { - if (!content || typeof content !== 'string') { +export const determineEditorType = (content: string | null): EditorType => { + if (!content) { return 'text'; }