Skip to content

Commit

Permalink
feat(viewer): recognise URLs in text value and convert into link (DSP…
Browse files Browse the repository at this point in the history
…-1595) (#315)

* feat(action): new pipe to recognize urls in a string

* feat(viewer): use linkify pipe in text and html values

* test(viewer): fix tests with linkify pipe

* chore(action): expand linkify pipe

* chore(action): fix some typos and refactor code

* chore(action): fix typo
  • Loading branch information
kilchenmann committed Jul 14, 2021
1 parent fa4950e commit f08b50d
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 155 deletions.
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>

0 comments on commit f08b50d

Please sign in to comment.