@hmcts/ccd-case-ui-toolkit 7.2.58 → 7.2.59-markdown-add-validation

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import * as i1$1 from '@angular/router';
8
8
  import { RouterModule, NavigationStart, NavigationEnd } from '@angular/router';
9
9
  import * as i4 from '@angular/forms';
10
10
  import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormArray, FormGroup, FormControl, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
11
- import { BehaviorSubject, throwError, Subject, EMPTY, Observable, of, combineLatest, timer, forkJoin, fromEvent, Subscription } from 'rxjs';
11
+ import { BehaviorSubject, throwError, Subject, EMPTY, Observable, skip, of, combineLatest, timer, forkJoin, fromEvent, Subscription } from 'rxjs';
12
12
  import * as i1$2 from '@angular/common/http';
13
13
  import { HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
14
14
  import { distinctUntilChanged, catchError, map, publish, refCount, switchMap, debounceTime, take, delay, finalize, timeout, mergeMap, retryWhen, tap, delayWhen, publishReplay, first, takeUntil, filter } from 'rxjs/operators';
@@ -5093,10 +5093,15 @@ class FormValidatorsService {
5093
5093
  return validator;
5094
5094
  }
5095
5095
  static markDownPatternValidator() {
5096
- const pattern = /(\[[^\]]{0,500}\]\([^)]{0,500}\)|!\[[^\]]{0,500}\]\([^)]{0,500}\)|<img[^>]{0,500}>|<a[^>]{0,500}>.*?<\/a>)/;
5096
+ // Matches: [text](url), ![alt](url), <img ...>, <a ...>...</a>
5097
+ const inlineMarkdownPattern = /(?:!?\[[^\]]{0,500}\]\([^)]{0,500}\)|<(?:img\b[^>]{0,500}>|a\b[^>]{0,500}>[\s\S]*?<\/a>))/i;
5098
+ // Matches: [text][id], ![alt][id], and the collapsed form [text][]
5099
+ const referenceBoxPattern = /(!)?\[((?:[^[\]\\]|\\.){0,500})\]\s*\[([^\]]{0,100})\]/;
5100
+ // Matches: autolinks such as <http://example.com>
5101
+ const autolinkPattern = /<(?:[A-Za-z][A-Za-z0-9+.-]*:[^ <>\n]*|[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)>/;
5097
5102
  return (control) => {
5098
5103
  const value = control?.value?.toString().trim();
5099
- return (value && pattern.test(value)) ? { markDownPattern: {} } : null;
5104
+ return (value && (inlineMarkdownPattern.test(value) || referenceBoxPattern.test(value) || this.matchesReferenceUrlDef(value) || autolinkPattern.test(value) || this.hasMultiBracket(value))) ? { markDownPattern: {} } : null;
5100
5105
  };
5101
5106
  }
5102
5107
  // TODO: Strip this out as it's only here for the moment because
@@ -5114,6 +5119,74 @@ class FormValidatorsService {
5114
5119
  }
5115
5120
  return control;
5116
5121
  }
5122
+ // Check for multi-bracket markdown links and validate destination URL
5123
+ static hasMultiBracket(value) {
5124
+ // Sonar-friendly detector: opening-run + text + first closing ']'
5125
+ const openingTextClosePattern = /\[{1,10}[^[\]\n]{1,60}\]/;
5126
+ // Can add an additional RegEx for additional URL validation rules if needed here
5127
+ let scanIndex = 0;
5128
+ const totalLength = value.length;
5129
+ while (scanIndex < totalLength) {
5130
+ const seg = this.findOpeningTextClose(value, scanIndex, openingTextClosePattern);
5131
+ if (!seg) {
5132
+ return false; // no candidate -> no match
5133
+ }
5134
+ const runs = this.extendClosingRunAndRequireParen(value, seg.absStart, seg.afterFirstClose);
5135
+ // if there is more than one opening '[' and there is at least a matching number of closing ']'
5136
+ if (runs && runs.openingRunCount > 1 && runs.openingRunCount === runs.closingRunCount) {
5137
+ // If there were additional validation rules, they would be applied here
5138
+ return true;
5139
+ }
5140
+ // Advance to avoid stalling on overlaps
5141
+ scanIndex = seg.absStart + 1;
5142
+ }
5143
+ return false;
5144
+ }
5145
+ // Find opening '[' run, text, and first closing ']'
5146
+ static findOpeningTextClose(source, fromIndex, pattern) {
5147
+ const slice = source.slice(fromIndex);
5148
+ const match = pattern.exec(slice);
5149
+ if (!match) {
5150
+ return null;
5151
+ }
5152
+ const absStart = fromIndex + match.index;
5153
+ const afterFirstClose = absStart + match[0].length; // index just after the first ']'
5154
+ return { absStart, afterFirstClose };
5155
+ }
5156
+ // Count opening '[' run, extend the ']' run, and require '(' right after the full ']' run
5157
+ static extendClosingRunAndRequireParen(source, absStart, afterFirstClose) {
5158
+ const n = source.length;
5159
+ // Count opening '[' run (e.g., '[[[')
5160
+ let openingRunCount = 0;
5161
+ for (let i = absStart; i < n && source[i] === '['; i++) {
5162
+ openingRunCount++;
5163
+ }
5164
+ // Extend closing ']' run forward from the first one
5165
+ let closingRunCount = 1;
5166
+ let afterClosingRun = afterFirstClose;
5167
+ while (afterClosingRun < n && source[afterClosingRun] === ']') {
5168
+ closingRunCount++;
5169
+ afterClosingRun++;
5170
+ }
5171
+ return { openingRunCount, closingRunCount, afterOpenParen: afterClosingRun + 1 };
5172
+ }
5173
+ static isValidReferenceUrlTitleTail(tail) {
5174
+ const possibleTitle = tail.trim();
5175
+ // Accept exactly one of: "title", 'title', (title) — bounded and single-line.
5176
+ if (!possibleTitle || /^"[^"\r\n]{0,300}"$/.test(possibleTitle) || /^'[^'\r\n]{0,300}'$/.test(possibleTitle) || /^\([^)\r\n]{0,300}\)$/.test(possibleTitle)) {
5177
+ return true;
5178
+ }
5179
+ return false;
5180
+ }
5181
+ static matchesReferenceUrlDef(line) {
5182
+ // Single-line, pragmatic CommonMark-style reference definition e.g. [text]: http://example.com
5183
+ const baseReferenceUrlPattern = /^[ \t]{0,3}\[([^\]]{1,100})\]:[ \t]*<?([^\s>]{1,2048})>?[ \t]*([^ \t\r\n].*)?$/m;
5184
+ const mainRegEx = baseReferenceUrlPattern.exec(line);
5185
+ if (!mainRegEx)
5186
+ return false;
5187
+ const tail = mainRegEx[3] ?? "";
5188
+ return this.isValidReferenceUrlTitleTail(tail);
5189
+ }
5117
5190
  static ɵfac = function FormValidatorsService_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || FormValidatorsService)(); };
