Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(viewer): recognise URLs in text value and convert into link (DSP-1595) #315

Merged
merged 6 commits into from Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
123 changes: 63 additions & 60 deletions projects/dsp-ui/src/lib/action/action.module.ts
@@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
Expand All @@ -10,14 +11,15 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ConfirmationDialogComponent } from './components/confirmation-dialog/confirmation-dialog.component';
import { ConfirmationMessageComponent } from './components/confirmation-dialog/confirmation-message/confirmation-message.component';
import { LoginFormComponent } from './components/login-form/login-form.component';
import { MessageComponent } from './components/message/message.component';
import { ProgressIndicatorComponent } from './components/progress-indicator/progress-indicator.component';
import { SelectProjectComponent } from './components/select-project/select-project.component';
import { SelectedResourcesComponent } from './components/selected-resources/selected-resources.component';
import { SortButtonComponent } from './components/sort-button/sort-button.component';
import { StringLiteralInputComponent } from './components/string-literal-input/string-literal-input.component';
import { AdminImageDirective } from './directives/admin-image/admin-image.directive';
Expand All @@ -27,69 +29,70 @@ import { ReversePipe } from './pipes/array-transformation/reverse.pipe';
import { SortByPipe } from './pipes/array-transformation/sort-by.pipe';
import { FormattedBooleanPipe } from './pipes/formatting/formatted-boolean.pipe';
import { KnoraDatePipe } from './pipes/formatting/knoradate.pipe';
import { LinkifyPipe } from './pipes/string-transformation/linkify.pipe';
import { StringifyStringLiteralPipe } from './pipes/string-transformation/stringify-string-literal.pipe';
import { TruncatePipe } from './pipes/string-transformation/truncate.pipe';
import { SelectProjectComponent } from './components/select-project/select-project.component';
import { SelectedResourcesComponent } from './components/selected-resources/selected-resources.component';

@NgModule({
declarations: [
AdminImageDirective,
ConfirmationDialogComponent,
ConfirmationMessageComponent,
ExistingNameDirective,
FormattedBooleanPipe,
GndDirective,
KnoraDatePipe,
LoginFormComponent,
MessageComponent,
ProgressIndicatorComponent,
ReversePipe,
SortButtonComponent,
SortByPipe,
StringifyStringLiteralPipe,
StringLiteralInputComponent,
SelectProjectComponent,
TruncatePipe,
SelectedResourcesComponent
],
imports: [
BrowserAnimationsModule,
CommonModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatListModule,
MatAutocompleteModule,
FormsModule,
ReactiveFormsModule,
MatMenuModule,
MatSnackBarModule
],
exports: [
AdminImageDirective,
ConfirmationDialogComponent,
ConfirmationMessageComponent,
ExistingNameDirective,
FormattedBooleanPipe,
GndDirective,
KnoraDatePipe,
LoginFormComponent,
MessageComponent,
ProgressIndicatorComponent,
ReversePipe,
SortButtonComponent,
SortByPipe,
StringifyStringLiteralPipe,
StringLiteralInputComponent,
SelectProjectComponent,
TruncatePipe,
SelectedResourcesComponent
]
declarations: [
AdminImageDirective,
ConfirmationDialogComponent,
ConfirmationMessageComponent,
ExistingNameDirective,
FormattedBooleanPipe,
GndDirective,
KnoraDatePipe,
LoginFormComponent,
MessageComponent,
ProgressIndicatorComponent,
ReversePipe,
SortButtonComponent,
SortByPipe,
StringifyStringLiteralPipe,
StringLiteralInputComponent,
SelectProjectComponent,
LinkifyPipe,
TruncatePipe,
SelectedResourcesComponent
],
imports: [
BrowserAnimationsModule,
CommonModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatDialogModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatListModule,
MatAutocompleteModule,
FormsModule,
ReactiveFormsModule,
MatMenuModule,
MatSnackBarModule
],
exports: [
AdminImageDirective,
ConfirmationDialogComponent,
ConfirmationMessageComponent,
ExistingNameDirective,
FormattedBooleanPipe,
GndDirective,
KnoraDatePipe,
LoginFormComponent,
MessageComponent,
ProgressIndicatorComponent,
ReversePipe,
SortButtonComponent,
SortByPipe,
StringifyStringLiteralPipe,
StringLiteralInputComponent,
SelectProjectComponent,
LinkifyPipe,
TruncatePipe,
SelectedResourcesComponent
]
})

