From 362f9096e222b0c6073f2d330085e93f15219131 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 2 Dec 2024 02:25:05 +0100 Subject: [PATCH 1/4] added auto-list creation & changed bullet sign --- .../posting-content-part.components.ts | 8 ++- .../posting-markdown-editor.component.ts | 38 ++++++++++++ .../model/actions/bulleted-list.action.ts | 2 +- .../model/actions/list.action.ts | 21 ++++--- .../model/actions/ordered-list.action.ts | 2 +- .../monaco-editor/monaco-editor.component.ts | 13 ++++ .../posting-content-part.component.spec.ts | 46 +++++++++++++++ ...postings-markdown-editor.component.spec.ts | 59 +++++++++++++------ .../monaco-editor.component.spec.ts | 24 ++++++++ 9 files changed, 182 insertions(+), 31 deletions(-) diff --git a/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts b/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts index 6c8322045888..a485184bd3df 100644 --- a/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts +++ b/src/main/webapp/app/shared/metis/posting-content/posting-content-part/posting-content-part.components.ts @@ -76,15 +76,21 @@ export class PostingContentPartComponent implements OnInit { processContent() { if (this.postingContentPart.contentBeforeReference) { this.processedContentBeforeReference = this.escapeNumberedList(this.postingContentPart.contentBeforeReference); + this.processedContentBeforeReference = this.escapeUnorderedList(this.processedContentBeforeReference); } if (this.postingContentPart.contentAfterReference) { this.processedContentAfterReference = this.escapeNumberedList(this.postingContentPart.contentAfterReference); + this.processedContentAfterReference = this.escapeUnorderedList(this.processedContentAfterReference); } } escapeNumberedList(content: string): string { - return content.replace(/^(\s*\d+)\. /gm, '$1\\. '); + return content.replace(/^(\s*\d+)\. /gm, '$1\\. '); + } + + escapeUnorderedList(content: string): string { + return content.replace(/^(- )/gm, '\\$1'); } /** diff --git a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts index 03a87624d52f..4af2edf725f6 100644 --- a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts +++ b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts @@ -15,6 +15,7 @@ import { inject, input, } from '@angular/core'; +import monaco from 'monaco-editor'; import { ViewContainerRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { MetisService } from 'app/shared/metis/metis.service'; @@ -122,6 +123,43 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces ngAfterViewInit(): void { this.markdownEditor.enableTextFieldMode(); + + const editor = this.markdownEditor.monacoEditor; + if (editor) { + if (editor) { + editor.onDidChangeModelContent((event: { changes: string | any[] }) => { + const position = editor.getPosition(); + if (!position) { + return; + } + + const model = editor.getModel(); + if (!model) { + return; + } + + const lineContent = model.getLineContent(position.lineNumber).trimStart(); + const hasPrefix = lineContent.startsWith('- ') || /^\s*1\. /.test(lineContent); + if (hasPrefix && event.changes.length === 1 && (event.changes[0].text.startsWith('- ') || event.changes[0].text.startsWith('1. '))) { + return; + } + + if (hasPrefix) { + this.handleKeyDown(model, position.lineNumber); + } + }); + } + } + } + + private handleKeyDown(model: monaco.editor.ITextModel, lineNumber: number): void { + const lineContent = model.getLineContent(lineNumber).trimStart(); + + if (lineContent.startsWith('- ')) { + this.markdownEditor.handleActionClick(new MouseEvent('click'), this.defaultActions.find((action) => action instanceof BulletedListAction)!); + } else if (/^\d+\. /.test(lineContent)) { + this.markdownEditor.handleActionClick(new MouseEvent('click'), this.defaultActions.find((action) => action instanceof OrderedListAction)!); + } } /** diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/bulleted-list.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/bulleted-list.action.ts index d7bdd9aa1d1e..508f03649209 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/bulleted-list.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/bulleted-list.action.ts @@ -1,7 +1,7 @@ import { faListUl } from '@fortawesome/free-solid-svg-icons'; import { ListAction } from './list.action'; -const BULLET_PREFIX = '• '; +const BULLET_PREFIX = '- '; /** * Action used to add or modify a bullet-point list in the text editor. diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts index 659f4bed285c..5f404a25535e 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/list.action.ts @@ -37,7 +37,7 @@ export abstract class ListAction extends TextEditorAction { */ protected stripAnyListPrefix(line: string): string { const numberedListRegex = /^\s*\d+\.\s+/; - const bulletListRegex = /^\s*[-*+•]\s+/; + const bulletListRegex = /^\s*[-*+]\s+/; if (numberedListRegex.test(line)) { return line.replace(numberedListRegex, ''); @@ -91,10 +91,13 @@ export abstract class ListAction extends TextEditorAction { } if (position.getColumn() === currentLineText.length + 1) { - const lineWithoutPrefix = this.stripAnyListPrefix(currentLineText); const newPrefix = this.getPrefix(1); - const updatedLine = currentLineText.startsWith(newPrefix) ? lineWithoutPrefix : newPrefix + lineWithoutPrefix; + if (currentLineText.startsWith(newPrefix)) { + return; + } + + const updatedLine = newPrefix + currentLineText; editor.replaceTextAtRange( new TextEditorRange(new TextEditorPosition(position.getLineNumber(), 1), new TextEditorPosition(position.getLineNumber(), currentLineText.length + 1)), @@ -110,7 +113,7 @@ export abstract class ListAction extends TextEditorAction { // Determine if all lines have the current prefix let allLinesHaveCurrentPrefix; - if (this.getPrefix(1) != '• ') { + if (this.getPrefix(1) != '- ') { const numberedListRegex = /^\s*\d+\.\s+/; allLinesHaveCurrentPrefix = lines.every((line) => numberedListRegex.test(line)); } else { @@ -124,7 +127,7 @@ export abstract class ListAction extends TextEditorAction { const linesWithoutPrefix = lines.map((line) => this.stripAnyListPrefix(line)); updatedLines = linesWithoutPrefix.map((line, index) => { - const prefix = this.getPrefix(index) != '• ' ? this.getPrefix(index + 1) : this.getPrefix(startLineNumber + index); + const prefix = this.getPrefix(index) != '- ' ? this.getPrefix(index + 1) : this.getPrefix(startLineNumber + index); return prefix + line; }); } @@ -141,7 +144,7 @@ export abstract class ListAction extends TextEditorAction { */ protected hasPrefix(line: string): boolean { const numberedListRegex = /^\s*\d+\.\s+/; - const bulletListRegex = /^\s*[•\-*+]\s+/; + const bulletListRegex = /^\s*[-\-*+]\s+/; return numberedListRegex.test(line) || bulletListRegex.test(line); } @@ -162,9 +165,9 @@ export abstract class ListAction extends TextEditorAction { if (isNumbered) { const match = currentLineText.match(/^\s*(\d+)\.\s+/); const currentNumber = match ? parseInt(match[1], 10) : 0; - nextLinePrefix = `${currentNumber + 1}. `; + nextLinePrefix = `${currentNumber + 1}. `; } else { - nextLinePrefix = '• '; + nextLinePrefix = '- '; } } @@ -187,7 +190,7 @@ export abstract class ListAction extends TextEditorAction { if (position) { const lineNumber = position.getLineNumber(); const lineContent = editor.getLineText(lineNumber); - const linePrefixMatch = lineContent.match(/^\s*(\d+\.\s+|[-*+•]\s+)/); + const linePrefixMatch = lineContent.match(/^\s*(\d+\.\s+|[-*+]\s+)/); if (linePrefixMatch) { const prefixLength = linePrefixMatch[0].length; diff --git a/src/main/webapp/app/shared/monaco-editor/model/actions/ordered-list.action.ts b/src/main/webapp/app/shared/monaco-editor/model/actions/ordered-list.action.ts index 06930260c14b..44bd5ea73027 100644 --- a/src/main/webapp/app/shared/monaco-editor/model/actions/ordered-list.action.ts +++ b/src/main/webapp/app/shared/monaco-editor/model/actions/ordered-list.action.ts @@ -12,7 +12,7 @@ export class OrderedListAction extends ListAction { } public getPrefix(lineNumber: number): string { - const space = lineNumber >= 10 ? ' ' : ' '; + const space = ' '; return `${lineNumber}.${space}`; } diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts b/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts index 73bb9f68f931..288f53cc9c07 100644 --- a/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts +++ b/src/main/webapp/app/shared/monaco-editor/monaco-editor.component.ts @@ -118,6 +118,19 @@ export class MonacoEditorComponent implements OnInit, OnDestroy { return convertedWords.join(' '); } + public onDidChangeModelContent(listener: (event: monaco.editor.IModelContentChangedEvent) => void): monaco.IDisposable { + return this._editor.onDidChangeModelContent(listener); + } + + public getModel() { + return this._editor.getModel(); + } + + public getLineContent(lineNumber: number): string { + const model = this._editor.getModel(); + return model ? model.getLineContent(lineNumber) : ''; + } + ngOnInit(): void { const resizeObserver = new ResizeObserver(() => { this._editor.layout(); diff --git a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts index 245f54d59a62..5a64278b8145 100644 --- a/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/posting-content/posting-content-part.component.spec.ts @@ -270,4 +270,50 @@ describe('PostingContentPartComponent', () => { expect(outputEmitter).not.toHaveBeenCalled(); }); }); + + describe('Content processing', () => { + it('should process content before and after reference with escaped numbered and unordered lists', () => { + const contentBefore = '1. This is a numbered list\n2. Another item\n- This is an unordered list'; + const contentAfter = '1. Numbered again\n- Unordered again'; + component.postingContentPart = { + contentBeforeReference: contentBefore, + contentAfterReference: contentAfter, + linkToReference: undefined, + queryParams: undefined, + referenceStr: undefined, + } as PostingContentPart; + fixture.detectChanges(); + + component.processContent(); + + expect(component.processedContentBeforeReference).toBe('1\\. This is a numbered list\n2\\. Another item\n\\- This is an unordered list'); + expect(component.processedContentAfterReference).toBe('1\\. Numbered again\n\\- Unordered again'); + }); + + it('should escape numbered lists correctly', () => { + const content = '1. First item\n2. Second item\n3. Third item'; + const escapedContent = component.escapeNumberedList(content); + expect(escapedContent).toBe('1\\. First item\n2\\. Second item\n3\\. Third item'); + }); + + it('should escape unordered lists correctly', () => { + const content = '- First item\n- Second item\n- Third item'; + const escapedContent = component.escapeUnorderedList(content); + expect(escapedContent).toBe('\\- First item\n\\- Second item\n\\- Third item'); + }); + + it('should not escape text without numbered or unordered lists', () => { + const content = 'This is just a paragraph.\nAnother paragraph.'; + const escapedNumbered = component.escapeNumberedList(content); + const escapedUnordered = component.escapeUnorderedList(content); + expect(escapedNumbered).toBe(content); + expect(escapedUnordered).toBe(content); + }); + + it('should handle mixed numbered and unordered lists in content', () => { + const content = '1. Numbered item\n- Unordered item\n2. Another numbered item\n- Another unordered item'; + const escapedContent = component.escapeNumberedList(component.escapeUnorderedList(content)); + expect(escapedContent).toBe('1\\. Numbered item\n\\- Unordered item\n2\\. Another numbered item\n\\- Another unordered item'); + }); + }); }); diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index 10a6a49276f9..593a7d39d2ff 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -37,6 +37,7 @@ import { TextEditorPosition } from 'app/shared/monaco-editor/model/actions/adapt import { BulletedListAction } from 'app/shared/monaco-editor/model/actions/bulleted-list.action'; import { OrderedListAction } from 'app/shared/monaco-editor/model/actions/ordered-list.action'; import { ListAction } from '../../../../../../../main/webapp/app/shared/monaco-editor/model/actions/list.action'; +import monaco from 'monaco-editor'; describe('PostingsMarkdownEditor', () => { let component: PostingMarkdownEditorComponent; @@ -124,6 +125,7 @@ describe('PostingsMarkdownEditor', () => { MockProvider(ChannelService), { provide: Overlay, useValue: mockOverlay }, { provide: OverlayPositionBuilder, useValue: overlayPositionBuilderMock }, + { provide: MarkdownEditorMonacoComponent, useValue: mockMarkdownEditorComponent }, ], declarations: [PostingMarkdownEditorComponent, MockComponent(MarkdownEditorMonacoComponent)], }) @@ -140,6 +142,8 @@ describe('PostingsMarkdownEditor', () => { findLectureWithDetailsSpy.mockReturnValue(returnValue); fixture.autoDetectChanges(); mockMarkdownEditorComponent = fixture.debugElement.query(By.directive(MarkdownEditorMonacoComponent)).componentInstance; + mockMarkdownEditorComponent.monacoEditor.getPosition = jest.fn(); + mockMarkdownEditorComponent.monacoEditor.getModel = jest.fn(); component.ngOnInit(); component.content = metisPostExerciseUser1.content; @@ -361,15 +365,15 @@ describe('PostingsMarkdownEditor', () => { getLineNumber: () => 1, getColumn: () => 6, } as TextEditorPosition); - mockEditor.getLineText.mockReturnValue('• First line'); + mockEditor.getLineText.mockReturnValue('- First line'); bulletedListAction.run(mockEditor); const { preventDefaultSpy } = simulateKeydownEvent(mockEditor, 'Enter', { shiftKey: true }); expect(preventDefaultSpy).toHaveBeenCalled(); - expect(mockEditor.replaceTextAtRange).toHaveBeenCalledWith(expect.any(TextEditorRange), '\n• '); - expect(mockEditor.setPosition).toHaveBeenCalledWith(new TextEditorPosition(2, 4)); + expect(mockEditor.replaceTextAtRange).toHaveBeenCalledWith(expect.any(TextEditorRange), '\n- '); + expect(mockEditor.setPosition).toHaveBeenCalledWith(new TextEditorPosition(2, 3)); }); it('should handle Cmd+Enter correctly without inserting double line breaks', () => { @@ -379,7 +383,7 @@ describe('PostingsMarkdownEditor', () => { getLineNumber: () => 1, getColumn: () => 6, } as TextEditorPosition); - mockEditor.getLineText.mockReturnValue('• First line'); + mockEditor.getLineText.mockReturnValue('- First line'); bulletedListAction.run(mockEditor); @@ -387,8 +391,8 @@ describe('PostingsMarkdownEditor', () => { expect(preventDefaultSpy).toHaveBeenCalled(); expect(stopPropagationSpy).toHaveBeenCalled(); - expect(mockEditor.replaceTextAtRange).toHaveBeenCalledWith(expect.any(TextEditorRange), '\n• '); - expect(mockEditor.setPosition).toHaveBeenCalledWith(new TextEditorPosition(2, 4)); + expect(mockEditor.replaceTextAtRange).toHaveBeenCalledWith(expect.any(TextEditorRange), '\n- '); + expect(mockEditor.setPosition).toHaveBeenCalledWith(new TextEditorPosition(2, 3)); }); const simulateListAction = (action: TextEditorAction, selectedText: string, expectedText: string, startLineNumber: number = 1) => { @@ -437,14 +441,14 @@ describe('PostingsMarkdownEditor', () => { it('should add bulleted list prefixes correctly', () => { const bulletedListAction = component.defaultActions.find((action: any) => action instanceof BulletedListAction) as BulletedListAction; const selectedText = `First line\nSecond line\nThird line`; - const expectedText = `• First line\n• Second line\n• Third line`; + const expectedText = `- First line\n- Second line\n- Third line`; simulateListAction(bulletedListAction, selectedText, expectedText); }); it('should remove bulleted list prefixes correctly when toggled', () => { const bulletedListAction = component.defaultActions.find((action: any) => action instanceof BulletedListAction) as BulletedListAction; - const selectedText = `• First line\n• Second line\n• Third line`; + const selectedText = `- First line\n- Second line\n- Third line`; const expectedText = `First line\nSecond line\nThird line`; simulateListAction(bulletedListAction, selectedText, expectedText); @@ -453,7 +457,7 @@ describe('PostingsMarkdownEditor', () => { it('should add ordered list prefixes correctly starting from 1', () => { const orderedListAction = component.defaultActions.find((action: any) => action instanceof OrderedListAction) as OrderedListAction; const selectedText = `First line\nSecond line\nThird line`; - const expectedText = `1. First line\n2. Second line\n3. Third line`; + const expectedText = `1. First line\n2. Second line\n3. Third line`; simulateListAction(orderedListAction, selectedText, expectedText); }); @@ -469,8 +473,8 @@ describe('PostingsMarkdownEditor', () => { it('should switch from bulleted list to ordered list correctly', () => { const bulletedListAction = component.defaultActions.find((action: any) => action instanceof BulletedListAction) as BulletedListAction; const orderedListAction = component.defaultActions.find((action: any) => action instanceof OrderedListAction) as OrderedListAction; - const bulletedText = `• First line\n• Second line\n• Third line`; - const expectedOrderedText = `1. First line\n2. Second line\n3. Third line`; + const bulletedText = `- First line\n- Second line\n- Third line`; + const expectedOrderedText = `1. First line\n2. Second line\n3. Third line`; simulateListAction(bulletedListAction, bulletedText, `First line\nSecond line\nThird line`); @@ -482,7 +486,7 @@ describe('PostingsMarkdownEditor', () => { const orderedListAction = component.defaultActions.find((action: any) => action instanceof OrderedListAction) as OrderedListAction; const bulletedListAction = component.defaultActions.find((action: any) => action instanceof BulletedListAction) as BulletedListAction; const orderedText = `1. First line\n2. Second line\n3. Third line`; - const expectedBulletedText = `• First line\n• Second line\n• Third line`; + const expectedBulletedText = `- First line\n- Second line\n- Third line`; simulateListAction(orderedListAction, orderedText, `First line\nSecond line\nThird line`); @@ -493,7 +497,7 @@ describe('PostingsMarkdownEditor', () => { it('should start ordered list numbering from 1 regardless of an inline list', () => { const orderedListAction = component.defaultActions.find((action: any) => action instanceof OrderedListAction) as OrderedListAction; const selectedText = `Some previous text\n1. First line\n2. Second line\n3. Third line`; - const expectedText = `1. Some previous text\n2. First line\n3. Second line\n4. Third line`; + const expectedText = `1. Some previous text\n2. First line\n3. Second line\n4. Third line`; simulateListAction(orderedListAction, selectedText, expectedText); }); @@ -502,8 +506,8 @@ describe('PostingsMarkdownEditor', () => { const bulletedListAction = component.defaultActions.find((action: any) => action instanceof BulletedListAction) as BulletedListAction; const orderedListAction = component.defaultActions.find((action: any) => action instanceof OrderedListAction) as OrderedListAction; - const bulletedText = `• First line\n• Second line\n• Third line`; - const expectedOrderedText = `1. First line\n2. Second line\n3. Third line`; + const bulletedText = `- First line\n- Second line\n- Third line`; + const expectedOrderedText = `1. First line\n2. Second line\n3. Third line`; simulateListAction(bulletedListAction, `First line\nSecond line\nThird line`, bulletedText); @@ -517,13 +521,13 @@ describe('PostingsMarkdownEditor', () => { const initialText = `First line\nSecond line\nThird line`; - const bulletedText = `• First line\n• Second line\n• Third line`; + const bulletedText = `- First line\n- Second line\n- Third line`; simulateListAction(bulletedListAction, initialText, bulletedText); mockEditor.replaceTextAtRange.mockClear(); simulateListAction(bulletedListAction, bulletedText, initialText); - const orderedText = `1. First line\n2. Second line\n3. Third line`; + const orderedText = `1. First line\n2. Second line\n3. Third line`; mockEditor.replaceTextAtRange.mockClear(); simulateListAction(orderedListAction, initialText, orderedText); @@ -537,14 +541,31 @@ describe('PostingsMarkdownEditor', () => { const initialText = `First line\nSecond line\nThird line`; - const bulletedText = `• First line\n• Second line\n• Third line`; + const bulletedText = `- First line\n- Second line\n- Third line`; simulateListAction(bulletedListAction, initialText, bulletedText); - const orderedText = `1. First line\n2. Second line\n3. Third line`; + const orderedText = `1. First line\n2. Second line\n3. Third line`; mockEditor.replaceTextAtRange.mockClear(); simulateListAction(orderedListAction, bulletedText, orderedText); mockEditor.replaceTextAtRange.mockClear(); simulateListAction(bulletedListAction, orderedText, bulletedText); }); + + it('should handle key down event and invoke the correct action', () => { + const bulletedListAction = new BulletedListAction(); + + component.defaultActions = [bulletedListAction]; + + const handleActionClickSpy = jest.spyOn(component.markdownEditor, 'handleActionClick'); + + const mockModel = { + getLineContent: jest.fn().mockReturnValue('- List item'), + } as unknown as monaco.editor.ITextModel; + const mockPosition = { lineNumber: 1 } as monaco.Position; + + (component as any).handleKeyDown(mockModel, mockPosition.lineNumber); + + expect(handleActionClickSpy).toHaveBeenCalledWith(expect.any(MouseEvent), bulletedListAction); + }); }); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts index 244e9004ac43..c611f6fbebf1 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts @@ -289,4 +289,28 @@ describe('MonacoEditorComponent', () => { comp.setText(originalText); expect(comp.getText()).toBe(originalText); }); + + it('should register a listener for model content changes', () => { + const listenerStub = jest.fn(); + fixture.detectChanges(); + const disposable = comp.onDidChangeModelContent(listenerStub); + comp.setText(singleLineText); + expect(listenerStub).toHaveBeenCalled(); + disposable.dispose(); + }); + + it('should retrieve the editor model', () => { + fixture.detectChanges(); + comp.setText(singleLineText); + const model = comp.getModel(); + expect(model).not.toBeNull(); + expect(model?.getValue()).toBe(singleLineText); + }); + + it('should get the content of a specific line', () => { + fixture.detectChanges(); + comp.setText(multiLineText); + const lineContent = comp.getLineContent(2); + expect(lineContent).toBe('static void main() {'); + }); }); From 1cb77e81413739572741aec29dad663d95ca1ebc Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 2 Dec 2024 02:27:18 +0100 Subject: [PATCH 2/4] refactor --- .../posting-markdown-editor.component.ts | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts index 4af2edf725f6..7de0a478e7ca 100644 --- a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts +++ b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts @@ -126,29 +126,27 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces const editor = this.markdownEditor.monacoEditor; if (editor) { - if (editor) { - editor.onDidChangeModelContent((event: { changes: string | any[] }) => { - const position = editor.getPosition(); - if (!position) { - return; - } - - const model = editor.getModel(); - if (!model) { - return; - } - - const lineContent = model.getLineContent(position.lineNumber).trimStart(); - const hasPrefix = lineContent.startsWith('- ') || /^\s*1\. /.test(lineContent); - if (hasPrefix && event.changes.length === 1 && (event.changes[0].text.startsWith('- ') || event.changes[0].text.startsWith('1. '))) { - return; - } - - if (hasPrefix) { - this.handleKeyDown(model, position.lineNumber); - } - }); - } + editor.onDidChangeModelContent((event: { changes: string | any[] }) => { + const position = editor.getPosition(); + if (!position) { + return; + } + + const model = editor.getModel(); + if (!model) { + return; + } + + const lineContent = model.getLineContent(position.lineNumber).trimStart(); + const hasPrefix = lineContent.startsWith('- ') || /^\s*1\. /.test(lineContent); + if (hasPrefix && event.changes.length === 1 && (event.changes[0].text.startsWith('- ') || event.changes[0].text.startsWith('1. '))) { + return; + } + + if (hasPrefix) { + this.handleKeyDown(model, position.lineNumber); + } + }); } } From ea1051650a97079147bf1821b2d3c8c4a38b10d2 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 2 Dec 2024 02:45:50 +0100 Subject: [PATCH 3/4] test fix --- .../postings-markdown-editor.component.spec.ts | 2 -- .../monaco-editor/monaco-editor-action.integration.spec.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index 593a7d39d2ff..a184d4438a2f 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -142,8 +142,6 @@ describe('PostingsMarkdownEditor', () => { findLectureWithDetailsSpy.mockReturnValue(returnValue); fixture.autoDetectChanges(); mockMarkdownEditorComponent = fixture.debugElement.query(By.directive(MarkdownEditorMonacoComponent)).componentInstance; - mockMarkdownEditorComponent.monacoEditor.getPosition = jest.fn(); - mockMarkdownEditorComponent.monacoEditor.getModel = jest.fn(); component.ngOnInit(); component.content = metisPostExerciseUser1.content; diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts index 2de7e4afcf89..4551f36513f9 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor-action.integration.spec.ts @@ -138,7 +138,7 @@ describe('MonacoEditorActionIntegration', () => { const action = new OrderedListAction(); comp.registerAction(action); action.executeInCurrentEditor(); - expect(comp.getText()).toBe('1. '); + expect(comp.getText()).toBe('1. '); }); it.each([1, 2, 3])('Should toggle heading %i on selected line', (headingLevel) => { From 34c27a0287c1b67047d5bdb4e8effe917c49940a Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Mon, 2 Dec 2024 02:56:08 +0100 Subject: [PATCH 4/4] refactor --- .../posting-markdown-editor.component.ts | 2 +- .../postings-markdown-editor.component.spec.ts | 11 +++++++++++ .../monaco-editor/monaco-editor.component.spec.ts | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts index 7de0a478e7ca..c176eed51aa0 100644 --- a/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts +++ b/src/main/webapp/app/shared/metis/posting-markdown-editor/posting-markdown-editor.component.ts @@ -126,7 +126,7 @@ export class PostingMarkdownEditorComponent implements OnInit, ControlValueAcces const editor = this.markdownEditor.monacoEditor; if (editor) { - editor.onDidChangeModelContent((event: { changes: string | any[] }) => { + editor.onDidChangeModelContent((event: monaco.editor.IModelContentChangedEvent) => { const position = editor.getPosition(); if (!position) { return; diff --git a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts index a184d4438a2f..80ef5cf80a23 100644 --- a/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/metis/postings-markdown-editor/postings-markdown-editor.component.spec.ts @@ -566,4 +566,15 @@ describe('PostingsMarkdownEditor', () => { expect(handleActionClickSpy).toHaveBeenCalledWith(expect.any(MouseEvent), bulletedListAction); }); + + it('should handle invalid line content gracefully', () => { + const mockModel = { + getLineContent: jest.fn().mockReturnValue(''), + } as unknown as monaco.editor.ITextModel; + const mockPosition = { lineNumber: 1 } as monaco.Position; + const handleActionClickSpy = jest.spyOn(component.markdownEditor, 'handleActionClick'); + + (component as any).handleKeyDown(mockModel, mockPosition.lineNumber); + expect(handleActionClickSpy).not.toHaveBeenCalled(); + }); }); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts index c611f6fbebf1..462ba750f969 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts @@ -313,4 +313,18 @@ describe('MonacoEditorComponent', () => { const lineContent = comp.getLineContent(2); expect(lineContent).toBe('static void main() {'); }); + + it('should handle invalid line numbers in getLineContent', () => { + fixture.detectChanges(); + comp.setText(multiLineText); + + // Invalid line numbers + expect(() => comp.getLineContent(0)).toThrow(); + expect(() => comp.getLineContent(-1)).toThrow(); + expect(() => comp.getLineContent(999)).toThrow(); + + // Empty line + comp.setText('line1\n\nline3'); + expect(comp.getLineContent(2)).toBe(''); + }); });