5118
5191
  static ɵprov = /*@__PURE__*/ i0.ɵɵdefineInjectable({ token: FormValidatorsService, factory: FormValidatorsService.ɵfac });
5119
5192
  }
@@ -6533,11 +6606,8 @@ class LabelSubstitutorDirective {
6533
6606
  this.initialHintText = this.caseField.hint_text;
6534
6607
  this.caseField.originalLabel = this.caseField.label;
6535
6608
  this.formGroup = this.formGroup || new FormGroup({});
6536
- this.languageSubscription = this.rpxTranslationService.language$.subscribe(() => {
6537
- // timeout is required to prevent race conditions with translation pipe
6538
- setTimeout(() => {
6539
- this.onLanguageChange();
6540
- }, 100);
6609
+ this.languageSubscription = this.rpxTranslationService.language$.pipe(skip(1)).subscribe(() => {
6610
+ this.onLanguageChange();
6541
6611
  });
6542
6612
  this.applySubstitutions();
6543
6613
  }
@@ -7346,7 +7416,6 @@ class DocumentManagementService {
7346
7416
  isDocumentSecureModeEnabled() {
7347
7417
  const documentSecureModeCaseTypeExclusions = this.appConfig.getCdamExclusionList()?.split(',');
7348
7418
  const isDocumentOnExclusionList = documentSecureModeCaseTypeExclusions?.includes(this.caseTypeId);
7349
- this.appConfig.logMessage(`isDocumentOnExclusionList: ${isDocumentOnExclusionList}`);
7350
7419
  if (!isDocumentOnExclusionList) {
7351
7420
  return true;
7352
7421
  }
@@ -15252,6 +15321,7 @@ class WriteDocumentFieldComponent extends AbstractFieldWriteComponent {
15252
15321
  jurisdictionId;
15253
15322
  caseTypeId;
15254
15323
  caseTypeExclusions;
15324
+ caseId;
15255
15325
  // Should the file upload use CDAM
15256
15326
  fileSecureModeOn = false;
15257
15327
  constructor(appConfig, caseNotifier, documentManagement, dialog, fileUploadStateService, jurisdictionService) {
@@ -15272,6 +15342,7 @@ class WriteDocumentFieldComponent extends AbstractFieldWriteComponent {
15272
15342
  if (caseDetails) {
15273
15343
  this.caseTypeId = caseDetails?.case_type?.id;
15274
15344
  this.jurisdictionId = caseDetails?.case_type?.jurisdiction?.id;
15345
+ this.caseId = caseDetails?.case_id;
15275
15346
  }
15276
15347
  if (jurisdiction) {
15277
15348
  this.jurisdictionId = jurisdiction.id;
@@ -15290,12 +15361,8 @@ class WriteDocumentFieldComponent extends AbstractFieldWriteComponent {
15290
15361
  }
15291
15362
  // use the documentManagement service to check if the document upload should use CDAM
15292
15363
  if (this.documentManagement.isDocumentSecureModeEnabled()) {
15293
- this.appConfig.logMessage(`CDAM is enabled for case with case ref:: ${caseDetails?.case_id}`);
15294
15364
  this.fileSecureModeOn = true;
15295
15365
  }
15296
- else {
15297
- this.appConfig.logMessage(`CDAM is disabled for case with case ref:: ${caseDetails?.case_id}`);
15298
- }
15299
15366
  this.dialogConfig = initDialog();
15300
15367
  let document = this.caseField.value || { document_url: null, document_binary_url: null, document_filename: null };
15301
15368
  document = this.fileSecureModeOn && !document.document_hash ? { ...document, document_hash: null } : document;
@@ -15529,6 +15596,13 @@ class WriteDocumentFieldComponent extends AbstractFieldWriteComponent {
15529
15596
  return documentUpload;
15530
15597
  }
15531
15598
  handleDocumentUploadResult(result) {
15599
+ // use the documentManagement service to check if the document upload should use CDAM
15600
+ if (this.documentManagement.isDocumentSecureModeEnabled()) {
15601
+ this.appConfig.logMessage(`CDAM is enabled for case with case ref:: ${this.caseId}`);
15602
+ }
15603
+ else {
15604
+ this.appConfig.logMessage(`CDAM is disabled for case with case ref:: ${this.caseId}`);
15605
+ }
15532
15606
  if (!this.uploadedDocument) {
15533
15607
  if (this.fileSecureModeOn) {
15534
15608
  this.createDocumentForm({ document_url: null, document_binary_url: null, document_filename: null, document_hash: null });