export class DspActionModule { }
1 change: 1 addition & 0 deletions projects/dsp-ui/src/lib/action/index.ts
Expand Up @@ -5,6 +5,7 @@ export * from './pipes/formatting/formatted-boolean.pipe';
export * from './pipes/formatting/knoradate.pipe';
export * from './pipes/array-transformation/reverse.pipe';
export * from './pipes/array-transformation/sort-by.pipe';
export * from './pipes/string-transformation/linkify.pipe';
export * from './pipes/string-transformation/truncate.pipe';
export * from './pipes/string-transformation/stringify-string-literal.pipe';

Expand Down
@@ -0,0 +1,32 @@
import { LinkifyPipe } from './linkify.pipe';

describe('LinkifyPipe', () => {

let pipe: LinkifyPipe;

beforeEach(() => {
pipe = new LinkifyPipe();
});

it('create an instance', () => {
expect(pipe).toBeTruthy();
});

it('should recognize the url without protocol but with hashtag at the end', () => {
const text = 'You can visit the app on app.dasch.swiss/#';
const linkifiedSnippet = pipe.transform(text);
expect(linkifiedSnippet).toEqual('You can visit the app on <a href="http://app.dasch.swiss/#" target="_blank">app.dasch.swiss/#</a>');
});

it('should recognize the url with protocol followed by full stop', () => {
const text = 'You can visit the app on https://app.dasch.swiss.';
const linkifiedSnippet = pipe.transform(text);
expect(linkifiedSnippet).toEqual('You can visit the app on <a href="https://app.dasch.swiss" target="_blank">https://app.dasch.swiss</a>.');
});

it('should recognize both urls in the example text', () => {
const text = 'You can visit the app on https://app.dasch.swiss and the documentation on docs.dasch.swiss.';
const linkifiedSnippet = pipe.transform(text);
expect(linkifiedSnippet).toEqual('You can visit the app on <a href="https://app.dasch.swiss" target="_blank">https://app.dasch.swiss</a> and the documentation on <a href="http://docs.dasch.swiss" target="_blank">docs.dasch.swiss</a>.');
});
});
@@ -0,0 +1,57 @@
import { Pipe, PipeTransform } from '@angular/core';

/**
* This pipe analyses a string and converts any url into a href tag
*
*/
@Pipe({
name: 'dspLinkify'
})
export class LinkifyPipe implements PipeTransform {

transform(value: string): string {
let stylizedText: string = '';
if (value && value.length > 0) {
for (let str of value.split(' ')) {
// if string/url ends with a full stop '.' or colon ':' or comma ',' or semicolon ';' the pipe will not recognize the url
const lastChar = str.substring(str.length - 1);
const endsWithFullStop = (lastChar === '.' || lastChar === ':' || lastChar === ',' || lastChar === ';');
let end = ' ';
if (endsWithFullStop) {
str = str.slice(0, -1);
end = '.'
}
if (this._recognizeUrl(str)) {
const url = this._setProtocol(str);
stylizedText += `<a href="${url}" target="_blank">${str}</a>${end}`;
} else {
stylizedText += str + end;
}
}
return stylizedText.trim();
} else {
return value;
}
}

private _recognizeUrl(str: string): boolean {
const pattern = new RegExp(
'^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$',
'i'
); // fragment locator
return pattern.test(str);
}

private _setProtocol(url: string): string {
if (!/^(?:f|ht)tps?\:\/\//.test(url)) {
url = 'http://' + url;
}
return url;
}

}
@@ -1,4 +1,4 @@
<div [innerHTML]="htmlFromKnora" class="value"></div>
<div [innerHTML]="htmlFromKnora | dspLinkify" class="value"></div>
<div *ngIf="comment">
<label>{{commentLabel}}: </label><span class="comment">{{comment}}</span>
</div>