Skip to content

Commit

Permalink
feat(edit-content) add language variable to wysiwyg and autodetect la…
Browse files Browse the repository at this point in the history
…nguage code (#30419)

### Proposed Changes
* Add support to WYSIWYG to add Language Variables
* Auto-detect language in code.
* Add Velocity to code detection

### Checklist
- [x] Tests
- [ ] Translations
- [ ] Security Implications Contemplated (add notes if applicable)

### Additional Info
** any additional useful context or info **

### Screenshots


https://github.com/user-attachments/assets/3f8ac8bd-e89b-471f-a669-e044b675154a
  • Loading branch information
oidacra authored Oct 23, 2024
1 parent faab8bb commit 1da9702
Show file tree
Hide file tree
Showing 18 changed files with 856 additions and 233 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,9 @@ describe('DotLanguagesService', () => {
spectator.service.getByISOCode('test').subscribe();
spectator.expectOne(`${LANGUAGE_API_URL}/test`, HttpMethod.GET);
});

it('should get language variables', () => {
spectator.service.getLanguageVariables().subscribe();
spectator.expectOne(`${LANGUAGE_API_URL}/variables`, HttpMethod.GET);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@ import { inject, Injectable } from '@angular/core';

import { pluck } from 'rxjs/operators';

import { DotCMSResponse } from '@dotcms/dotcms-js';
import { DotAddLanguage, DotLanguage, DotLanguagesISO } from '@dotcms/dotcms-models';

export const LANGUAGE_API_URL = '/api/v2/languages';

export const LANGUAGE_API_URL_WITH_VARS = '/api/v2/languages?countLangVars=true';

export interface DotLanguageVariables {
total: number;
variables: Record<string, DotLanguageVariableEntry>;
}

export interface DotLanguageVariableEntry {
[languageCode: string]: {
identifier: string;
value: string;
};
}

/**
* Provide util methods to get Languages available in the system.
* @export
Expand Down Expand Up @@ -114,4 +127,15 @@ export class DotLanguagesService {
getISO(): Observable<DotLanguagesISO> {
return this.httpClient.get(`${LANGUAGE_API_URL}/iso`).pipe(pluck('entity'));
}

/**
* Get language variables.
*
* @returns {Observable<Record<string, DotLanguageVariableEntry>>} An observable of the language variables.
*/
getLanguageVariables(): Observable<Record<string, DotLanguageVariableEntry>> {
return this.httpClient
.get<DotCMSResponse<DotLanguageVariables>>(`${LANGUAGE_API_URL}/variables`)
.pipe(pluck('entity', 'variables'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class DotEditContentCategoryFieldComponent implements OnInit {
* @returns {Boolean} - True if there are selected categories, false otherwise.
*/
$hasSelectedCategories = computed(() => !!this.store.selected());

/**
* Getter to retrieve the category field control.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Velocity language definition for Monaco Editor
export const dotVelocityLanguageDefinition: monaco.languages.IMonarchLanguage = {
defaultToken: '',
tokenPostfix: '.vtl',
ignoreCase: true,

brackets: [
{ open: '{', close: '}', token: 'delimiter.curly' },
{ open: '[', close: ']', token: 'delimiter.square' },
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
{ open: '<', close: '>', token: 'delimiter.angle' }
],

keywords: [
'foreach',
'if',
'else',
'elseif',
'end',
'set',
'parse',
'include',
'macro',
'stop',
'dotParse'
],

tokenizer: {
root: [
// HTML Comments
[/<!--/, 'comment.html', '@htmlComment'],

// Velocity Directives

[/#(foreach|if|else|elseif|end|set|parse|include|macro|stop)\b/, 'keyword.velocity'],

[/#(dotParse)\b/, 'keyword.dotparse.velocity'],

// Velocity Variables (más inclusivo)
[
/(\$!?)(\{?)([a-zA-Z_][\w-]*(?:\.[a-zA-Z_][\w-]*)*)/,
{
cases: {
'$2=={': [
'variable.velocity.delimiter',
'variable.velocity.delimiter',
{ token: 'variable.velocity', next: '@velocityVariable' }
],
'$1==$': ['variable.velocity.delimiter', '', 'variable.velocity'],
'@default': ['', '', 'variable.velocity']
}
}
],

// Variables simples que comienzan con $
[/\$[a-zA-Z][\w-]*/, 'variable.velocity'],

// HTML Tags
[/<\/?[\w\-:.]+/, 'tag.html'],

// HTML Attributes
[/[a-zA-Z][a-zA-Z0-9_-]*(?=\s*=)/, 'attribute.name.html'],
[/"[^"]*"|'[^']*'/, 'attribute.value.html'],

// Velocity Comments
[/##[^\n]*/, 'comment.velocity'],
[/#\*(?!\*)/, 'comment.velocity', '@velocityComment'],

// Strings
[/"/, 'string.velocity', '@string_double'],
[/'/, 'string.velocity', '@string_single'],

// Other syntax
[/[{}()[\]]/, 'delimiter.velocity'],
[/[<>]/, 'delimiter.angle.velocity'],
[/[;,.]/, 'delimiter.velocity']
],

htmlComment: [
[/[^-]+/, 'comment.html'],
[/-->/, 'comment.html', '@pop'],
[/-/, 'comment.html']
],

htmlAttributeValue: [
[/[^"]+/, 'string.html'],
[
/(\$!?\{?)([a-zA-Z][\w-]*(?:\.[a-zA-Z][\w-]*)*(?:\([^)]*\))?)(\})?/,
['variable.velocity', 'variable.velocity', 'variable.velocity']
],
[/\$[a-zA-Z][\w-]*/, 'variable.velocity'],
[/"/, { token: 'string.html', next: '@pop' }]
],

string_double: [
[/[^\\"$]+/, 'string.velocity'],
[/\\./, 'string.escape.velocity'],
[
/(\$!?\{?)([a-zA-Z][\w-]*(?:\.[a-zA-Z][\w-]*)*(?:\([^)]*\))?)(\})?/,
['variable.velocity', 'variable.velocity', 'variable.velocity']
],
[/\$[a-zA-Z][\w-]*/, 'variable.velocity'],
[/"/, 'string.velocity', '@pop']
],

string_single: [
[/[^\\'$]+/, 'string.velocity'],
[/\\./, 'string.escape.velocity'],
[
/(\$!?\{?)([a-zA-Z][\w-]*(?:\.[a-zA-Z][\w-]*)*(?:\([^)]*\))?)(\})?/,
['variable.velocity', 'variable.velocity', 'variable.velocity']
],
[/\$[a-zA-Z][\w-]*/, 'variable.velocity'],
[/'/, 'string.velocity', '@pop']
],

velocityComment: [
[/[^*#]+/, 'comment.velocity'],
[/#\*/, 'comment.velocity', '@push'],
[/\*#/, 'comment.velocity', '@pop'],
[/[*#]/, 'comment.velocity']
],

velocityVariable: [
[/\}/, 'variable.velocity.delimiter', '@pop'],
[/\(/, 'delimiter.parenthesis', '@velocityMethod'],
[/[^}()]/, 'variable.velocity']
],

velocityMethod: [
[/\)/, 'delimiter.parenthesis', '@pop'],
[/\(/, 'delimiter.parenthesis', '@push'],
[/[^()]/, 'variable.velocity']
]
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<ngx-monaco-editor
(init)="onEditorInit()"
(init)="onEditorInit($event)"
[options]="$monacoOptions()"
#editorRef
data-testId="code-editor"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@use "variables" as *;

ngx-monaco-editor {
height: 300px;
width: 100%;
border: $field-border-size solid $color-palette-gray-400;
border-radius: $border-radius-md;
display: flex;
:host {
ngx-monaco-editor {
height: 400px;
width: 100%;
padding: $spacing-0;
display: flex;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { WYSIWYG_MOCK } from '../../mocks/dot-edit-content-wysiwyg-field.mock';

describe('DotWysiwygMonacoComponent', () => {
let spectator: Spectator<DotWysiwygMonacoComponent>;
let component: DotWysiwygMonacoComponent;

const createComponent = createComponentFactory({
component: DotWysiwygMonacoComponent,
Expand All @@ -37,24 +38,27 @@ describe('DotWysiwygMonacoComponent', () => {
field: WYSIWYG_MOCK
} as unknown
});
});

it('should set default language', () => {
expect(spectator.component.$language()).toBe(DEFAULT_MONACO_LANGUAGE);
component = spectator.component;
});

it('should set custom language', () => {
const customLanguage = 'javascript';
spectator.setInput('language', customLanguage);
expect(spectator.component.$language()).toBe(customLanguage);
it('should set default language', () => {
expect(component.$language()).toBe(DEFAULT_MONACO_LANGUAGE);
});

it('should generate correct Monaco options', () => {
it('should generate correct Monaco options', async () => {
const expectedOptions = {
...DEFAULT_WYSIWYG_FIELD_MONACO_CONFIG,
language: DEFAULT_MONACO_LANGUAGE
language: 'plaintext' // due the auto detect language is plaintext
};
expect(spectator.component.$monacoOptions()).toEqual(expectedOptions);

// Wait for any potential asynchronous operations to complete
await spectator.fixture.whenStable();

// Force change detection
spectator.detectChanges();

expect(component.$monacoOptions()).toEqual(expectedOptions);
});

it('should parse custom props from field variables', () => {
Expand All @@ -73,8 +77,15 @@ describe('DotWysiwygMonacoComponent', () => {
const expectedOptions = {
...DEFAULT_WYSIWYG_FIELD_MONACO_CONFIG,
...customProps,
language: DEFAULT_MONACO_LANGUAGE
language: 'plaintext' // due the auto detect language is plaintext
};
expect(spectator.component.$monacoOptions()).toEqual(expectedOptions);
expect(component.$monacoOptions()).toEqual(expectedOptions);
});

it('should register Velocity language when Monaco is loaded', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const registerSpy = jest.spyOn(component as any, 'registerVelocityLanguage');
component.ngOnInit();
expect(registerSpy).toHaveBeenCalled();
});
});
Loading

0 comments on commit 1da9702

Please sign in to comment.