@lifecorner/angular-ui-kit 3.6.0 → 3.7.0

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.
@@ -1,6 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from "@angular/core";
3
- import { DomSanitizer } from "@angular/platform-browser";
2
+ import { ChangeDetectionStrategy, Component, computed, input, signal } from "@angular/core";
4
3
  import { IconComponent, ɵprovideKitIcons } from "@lifecorner/angular-ui-kit/icon";
5
4
  import { ICON_CHECK } from "@lifecorner/angular-ui-kit/icons";
6
5
  const SHARED_RULES = [
@@ -73,17 +72,13 @@ const LANGUAGE_RULES = {
73
72
  }
74
73
  ],
75
74
  html: [
76
- {
77
- pattern: /<!--[\s\S]*?-->/g,
78
- className: "token-comment"
79
- },
80
75
  ...SHARED_RULES,
81
76
  {
82
- pattern: /&lt;\/?[\w-]+/g,
77
+ pattern: /<\/?[\w-]+/g,
83
78
  className: "token-tag"
84
79
  },
85
80
  {
86
- pattern: /\/?&gt;/g,
81
+ pattern: /\/?>/g,
87
82
  className: "token-tag"
88
83
  },
89
84
  {
@@ -242,22 +237,21 @@ const LANGUAGE_RULES = {
242
237
  }
243
238
  ]
244
239
  };
245
- function escapeHtml(text) {
246
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
247
- }
248
- function highlightCode(code, language) {
249
- const escaped = escapeHtml(code);
240
+ const EMPTY_LINE = [{
241
+ text: String.fromCharCode(160),
242
+ className: null
243
+ }];
244
+ function tokenize(code, language) {
250
245
  const rules = LANGUAGE_RULES[language];
251
- if (!rules) return escaped;
246
+ if (!rules) return [];
252
247
  const tokens = [];
253
248
  for (const rule of rules) {
254
249
  const regex = new RegExp(rule.pattern.source, rule.pattern.flags);
255
250
  let match;
256
- while ((match = regex.exec(escaped)) !== null) tokens.push({
251
+ while ((match = regex.exec(code)) !== null) tokens.push({
257
252
  start: match.index,
258
253
  end: match.index + match[0].length,
259
- className: rule.className,
260
- text: match[0]
254
+ className: rule.className
261
255
  });
262
256
  }
263
257
  tokens.sort((a, b) => a.start - b.start || b.end - a.end);
@@ -267,15 +261,35 @@ function highlightCode(code, language) {
267
261
  filtered.push(token);
268
262
  lastEnd = token.end;
269
263
  }
270
- let result = "";
264
+ return filtered;
265
+ }
266
+ function highlightLines(code, language) {
267
+ const lines = [[]];
268
+ const append = (text, className) => {
269
+ text.split("\n").forEach((part, i) => {
270
+ if (i > 0) lines.push([]);
271
+ if (!part) return;
272
+ const line = lines[lines.length - 1];
273
+ const cls = i === 0 ? className : null;
274
+ const last = line[line.length - 1];
275
+ if (cls === null && last?.className === null) line[line.length - 1] = {
276
+ text: last.text + part,
277
+ className: null
278
+ };
279
+ else line.push({
280
+ text: part,
281
+ className: cls
282
+ });
283
+ });
284
+ };
271
285
  let cursor = 0;
272
- for (const token of filtered) {
273
- if (token.start > cursor) result += escaped.slice(cursor, token.start);
274
- result += `<span class="${token.className}">${token.text}</span>`;
286
+ for (const token of tokenize(code, language)) {
287
+ append(code.slice(cursor, token.start), null);
288
+ append(code.slice(token.start, token.end), token.className);
275
289
  cursor = token.end;
276
290
  }
277
- result += escaped.slice(cursor);
278
- return result;
291
+ append(code.slice(cursor), null);
292
+ return lines.map((line) => line.length ? line : EMPTY_LINE);
279
293
  }
280
294
  var CodeBlockComponent = class CodeBlockComponent {
281
295
  code = input.required(...ngDevMode ? [{ debugName: "code" }] : /* istanbul ignore next */ []);
@@ -284,23 +298,25 @@ var CodeBlockComponent = class CodeBlockComponent {
284
298
  showLineNumbers = input(true, ...ngDevMode ? [{ debugName: "showLineNumbers" }] : /* istanbul ignore next */ []);
285
299
  showCopy = input(true, ...ngDevMode ? [{ debugName: "showCopy" }] : /* istanbul ignore next */ []);
286
300
  showHeader = input(true, ...ngDevMode ? [{ debugName: "showHeader" }] : /* istanbul ignore next */ []);
301
+ regionLabel = input(void 0, ...ngDevMode ? [{ debugName: "regionLabel" }] : /* istanbul ignore next */ []);
302
+ wrap = input(false, ...ngDevMode ? [{ debugName: "wrap" }] : /* istanbul ignore next */ []);
287
303
  copyLabel = input("Copy", ...ngDevMode ? [{ debugName: "copyLabel" }] : /* istanbul ignore next */ []);
288
304
  copyAriaLabel = input("Copy code", ...ngDevMode ? [{ debugName: "copyAriaLabel" }] : /* istanbul ignore next */ []);
289
305
  copiedLabel = input("Copied", ...ngDevMode ? [{ debugName: "copiedLabel" }] : /* istanbul ignore next */ []);
290
306
  copiedAnnouncement = input("Copied to clipboard", ...ngDevMode ? [{ debugName: "copiedAnnouncement" }] : /* istanbul ignore next */ []);
291
307
  copied = signal(false, ...ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []);
292
- sanitizer = inject(DomSanitizer);
293
- highlightedLines = computed(() => {
294
- return highlightCode(this.code(), this.language()).split("\n").map((line) => this.sanitizer.bypassSecurityTrustHtml(line || "&nbsp;"));
295
- }, ...ngDevMode ? [{ debugName: "highlightedLines" }] : /* istanbul ignore next */ []);
308
+ highlightedLines = computed(() => highlightLines(this.code(), this.language()), ...ngDevMode ? [{ debugName: "highlightedLines" }] : /* istanbul ignore next */ []);
296
309
  lineNumbers = computed(() => {
297
310
  return this.code().split("\n").map((_, i) => i + 1);
298
311
  }, ...ngDevMode ? [{ debugName: "lineNumbers" }] : /* istanbul ignore next */ []);
299
312
  headerLabel = computed(() => {
300
313
  return this.filename() || this.language();
301
314
  }, ...ngDevMode ? [{ debugName: "headerLabel" }] : /* istanbul ignore next */ []);
315
+ regionName = computed(() => this.regionLabel() || this.headerLabel(), ...ngDevMode ? [{ debugName: "regionName" }] : /* istanbul ignore next */ []);
302
316
  codeClasses = computed(() => {
303
- return ["code-block", `code-block--${this.language()}`].join(" ");
317
+ const classes = ["code-block", `code-block--${this.language()}`];
318
+ if (this.wrap()) classes.push("code-block--wrap");
319
+ return classes.join(" ");
304
320
  }, ...ngDevMode ? [{ debugName: "codeClasses" }] : /* istanbul ignore next */ []);
305
321
  async copyToClipboard() {
306
322
  try {
@@ -366,6 +382,20 @@ var CodeBlockComponent = class CodeBlockComponent {
366
382
  isRequired: false,
367
383
  transformFunction: null
368
384
  },
385
+ regionLabel: {
386
+ classPropertyName: "regionLabel",
387
+ publicName: "regionLabel",
388
+ isSignal: true,
389
+ isRequired: false,
390
+ transformFunction: null
391
+ },
392
+ wrap: {
393
+ classPropertyName: "wrap",
394
+ publicName: "wrap",
395
+ isSignal: true,
396
+ isRequired: false,
397
+ transformFunction: null
398
+ },
369
399
  copyLabel: {
370
400
  classPropertyName: "copyLabel",
371
401
  publicName: "copyLabel",
@@ -396,8 +426,8 @@ var CodeBlockComponent = class CodeBlockComponent {
396
426
  }
397
427
  },
398
428
  ngImport: i0,
399
- template: "<div [class]=\"codeClasses()\">\n @if (showHeader()) {\n <div class=\"code-block__header\">\n <span class=\"code-block__language\">{{ headerLabel() }}</span>\n @if (showCopy()) {\n <button\n type=\"button\"\n class=\"code-block__copy\"\n [attr.aria-label]=\"copied() ? copiedLabel() : copyAriaLabel()\"\n (click)=\"copyToClipboard()\"\n >\n @if (copied()) {\n <lc-icon name=\"check\" size=\"sm\" />\n <span>{{ copiedLabel() }}</span>\n } @else {\n <lc-icon name=\"clipboard-document\" size=\"sm\" />\n <span>{{ copyLabel() }}</span>\n }\n </button>\n <span class=\"code-block__live\" role=\"status\" aria-live=\"polite\">{{ copied() ? copiedAnnouncement() : '' }}</span>\n }\n </div>\n }\n <div class=\"code-block__body\">\n <pre class=\"code-block__pre\"><code class=\"code-block__code\">@for (line of highlightedLines(); track $index; let i = $index) {<span class=\"code-block__line\">@if (showLineNumbers()) {<span class=\"code-block__line-number\">{{ lineNumbers()[i] }}</span>}<span class=\"code-block__line-content\" [innerHTML]=\"line\"></span></span>\n}</code></pre>\n </div>\n</div>\n",
400
- styles: [".code-block{--lc-code-block-bg: #0c1722;--lc-code-block-ink: #eef4f6;--lc-code-block-muted: #93a4ad;--lc-code-block-muted-strong: #c4d2d9;--lc-code-block-border: #24343f;position:relative;border:1px solid var(--lc-code-block-border);border-radius:var(--border-radius-md);overflow:hidden;font-family:var(--typography-font-family-mono, \"SF Mono\", \"Fira Code\", \"Fira Mono\", monospace);font-size:var(--typography-font-size-sm, .875rem);line-height:1.1;background-color:var(--lc-code-block-bg);color:var(--lc-code-block-ink)}.code-block__header{display:flex;justify-content:space-between;align-items:center;min-height:2.75rem;padding:0 .25rem 0 1rem;background-color:#00000040;border-bottom:1px solid var(--lc-code-block-border)}.code-block__language{font-size:var(--typography-font-size-xs, .75rem);color:var(--lc-code-block-muted);text-transform:lowercase;-webkit-user-select:none;user-select:none}.code-block__copy{display:inline-flex;align-items:center;justify-content:center;gap:.25rem;min-height:2.75rem;min-width:2.75rem;padding:0 .75rem;border:none;border-radius:var(--border-radius-sm);background:transparent;color:var(--lc-code-block-muted);font-size:var(--typography-font-size-xs, .75rem);font-family:inherit;cursor:pointer;transition:color .15s ease,background-color .15s ease}.code-block__copy:hover{color:var(--lc-code-block-muted-strong);background-color:#ffffff0f}.code-block__copy:focus-visible{outline:none;color:var(--lc-code-block-muted-strong);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 40%,transparent)}.code-block__live{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.code-block__body{overflow-x:auto}.code-block__pre{margin:0;padding:1rem;background:transparent;color:inherit;border-radius:0}.code-block__code{display:block}.code-block__line{display:flex}.code-block__line-number{display:inline-block;min-width:2.5rem;padding-right:1rem;text-align:right;color:var(--lc-code-block-muted);-webkit-user-select:none;user-select:none;flex-shrink:0}.code-block__line-content{flex:1;white-space:pre}.code-block__line-content ::ng-deep .token-keyword{color:#c678dd}.code-block__line-content ::ng-deep .token-string{color:#98c379}.code-block__line-content ::ng-deep .token-number{color:#d19a66}.code-block__line-content ::ng-deep .token-comment{color:#5c6370;font-style:italic}.code-block__line-content ::ng-deep .token-type{color:#e5c07b}.code-block__line-content ::ng-deep .token-constant{color:#d19a66}.code-block__line-content ::ng-deep .token-decorator{color:#61afef}.code-block__line-content ::ng-deep .token-tag{color:#e06c75}.code-block__line-content ::ng-deep .token-attr{color:#d19a66}.code-block__line-content ::ng-deep .token-selector{color:#e06c75}.code-block__line-content ::ng-deep .token-property{color:#61afef}.code-block__line-content ::ng-deep .token-unit{color:#d19a66}.code-block__line-content ::ng-deep .token-variable{color:#e06c75}.code-block__line-content ::ng-deep .token-builtin{color:#61afef}.code-block__line-content ::ng-deep .token-flag{color:#56b6c2}\n"],
429
+ template: "<div [class]=\"codeClasses()\">\n @if (showHeader()) {\n <div class=\"code-block__header\">\n <span class=\"code-block__language\">{{ headerLabel() }}</span>\n @if (showCopy()) {\n <button\n type=\"button\"\n class=\"code-block__copy\"\n [attr.aria-label]=\"copied() ? copiedLabel() : copyAriaLabel()\"\n (click)=\"copyToClipboard()\"\n >\n @if (copied()) {\n <lc-icon name=\"check\" size=\"sm\" [decorative]=\"true\" />\n <span>{{ copiedLabel() }}</span>\n } @else {\n <lc-icon name=\"clipboard-document\" size=\"sm\" [decorative]=\"true\" />\n <span>{{ copyLabel() }}</span>\n }\n </button>\n <span class=\"code-block__live\" role=\"status\" aria-live=\"polite\">{{ copied() ? copiedAnnouncement() : '' }}</span>\n }\n </div>\n }\n <div\n class=\"code-block__body\"\n [attr.tabindex]=\"wrap() ? null : 0\"\n [attr.role]=\"wrap() ? null : 'region'\"\n [attr.aria-label]=\"wrap() ? null : regionName()\"\n >\n <pre class=\"code-block__pre\"><code class=\"code-block__code\">@for (line of highlightedLines(); track $index; let i = $index) {<span class=\"code-block__line\">@if (showLineNumbers()) {<span class=\"code-block__line-number\">{{ lineNumbers()[i] }}</span>}<span class=\"code-block__line-content\">@for (segment of line; track $index) {@if (segment.className) {<span [attr.class]=\"segment.className\">{{ segment.text }}</span>}@else{{{ segment.text }}}}</span></span>\n}</code></pre>\n </div>\n</div>\n",
430
+ styles: [".code-block{--lc-code-block-bg: #0c1722;--lc-code-block-ink: #eef4f6;--lc-code-block-muted: #93a4ad;--lc-code-block-muted-strong: #c4d2d9;--lc-code-block-border: #24343f;position:relative;border:1px solid var(--lc-code-block-border);border-radius:var(--border-radius-md);overflow:hidden;font-family:var(--typography-font-family-mono, \"SF Mono\", \"Fira Code\", \"Fira Mono\", monospace);font-size:var(--typography-font-size-sm, .875rem);line-height:1.1;background-color:var(--lc-code-block-bg);color:var(--lc-code-block-ink)}.code-block__header{display:flex;justify-content:space-between;align-items:center;min-height:2.75rem;padding:0 .25rem 0 1rem;background-color:#00000040;border-bottom:1px solid var(--lc-code-block-border)}.code-block__language{font-size:var(--typography-font-size-xs, .75rem);color:var(--lc-code-block-muted);text-transform:lowercase;-webkit-user-select:none;user-select:none}.code-block__copy{display:inline-flex;align-items:center;justify-content:center;gap:.25rem;min-height:2.75rem;min-width:2.75rem;padding:0 .75rem;border:none;border-radius:var(--border-radius-sm);background:transparent;color:var(--lc-code-block-muted);font-size:var(--typography-font-size-xs, .75rem);font-family:inherit;cursor:pointer;transition:color .15s ease,background-color .15s ease}.code-block__copy:hover{color:var(--lc-code-block-muted-strong);background-color:#ffffff0f}.code-block__copy:focus-visible{outline:none;color:var(--lc-code-block-muted-strong);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 40%,transparent)}.code-block__live{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.code-block__body{overflow-x:auto}.code-block__body:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-4px}.code-block__body:focus-visible{border-radius:0 0 calc(var(--border-radius-md) - 1px) calc(var(--border-radius-md) - 1px)}.code-block__body:first-child:focus-visible{border-radius:calc(var(--border-radius-md) - 1px)}@media(forced-colors:active){.code-block__body:focus-visible{outline-color:Highlight}}.code-block__pre{margin:0;padding:1rem;background:transparent;color:inherit;border-radius:0;overflow:visible}.code-block__code{display:block}.code-block__line{display:flex}.code-block__line-number{display:inline-block;min-width:2.5rem;padding-right:1rem;text-align:right;color:var(--lc-code-block-muted);-webkit-user-select:none;user-select:none;flex-shrink:0}.code-block__line-content{flex:1;white-space:pre}.code-block__line-content ::ng-deep .token-keyword{color:#c678dd}.code-block__line-content ::ng-deep .token-string{color:#98c379}.code-block__line-content ::ng-deep .token-number{color:#d19a66}.code-block__line-content ::ng-deep .token-comment{color:var(--lc-code-block-muted);font-style:italic}.code-block__line-content ::ng-deep .token-type{color:#e5c07b}.code-block__line-content ::ng-deep .token-constant{color:#d19a66}.code-block__line-content ::ng-deep .token-decorator{color:#61afef}.code-block__line-content ::ng-deep .token-tag{color:#e06c75}.code-block__line-content ::ng-deep .token-attr{color:#d19a66}.code-block__line-content ::ng-deep .token-selector{color:#e06c75}.code-block__line-content ::ng-deep .token-property{color:#61afef}.code-block__line-content ::ng-deep .token-unit{color:#d19a66}.code-block__line-content ::ng-deep .token-variable{color:#e06c75}.code-block__line-content ::ng-deep .token-builtin{color:#61afef}.code-block__line-content ::ng-deep .token-flag{color:#56b6c2}.code-block--wrap .code-block__line-content{white-space:pre-wrap;overflow-wrap:anywhere;min-width:0}\n"],
401
431
  dependencies: [{
402
432
  kind: "component",
403
433
  type: IconComponent,
@@ -429,8 +459,8 @@ i0.ɵɵngDeclareClassMetadata({
429
459
  imports: [IconComponent],
430
460
  viewProviders: [ɵprovideKitIcons([ICON_CHECK])],
431
461
  changeDetection: ChangeDetectionStrategy.OnPush,
432
- template: "<div [class]=\"codeClasses()\">\n @if (showHeader()) {\n <div class=\"code-block__header\">\n <span class=\"code-block__language\">{{ headerLabel() }}</span>\n @if (showCopy()) {\n <button\n type=\"button\"\n class=\"code-block__copy\"\n [attr.aria-label]=\"copied() ? copiedLabel() : copyAriaLabel()\"\n (click)=\"copyToClipboard()\"\n >\n @if (copied()) {\n <lc-icon name=\"check\" size=\"sm\" />\n <span>{{ copiedLabel() }}</span>\n } @else {\n <lc-icon name=\"clipboard-document\" size=\"sm\" />\n <span>{{ copyLabel() }}</span>\n }\n </button>\n <span class=\"code-block__live\" role=\"status\" aria-live=\"polite\">{{ copied() ? copiedAnnouncement() : '' }}</span>\n }\n </div>\n }\n <div class=\"code-block__body\">\n <pre class=\"code-block__pre\"><code class=\"code-block__code\">@for (line of highlightedLines(); track $index; let i = $index) {<span class=\"code-block__line\">@if (showLineNumbers()) {<span class=\"code-block__line-number\">{{ lineNumbers()[i] }}</span>}<span class=\"code-block__line-content\" [innerHTML]=\"line\"></span></span>\n}</code></pre>\n </div>\n</div>\n",
433
- styles: [".code-block{--lc-code-block-bg: #0c1722;--lc-code-block-ink: #eef4f6;--lc-code-block-muted: #93a4ad;--lc-code-block-muted-strong: #c4d2d9;--lc-code-block-border: #24343f;position:relative;border:1px solid var(--lc-code-block-border);border-radius:var(--border-radius-md);overflow:hidden;font-family:var(--typography-font-family-mono, \"SF Mono\", \"Fira Code\", \"Fira Mono\", monospace);font-size:var(--typography-font-size-sm, .875rem);line-height:1.1;background-color:var(--lc-code-block-bg);color:var(--lc-code-block-ink)}.code-block__header{display:flex;justify-content:space-between;align-items:center;min-height:2.75rem;padding:0 .25rem 0 1rem;background-color:#00000040;border-bottom:1px solid var(--lc-code-block-border)}.code-block__language{font-size:var(--typography-font-size-xs, .75rem);color:var(--lc-code-block-muted);text-transform:lowercase;-webkit-user-select:none;user-select:none}.code-block__copy{display:inline-flex;align-items:center;justify-content:center;gap:.25rem;min-height:2.75rem;min-width:2.75rem;padding:0 .75rem;border:none;border-radius:var(--border-radius-sm);background:transparent;color:var(--lc-code-block-muted);font-size:var(--typography-font-size-xs, .75rem);font-family:inherit;cursor:pointer;transition:color .15s ease,background-color .15s ease}.code-block__copy:hover{color:var(--lc-code-block-muted-strong);background-color:#ffffff0f}.code-block__copy:focus-visible{outline:none;color:var(--lc-code-block-muted-strong);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 40%,transparent)}.code-block__live{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.code-block__body{overflow-x:auto}.code-block__pre{margin:0;padding:1rem;background:transparent;color:inherit;border-radius:0}.code-block__code{display:block}.code-block__line{display:flex}.code-block__line-number{display:inline-block;min-width:2.5rem;padding-right:1rem;text-align:right;color:var(--lc-code-block-muted);-webkit-user-select:none;user-select:none;flex-shrink:0}.code-block__line-content{flex:1;white-space:pre}.code-block__line-content ::ng-deep .token-keyword{color:#c678dd}.code-block__line-content ::ng-deep .token-string{color:#98c379}.code-block__line-content ::ng-deep .token-number{color:#d19a66}.code-block__line-content ::ng-deep .token-comment{color:#5c6370;font-style:italic}.code-block__line-content ::ng-deep .token-type{color:#e5c07b}.code-block__line-content ::ng-deep .token-constant{color:#d19a66}.code-block__line-content ::ng-deep .token-decorator{color:#61afef}.code-block__line-content ::ng-deep .token-tag{color:#e06c75}.code-block__line-content ::ng-deep .token-attr{color:#d19a66}.code-block__line-content ::ng-deep .token-selector{color:#e06c75}.code-block__line-content ::ng-deep .token-property{color:#61afef}.code-block__line-content ::ng-deep .token-unit{color:#d19a66}.code-block__line-content ::ng-deep .token-variable{color:#e06c75}.code-block__line-content ::ng-deep .token-builtin{color:#61afef}.code-block__line-content ::ng-deep .token-flag{color:#56b6c2}\n"]
462
+ template: "<div [class]=\"codeClasses()\">\n @if (showHeader()) {\n <div class=\"code-block__header\">\n <span class=\"code-block__language\">{{ headerLabel() }}</span>\n @if (showCopy()) {\n <button\n type=\"button\"\n class=\"code-block__copy\"\n [attr.aria-label]=\"copied() ? copiedLabel() : copyAriaLabel()\"\n (click)=\"copyToClipboard()\"\n >\n @if (copied()) {\n <lc-icon name=\"check\" size=\"sm\" [decorative]=\"true\" />\n <span>{{ copiedLabel() }}</span>\n } @else {\n <lc-icon name=\"clipboard-document\" size=\"sm\" [decorative]=\"true\" />\n <span>{{ copyLabel() }}</span>\n }\n </button>\n <span class=\"code-block__live\" role=\"status\" aria-live=\"polite\">{{ copied() ? copiedAnnouncement() : '' }}</span>\n }\n </div>\n }\n <div\n class=\"code-block__body\"\n [attr.tabindex]=\"wrap() ? null : 0\"\n [attr.role]=\"wrap() ? null : 'region'\"\n [attr.aria-label]=\"wrap() ? null : regionName()\"\n >\n <pre class=\"code-block__pre\"><code class=\"code-block__code\">@for (line of highlightedLines(); track $index; let i = $index) {<span class=\"code-block__line\">@if (showLineNumbers()) {<span class=\"code-block__line-number\">{{ lineNumbers()[i] }}</span>}<span class=\"code-block__line-content\">@for (segment of line; track $index) {@if (segment.className) {<span [attr.class]=\"segment.className\">{{ segment.text }}</span>}@else{{{ segment.text }}}}</span></span>\n}</code></pre>\n </div>\n</div>\n",
463
+ styles: [".code-block{--lc-code-block-bg: #0c1722;--lc-code-block-ink: #eef4f6;--lc-code-block-muted: #93a4ad;--lc-code-block-muted-strong: #c4d2d9;--lc-code-block-border: #24343f;position:relative;border:1px solid var(--lc-code-block-border);border-radius:var(--border-radius-md);overflow:hidden;font-family:var(--typography-font-family-mono, \"SF Mono\", \"Fira Code\", \"Fira Mono\", monospace);font-size:var(--typography-font-size-sm, .875rem);line-height:1.1;background-color:var(--lc-code-block-bg);color:var(--lc-code-block-ink)}.code-block__header{display:flex;justify-content:space-between;align-items:center;min-height:2.75rem;padding:0 .25rem 0 1rem;background-color:#00000040;border-bottom:1px solid var(--lc-code-block-border)}.code-block__language{font-size:var(--typography-font-size-xs, .75rem);color:var(--lc-code-block-muted);text-transform:lowercase;-webkit-user-select:none;user-select:none}.code-block__copy{display:inline-flex;align-items:center;justify-content:center;gap:.25rem;min-height:2.75rem;min-width:2.75rem;padding:0 .75rem;border:none;border-radius:var(--border-radius-sm);background:transparent;color:var(--lc-code-block-muted);font-size:var(--typography-font-size-xs, .75rem);font-family:inherit;cursor:pointer;transition:color .15s ease,background-color .15s ease}.code-block__copy:hover{color:var(--lc-code-block-muted-strong);background-color:#ffffff0f}.code-block__copy:focus-visible{outline:none;color:var(--lc-code-block-muted-strong);box-shadow:0 0 0 3px color-mix(in srgb,var(--color-primary-500) 40%,transparent)}.code-block__live{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.code-block__body{overflow-x:auto}.code-block__body:focus-visible{outline:2px solid var(--color-primary-500);outline-offset:-4px}.code-block__body:focus-visible{border-radius:0 0 calc(var(--border-radius-md) - 1px) calc(var(--border-radius-md) - 1px)}.code-block__body:first-child:focus-visible{border-radius:calc(var(--border-radius-md) - 1px)}@media(forced-colors:active){.code-block__body:focus-visible{outline-color:Highlight}}.code-block__pre{margin:0;padding:1rem;background:transparent;color:inherit;border-radius:0;overflow:visible}.code-block__code{display:block}.code-block__line{display:flex}.code-block__line-number{display:inline-block;min-width:2.5rem;padding-right:1rem;text-align:right;color:var(--lc-code-block-muted);-webkit-user-select:none;user-select:none;flex-shrink:0}.code-block__line-content{flex:1;white-space:pre}.code-block__line-content ::ng-deep .token-keyword{color:#c678dd}.code-block__line-content ::ng-deep .token-string{color:#98c379}.code-block__line-content ::ng-deep .token-number{color:#d19a66}.code-block__line-content ::ng-deep .token-comment{color:var(--lc-code-block-muted);font-style:italic}.code-block__line-content ::ng-deep .token-type{color:#e5c07b}.code-block__line-content ::ng-deep .token-constant{color:#d19a66}.code-block__line-content ::ng-deep .token-decorator{color:#61afef}.code-block__line-content ::ng-deep .token-tag{color:#e06c75}.code-block__line-content ::ng-deep .token-attr{color:#d19a66}.code-block__line-content ::ng-deep .token-selector{color:#e06c75}.code-block__line-content ::ng-deep .token-property{color:#61afef}.code-block__line-content ::ng-deep .token-unit{color:#d19a66}.code-block__line-content ::ng-deep .token-variable{color:#e06c75}.code-block__line-content ::ng-deep .token-builtin{color:#61afef}.code-block__line-content ::ng-deep .token-flag{color:#56b6c2}.code-block--wrap .code-block__line-content{white-space:pre-wrap;overflow-wrap:anywhere;min-width:0}\n"]
434
464
  }]
435
465
  }],
436
466
  propDecorators: {
@@ -482,6 +512,22 @@ i0.ɵɵngDeclareClassMetadata({
482
512
  required: false
483
513
  }]
484
514
  }],
515
+ regionLabel: [{
516
+ type: i0.Input,
517
+ args: [{
518
+ isSignal: true,
519
+ alias: "regionLabel",
520
+ required: false
521
+ }]
522
+ }],
523
+ wrap: [{
524
+ type: i0.Input,
525
+ args: [{
526
+ isSignal: true,
527
+ alias: "wrap",
528
+ required: false
529
+ }]
530
+ }],
485
531
  copyLabel: [{
486
532
  type: i0.Input,
487
533
  args: [{
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-code-block.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/code-block/code-block.component.ts","../../../../libs/angular-ui-kit/src/lib/code-block/code-block.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n inject,\n input,\n computed,\n signal,\n} from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHECK } from '@lifecorner/angular-ui-kit/icons';\n\nexport type CodeBlockLanguage =\n | 'typescript'\n | 'javascript'\n | 'html'\n | 'css'\n | 'scss'\n | 'json'\n | 'bash'\n | 'python'\n | 'java'\n | 'text';\n\ninterface TokenRule {\n pattern: RegExp;\n className: string;\n}\n\nconst SHARED_RULES: TokenRule[] = [\n // Strings (double/single/backtick)\n { pattern: /`(?:[^`\\\\]|\\\\.)*`/g, className: 'token-string' },\n { pattern: /\"(?:[^\"\\\\]|\\\\.)*\"/g, className: 'token-string' },\n { pattern: /'(?:[^'\\\\]|\\\\.)*'/g, className: 'token-string' },\n // Numbers\n { pattern: /\\b\\d+(?:\\.\\d+)?\\b/g, className: 'token-number' },\n];\n\nconst LANGUAGE_RULES: Record<string, TokenRule[]> = {\n typescript: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /@\\w+/g, className: 'token-decorator' },\n { pattern: /\\b(?:import|export|from|const|let|var|function|return|if|else|for|while|class|interface|type|enum|extends|implements|new|this|super|async|await|yield|of|in|as|is|readonly|public|private|protected|static|abstract|override|get|set|constructor|declare|namespace|module|require)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:string|number|boolean|void|null|undefined|never|any|unknown|object|symbol|bigint)\\b/g, className: 'token-type' },\n { pattern: /\\b(?:true|false|null|undefined|NaN|Infinity)\\b/g, className: 'token-constant' },\n { pattern: /(?<=\\b(?:class|interface|type|enum|extends|implements)\\s+)\\w+/g, className: 'token-type' },\n ],\n javascript: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /\\b(?:import|export|from|const|let|var|function|return|if|else|for|while|class|extends|new|this|super|async|await|yield|of|in|typeof|instanceof|delete|throw|try|catch|finally|switch|case|default|break|continue|do)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:true|false|null|undefined|NaN|Infinity)\\b/g, className: 'token-constant' },\n ],\n html: [\n { pattern: /<!--[\\s\\S]*?-->/g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /&lt;\\/?[\\w-]+/g, className: 'token-tag' },\n { pattern: /\\/?&gt;/g, className: 'token-tag' },\n { pattern: /\\b[\\w-]+(?==)/g, className: 'token-attr' },\n ],\n css: [\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /[.#][\\w-]+/g, className: 'token-selector' },\n { pattern: /\\b(?:px|rem|em|%|vh|vw|s|ms|deg|fr)\\b/g, className: 'token-unit' },\n { pattern: /[\\w-]+(?=\\s*:)/g, className: 'token-property' },\n ],\n scss: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /\\$[\\w-]+/g, className: 'token-variable' },\n { pattern: /@(?:use|forward|import|mixin|include|extend|if|else|each|for|while|function|return)\\b/g, className: 'token-keyword' },\n { pattern: /&/g, className: 'token-selector' },\n { pattern: /[.#][\\w-]+/g, className: 'token-selector' },\n ],\n json: [\n ...SHARED_RULES,\n { pattern: /\\b(?:true|false|null)\\b/g, className: 'token-constant' },\n { pattern: /\"(?:[^\"\\\\]|\\\\.)*\"(?=\\s*:)/g, className: 'token-property' },\n ],\n bash: [\n { pattern: /#.*$/gm, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /\\$[\\w{][\\w}]*/g, className: 'token-variable' },\n { pattern: /\\b(?:if|then|else|elif|fi|for|do|done|while|until|case|esac|function|in|select|time|coproc)\\b/g, className: 'token-keyword' },\n { pattern: /(?:^|\\s)(?:npm|npx|yarn|pnpm|git|cd|ls|mkdir|rm|cp|mv|echo|cat|grep|find|sed|awk|curl|wget|docker|node|python|pip|brew|apt|sudo)\\b/g, className: 'token-builtin' },\n { pattern: /--?[\\w-]+/g, className: 'token-flag' },\n ],\n python: [\n { pattern: /#.*$/gm, className: 'token-comment' },\n { pattern: /\"\"\"[\\s\\S]*?\"\"\"/g, className: 'token-string' },\n { pattern: /'''[\\s\\S]*?'''/g, className: 'token-string' },\n ...SHARED_RULES,\n { pattern: /@\\w+/g, className: 'token-decorator' },\n { pattern: /\\b(?:def|class|import|from|as|return|if|elif|else|for|while|break|continue|pass|raise|try|except|finally|with|yield|lambda|assert|del|global|nonlocal|async|await|in|not|and|or|is)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:True|False|None)\\b/g, className: 'token-constant' },\n { pattern: /\\b(?:str|int|float|bool|list|dict|tuple|set|bytes|type|object|Exception|ValueError|TypeError|KeyError|IndexError|AttributeError|RuntimeError|StopIteration)\\b/g, className: 'token-type' },\n { pattern: /\\b(?:print|len|range|enumerate|zip|map|filter|sorted|reversed|isinstance|issubclass|hasattr|getattr|setattr|super|property|staticmethod|classmethod|open|input)\\b/g, className: 'token-builtin' },\n { pattern: /\\bself\\b/g, className: 'token-variable' },\n ],\n java: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /@\\w+/g, className: 'token-decorator' },\n { pattern: /\\b(?:abstract|assert|break|case|catch|class|continue|default|do|else|enum|extends|final|finally|for|if|implements|import|instanceof|interface|native|new|package|private|protected|public|return|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|volatile|while|var|record|sealed|permits|yield)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:true|false|null)\\b/g, className: 'token-constant' },\n { pattern: /\\b(?:void|int|long|short|byte|float|double|char|boolean|String|Integer|Long|Double|Float|Boolean|Character|Object|Class|List|Map|Set|ArrayList|HashMap|HashSet|Optional|Stream|Collectors)\\b/g, className: 'token-type' },\n { pattern: /\\b(?:System|Math|Arrays|Collections|Objects|Thread|Runnable)\\b/g, className: 'token-builtin' },\n ],\n};\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n}\n\nfunction highlightCode(code: string, language: string): string {\n const escaped = escapeHtml(code);\n const rules = LANGUAGE_RULES[language];\n if (!rules) return escaped;\n\n // Collect all matches with positions\n const tokens: { start: number; end: number; className: string; text: string }[] = [];\n\n for (const rule of rules) {\n const regex = new RegExp(rule.pattern.source, rule.pattern.flags);\n let match: RegExpExecArray | null;\n while ((match = regex.exec(escaped)) !== null) {\n tokens.push({\n start: match.index,\n end: match.index + match[0].length,\n className: rule.className,\n text: match[0],\n });\n }\n }\n\n // Sort by start position, longer matches first for same position\n tokens.sort((a, b) => a.start - b.start || b.end - a.end);\n\n // Remove overlapping tokens (keep first/longest)\n const filtered: typeof tokens = [];\n let lastEnd = 0;\n for (const token of tokens) {\n if (token.start >= lastEnd) {\n filtered.push(token);\n lastEnd = token.end;\n }\n }\n\n // Build highlighted string\n let result = '';\n let cursor = 0;\n for (const token of filtered) {\n if (token.start > cursor) {\n result += escaped.slice(cursor, token.start);\n }\n result += `<span class=\"${token.className}\">${token.text}</span>`;\n cursor = token.end;\n }\n result += escaped.slice(cursor);\n\n return result;\n}\n\n@Component({\n selector: 'lc-code-block',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_CHECK])],\n templateUrl: './code-block.component.html',\n styleUrls: ['./code-block.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Code block component for displaying syntax-highlighted source code.\n *\n * Features:\n * - Syntax highlighting for 10+ languages (TypeScript, Python, Java, etc.)\n * - Optional line numbers\n * - Copy-to-clipboard button\n * - Optional header with filename and language label\n * - Dark-mode compatible color scheme\n *\n * @example\n * ```html\n * <lc-code-block [code]=\"source\" language=\"typescript\" [showLineNumbers]=\"true\" />\n * ```\n */\nexport class CodeBlockComponent {\n /** Code content to display */\n code = input.required<string>();\n\n /** Language label */\n language = input<CodeBlockLanguage>('text');\n\n /** Optional filename to display */\n filename = input<string>();\n\n /** Whether to show line numbers */\n showLineNumbers = input<boolean>(true);\n\n /** Whether to show copy button */\n showCopy = input<boolean>(true);\n\n /** Whether the code block has a header bar */\n showHeader = input<boolean>(true);\n\n /** Visible label of the copy button. */\n copyLabel = input('Copy');\n\n /** Accessible name of the copy button. */\n copyAriaLabel = input('Copy code');\n\n /** Label of the copy button after copying (also its accessible name). */\n copiedLabel = input('Copied');\n\n /** Announced to screen readers after copying. */\n copiedAnnouncement = input('Copied to clipboard');\n\n protected copied = signal(false);\n\n private readonly sanitizer = inject(DomSanitizer);\n\n protected highlightedLines = computed<SafeHtml[]>(() => {\n const highlighted = highlightCode(this.code(), this.language());\n return highlighted.split('\\n').map(line =>\n this.sanitizer.bypassSecurityTrustHtml(line || '&nbsp;')\n );\n });\n\n protected lineNumbers = computed(() => {\n return this.code().split('\\n').map((_, i) => i + 1);\n });\n\n protected headerLabel = computed(() => {\n return this.filename() || this.language();\n });\n\n protected codeClasses = computed(() => {\n return ['code-block', `code-block--${this.language()}`].join(' ');\n });\n\n protected async copyToClipboard(): Promise<void> {\n try {\n await navigator.clipboard.writeText(this.code());\n this.copied.set(true);\n setTimeout(() => this.copied.set(false), 2000);\n } catch {\n // Fallback: just ignore if clipboard API not available\n }\n }\n}\n","<div [class]=\"codeClasses()\">\n @if (showHeader()) {\n <div class=\"code-block__header\">\n <span class=\"code-block__language\">{{ headerLabel() }}</span>\n @if (showCopy()) {\n <button\n type=\"button\"\n class=\"code-block__copy\"\n [attr.aria-label]=\"copied() ? copiedLabel() : copyAriaLabel()\"\n (click)=\"copyToClipboard()\"\n >\n @if (copied()) {\n <lc-icon name=\"check\" size=\"sm\" />\n <span>{{ copiedLabel() }}</span>\n } @else {\n <lc-icon name=\"clipboard-document\" size=\"sm\" />\n <span>{{ copyLabel() }}</span>\n }\n </button>\n <span class=\"code-block__live\" role=\"status\" aria-live=\"polite\">{{ copied() ? copiedAnnouncement() : '' }}</span>\n }\n </div>\n }\n <div class=\"code-block__body\">\n <pre class=\"code-block__pre\"><code class=\"code-block__code\">@for (line of highlightedLines(); track $index; let i = $index) {<span class=\"code-block__line\">@if (showLineNumbers()) {<span class=\"code-block__line-number\">{{ lineNumbers()[i] }}</span>}<span class=\"code-block__line-content\" [innerHTML]=\"line\"></span></span>\n}</code></pre>\n </div>\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-code-block.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/code-block/code-block.component.ts","../../../../libs/angular-ui-kit/src/lib/code-block/code-block.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n input,\n computed,\n signal,\n} from '@angular/core';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_CHECK } from '@lifecorner/angular-ui-kit/icons';\n\nexport type CodeBlockLanguage =\n | 'typescript'\n | 'javascript'\n | 'html'\n | 'css'\n | 'scss'\n | 'json'\n | 'bash'\n | 'python'\n | 'java'\n | 'text';\n\ninterface TokenRule {\n pattern: RegExp;\n className: string;\n}\n\nconst SHARED_RULES: TokenRule[] = [\n // Strings (double/single/backtick)\n { pattern: /`(?:[^`\\\\]|\\\\.)*`/g, className: 'token-string' },\n { pattern: /\"(?:[^\"\\\\]|\\\\.)*\"/g, className: 'token-string' },\n { pattern: /'(?:[^'\\\\]|\\\\.)*'/g, className: 'token-string' },\n // Numbers\n { pattern: /\\b\\d+(?:\\.\\d+)?\\b/g, className: 'token-number' },\n];\n\nconst LANGUAGE_RULES: Record<string, TokenRule[]> = {\n typescript: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /@\\w+/g, className: 'token-decorator' },\n { pattern: /\\b(?:import|export|from|const|let|var|function|return|if|else|for|while|class|interface|type|enum|extends|implements|new|this|super|async|await|yield|of|in|as|is|readonly|public|private|protected|static|abstract|override|get|set|constructor|declare|namespace|module|require)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:string|number|boolean|void|null|undefined|never|any|unknown|object|symbol|bigint)\\b/g, className: 'token-type' },\n { pattern: /\\b(?:true|false|null|undefined|NaN|Infinity)\\b/g, className: 'token-constant' },\n { pattern: /(?<=\\b(?:class|interface|type|enum|extends|implements)\\s+)\\w+/g, className: 'token-type' },\n ],\n javascript: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /\\b(?:import|export|from|const|let|var|function|return|if|else|for|while|class|extends|new|this|super|async|await|yield|of|in|typeof|instanceof|delete|throw|try|catch|finally|switch|case|default|break|continue|do)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:true|false|null|undefined|NaN|Infinity)\\b/g, className: 'token-constant' },\n ],\n // The tokenizer reads the raw source (3.6.1): `<` and `>` instead of the\n // `&lt;`/`&gt;` the escaped text had. Up to 3.6.0 an html comment rule ran on\n // that escaped text and never matched; it is gone, so comments stay plain.\n html: [\n ...SHARED_RULES,\n { pattern: /<\\/?[\\w-]+/g, className: 'token-tag' },\n { pattern: /\\/?>/g, className: 'token-tag' },\n { pattern: /\\b[\\w-]+(?==)/g, className: 'token-attr' },\n ],\n css: [\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /[.#][\\w-]+/g, className: 'token-selector' },\n { pattern: /\\b(?:px|rem|em|%|vh|vw|s|ms|deg|fr)\\b/g, className: 'token-unit' },\n { pattern: /[\\w-]+(?=\\s*:)/g, className: 'token-property' },\n ],\n scss: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /\\$[\\w-]+/g, className: 'token-variable' },\n { pattern: /@(?:use|forward|import|mixin|include|extend|if|else|each|for|while|function|return)\\b/g, className: 'token-keyword' },\n { pattern: /&/g, className: 'token-selector' },\n { pattern: /[.#][\\w-]+/g, className: 'token-selector' },\n ],\n json: [\n ...SHARED_RULES,\n { pattern: /\\b(?:true|false|null)\\b/g, className: 'token-constant' },\n { pattern: /\"(?:[^\"\\\\]|\\\\.)*\"(?=\\s*:)/g, className: 'token-property' },\n ],\n bash: [\n { pattern: /#.*$/gm, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /\\$[\\w{][\\w}]*/g, className: 'token-variable' },\n { pattern: /\\b(?:if|then|else|elif|fi|for|do|done|while|until|case|esac|function|in|select|time|coproc)\\b/g, className: 'token-keyword' },\n { pattern: /(?:^|\\s)(?:npm|npx|yarn|pnpm|git|cd|ls|mkdir|rm|cp|mv|echo|cat|grep|find|sed|awk|curl|wget|docker|node|python|pip|brew|apt|sudo)\\b/g, className: 'token-builtin' },\n { pattern: /--?[\\w-]+/g, className: 'token-flag' },\n ],\n python: [\n { pattern: /#.*$/gm, className: 'token-comment' },\n { pattern: /\"\"\"[\\s\\S]*?\"\"\"/g, className: 'token-string' },\n { pattern: /'''[\\s\\S]*?'''/g, className: 'token-string' },\n ...SHARED_RULES,\n { pattern: /@\\w+/g, className: 'token-decorator' },\n { pattern: /\\b(?:def|class|import|from|as|return|if|elif|else|for|while|break|continue|pass|raise|try|except|finally|with|yield|lambda|assert|del|global|nonlocal|async|await|in|not|and|or|is)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:True|False|None)\\b/g, className: 'token-constant' },\n { pattern: /\\b(?:str|int|float|bool|list|dict|tuple|set|bytes|type|object|Exception|ValueError|TypeError|KeyError|IndexError|AttributeError|RuntimeError|StopIteration)\\b/g, className: 'token-type' },\n { pattern: /\\b(?:print|len|range|enumerate|zip|map|filter|sorted|reversed|isinstance|issubclass|hasattr|getattr|setattr|super|property|staticmethod|classmethod|open|input)\\b/g, className: 'token-builtin' },\n { pattern: /\\bself\\b/g, className: 'token-variable' },\n ],\n java: [\n { pattern: /\\/\\/.*$/gm, className: 'token-comment' },\n { pattern: /\\/\\*[\\s\\S]*?\\*\\//g, className: 'token-comment' },\n ...SHARED_RULES,\n { pattern: /@\\w+/g, className: 'token-decorator' },\n { pattern: /\\b(?:abstract|assert|break|case|catch|class|continue|default|do|else|enum|extends|final|finally|for|if|implements|import|instanceof|interface|native|new|package|private|protected|public|return|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|volatile|while|var|record|sealed|permits|yield)\\b/g, className: 'token-keyword' },\n { pattern: /\\b(?:true|false|null)\\b/g, className: 'token-constant' },\n { pattern: /\\b(?:void|int|long|short|byte|float|double|char|boolean|String|Integer|Long|Double|Float|Boolean|Character|Object|Class|List|Map|Set|ArrayList|HashMap|HashSet|Optional|Stream|Collectors)\\b/g, className: 'token-type' },\n { pattern: /\\b(?:System|Math|Arrays|Collections|Objects|Thread|Runnable)\\b/g, className: 'token-builtin' },\n ],\n};\n\n/** A run of source text; `className` is one `token-*` class, or `null` for plain text. */\ninterface CodeSegment {\n readonly text: string;\n readonly className: string | null;\n}\n\n/** One rendered line: its segments in order. */\ntype CodeLine = readonly CodeSegment[];\n\n/**\n * What an empty line renders: U+00A0, the glyph the `&nbsp;` of 3.6.0\n * produced, so the line keeps its height and a selection its text.\n */\nconst EMPTY_LINE: CodeLine = [{ text: String.fromCharCode(0xa0), className: null }];\n\n/** Matches of the language's rules on the raw source, sorted, without overlaps. */\nfunction tokenize(code: string, language: string): { start: number; end: number; className: string }[] {\n const rules = LANGUAGE_RULES[language];\n if (!rules) return [];\n\n // Collect all matches with positions\n const tokens: { start: number; end: number; className: string }[] = [];\n\n for (const rule of rules) {\n const regex = new RegExp(rule.pattern.source, rule.pattern.flags);\n let match: RegExpExecArray | null;\n while ((match = regex.exec(code)) !== null) {\n tokens.push({\n start: match.index,\n end: match.index + match[0].length,\n className: rule.className,\n });\n }\n }\n\n // Sort by start position, longer matches first for same position\n tokens.sort((a, b) => a.start - b.start || b.end - a.end);\n\n // Remove overlapping tokens (keep first/longest)\n const filtered: typeof tokens = [];\n let lastEnd = 0;\n for (const token of tokens) {\n if (token.start >= lastEnd) {\n filtered.push(token);\n lastEnd = token.end;\n }\n }\n return filtered;\n}\n\n/**\n * The highlighted source as lines of text segments, rendered by the template\n * as text nodes and `<span class=\"token-…\">` elements: no markup string, so\n * nothing for Trusted Types to block and nothing in the source is parsed.\n *\n * A token that runs across a line break keeps its class on the part before\n * the first break only; the later parts are plain, as the HTML parser closed\n * the open span at each line end up to 3.6.0.\n */\nfunction highlightLines(code: string, language: string): CodeLine[] {\n const lines: CodeSegment[][] = [[]];\n const append = (text: string, className: string | null) => {\n text.split('\\n').forEach((part, i) => {\n if (i > 0) lines.push([]);\n if (!part) return;\n const line = lines[lines.length - 1];\n const cls = i === 0 ? className : null;\n const last = line[line.length - 1];\n if (cls === null && last?.className === null) {\n line[line.length - 1] = { text: last.text + part, className: null };\n } else {\n line.push({ text: part, className: cls });\n }\n });\n };\n\n let cursor = 0;\n for (const token of tokenize(code, language)) {\n append(code.slice(cursor, token.start), null);\n append(code.slice(token.start, token.end), token.className);\n cursor = token.end;\n }\n append(code.slice(cursor), null);\n\n return lines.map(line => (line.length ? line : EMPTY_LINE));\n}\n\n@Component({\n selector: 'lc-code-block',\n standalone: true,\n imports: [IconComponent],\n viewProviders: [ɵprovideKitIcons([ICON_CHECK])],\n templateUrl: './code-block.component.html',\n styleUrls: ['./code-block.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\n/**\n * Code block component for displaying syntax-highlighted source code.\n *\n * Features:\n * - Syntax highlighting for 10+ languages (TypeScript, Python, Java, etc.)\n * - Optional line numbers\n * - Copy-to-clipboard button\n * - Optional header with filename and language label\n * - Dark-mode compatible color scheme\n * - Keyboard: the body is a focusable, named region that scrolls with the\n * arrow keys; `wrap` wraps instead\n *\n * @example\n * ```html\n * <lc-code-block [code]=\"source\" language=\"typescript\" [showLineNumbers]=\"true\" />\n * ```\n */\nexport class CodeBlockComponent {\n /** Code content to display */\n code = input.required<string>();\n\n /** Language label */\n language = input<CodeBlockLanguage>('text');\n\n /** Optional filename to display */\n filename = input<string>();\n\n /** Whether to show line numbers */\n showLineNumbers = input<boolean>(true);\n\n /** Whether to show copy button */\n showCopy = input<boolean>(true);\n\n /** Whether the code block has a header bar */\n showHeader = input<boolean>(true);\n\n /**\n * Accessible name of the scrollable code region (`role=\"region\"`). When it\n * is unset or empty, the `filename` is used, then the `language`. Set a\n * short, localised name that is unique on the page, such as \"Befehl für\n * Claude Code\". Not used when `wrap` is set.\n * @default undefined\n */\n regionLabel = input<string | undefined>(undefined);\n\n /**\n * Wraps long lines (`white-space: pre-wrap`, breaking anywhere if needed)\n * instead of scrolling sideways. A wrapped block doesn't scroll, so its body\n * is neither a tab stop nor a region.\n * @default false\n */\n wrap = input<boolean>(false);\n\n /** Visible label of the copy button. */\n copyLabel = input('Copy');\n\n /** Accessible name of the copy button. */\n copyAriaLabel = input('Copy code');\n\n /** Label of the copy button after copying (also its accessible name). */\n copiedLabel = input('Copied');\n\n /** Announced to screen readers after copying. */\n copiedAnnouncement = input('Copied to clipboard');\n\n protected copied = signal(false);\n\n protected highlightedLines = computed<CodeLine[]>(() =>\n highlightLines(this.code(), this.language())\n );\n\n protected lineNumbers = computed(() => {\n return this.code().split('\\n').map((_, i) => i + 1);\n });\n\n protected headerLabel = computed(() => {\n return this.filename() || this.language();\n });\n\n /** The region's name: `regionLabel`, then `filename`, then `language` (`''` falls through). */\n protected regionName = computed(() => this.regionLabel() || this.headerLabel());\n\n protected codeClasses = computed(() => {\n const classes = ['code-block', `code-block--${this.language()}`];\n if (this.wrap()) classes.push('code-block--wrap');\n return classes.join(' ');\n });\n\n protected async copyToClipboard(): Promise<void> {\n try {\n await navigator.clipboard.writeText(this.code());\n this.copied.set(true);\n setTimeout(() => this.copied.set(false), 2000);\n } catch {\n // Fallback: just ignore if clipboard API not available\n }\n }\n}\n","<div [class]=\"codeClasses()\">\n @if (showHeader()) {\n <div class=\"code-block__header\">\n <span class=\"code-block__language\">{{ headerLabel() }}</span>\n @if (showCopy()) {\n <button\n type=\"button\"\n class=\"code-block__copy\"\n [attr.aria-label]=\"copied() ? copiedLabel() : copyAriaLabel()\"\n (click)=\"copyToClipboard()\"\n >\n @if (copied()) {\n <lc-icon name=\"check\" size=\"sm\" [decorative]=\"true\" />\n <span>{{ copiedLabel() }}</span>\n } @else {\n <lc-icon name=\"clipboard-document\" size=\"sm\" [decorative]=\"true\" />\n <span>{{ copyLabel() }}</span>\n }\n </button>\n <span class=\"code-block__live\" role=\"status\" aria-live=\"polite\">{{ copied() ? copiedAnnouncement() : '' }}</span>\n }\n </div>\n }\n <div\n class=\"code-block__body\"\n [attr.tabindex]=\"wrap() ? null : 0\"\n [attr.role]=\"wrap() ? null : 'region'\"\n [attr.aria-label]=\"wrap() ? null : regionName()\"\n >\n <pre class=\"code-block__pre\"><code class=\"code-block__code\">@for (line of highlightedLines(); track $index; let i = $index) {<span class=\"code-block__line\">@if (showLineNumbers()) {<span class=\"code-block__line-number\">{{ lineNumbers()[i] }}</span>}<span class=\"code-block__line-content\">@for (segment of line; track $index) {@if (segment.className) {<span [attr.class]=\"segment.className\">{{ segment.text }}</span>}@else{{{ segment.text }}}}</span></span>\n}</code></pre>\n </div>\n</div>\n"],"mappings":"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"}
@@ -487,6 +487,8 @@ var DocumentViewerComponent = class DocumentViewerComponent {
487
487
  "showLineNumbers",
488
488
  "showCopy",
489
489
  "showHeader",
490
+ "regionLabel",
491
+ "wrap",
490
492
  "copyLabel",
491
493
  "copyAriaLabel",
492
494
  "copiedLabel",
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-document-viewer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/document-viewer/document-viewer.component.ts","../../../../libs/angular-ui-kit/src/lib/document-viewer/document-viewer.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n OnDestroy,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n signal,\n untracked,\n} from '@angular/core';\nimport { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_EXCLAMATION_TRIANGLE, ICON_FILE, ICON_FILE_TEXT, ICON_PLUS } from '@lifecorner/angular-ui-kit/icons';\nimport { SpinnerComponent } from '@lifecorner/angular-ui-kit/spinner';\nimport { CodeBlockComponent, CodeBlockLanguage } from '@lifecorner/angular-ui-kit/code-block';\nimport { MarkdownComponent } from '@lifecorner/angular-ui-kit/markdown';\nimport { isSafeResourceUrl } from '@lifecorner/angular-ui-kit/internal/url';\n\nexport type DocumentType = 'pdf' | 'markdown' | 'image' | 'text' | 'code' | 'auto';\n\nconst EXTENSION_TYPE_MAP: Record<string, DocumentType> = {\n pdf: 'pdf',\n md: 'markdown',\n markdown: 'markdown',\n png: 'image',\n jpg: 'image',\n jpeg: 'image',\n gif: 'image',\n webp: 'image',\n svg: 'image',\n bmp: 'image',\n ts: 'code',\n js: 'code',\n tsx: 'code',\n jsx: 'code',\n py: 'code',\n java: 'code',\n html: 'code',\n css: 'code',\n scss: 'code',\n json: 'code',\n sh: 'code',\n bash: 'code',\n txt: 'text',\n log: 'text',\n csv: 'text',\n};\n\nconst EXTENSION_LANGUAGE_MAP: Record<string, CodeBlockLanguage> = {\n ts: 'typescript',\n tsx: 'typescript',\n js: 'javascript',\n jsx: 'javascript',\n py: 'python',\n java: 'java',\n html: 'html',\n css: 'css',\n scss: 'scss',\n json: 'json',\n sh: 'bash',\n bash: 'bash',\n};\n\nconst TYPE_ICONS: Record<string, string> = {\n pdf: 'document-text',\n markdown: 'document-text',\n image: 'photo',\n text: 'document',\n code: 'code-bracket',\n unknown: 'document',\n};\n\n/**\n * Document viewer component for previewing various file types.\n *\n * Features:\n * - Auto-detects file type from URL extension or explicit type input\n * - PDF rendering via browser-native iframe viewer\n * - Markdown rendered through `lc-markdown` (escaped and sanitised — a fetched\n * `.md` file cannot inject markup)\n * - Image display with zoom controls (25% – 500%)\n * - Code display using the built-in code block component\n * - Plain text display\n * - Toolbar with filename, type badge, zoom, download, and fullscreen\n * - Loading and error states\n * - Dark/light theme support\n *\n * `src` is only ever embedded (iframe) or downloaded when it is a http(s),\n * blob: or relative URL — or a `data:application/pdf` — so a `javascript:` URL\n * handed in as a document can neither run in the frame nor be \"downloaded\".\n *\n * @example\n * ```html\n * <lc-document-viewer src=\"https://example.com/report.pdf\" />\n * <lc-document-viewer [content]=\"markdownString\" type=\"markdown\" filename=\"README.md\" />\n * <lc-document-viewer src=\"/assets/diagram.png\" />\n * ```\n */\n@Component({\n selector: 'lc-document-viewer',\n standalone: true,\n imports: [IconComponent, SpinnerComponent, CodeBlockComponent, MarkdownComponent],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS, ICON_EXCLAMATION_TRIANGLE, ICON_FILE, ICON_FILE_TEXT])],\n templateUrl: './document-viewer.component.html',\n styleUrls: ['./document-viewer.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n host: {\n '[class]': '\"doc-viewer\"',\n '[style.height]': 'height()',\n },\n})\nexport class DocumentViewerComponent implements OnDestroy {\n /** URL of the document to display (for PDF, images, or remote files) */\n readonly src = input<string>('');\n\n /** Direct content string (for markdown, text, or code — takes precedence over src for content-based types) */\n readonly content = input<string>('');\n\n /** File type — set to 'auto' to detect from URL extension */\n readonly type = input<DocumentType>('auto');\n\n /** Display name shown in the toolbar */\n readonly filename = input<string>('');\n\n /** Code language for syntax highlighting (when type is 'code') */\n readonly language = input<CodeBlockLanguage>('text');\n\n /** Whether to display the toolbar */\n readonly showToolbar = input(true);\n\n /** Whether to show the download button in the toolbar */\n readonly showDownload = input(true);\n\n /** Height of the viewer container */\n readonly height = input('500px');\n\n /** Accessible name of the zoom-out button. */\n readonly zoomOutAriaLabel = input('Zoom out');\n\n /** Accessible name of the zoom-in button. */\n readonly zoomInAriaLabel = input('Zoom in');\n\n /** Accessible name of the reset-zoom button. */\n readonly resetZoomAriaLabel = input('Reset zoom');\n\n /** Accessible name of the toolbar download button. */\n readonly downloadAriaLabel = input('Download');\n\n /** Accessible name of the fullscreen button. */\n readonly fullscreenAriaLabel = input('Fullscreen');\n\n /** Accessible name of the fullscreen button while in fullscreen. */\n readonly exitFullscreenAriaLabel = input('Exit fullscreen');\n\n /** Shown while the document loads. */\n readonly loadingLabel = input('Loading document…');\n\n /** Title shown for file types without a preview. */\n readonly previewUnavailableTitle = input('Preview not available');\n\n /** Message shown for file types without a preview. */\n readonly previewUnavailableMessage = input('This file type cannot be previewed.');\n\n /** Label of the download button shown instead of a preview. */\n readonly downloadFileLabel = input('Download file');\n\n /** Toolbar name when neither `filename` nor a file name in `src` is available. */\n readonly untitledLabel = input('Document');\n\n /** Type badge for Markdown documents. */\n readonly markdownTypeLabel = input('Markdown');\n\n /** Type badge for images. */\n readonly imageTypeLabel = input('Image');\n\n /** Type badge for plain text. */\n readonly textTypeLabel = input('Text');\n\n /** Type badge for other files. */\n readonly fileTypeLabel = input('File');\n\n /** Prefix of the error shown when fetching the document fails. */\n readonly loadErrorLabel = input('Failed to load document:');\n\n /** Error shown when `src` is not an allowed URL (e.g. `javascript:`). */\n readonly blockedUrlMessage = input('This document URL cannot be displayed');\n\n // ── Internal state ───────────────────────────────────────────────────────\n\n protected loading = signal(true);\n protected error = signal<string | null>(null);\n protected fetchedContent = signal<string>('');\n protected zoom = signal(100);\n protected isFullscreen = signal(false);\n\n private readonly sanitizer = inject(DomSanitizer);\n private readonly elementRef = inject(ElementRef);\n private fullscreenHandler: (() => void) | null = null;\n\n // ── Computed values ──────────────────────────────────────────────────────\n\n /** Resolved document type from input or URL extension */\n protected resolvedType = computed<DocumentType | 'unknown'>(() => {\n const t = this.type();\n if (t !== 'auto') return t;\n const ext = this.extractExtension(this.src());\n return ext && EXTENSION_TYPE_MAP[ext] ? EXTENSION_TYPE_MAP[ext] : 'unknown';\n });\n\n /** Display name for the toolbar */\n protected displayName = computed(() => {\n if (this.filename()) return this.filename();\n const src = this.src();\n if (!src) return this.untitledLabel();\n try {\n const url = new URL(src, 'http://localhost');\n const segments = url.pathname.split('/');\n return segments[segments.length - 1] || this.untitledLabel();\n } catch {\n return src.split('/').pop() || this.untitledLabel();\n }\n });\n\n /** Icon name for the document type */\n protected typeIcon = computed(() => TYPE_ICONS[this.resolvedType()] || TYPE_ICONS['unknown']);\n\n /** Type label for the toolbar badge */\n protected typeLabel = computed(() => {\n const t = this.resolvedType();\n switch (t) {\n case 'pdf': return 'PDF';\n case 'markdown': return this.markdownTypeLabel();\n case 'image': return this.imageTypeLabel();\n case 'text': return this.textTypeLabel();\n case 'code': return this.resolvedLanguage().toUpperCase();\n default: return this.fileTypeLabel();\n }\n });\n\n /** Resolved code language */\n protected resolvedLanguage = computed<CodeBlockLanguage>(() => {\n const lang = this.language();\n if (lang !== 'text') return lang;\n const ext = this.extractExtension(this.src());\n return ext && EXTENSION_LANGUAGE_MAP[ext] ? EXTENSION_LANGUAGE_MAP[ext] : 'text';\n });\n\n /** Whether `src` may be embedded in a frame or downloaded at all. */\n protected srcEmbeddable = computed(() => isSafeResourceUrl(this.src()));\n\n /**\n * `src` as the iframe accepts it. Bypassing the resource-URL check is what\n * lets a PDF URL into an iframe at all; it is only done for URLs that passed\n * `isSafeResourceUrl` — anything else resolves to an empty frame.\n */\n protected safeSrc = computed<SafeResourceUrl>(() => {\n const src = this.src();\n return this.sanitizer.bypassSecurityTrustResourceUrl(this.srcEmbeddable() ? src : 'about:blank');\n });\n\n /** Effective content: direct content input or fetched content */\n protected effectiveContent = computed(() => {\n return this.content() || this.fetchedContent();\n });\n\n /** Image transform style for zoom */\n protected imageTransform = computed(() => {\n const z = this.zoom() / 100;\n return `scale(${z})`;\n });\n\n /** Whether zoom controls should be shown */\n protected showZoom = computed(() => {\n const t = this.resolvedType();\n return t === 'image';\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────────\n\n constructor() {\n // Load whenever the document changes — not just once. A new `src` starts a\n // fresh load (and cancels the one still in flight, so a slow old response\n // can't overwrite the new document), resets any earlier error, and re-enters\n // the loading state; a `content` string short-circuits all of that.\n effect((onCleanup) => {\n const type = this.resolvedType();\n const src = this.src();\n const content = this.content();\n const controller = typeof AbortController === 'function' ? new AbortController() : null;\n onCleanup(() => controller?.abort());\n untracked(() => this.load(type, src, content, controller));\n });\n }\n\n private load(\n type: DocumentType | 'unknown',\n src: string,\n content: string,\n controller: AbortController | null,\n ): void {\n this.error.set(null);\n this.fetchedContent.set('');\n\n if (content) {\n this.loading.set(false);\n return;\n }\n\n if (!src) {\n this.loading.set(false);\n this.error.set('No source or content provided');\n return;\n }\n\n // PDF and images are embedded directly — no fetch needed. They do have to be\n // embeddable, though: a `javascript:` (or other exotic) URL is refused up\n // front instead of being handed to an iframe.\n if (type === 'pdf' || type === 'image') {\n if (!this.srcEmbeddable()) {\n this.loading.set(false);\n this.error.set(this.blockedUrlMessage());\n return;\n }\n // Not \"loading\": the frame/img element only exists once loading is off,\n // and it is its own load event that would end the state — a deadlock.\n this.loading.set(false);\n return;\n }\n\n // Fetch text-based content\n if (type === 'markdown' || type === 'text' || type === 'code') {\n this.loading.set(true);\n this.fetchContent(src, controller);\n } else {\n this.loading.set(false);\n }\n }\n\n ngOnDestroy(): void {\n if (this.fullscreenHandler) {\n document.removeEventListener('fullscreenchange', this.fullscreenHandler);\n }\n }\n\n // ── Actions ──────────────────────────────────────────────────────────────\n\n protected zoomIn(): void {\n this.zoom.update(z => Math.min(z + 25, 500));\n }\n\n protected zoomOut(): void {\n this.zoom.update(z => Math.max(z - 25, 25));\n }\n\n protected resetZoom(): void {\n this.zoom.set(100);\n }\n\n protected download(): void {\n const src = this.src();\n // A download is a click the user never sees the target of — only URLs that\n // could be embedded are worth clicking.\n if (!src || !this.srcEmbeddable()) return;\n const a = document.createElement('a');\n a.href = src;\n a.download = this.displayName();\n a.target = '_blank';\n a.rel = 'noopener noreferrer';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }\n\n protected toggleFullscreen(): void {\n const el = this.elementRef.nativeElement as HTMLElement;\n if (!document.fullscreenElement) {\n el.requestFullscreen?.();\n this.isFullscreen.set(true);\n } else {\n document.exitFullscreen?.();\n this.isFullscreen.set(false);\n }\n\n if (!this.fullscreenHandler) {\n this.fullscreenHandler = () => {\n this.isFullscreen.set(!!document.fullscreenElement);\n };\n document.addEventListener('fullscreenchange', this.fullscreenHandler);\n }\n }\n\n protected onIframeLoad(): void {\n this.loading.set(false);\n }\n\n protected onImageLoad(): void {\n this.loading.set(false);\n }\n\n protected onImageError(): void {\n this.loading.set(false);\n this.error.set('Failed to load image');\n }\n\n // ── Private helpers ──────────────────────────────────────────────────────\n\n private extractExtension(url: string): string | null {\n if (!url) return null;\n try {\n const pathname = new URL(url, 'http://localhost').pathname;\n const parts = pathname.split('.');\n return parts.length > 1 ? parts.pop()!.toLowerCase() : null;\n } catch {\n const parts = url.split('.');\n return parts.length > 1 ? parts.pop()!.toLowerCase().split('?')[0] : null;\n }\n }\n\n private async fetchContent(url: string, controller: AbortController | null): Promise<void> {\n if (typeof fetch !== 'function') {\n this.loading.set(false);\n this.error.set('Failed to load document: fetch is not available');\n return;\n }\n try {\n const response = await fetch(url, controller ? { signal: controller.signal } : undefined);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n const text = await response.text();\n // A response for a document that has since been replaced is dropped: the\n // effect that replaced it aborted this controller and started its own load.\n if (controller?.signal.aborted) return;\n this.fetchedContent.set(text);\n this.loading.set(false);\n } catch (err) {\n if (controller?.signal.aborted) return;\n this.loading.set(false);\n this.error.set(`${this.loadErrorLabel()} ${err instanceof Error ? err.message : 'Unknown error'}`);\n }\n }\n}\n","<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"doc-viewer__toolbar\">\n <div class=\"doc-viewer__toolbar-left\">\n <lc-icon [name]=\"typeIcon()\" size=\"sm\" />\n <span class=\"doc-viewer__filename\">{{ displayName() }}</span>\n <span class=\"doc-viewer__badge\">{{ typeLabel() }}</span>\n </div>\n <div class=\"doc-viewer__toolbar-right\">\n @if (showZoom()) {\n <div class=\"doc-viewer__zoom\">\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"zoomOutAriaLabel()\"\n (click)=\"zoomOut()\"\n >\n <lc-icon name=\"minus\" size=\"xs\" />\n </button>\n <span class=\"doc-viewer__zoom-value\">{{ zoom() }}%</span>\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"zoomInAriaLabel()\"\n (click)=\"zoomIn()\"\n >\n <lc-icon name=\"plus\" size=\"xs\" />\n </button>\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"resetZoomAriaLabel()\"\n (click)=\"resetZoom()\"\n >\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" />\n </button>\n </div>\n }\n @if (showDownload() && src()) {\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"downloadAriaLabel()\"\n (click)=\"download()\"\n >\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" />\n </button>\n }\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"isFullscreen() ? exitFullscreenAriaLabel() : fullscreenAriaLabel()\"\n (click)=\"toggleFullscreen()\"\n >\n <lc-icon [name]=\"isFullscreen() ? 'arrows-pointing-in' : 'arrows-pointing-out'\" size=\"sm\" />\n </button>\n </div>\n </div>\n}\n\n<!-- Content area -->\n<div class=\"doc-viewer__content\">\n @if (loading()) {\n <div class=\"doc-viewer__loading\">\n <lc-spinner size=\"lg\" />\n <span>{{ loadingLabel() }}</span>\n </div>\n }\n\n @if (error(); as errorMsg) {\n <div class=\"doc-viewer__error\">\n <lc-icon name=\"exclamation-triangle\" size=\"lg\" color=\"var(--color-error-default)\" />\n <span>{{ errorMsg }}</span>\n </div>\n }\n\n @if (!loading() && !error()) {\n @switch (resolvedType()) {\n @case ('pdf') {\n <iframe\n class=\"doc-viewer__iframe\"\n [src]=\"safeSrc()\"\n [title]=\"displayName()\"\n type=\"application/pdf\"\n (load)=\"onIframeLoad()\"\n ></iframe>\n }\n\n @case ('image') {\n <div class=\"doc-viewer__image-container\">\n <img\n class=\"doc-viewer__image\"\n [src]=\"src()\"\n [alt]=\"displayName()\"\n [style.transform]=\"imageTransform()\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\"\n />\n </div>\n }\n\n @case ('markdown') {\n <!-- lc-markdown escapes the source and runs Angular's sanitizer — the\n one markdown renderer in the kit, so its safety work isn't duplicated\n (or missed) here. -->\n <div class=\"doc-viewer__markdown\">\n <lc-markdown [content]=\"effectiveContent()\" linkTarget=\"_blank\" />\n </div>\n }\n\n @case ('code') {\n <lc-code-block\n [code]=\"effectiveContent()\"\n [language]=\"resolvedLanguage()\"\n [filename]=\"displayName()\"\n [showLineNumbers]=\"true\"\n [showHeader]=\"false\"\n />\n }\n\n @case ('text') {\n <pre class=\"doc-viewer__text\">{{ effectiveContent() }}</pre>\n }\n\n @default {\n <div class=\"doc-viewer__unsupported\">\n <lc-icon name=\"document\" size=\"lg\" />\n <span class=\"doc-viewer__unsupported-title\">{{ previewUnavailableTitle() }}</span>\n <span class=\"doc-viewer__unsupported-msg\">{{ previewUnavailableMessage() }}</span>\n @if (src()) {\n <button type=\"button\" class=\"doc-viewer__download-btn\" (click)=\"download()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" />\n {{ downloadFileLabel() }}\n </button>\n }\n </div>\n }\n }\n }\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-document-viewer.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/document-viewer/document-viewer.component.ts","../../../../libs/angular-ui-kit/src/lib/document-viewer/document-viewer.component.html"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n OnDestroy,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n signal,\n untracked,\n} from '@angular/core';\nimport { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';\nimport { IconComponent, ɵprovideKitIcons } from '@lifecorner/angular-ui-kit/icon';\nimport { ICON_EXCLAMATION_TRIANGLE, ICON_FILE, ICON_FILE_TEXT, ICON_PLUS } from '@lifecorner/angular-ui-kit/icons';\nimport { SpinnerComponent } from '@lifecorner/angular-ui-kit/spinner';\nimport { CodeBlockComponent, CodeBlockLanguage } from '@lifecorner/angular-ui-kit/code-block';\nimport { MarkdownComponent } from '@lifecorner/angular-ui-kit/markdown';\nimport { isSafeResourceUrl } from '@lifecorner/angular-ui-kit/internal/url';\n\nexport type DocumentType = 'pdf' | 'markdown' | 'image' | 'text' | 'code' | 'auto';\n\nconst EXTENSION_TYPE_MAP: Record<string, DocumentType> = {\n pdf: 'pdf',\n md: 'markdown',\n markdown: 'markdown',\n png: 'image',\n jpg: 'image',\n jpeg: 'image',\n gif: 'image',\n webp: 'image',\n svg: 'image',\n bmp: 'image',\n ts: 'code',\n js: 'code',\n tsx: 'code',\n jsx: 'code',\n py: 'code',\n java: 'code',\n html: 'code',\n css: 'code',\n scss: 'code',\n json: 'code',\n sh: 'code',\n bash: 'code',\n txt: 'text',\n log: 'text',\n csv: 'text',\n};\n\nconst EXTENSION_LANGUAGE_MAP: Record<string, CodeBlockLanguage> = {\n ts: 'typescript',\n tsx: 'typescript',\n js: 'javascript',\n jsx: 'javascript',\n py: 'python',\n java: 'java',\n html: 'html',\n css: 'css',\n scss: 'scss',\n json: 'json',\n sh: 'bash',\n bash: 'bash',\n};\n\nconst TYPE_ICONS: Record<string, string> = {\n pdf: 'document-text',\n markdown: 'document-text',\n image: 'photo',\n text: 'document',\n code: 'code-bracket',\n unknown: 'document',\n};\n\n/**\n * Document viewer component for previewing various file types.\n *\n * Features:\n * - Auto-detects file type from URL extension or explicit type input\n * - PDF rendering via browser-native iframe viewer\n * - Markdown rendered through `lc-markdown` (escaped and sanitised — a fetched\n * `.md` file cannot inject markup)\n * - Image display with zoom controls (25% – 500%)\n * - Code display using the built-in code block component\n * - Plain text display\n * - Toolbar with filename, type badge, zoom, download, and fullscreen\n * - Loading and error states\n * - Dark/light theme support\n *\n * `src` is only ever embedded (iframe) or downloaded when it is a http(s),\n * blob: or relative URL — or a `data:application/pdf` — so a `javascript:` URL\n * handed in as a document can neither run in the frame nor be \"downloaded\".\n *\n * @example\n * ```html\n * <lc-document-viewer src=\"https://example.com/report.pdf\" />\n * <lc-document-viewer [content]=\"markdownString\" type=\"markdown\" filename=\"README.md\" />\n * <lc-document-viewer src=\"/assets/diagram.png\" />\n * ```\n */\n@Component({\n selector: 'lc-document-viewer',\n standalone: true,\n imports: [IconComponent, SpinnerComponent, CodeBlockComponent, MarkdownComponent],\n viewProviders: [ɵprovideKitIcons([ICON_PLUS, ICON_EXCLAMATION_TRIANGLE, ICON_FILE, ICON_FILE_TEXT])],\n templateUrl: './document-viewer.component.html',\n styleUrls: ['./document-viewer.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n host: {\n '[class]': '\"doc-viewer\"',\n '[style.height]': 'height()',\n },\n})\nexport class DocumentViewerComponent implements OnDestroy {\n /** URL of the document to display (for PDF, images, or remote files) */\n readonly src = input<string>('');\n\n /** Direct content string (for markdown, text, or code — takes precedence over src for content-based types) */\n readonly content = input<string>('');\n\n /** File type — set to 'auto' to detect from URL extension */\n readonly type = input<DocumentType>('auto');\n\n /** Display name shown in the toolbar */\n readonly filename = input<string>('');\n\n /** Code language for syntax highlighting (when type is 'code') */\n readonly language = input<CodeBlockLanguage>('text');\n\n /** Whether to display the toolbar */\n readonly showToolbar = input(true);\n\n /** Whether to show the download button in the toolbar */\n readonly showDownload = input(true);\n\n /** Height of the viewer container */\n readonly height = input('500px');\n\n /** Accessible name of the zoom-out button. */\n readonly zoomOutAriaLabel = input('Zoom out');\n\n /** Accessible name of the zoom-in button. */\n readonly zoomInAriaLabel = input('Zoom in');\n\n /** Accessible name of the reset-zoom button. */\n readonly resetZoomAriaLabel = input('Reset zoom');\n\n /** Accessible name of the toolbar download button. */\n readonly downloadAriaLabel = input('Download');\n\n /** Accessible name of the fullscreen button. */\n readonly fullscreenAriaLabel = input('Fullscreen');\n\n /** Accessible name of the fullscreen button while in fullscreen. */\n readonly exitFullscreenAriaLabel = input('Exit fullscreen');\n\n /** Shown while the document loads. */\n readonly loadingLabel = input('Loading document…');\n\n /** Title shown for file types without a preview. */\n readonly previewUnavailableTitle = input('Preview not available');\n\n /** Message shown for file types without a preview. */\n readonly previewUnavailableMessage = input('This file type cannot be previewed.');\n\n /** Label of the download button shown instead of a preview. */\n readonly downloadFileLabel = input('Download file');\n\n /** Toolbar name when neither `filename` nor a file name in `src` is available. */\n readonly untitledLabel = input('Document');\n\n /** Type badge for Markdown documents. */\n readonly markdownTypeLabel = input('Markdown');\n\n /** Type badge for images. */\n readonly imageTypeLabel = input('Image');\n\n /** Type badge for plain text. */\n readonly textTypeLabel = input('Text');\n\n /** Type badge for other files. */\n readonly fileTypeLabel = input('File');\n\n /** Prefix of the error shown when fetching the document fails. */\n readonly loadErrorLabel = input('Failed to load document:');\n\n /** Error shown when `src` is not an allowed URL (e.g. `javascript:`). */\n readonly blockedUrlMessage = input('This document URL cannot be displayed');\n\n // ── Internal state ───────────────────────────────────────────────────────\n\n protected loading = signal(true);\n protected error = signal<string | null>(null);\n protected fetchedContent = signal<string>('');\n protected zoom = signal(100);\n protected isFullscreen = signal(false);\n\n private readonly sanitizer = inject(DomSanitizer);\n private readonly elementRef = inject(ElementRef);\n private fullscreenHandler: (() => void) | null = null;\n\n // ── Computed values ──────────────────────────────────────────────────────\n\n /** Resolved document type from input or URL extension */\n protected resolvedType = computed<DocumentType | 'unknown'>(() => {\n const t = this.type();\n if (t !== 'auto') return t;\n const ext = this.extractExtension(this.src());\n return ext && EXTENSION_TYPE_MAP[ext] ? EXTENSION_TYPE_MAP[ext] : 'unknown';\n });\n\n /** Display name for the toolbar */\n protected displayName = computed(() => {\n if (this.filename()) return this.filename();\n const src = this.src();\n if (!src) return this.untitledLabel();\n try {\n const url = new URL(src, 'http://localhost');\n const segments = url.pathname.split('/');\n return segments[segments.length - 1] || this.untitledLabel();\n } catch {\n return src.split('/').pop() || this.untitledLabel();\n }\n });\n\n /** Icon name for the document type */\n protected typeIcon = computed(() => TYPE_ICONS[this.resolvedType()] || TYPE_ICONS['unknown']);\n\n /** Type label for the toolbar badge */\n protected typeLabel = computed(() => {\n const t = this.resolvedType();\n switch (t) {\n case 'pdf': return 'PDF';\n case 'markdown': return this.markdownTypeLabel();\n case 'image': return this.imageTypeLabel();\n case 'text': return this.textTypeLabel();\n case 'code': return this.resolvedLanguage().toUpperCase();\n default: return this.fileTypeLabel();\n }\n });\n\n /** Resolved code language */\n protected resolvedLanguage = computed<CodeBlockLanguage>(() => {\n const lang = this.language();\n if (lang !== 'text') return lang;\n const ext = this.extractExtension(this.src());\n return ext && EXTENSION_LANGUAGE_MAP[ext] ? EXTENSION_LANGUAGE_MAP[ext] : 'text';\n });\n\n /** Whether `src` may be embedded in a frame or downloaded at all. */\n protected srcEmbeddable = computed(() => isSafeResourceUrl(this.src()));\n\n /**\n * `src` as the iframe accepts it. Bypassing the resource-URL check is what\n * lets a PDF URL into an iframe at all; it is only done for URLs that passed\n * `isSafeResourceUrl` — anything else resolves to an empty frame.\n */\n protected safeSrc = computed<SafeResourceUrl>(() => {\n const src = this.src();\n return this.sanitizer.bypassSecurityTrustResourceUrl(this.srcEmbeddable() ? src : 'about:blank');\n });\n\n /** Effective content: direct content input or fetched content */\n protected effectiveContent = computed(() => {\n return this.content() || this.fetchedContent();\n });\n\n /** Image transform style for zoom */\n protected imageTransform = computed(() => {\n const z = this.zoom() / 100;\n return `scale(${z})`;\n });\n\n /** Whether zoom controls should be shown */\n protected showZoom = computed(() => {\n const t = this.resolvedType();\n return t === 'image';\n });\n\n // ── Lifecycle ────────────────────────────────────────────────────────────\n\n constructor() {\n // Load whenever the document changes — not just once. A new `src` starts a\n // fresh load (and cancels the one still in flight, so a slow old response\n // can't overwrite the new document), resets any earlier error, and re-enters\n // the loading state; a `content` string short-circuits all of that.\n effect((onCleanup) => {\n const type = this.resolvedType();\n const src = this.src();\n const content = this.content();\n const controller = typeof AbortController === 'function' ? new AbortController() : null;\n onCleanup(() => controller?.abort());\n untracked(() => this.load(type, src, content, controller));\n });\n }\n\n private load(\n type: DocumentType | 'unknown',\n src: string,\n content: string,\n controller: AbortController | null,\n ): void {\n this.error.set(null);\n this.fetchedContent.set('');\n\n if (content) {\n this.loading.set(false);\n return;\n }\n\n if (!src) {\n this.loading.set(false);\n this.error.set('No source or content provided');\n return;\n }\n\n // PDF and images are embedded directly — no fetch needed. They do have to be\n // embeddable, though: a `javascript:` (or other exotic) URL is refused up\n // front instead of being handed to an iframe.\n if (type === 'pdf' || type === 'image') {\n if (!this.srcEmbeddable()) {\n this.loading.set(false);\n this.error.set(this.blockedUrlMessage());\n return;\n }\n // Not \"loading\": the frame/img element only exists once loading is off,\n // and it is its own load event that would end the state — a deadlock.\n this.loading.set(false);\n return;\n }\n\n // Fetch text-based content\n if (type === 'markdown' || type === 'text' || type === 'code') {\n this.loading.set(true);\n this.fetchContent(src, controller);\n } else {\n this.loading.set(false);\n }\n }\n\n ngOnDestroy(): void {\n if (this.fullscreenHandler) {\n document.removeEventListener('fullscreenchange', this.fullscreenHandler);\n }\n }\n\n // ── Actions ──────────────────────────────────────────────────────────────\n\n protected zoomIn(): void {\n this.zoom.update(z => Math.min(z + 25, 500));\n }\n\n protected zoomOut(): void {\n this.zoom.update(z => Math.max(z - 25, 25));\n }\n\n protected resetZoom(): void {\n this.zoom.set(100);\n }\n\n protected download(): void {\n const src = this.src();\n // A download is a click the user never sees the target of — only URLs that\n // could be embedded are worth clicking.\n if (!src || !this.srcEmbeddable()) return;\n const a = document.createElement('a');\n a.href = src;\n a.download = this.displayName();\n a.target = '_blank';\n a.rel = 'noopener noreferrer';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n }\n\n protected toggleFullscreen(): void {\n const el = this.elementRef.nativeElement as HTMLElement;\n if (!document.fullscreenElement) {\n el.requestFullscreen?.();\n this.isFullscreen.set(true);\n } else {\n document.exitFullscreen?.();\n this.isFullscreen.set(false);\n }\n\n if (!this.fullscreenHandler) {\n this.fullscreenHandler = () => {\n this.isFullscreen.set(!!document.fullscreenElement);\n };\n document.addEventListener('fullscreenchange', this.fullscreenHandler);\n }\n }\n\n protected onIframeLoad(): void {\n this.loading.set(false);\n }\n\n protected onImageLoad(): void {\n this.loading.set(false);\n }\n\n protected onImageError(): void {\n this.loading.set(false);\n this.error.set('Failed to load image');\n }\n\n // ── Private helpers ──────────────────────────────────────────────────────\n\n private extractExtension(url: string): string | null {\n if (!url) return null;\n try {\n const pathname = new URL(url, 'http://localhost').pathname;\n const parts = pathname.split('.');\n return parts.length > 1 ? parts.pop()!.toLowerCase() : null;\n } catch {\n const parts = url.split('.');\n return parts.length > 1 ? parts.pop()!.toLowerCase().split('?')[0] : null;\n }\n }\n\n private async fetchContent(url: string, controller: AbortController | null): Promise<void> {\n if (typeof fetch !== 'function') {\n this.loading.set(false);\n this.error.set('Failed to load document: fetch is not available');\n return;\n }\n try {\n const response = await fetch(url, controller ? { signal: controller.signal } : undefined);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n const text = await response.text();\n // A response for a document that has since been replaced is dropped: the\n // effect that replaced it aborted this controller and started its own load.\n if (controller?.signal.aborted) return;\n this.fetchedContent.set(text);\n this.loading.set(false);\n } catch (err) {\n if (controller?.signal.aborted) return;\n this.loading.set(false);\n this.error.set(`${this.loadErrorLabel()} ${err instanceof Error ? err.message : 'Unknown error'}`);\n }\n }\n}\n","<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"doc-viewer__toolbar\">\n <div class=\"doc-viewer__toolbar-left\">\n <lc-icon [name]=\"typeIcon()\" size=\"sm\" />\n <span class=\"doc-viewer__filename\">{{ displayName() }}</span>\n <span class=\"doc-viewer__badge\">{{ typeLabel() }}</span>\n </div>\n <div class=\"doc-viewer__toolbar-right\">\n @if (showZoom()) {\n <div class=\"doc-viewer__zoom\">\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"zoomOutAriaLabel()\"\n (click)=\"zoomOut()\"\n >\n <lc-icon name=\"minus\" size=\"xs\" />\n </button>\n <span class=\"doc-viewer__zoom-value\">{{ zoom() }}%</span>\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"zoomInAriaLabel()\"\n (click)=\"zoomIn()\"\n >\n <lc-icon name=\"plus\" size=\"xs\" />\n </button>\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"resetZoomAriaLabel()\"\n (click)=\"resetZoom()\"\n >\n <lc-icon name=\"arrows-pointing-out\" size=\"xs\" />\n </button>\n </div>\n }\n @if (showDownload() && src()) {\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"downloadAriaLabel()\"\n (click)=\"download()\"\n >\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" />\n </button>\n }\n <button\n type=\"button\"\n class=\"doc-viewer__btn\"\n [attr.aria-label]=\"isFullscreen() ? exitFullscreenAriaLabel() : fullscreenAriaLabel()\"\n (click)=\"toggleFullscreen()\"\n >\n <lc-icon [name]=\"isFullscreen() ? 'arrows-pointing-in' : 'arrows-pointing-out'\" size=\"sm\" />\n </button>\n </div>\n </div>\n}\n\n<!-- Content area -->\n<div class=\"doc-viewer__content\">\n @if (loading()) {\n <div class=\"doc-viewer__loading\">\n <lc-spinner size=\"lg\" />\n <span>{{ loadingLabel() }}</span>\n </div>\n }\n\n @if (error(); as errorMsg) {\n <div class=\"doc-viewer__error\">\n <lc-icon name=\"exclamation-triangle\" size=\"lg\" color=\"var(--color-error-default)\" />\n <span>{{ errorMsg }}</span>\n </div>\n }\n\n @if (!loading() && !error()) {\n @switch (resolvedType()) {\n @case ('pdf') {\n <iframe\n class=\"doc-viewer__iframe\"\n [src]=\"safeSrc()\"\n [title]=\"displayName()\"\n type=\"application/pdf\"\n (load)=\"onIframeLoad()\"\n ></iframe>\n }\n\n @case ('image') {\n <div class=\"doc-viewer__image-container\">\n <img\n class=\"doc-viewer__image\"\n [src]=\"src()\"\n [alt]=\"displayName()\"\n [style.transform]=\"imageTransform()\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\"\n />\n </div>\n }\n\n @case ('markdown') {\n <!-- lc-markdown escapes the source and runs Angular's sanitizer — the\n one markdown renderer in the kit, so its safety work isn't duplicated\n (or missed) here. -->\n <div class=\"doc-viewer__markdown\">\n <lc-markdown [content]=\"effectiveContent()\" linkTarget=\"_blank\" />\n </div>\n }\n\n @case ('code') {\n <lc-code-block\n [code]=\"effectiveContent()\"\n [language]=\"resolvedLanguage()\"\n [filename]=\"displayName()\"\n [showLineNumbers]=\"true\"\n [showHeader]=\"false\"\n />\n }\n\n @case ('text') {\n <pre class=\"doc-viewer__text\">{{ effectiveContent() }}</pre>\n }\n\n @default {\n <div class=\"doc-viewer__unsupported\">\n <lc-icon name=\"document\" size=\"lg\" />\n <span class=\"doc-viewer__unsupported-title\">{{ previewUnavailableTitle() }}</span>\n <span class=\"doc-viewer__unsupported-msg\">{{ previewUnavailableMessage() }}</span>\n @if (src()) {\n <button type=\"button\" class=\"doc-viewer__download-btn\" (click)=\"download()\">\n <lc-icon name=\"arrow-down-tray\" size=\"sm\" />\n {{ downloadFileLabel() }}\n </button>\n }\n </div>\n }\n }\n }\n</div>\n"],"mappings":"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"}
@@ -564,6 +564,8 @@ var MarkdownComponent = class MarkdownComponent {
564
564
  "showLineNumbers",
565
565
  "showCopy",
566
566
  "showHeader",
567
+ "regionLabel",
568
+ "wrap",
567
569
  "copyLabel",
568
570
  "copyAriaLabel",
569
571
  "copiedLabel",
@@ -1 +1 @@
1
- {"version":3,"file":"lifecorner-angular-ui-kit-markdown.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/markdown/markdown.component.ts","../../../../libs/angular-ui-kit/src/lib/markdown/markdown.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n computed,\n signal,\n effect,\n untracked,\n inject,\n ElementRef,\n OnDestroy,\n SecurityContext,\n ViewEncapsulation,\n} from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { HttpClient } from '@angular/common/http';\nimport { Subscription } from 'rxjs';\nimport { CodeBlockComponent, CodeBlockLanguage } from '@lifecorner/angular-ui-kit/code-block';\nimport { isSafeImageUrl, isSafeLinkUrl } from '@lifecorner/angular-ui-kit/internal/url';\n\nexport interface MarkdownHeading {\n level: number;\n text: string;\n id: string;\n}\n\nexport interface MarkdownLinkClick {\n href: string;\n event: MouseEvent;\n}\n\nexport interface MarkdownRendered {\n headings: MarkdownHeading[];\n}\n\nexport interface MarkdownChangesHighlighted {\n /** Number of changed/added blocks highlighted in the current render. */\n changedBlocks: number;\n}\n\nexport interface RenderPart {\n type: 'html' | 'code' | 'mermaid';\n index: number;\n safeHtml?: SafeHtml;\n code?: string;\n lang?: CodeBlockLanguage;\n}\n\n/**\n * Stand-in for a fenced code block while the rest of the markdown is processed.\n * Bracketed in NUL characters, which the source is stripped of first — so no\n * text a user writes can ever spell a placeholder and be mistaken for a block.\n * (The earlier `<!--CODE_BLOCK_N-->` form was: an escaped literal comment in\n * the text matched it and dereferenced a block that wasn't there.)\n */\nconst BLOCK_MARK = '\\u0000';\nconst blockPlaceholder = (idx: number): string => `${BLOCK_MARK}LCBLOCK${idx}${BLOCK_MARK}`;\n// eslint-disable-next-line no-control-regex -- the NUL bracket is the point: unforgeable\nconst BLOCK_PLACEHOLDER = /\\u0000LCBLOCK(\\d+)\\u0000/g;\n\n/** Instances numbered so their mermaid render ids never collide on one page. */\nlet markdownInstances = 0;\n\n/**\n * Markdown renderer component.\n *\n * Renders GitHub-Flavored Markdown (GFM) to sanitized HTML with\n * optional syntax highlighting via `<lc-code-block>`.\n *\n * Supports headings (+ anchors), paragraphs, ordered/unordered lists, task lists\n * (`- [ ]` / `- [x]` → accessible checkbox), blockquotes, horizontal rules,\n * images, GFM tables (column alignment + horizontal scroll), bold/italic/\n * strikethrough, inline code, fenced code (incl. `mermaid`), explicit links and\n * autolinks (bare URLs / `www.` / emails, never inside code).\n *\n * Optionally highlights *changed* blocks in place: pass the pre-edit markdown as\n * `previousContent` and set `highlightChanges` — added/edited blocks (diffed at\n * block / list-item level) gain a left accent bar + subtle tint, can auto-fade\n * (`changeHighlightFadeMs`) and scroll into view (`scrollToFirstChange`).\n *\n * @example\n * ```html\n * <lc-markdown [content]=\"'# Hello World'\" />\n * <lc-markdown [src]=\"'/docs/readme.md'\" />\n * <lc-markdown\n * [content]=\"current\" [previousContent]=\"prev\"\n * [highlightChanges]=\"true\" [changeHighlightFadeMs]=\"3000\" />\n * ```\n */\n@Component({\n selector: 'lc-markdown',\n standalone: true,\n imports: [CodeBlockComponent],\n templateUrl: './markdown.component.html',\n styleUrls: ['./markdown.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n})\nexport class MarkdownComponent implements OnDestroy {\n private readonly sanitizer = inject(DomSanitizer);\n private readonly instanceId = ++markdownInstances;\n private readonly http = inject(HttpClient);\n private readonly host = inject(ElementRef<HTMLElement>);\n private httpSub?: Subscription;\n private fadeTimer?: ReturnType<typeof setTimeout>;\n private scrollTimer?: ReturnType<typeof setTimeout>;\n private mermaidApiPromise?: Promise<any>;\n private mermaidInitialized = false;\n private mermaidRenderRun = 0;\n\n /** URL or path to load markdown from */\n readonly src = input<string>();\n\n /** Raw markdown string */\n readonly content = input<string>();\n\n /** Display variant */\n readonly variant = input<'default' | 'compact' | 'chat'>('default');\n\n /** Target for links */\n readonly linkTarget = input<'_self' | '_blank'>('_self');\n\n /** Whether to sanitize HTML output */\n readonly sanitize = input<boolean>(true);\n\n /** Whether to use code-block for fenced code */\n readonly syntaxHighlight = input<boolean>(true);\n\n /** Whether to show anchor links on headings */\n readonly showHeadingAnchors = input<boolean>(false);\n\n /** Base URL for resolving relative links/images */\n readonly baseUrl = input<string>();\n\n // -- Change highlighting --\n /** Enable change highlighting (requires `previousContent` to compute a diff). */\n readonly highlightChanges = input<boolean>(false);\n\n /**\n * The prior Markdown. When it differs from `content`, the changed/added blocks\n * in `content` are highlighted. The caller passes the pre-edit markdown.\n */\n readonly previousContent = input<string>();\n\n /** Auto-fade the highlight after N ms. 0 / undefined ⇒ persist until content changes. */\n readonly changeHighlightFadeMs = input<number>();\n\n /** Scroll the first changed block into view when highlights appear. */\n readonly scrollToFirstChange = input<boolean>(false);\n\n /** Screen-reader marker put in front of each highlighted block (`highlightChanges`). */\n readonly changedMarkerLabel = input('(geändert)');\n\n /** Announced after the count when one block changed: `1 Abschnitt geändert`. */\n readonly changedSummaryOneLabel = input('Abschnitt geändert');\n\n /** Announced after the count when several blocks changed: `3 Abschnitte geändert`. */\n readonly changedSummaryManyLabel = input('Abschnitte geändert');\n\n /** Shown while a Mermaid diagram renders. */\n readonly mermaidLoadingLabel = input('Rendering Mermaid diagram...');\n\n /** Shown above the source when a Mermaid diagram fails. */\n readonly mermaidErrorLabel = input('Mermaid diagram could not be rendered.');\n\n /** Shown above the source when the optional `mermaid` package cannot be loaded. */\n readonly mermaidUnavailableLabel = input('Mermaid runtime is not available.');\n\n /** Emitted when a link is clicked */\n readonly linkClick = output<MarkdownLinkClick>();\n\n /** Emitted after rendering with heading TOC */\n readonly rendered = output<MarkdownRendered>();\n\n /** Emitted after a render that produced change highlights. */\n readonly changesHighlighted = output<MarkdownChangesHighlighted>();\n\n /** Internal resolved markdown source */\n protected resolvedMarkdown = signal<string>('');\n\n /** Parsed result (computed once from resolvedMarkdown) */\n private parsed = computed(() => {\n const md = this.resolvedMarkdown();\n if (!md) {\n return {\n html: '',\n blocks: [] as { kind: 'code' | 'mermaid'; lang: string; code: string }[],\n headings: [] as MarkdownHeading[],\n };\n }\n return this.parseMarkdown(md);\n });\n\n /**\n * Rendered HTML after applying change highlights. When highlighting is off (or\n * there is no differing `previousContent`) this returns the parsed HTML\n * unchanged, so the non-highlight path is byte-for-byte identical to before.\n */\n private readonly highlightResult = computed<{ html: string; count: number }>(() => {\n const { html } = this.parsed();\n const prev = this.previousContent();\n if (\n !html ||\n !this.highlightChanges() ||\n prev == null ||\n prev === this.resolvedMarkdown()\n ) {\n return { html, count: 0 };\n }\n return this.applyChangeHighlights(html, prev);\n });\n\n /** Number of changed/added blocks highlighted in the current render. */\n protected readonly changedCount = computed(() => this.highlightResult().count);\n\n /** Visually-hidden polite summary announced when highlights appear. */\n protected readonly changeSummary = computed(() => {\n const n = this.changedCount();\n if (n <= 0) return '';\n return `${n} ${n === 1 ? this.changedSummaryOneLabel() : this.changedSummaryManyLabel()}`;\n });\n\n /** Whether the current highlights have faded out (after `changeHighlightFadeMs`). */\n protected readonly highlightsFaded = signal(false);\n\n /** Computed render parts (HTML chunks + code blocks interleaved) */\n protected renderParts = computed<RenderPart[]>(() => {\n const html = this.highlightResult().html;\n const { blocks } = this.parsed();\n if (!html) return [];\n\n if (this.syntaxHighlight() && blocks.length > 0) {\n return this.splitIntoParts(html, blocks);\n }\n\n // No code blocks — single HTML part\n return [{\n type: 'html',\n index: 0,\n safeHtml: this.sanitizer.bypassSecurityTrustHtml(this.sanitizeHtml(html, { next: 0 })),\n }];\n });\n\n /**\n * The HTML as it may go into the DOM. With `sanitize` on (the default) that is\n * Angular's HTML sanitizer's output — which drops every `id`, since `id` isn't\n * on its attribute allow-list. The heading ids are put back afterwards: they\n * are what anchors and the `rendered` TOC point at, and each is a `[\\w-]`\n * slug this parser produced itself, so re-adding them reopens nothing. The\n * cursor walks the parsed headings in document order across the parts.\n */\n private sanitizeHtml(html: string, cursor: { next: number }): string {\n const headings = this.headings();\n if (!this.sanitize()) {\n cursor.next += (html.match(/<h[1-6]\\b/g) ?? []).length;\n return html;\n }\n const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, html) || '';\n return sanitized.replace(/<h([1-6])(\\s[^>]*)?>/g, (tag, level: string, attrs = '') => {\n const heading = headings[cursor.next++];\n if (!heading || /\\sid=/.test(attrs)) return tag;\n return `<h${level} id=\"${heading.id}\"${attrs}>`;\n });\n }\n\n protected containerClasses = computed(() => {\n let classes = `lc-markdown lc-markdown--${this.variant()}`;\n if (this.highlightsFaded()) classes += ' lc-markdown--faded';\n return classes;\n });\n\n protected mermaidSvgs = signal<Record<number, SafeHtml>>({});\n protected mermaidErrors = signal<Record<number, string>>({});\n\n private headings = computed(() => this.parsed().headings);\n\n constructor() {\n // Load content from src when src changes\n effect(() => {\n const src = this.src();\n if (src) {\n this.loadFromUrl(src);\n }\n });\n\n // Use raw content when provided\n effect(() => {\n const content = this.content();\n if (content !== undefined) {\n this.resolvedMarkdown.set(content);\n }\n });\n\n // Emit rendered event whenever a render produced a (possibly empty) TOC\n effect(() => {\n const h = this.headings();\n untracked(() => this.rendered.emit({ headings: h }));\n });\n\n // Render Mermaid code fences as inline SVG diagrams.\n effect(() => {\n void this.renderMermaidParts(this.renderParts());\n });\n\n // React to a render that produced change highlights: emit, schedule fade,\n // and optionally scroll the first changed block into view.\n effect(() => {\n const count = this.highlightResult().count;\n untracked(() => this.onHighlightResult(count));\n });\n }\n\n ngOnDestroy(): void {\n this.httpSub?.unsubscribe();\n clearTimeout(this.fadeTimer);\n clearTimeout(this.scrollTimer);\n }\n\n protected onLinkClick(event: MouseEvent): void {\n const target = event.target as HTMLElement;\n const anchor = target.closest('a');\n if (anchor) {\n this.linkClick.emit({ href: anchor.href, event });\n }\n }\n\n // -- Change highlighting -----------------------------------------------------\n\n /** Side-effects for a render: emit, (re)arm the fade timer, optional scroll. */\n private onHighlightResult(count: number): void {\n clearTimeout(this.fadeTimer);\n clearTimeout(this.scrollTimer);\n // A fresh render starts un-faded (re-entrant: never reuse stale fade state).\n this.highlightsFaded.set(false);\n if (count <= 0) return;\n\n this.changesHighlighted.emit({ changedBlocks: count });\n\n const fadeMs = this.changeHighlightFadeMs();\n if (fadeMs && fadeMs > 0) {\n this.fadeTimer = setTimeout(() => this.highlightsFaded.set(true), fadeMs);\n }\n if (this.scrollToFirstChange()) {\n // Defer until the highlighted HTML has been written to the DOM.\n this.scrollTimer = setTimeout(() => this.scrollToFirstChanged(), 0);\n }\n }\n\n private scrollToFirstChanged(): void {\n if (typeof document === 'undefined') return;\n const el = this.host.nativeElement.querySelector('.lc-markdown__block--changed');\n el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n }\n\n /**\n * Wraps the changed/added top-level blocks of `html` with\n * `.lc-markdown__block--changed`. A block is \"changed\" when its normalized\n * text is not present among the blocks of `prevMarkdown`. Lists are diffed\n * per `<li>` and tables per `<tr>` so a single edited item highlights alone.\n */\n private applyChangeHighlights(\n html: string,\n prevMarkdown: string\n ): { html: string; count: number } {\n if (typeof document === 'undefined') return { html, count: 0 };\n\n const prevKeys = this.collectBlockKeys(this.parseMarkdown(prevMarkdown).html);\n const root = this.htmlToElement(html);\n if (!root) return { html, count: 0 };\n\n let count = 0;\n const markIfChanged = (el: Element): void => {\n const key = this.blockTextKey(el.textContent ?? '');\n if (!key || prevKeys.has(key)) return;\n el.classList.add('lc-markdown__block--changed');\n const sr = document.createElement('span');\n sr.className = 'lc-markdown__sr-only';\n sr.textContent = `${this.changedMarkerLabel()} `;\n el.insertBefore(sr, el.firstChild);\n count++;\n };\n\n this.eachLogicalBlock(root, markIfChanged);\n return { html: root.innerHTML, count };\n }\n\n /** Normalized text set of the logical blocks within an HTML fragment. */\n private collectBlockKeys(html: string): Set<string> {\n const keys = new Set<string>();\n const root = this.htmlToElement(html);\n if (!root) return keys;\n this.eachLogicalBlock(root, (el) => {\n const key = this.blockTextKey(el.textContent ?? '');\n if (key) keys.add(key);\n });\n return keys;\n }\n\n /**\n * Visits each diffable block under `root`: top-level elements, but descending\n * into `<ul>`/`<ol>` (per `<li>`) and `<table>` (per `<tr>`).\n */\n private eachLogicalBlock(root: Element, visit: (el: Element) => void): void {\n for (const node of Array.from(root.childNodes)) {\n if (node.nodeType !== 1) continue; // elements only (skips code placeholders)\n const el = node as Element;\n const tag = el.tagName;\n if (tag === 'UL' || tag === 'OL') {\n for (const li of Array.from(el.children)) {\n if (li.tagName === 'LI') visit(li);\n }\n } else if (tag === 'TABLE' || el.querySelector('table')) {\n // Tables (incl. the scroll wrapper div) diff per row.\n for (const tr of Array.from(el.querySelectorAll('tr'))) {\n visit(tr);\n }\n } else {\n visit(el);\n }\n }\n }\n\n private blockTextKey(text: string): string {\n return text.replace(/\\s+/g, ' ').trim().toLowerCase();\n }\n\n private htmlToElement(html: string): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n const div = document.createElement('div');\n div.innerHTML = html;\n return div;\n }\n\n private loadFromUrl(url: string): void {\n this.httpSub?.unsubscribe();\n this.httpSub = this.http.get(url, { responseType: 'text' }).subscribe({\n next: (text) => this.resolvedMarkdown.set(text),\n error: () => this.resolvedMarkdown.set(`*Failed to load ${url}*`),\n });\n }\n\n private splitIntoParts(\n html: string,\n blocks: { kind: 'code' | 'mermaid'; lang: string; code: string }[]\n ): RenderPart[] {\n const parts: RenderPart[] = [];\n const regex = new RegExp(BLOCK_PLACEHOLDER.source, 'g');\n const cursor = { next: 0 };\n let lastIndex = 0;\n let partIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(html)) !== null) {\n const before = html.slice(lastIndex, match.index);\n if (before.trim()) {\n parts.push({\n type: 'html',\n index: partIndex++,\n safeHtml: this.sanitizer.bypassSecurityTrustHtml(this.sanitizeHtml(before, cursor)),\n });\n }\n const block = blocks[parseInt(match[1], 10)];\n lastIndex = match.index + match[0].length;\n // Unforgeable placeholder, so this can only be a bug — but never a crash.\n if (!block) continue;\n if (block.kind === 'mermaid') {\n parts.push({\n type: 'mermaid',\n index: partIndex++,\n code: block.code,\n });\n } else {\n parts.push({\n type: 'code',\n index: partIndex++,\n code: block.code,\n lang: block.lang as CodeBlockLanguage,\n });\n }\n }\n\n const remaining = html.slice(lastIndex);\n if (remaining.trim()) {\n parts.push({\n type: 'html',\n index: partIndex++,\n safeHtml: this.sanitizer.bypassSecurityTrustHtml(this.sanitizeHtml(remaining, cursor)),\n });\n }\n\n return parts;\n }\n\n private parseMarkdown(md: string): {\n html: string;\n blocks: { kind: 'code' | 'mermaid'; lang: string; code: string }[];\n headings: MarkdownHeading[];\n } {\n const blocks: { kind: 'code' | 'mermaid'; lang: string; code: string }[] = [];\n const headings: MarkdownHeading[] = [];\n const baseUrl = this.baseUrl();\n const linkTarget = this.linkTarget();\n const showAnchors = this.showHeadingAnchors();\n\n // 1. Extract fenced code blocks → placeholders (see BLOCK_MARK: the source\n // is stripped of the marker character first, so it can't forge one)\n // eslint-disable-next-line no-control-regex -- strip the placeholder marker (see BLOCK_MARK)\n let processed = md.replace(/\\u0000/g, '').replace(\n /```(\\w*)\\n([\\s\\S]*?)```/g,\n (_match, lang: string, code: string) => {\n const idx = blocks.length;\n const normalizedLang = (lang || 'text').toLowerCase();\n blocks.push({\n kind: normalizedLang === 'mermaid' ? 'mermaid' : 'code',\n lang: normalizedLang,\n code: code.trimEnd(),\n });\n return blockPlaceholder(idx);\n }\n );\n\n // 2. Escape HTML entities — quotes included, so nothing the parser later\n // interpolates into an attribute (link/image URLs, alt text) can close it.\n processed = this.escapeHtml(processed);\n\n // 3. Headings (# to ######)\n processed = processed.replace(\n /^(#{1,6})\\s+(.+)$/gm,\n (_match, hashes: string, text: string) => {\n const level = hashes.length;\n const id = text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-');\n headings.push({ level, text, id });\n const anchor = showAnchors\n ? `<a href=\"#${id}\" class=\"lc-markdown__anchor\" aria-hidden=\"true\">#</a>`\n : '';\n return `<h${level} id=\"${id}\">${anchor}${text}</h${level}>`;\n }\n );\n\n // 4. Horizontal rules\n processed = processed.replace(/^---+$/gm, '<hr>');\n\n // 5. Blockquotes\n processed = processed.replace(\n /^(?:&gt;)\\s?(.*)$/gm,\n '<blockquote>$1</blockquote>'\n );\n // Merge consecutive blockquotes\n processed = processed.replace(\n /<\\/blockquote>\\n<blockquote>/g,\n '\\n'\n );\n\n // 6. Tables (GFM)\n processed = this.parseTables(processed);\n\n // 7. Task lists. Angular's HTML sanitizer strips <input>, so the checkbox is\n // rendered as a styled <span role=\"checkbox\"> (which survives sanitization and\n // carries accessible checked/disabled state) rather than a form control.\n processed = processed.replace(\n /^[-*]\\s+\\[[xX]\\]\\s+(.*)$/gm,\n '<li class=\"lc-markdown__task-item\">' +\n '<span class=\"lc-markdown__checkbox lc-markdown__checkbox--checked\" role=\"checkbox\" aria-checked=\"true\" aria-disabled=\"true\"></span>' +\n '<span class=\"lc-markdown__task-label\">$1</span></li>'\n );\n processed = processed.replace(\n /^[-*]\\s+\\[ \\]\\s+(.*)$/gm,\n '<li class=\"lc-markdown__task-item\">' +\n '<span class=\"lc-markdown__checkbox\" role=\"checkbox\" aria-checked=\"false\" aria-disabled=\"true\"></span>' +\n '<span class=\"lc-markdown__task-label\">$1</span></li>'\n );\n\n // 8. Unordered lists\n processed = processed.replace(\n /^[-*]\\s+(.*)$/gm,\n '<li>$1</li>'\n );\n // Wrap consecutive <li> in <ul>\n processed = processed.replace(\n /((?:<li[^>]*>.*<\\/li>\\n?)+)/g,\n '<ul>$1</ul>'\n );\n\n // 9. Ordered lists — marked while wrapping, so a run of them gets its <ol>\n // and isn't confused with the <li>s already inside <ul>s above.\n processed = processed.replace(\n /^\\d+\\.\\s+(.*)$/gm,\n '<li data-ol>$1</li>'\n );\n processed = processed.replace(\n /((?:<li data-ol>.*<\\/li>\\n?)+)/g,\n (run) => `<ol>${run.replace(/ data-ol/g, '')}</ol>`\n );\n\n // 10. Inline code\n processed = processed.replace(\n /`([^`]+)`/g,\n '<code>$1</code>'\n );\n\n // 11. Bold + italic combos\n processed = processed.replace(\n /\\*\\*\\*(.+?)\\*\\*\\*/g,\n '<strong><em>$1</em></strong>'\n );\n processed = processed.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n processed = processed.replace(/\\*(.+?)\\*/g, '<em>$1</em>');\n\n // 12. Strikethrough (GFM)\n processed = processed.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n // 13. Images. Only allow-listed URL schemes become a `src` (see\n // safe-url.ts); anything else — `javascript:`, unknown schemes — is\n // rendered as its alt text. This holds regardless of `sanitize`.\n processed = processed.replace(\n /!\\[([^\\]]*)\\]\\(([^)]+)\\)/g,\n (_match, alt: string, src: string) => {\n if (!isSafeImageUrl(src)) return alt;\n const resolvedSrc = baseUrl && !src.startsWith('http') ? `${baseUrl}/${src}` : src;\n return `<img src=\"${resolvedSrc}\" alt=\"${alt}\" class=\"lc-markdown__img\">`;\n }\n );\n\n // 14. Links — same rule: an unsafe URL leaves plain text, not a link.\n processed = processed.replace(\n /\\[([^\\]]+)\\]\\(([^)]+)\\)/g,\n (_match, text: string, href: string) => {\n if (!isSafeLinkUrl(href)) return text;\n const resolvedHref = baseUrl && !href.startsWith('http') && !href.startsWith('#')\n ? `${baseUrl}/${href}`\n : href;\n return `<a href=\"${resolvedHref}\" target=\"${linkTarget}\" rel=\"noopener\">${text}</a>`;\n }\n );\n\n // 14b. Autolinks — linkify bare URLs / www / emails in plain text. Runs after\n // explicit links and inline code so it can skip anything already inside an\n // <a> or <code> (and never touches tag attributes).\n processed = this.autolink(processed, linkTarget);\n\n // 15. Paragraphs — wrap standalone lines\n processed = processed\n .split('\\n\\n')\n .map((block) => {\n const trimmed = block.trim();\n if (!trimmed) return '';\n if (\n trimmed.startsWith('<h') ||\n trimmed.startsWith('<ul') ||\n trimmed.startsWith('<ol') ||\n trimmed.startsWith('<blockquote') ||\n trimmed.startsWith('<hr') ||\n trimmed.startsWith('<table') ||\n trimmed.startsWith('<div') ||\n trimmed.startsWith(BLOCK_MARK)\n ) {\n return trimmed;\n }\n return `<p>${trimmed.replace(/\\n/g, '<br>')}</p>`;\n })\n .join('\\n');\n\n // 16. Restore code block placeholders\n if (this.syntaxHighlight()) {\n // Leave placeholders — the template will render <lc-code-block> for each\n // No replacement needed here\n } else {\n processed = processed.replace(\n new RegExp(BLOCK_PLACEHOLDER.source, 'g'),\n (_match, idx: string) => {\n const block = blocks[parseInt(idx, 10)];\n if (!block) return '';\n return `<pre><code class=\"language-${block.lang}\">${this.escapeHtml(block.code)}</code></pre>`;\n }\n );\n }\n\n return { html: processed, blocks, headings };\n }\n\n private parseTables(text: string): string {\n return text.replace(\n /^(\\|.+\\|)\\n(\\|[-| :]+\\|)\\n((?:\\|.+\\|\\n?)*)/gm,\n (_match, header: string, separator: string, body: string) => {\n // Per-column alignment from the delimiter row: :--- left, :--: center,\n // ---: right. Surfaced as classes (the sanitizer strips inline `style`).\n const aligns = this.splitTableRow(separator).map((cell) => {\n const left = cell.startsWith(':');\n const right = cell.endsWith(':');\n if (left && right) return 'center';\n if (right) return 'right';\n if (left) return 'left';\n return '';\n });\n const alignClass = (col: number): string =>\n aligns[col] ? ` class=\"lc-markdown__cell--${aligns[col]}\"` : '';\n\n const headers = this.splitTableRow(header)\n .map((cell, i) => `<th scope=\"col\"${alignClass(i)}>${cell.trim()}</th>`)\n .join('');\n\n const rows = body\n .trim()\n .split('\\n')\n .map((row: string) => {\n const cells = this.splitTableRow(row)\n .map((cell, i) => `<td${alignClass(i)}>${cell.trim()}</td>`)\n .join('');\n return `<tr>${cells}</tr>`;\n })\n .join('');\n\n // Wrapped so wide tables scroll horizontally instead of breaking layout.\n return (\n `<div class=\"lc-markdown__table-wrap\">` +\n `<table class=\"lc-markdown__table\"><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>` +\n `</div>`\n );\n }\n );\n }\n\n /** Splits a pipe-table row into trimmed cells, dropping the leading/trailing edges. */\n private splitTableRow(row: string): string[] {\n return row\n .trim()\n .replace(/^\\|/, '')\n .replace(/\\|$/, '')\n .split('|')\n .map((cell) => cell.trim());\n }\n\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n\n /**\n * Linkifies bare URLs (`http(s)://…`, `www.…`) and email addresses in text,\n * leaving anything inside an existing `<a>` or `<code>` (and all tag\n * attributes) untouched. The HTML is tokenized into tags vs. text so only\n * top-level text runs are linkified.\n */\n private autolink(html: string, linkTarget: string): string {\n const url = /(?:https?:\\/\\/|www\\.)[^\\s<]*[^\\s<.,:;!?)\\]}'\"]/;\n const email = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/;\n const pattern = new RegExp(`${url.source}|${email.source}`, 'g');\n\n let depth = 0; // inside <a> or <code>\n return html\n .split(/(<[^>]+>)/g)\n .map((segment) => {\n if (segment.startsWith('<')) {\n if (/^<(a|code)[\\s>]/i.test(segment)) depth++;\n else if (/^<\\/(a|code)>/i.test(segment)) depth = Math.max(0, depth - 1);\n return segment;\n }\n if (depth > 0 || !segment) return segment;\n return segment.replace(pattern, (token) => {\n const isEmail = token.includes('@') && !/^(?:https?:\\/\\/|www\\.)/.test(token);\n const href = isEmail\n ? `mailto:${token}`\n : token.startsWith('www.')\n ? `https://${token}`\n : token;\n return `<a href=\"${href}\" target=\"${linkTarget}\" rel=\"noopener\">${token}</a>`;\n });\n })\n .join('');\n }\n\n protected mermaidSvgFor(index: number): SafeHtml | null {\n return this.mermaidSvgs()[index] ?? null;\n }\n\n protected mermaidErrorFor(index: number): string | null {\n return this.mermaidErrors()[index] ?? null;\n }\n\n private async renderMermaidParts(parts: RenderPart[]): Promise<void> {\n const mermaidParts = parts.filter((part) => part.type === 'mermaid' && !!part.code);\n this.mermaidSvgs.set({});\n this.mermaidErrors.set({});\n\n if (mermaidParts.length === 0) {\n return;\n }\n\n const runId = ++this.mermaidRenderRun;\n\n try {\n const mermaidApi = await this.getMermaidApi();\n if (runId !== this.mermaidRenderRun) return;\n\n const nextSvgs: Record<number, SafeHtml> = {};\n const nextErrors: Record<number, string> = {};\n\n for (const part of mermaidParts) {\n try {\n // Per instance, per run, per part: mermaid's render() removes any\n // element already carrying the id it is given, so an id shared with\n // another <lc-markdown> on the page would delete that one's diagram.\n const renderId = `lc-md-mermaid-${this.instanceId}-${runId}-${part.index}`;\n const { svg } = await mermaidApi.render(renderId, part.code!);\n if (runId !== this.mermaidRenderRun) return;\n // Mermaid emits self-contained SVG with internal <style> rules.\n // Angular HTML sanitization strips critical style declarations,\n // which leaves only labels visible (no edges/shapes). We trust\n // Mermaid output here and rely on Mermaid's strict security mode.\n nextSvgs[part.index] = this.sanitizer.bypassSecurityTrustHtml(svg);\n } catch {\n nextErrors[part.index] = this.mermaidErrorLabel();\n }\n }\n\n if (runId !== this.mermaidRenderRun) return;\n this.mermaidSvgs.set(nextSvgs);\n this.mermaidErrors.set(nextErrors);\n } catch {\n if (runId !== this.mermaidRenderRun) return;\n const fallbackErrors: Record<number, string> = {};\n for (const part of mermaidParts) {\n fallbackErrors[part.index] = this.mermaidUnavailableLabel();\n }\n this.mermaidErrors.set(fallbackErrors);\n }\n }\n\n private async getMermaidApi(): Promise<any> {\n if (!this.mermaidApiPromise) {\n this.mermaidApiPromise = import('mermaid').then((module) => {\n const api = module.default ?? module;\n if (!this.mermaidInitialized) {\n api.initialize({\n startOnLoad: false,\n securityLevel: 'strict',\n });\n this.mermaidInitialized = true;\n }\n return api;\n });\n }\n return this.mermaidApiPromise;\n }\n}\n","<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- delegated handler for clicks on rendered links, which are focusable anchors -->\n<div [class]=\"containerClasses()\" (click)=\"onLinkClick($event)\">\n @if (highlightChanges()) {\n <div class=\"lc-markdown__sr-only\" role=\"status\" aria-live=\"polite\">{{ changeSummary() }}</div>\n }\n @for (part of renderParts(); track part.index) {\n @if (part.type === 'html') {\n <div [innerHTML]=\"part.safeHtml\"></div>\n } @else if (part.type === 'code') {\n <lc-code-block\n [code]=\"part.code!\"\n [language]=\"part.lang ?? 'text'\"\n [showLineNumbers]=\"true\"\n [showCopy]=\"true\"\n />\n } @else {\n @if (mermaidSvgFor(part.index); as svg) {\n <div class=\"lc-markdown__mermaid\" [innerHTML]=\"svg\"></div>\n } @else if (mermaidErrorFor(part.index); as errorMessage) {\n <div class=\"lc-markdown__mermaid-fallback\">\n <p class=\"lc-markdown__mermaid-error\">{{ errorMessage }}</p>\n <lc-code-block\n [code]=\"part.code!\"\n [language]=\"'text'\"\n [showLineNumbers]=\"true\"\n [showCopy]=\"true\"\n />\n </div>\n } @else {\n <div class=\"lc-markdown__mermaid-loading\">{{ mermaidLoadingLabel() }}</div>\n }\n }\n }\n</div>\n"],"mappings":"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"}
1
+ {"version":3,"file":"lifecorner-angular-ui-kit-markdown.mjs","names":[],"sources":["../../../../libs/angular-ui-kit/src/lib/markdown/markdown.component.ts","../../../../libs/angular-ui-kit/src/lib/markdown/markdown.component.html"],"sourcesContent":["import {\n Component,\n ChangeDetectionStrategy,\n input,\n output,\n computed,\n signal,\n effect,\n untracked,\n inject,\n ElementRef,\n OnDestroy,\n SecurityContext,\n ViewEncapsulation,\n} from '@angular/core';\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\nimport { HttpClient } from '@angular/common/http';\nimport { Subscription } from 'rxjs';\nimport { CodeBlockComponent, CodeBlockLanguage } from '@lifecorner/angular-ui-kit/code-block';\nimport { isSafeImageUrl, isSafeLinkUrl } from '@lifecorner/angular-ui-kit/internal/url';\n\nexport interface MarkdownHeading {\n level: number;\n text: string;\n id: string;\n}\n\nexport interface MarkdownLinkClick {\n href: string;\n event: MouseEvent;\n}\n\nexport interface MarkdownRendered {\n headings: MarkdownHeading[];\n}\n\nexport interface MarkdownChangesHighlighted {\n /** Number of changed/added blocks highlighted in the current render. */\n changedBlocks: number;\n}\n\nexport interface RenderPart {\n type: 'html' | 'code' | 'mermaid';\n index: number;\n safeHtml?: SafeHtml;\n code?: string;\n lang?: CodeBlockLanguage;\n}\n\n/**\n * Stand-in for a fenced code block while the rest of the markdown is processed.\n * Bracketed in NUL characters, which the source is stripped of first — so no\n * text a user writes can ever spell a placeholder and be mistaken for a block.\n * (The earlier `<!--CODE_BLOCK_N-->` form was: an escaped literal comment in\n * the text matched it and dereferenced a block that wasn't there.)\n */\nconst BLOCK_MARK = '\\u0000';\nconst blockPlaceholder = (idx: number): string => `${BLOCK_MARK}LCBLOCK${idx}${BLOCK_MARK}`;\n// eslint-disable-next-line no-control-regex -- the NUL bracket is the point: unforgeable\nconst BLOCK_PLACEHOLDER = /\\u0000LCBLOCK(\\d+)\\u0000/g;\n\n/** Instances numbered so their mermaid render ids never collide on one page. */\nlet markdownInstances = 0;\n\n/**\n * Markdown renderer component.\n *\n * Renders GitHub-Flavored Markdown (GFM) to sanitized HTML with\n * optional syntax highlighting via `<lc-code-block>`.\n *\n * Supports headings (+ anchors), paragraphs, ordered/unordered lists, task lists\n * (`- [ ]` / `- [x]` → accessible checkbox), blockquotes, horizontal rules,\n * images, GFM tables (column alignment + horizontal scroll), bold/italic/\n * strikethrough, inline code, fenced code (incl. `mermaid`), explicit links and\n * autolinks (bare URLs / `www.` / emails, never inside code).\n *\n * Optionally highlights *changed* blocks in place: pass the pre-edit markdown as\n * `previousContent` and set `highlightChanges` — added/edited blocks (diffed at\n * block / list-item level) gain a left accent bar + subtle tint, can auto-fade\n * (`changeHighlightFadeMs`) and scroll into view (`scrollToFirstChange`).\n *\n * @example\n * ```html\n * <lc-markdown [content]=\"'# Hello World'\" />\n * <lc-markdown [src]=\"'/docs/readme.md'\" />\n * <lc-markdown\n * [content]=\"current\" [previousContent]=\"prev\"\n * [highlightChanges]=\"true\" [changeHighlightFadeMs]=\"3000\" />\n * ```\n */\n@Component({\n selector: 'lc-markdown',\n standalone: true,\n imports: [CodeBlockComponent],\n templateUrl: './markdown.component.html',\n styleUrls: ['./markdown.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n})\nexport class MarkdownComponent implements OnDestroy {\n private readonly sanitizer = inject(DomSanitizer);\n private readonly instanceId = ++markdownInstances;\n private readonly http = inject(HttpClient);\n private readonly host = inject(ElementRef<HTMLElement>);\n private httpSub?: Subscription;\n private fadeTimer?: ReturnType<typeof setTimeout>;\n private scrollTimer?: ReturnType<typeof setTimeout>;\n private mermaidApiPromise?: Promise<any>;\n private mermaidInitialized = false;\n private mermaidRenderRun = 0;\n\n /** URL or path to load markdown from */\n readonly src = input<string>();\n\n /** Raw markdown string */\n readonly content = input<string>();\n\n /** Display variant */\n readonly variant = input<'default' | 'compact' | 'chat'>('default');\n\n /** Target for links */\n readonly linkTarget = input<'_self' | '_blank'>('_self');\n\n /** Whether to sanitize HTML output */\n readonly sanitize = input<boolean>(true);\n\n /** Whether to use code-block for fenced code */\n readonly syntaxHighlight = input<boolean>(true);\n\n /** Whether to show anchor links on headings */\n readonly showHeadingAnchors = input<boolean>(false);\n\n /** Base URL for resolving relative links/images */\n readonly baseUrl = input<string>();\n\n // -- Change highlighting --\n /** Enable change highlighting (requires `previousContent` to compute a diff). */\n readonly highlightChanges = input<boolean>(false);\n\n /**\n * The prior Markdown. When it differs from `content`, the changed/added blocks\n * in `content` are highlighted. The caller passes the pre-edit markdown.\n */\n readonly previousContent = input<string>();\n\n /** Auto-fade the highlight after N ms. 0 / undefined ⇒ persist until content changes. */\n readonly changeHighlightFadeMs = input<number>();\n\n /** Scroll the first changed block into view when highlights appear. */\n readonly scrollToFirstChange = input<boolean>(false);\n\n /** Screen-reader marker put in front of each highlighted block (`highlightChanges`). */\n readonly changedMarkerLabel = input('(geändert)');\n\n /** Announced after the count when one block changed: `1 Abschnitt geändert`. */\n readonly changedSummaryOneLabel = input('Abschnitt geändert');\n\n /** Announced after the count when several blocks changed: `3 Abschnitte geändert`. */\n readonly changedSummaryManyLabel = input('Abschnitte geändert');\n\n /** Shown while a Mermaid diagram renders. */\n readonly mermaidLoadingLabel = input('Rendering Mermaid diagram...');\n\n /** Shown above the source when a Mermaid diagram fails. */\n readonly mermaidErrorLabel = input('Mermaid diagram could not be rendered.');\n\n /** Shown above the source when the optional `mermaid` package cannot be loaded. */\n readonly mermaidUnavailableLabel = input('Mermaid runtime is not available.');\n\n /** Emitted when a link is clicked */\n readonly linkClick = output<MarkdownLinkClick>();\n\n /** Emitted after rendering with heading TOC */\n readonly rendered = output<MarkdownRendered>();\n\n /** Emitted after a render that produced change highlights. */\n readonly changesHighlighted = output<MarkdownChangesHighlighted>();\n\n /** Internal resolved markdown source */\n protected resolvedMarkdown = signal<string>('');\n\n /** Parsed result (computed once from resolvedMarkdown) */\n private parsed = computed(() => {\n const md = this.resolvedMarkdown();\n if (!md) {\n return {\n html: '',\n blocks: [] as { kind: 'code' | 'mermaid'; lang: string; code: string }[],\n headings: [] as MarkdownHeading[],\n };\n }\n return this.parseMarkdown(md);\n });\n\n /**\n * Rendered HTML after applying change highlights. When highlighting is off (or\n * there is no differing `previousContent`) this returns the parsed HTML\n * unchanged, so the non-highlight path is byte-for-byte identical to before.\n */\n private readonly highlightResult = computed<{ html: string; count: number }>(() => {\n const { html } = this.parsed();\n const prev = this.previousContent();\n if (\n !html ||\n !this.highlightChanges() ||\n prev == null ||\n prev === this.resolvedMarkdown()\n ) {\n return { html, count: 0 };\n }\n return this.applyChangeHighlights(html, prev);\n });\n\n /** Number of changed/added blocks highlighted in the current render. */\n protected readonly changedCount = computed(() => this.highlightResult().count);\n\n /** Visually-hidden polite summary announced when highlights appear. */\n protected readonly changeSummary = computed(() => {\n const n = this.changedCount();\n if (n <= 0) return '';\n return `${n} ${n === 1 ? this.changedSummaryOneLabel() : this.changedSummaryManyLabel()}`;\n });\n\n /** Whether the current highlights have faded out (after `changeHighlightFadeMs`). */\n protected readonly highlightsFaded = signal(false);\n\n /** Computed render parts (HTML chunks + code blocks interleaved) */\n protected renderParts = computed<RenderPart[]>(() => {\n const html = this.highlightResult().html;\n const { blocks } = this.parsed();\n if (!html) return [];\n\n if (this.syntaxHighlight() && blocks.length > 0) {\n return this.splitIntoParts(html, blocks);\n }\n\n // No code blocks — single HTML part\n return [{\n type: 'html',\n index: 0,\n safeHtml: this.sanitizer.bypassSecurityTrustHtml(this.sanitizeHtml(html, { next: 0 })),\n }];\n });\n\n /**\n * The HTML as it may go into the DOM. With `sanitize` on (the default) that is\n * Angular's HTML sanitizer's output — which drops every `id`, since `id` isn't\n * on its attribute allow-list. The heading ids are put back afterwards: they\n * are what anchors and the `rendered` TOC point at, and each is a `[\\w-]`\n * slug this parser produced itself, so re-adding them reopens nothing. The\n * cursor walks the parsed headings in document order across the parts.\n */\n private sanitizeHtml(html: string, cursor: { next: number }): string {\n const headings = this.headings();\n if (!this.sanitize()) {\n cursor.next += (html.match(/<h[1-6]\\b/g) ?? []).length;\n return html;\n }\n const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, html) || '';\n return sanitized.replace(/<h([1-6])(\\s[^>]*)?>/g, (tag, level: string, attrs = '') => {\n const heading = headings[cursor.next++];\n if (!heading || /\\sid=/.test(attrs)) return tag;\n return `<h${level} id=\"${heading.id}\"${attrs}>`;\n });\n }\n\n protected containerClasses = computed(() => {\n let classes = `lc-markdown lc-markdown--${this.variant()}`;\n if (this.highlightsFaded()) classes += ' lc-markdown--faded';\n return classes;\n });\n\n protected mermaidSvgs = signal<Record<number, SafeHtml>>({});\n protected mermaidErrors = signal<Record<number, string>>({});\n\n private headings = computed(() => this.parsed().headings);\n\n constructor() {\n // Load content from src when src changes\n effect(() => {\n const src = this.src();\n if (src) {\n this.loadFromUrl(src);\n }\n });\n\n // Use raw content when provided\n effect(() => {\n const content = this.content();\n if (content !== undefined) {\n this.resolvedMarkdown.set(content);\n }\n });\n\n // Emit rendered event whenever a render produced a (possibly empty) TOC\n effect(() => {\n const h = this.headings();\n untracked(() => this.rendered.emit({ headings: h }));\n });\n\n // Render Mermaid code fences as inline SVG diagrams.\n effect(() => {\n void this.renderMermaidParts(this.renderParts());\n });\n\n // React to a render that produced change highlights: emit, schedule fade,\n // and optionally scroll the first changed block into view.\n effect(() => {\n const count = this.highlightResult().count;\n untracked(() => this.onHighlightResult(count));\n });\n }\n\n ngOnDestroy(): void {\n this.httpSub?.unsubscribe();\n clearTimeout(this.fadeTimer);\n clearTimeout(this.scrollTimer);\n }\n\n protected onLinkClick(event: MouseEvent): void {\n const target = event.target as HTMLElement;\n const anchor = target.closest('a');\n if (anchor) {\n this.linkClick.emit({ href: anchor.href, event });\n }\n }\n\n // -- Change highlighting -----------------------------------------------------\n\n /** Side-effects for a render: emit, (re)arm the fade timer, optional scroll. */\n private onHighlightResult(count: number): void {\n clearTimeout(this.fadeTimer);\n clearTimeout(this.scrollTimer);\n // A fresh render starts un-faded (re-entrant: never reuse stale fade state).\n this.highlightsFaded.set(false);\n if (count <= 0) return;\n\n this.changesHighlighted.emit({ changedBlocks: count });\n\n const fadeMs = this.changeHighlightFadeMs();\n if (fadeMs && fadeMs > 0) {\n this.fadeTimer = setTimeout(() => this.highlightsFaded.set(true), fadeMs);\n }\n if (this.scrollToFirstChange()) {\n // Defer until the highlighted HTML has been written to the DOM.\n this.scrollTimer = setTimeout(() => this.scrollToFirstChanged(), 0);\n }\n }\n\n private scrollToFirstChanged(): void {\n if (typeof document === 'undefined') return;\n const el = this.host.nativeElement.querySelector('.lc-markdown__block--changed');\n el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n }\n\n /**\n * Wraps the changed/added top-level blocks of `html` with\n * `.lc-markdown__block--changed`. A block is \"changed\" when its normalized\n * text is not present among the blocks of `prevMarkdown`. Lists are diffed\n * per `<li>` and tables per `<tr>` so a single edited item highlights alone.\n */\n private applyChangeHighlights(\n html: string,\n prevMarkdown: string\n ): { html: string; count: number } {\n if (typeof document === 'undefined') return { html, count: 0 };\n\n const prevKeys = this.collectBlockKeys(this.parseMarkdown(prevMarkdown).html);\n const root = this.htmlToElement(html);\n if (!root) return { html, count: 0 };\n\n let count = 0;\n const markIfChanged = (el: Element): void => {\n const key = this.blockTextKey(el.textContent ?? '');\n if (!key || prevKeys.has(key)) return;\n el.classList.add('lc-markdown__block--changed');\n const sr = document.createElement('span');\n sr.className = 'lc-markdown__sr-only';\n sr.textContent = `${this.changedMarkerLabel()} `;\n el.insertBefore(sr, el.firstChild);\n count++;\n };\n\n this.eachLogicalBlock(root, markIfChanged);\n return { html: root.innerHTML, count };\n }\n\n /** Normalized text set of the logical blocks within an HTML fragment. */\n private collectBlockKeys(html: string): Set<string> {\n const keys = new Set<string>();\n const root = this.htmlToElement(html);\n if (!root) return keys;\n this.eachLogicalBlock(root, (el) => {\n const key = this.blockTextKey(el.textContent ?? '');\n if (key) keys.add(key);\n });\n return keys;\n }\n\n /**\n * Visits each diffable block under `root`: top-level elements, but descending\n * into `<ul>`/`<ol>` (per `<li>`) and `<table>` (per `<tr>`).\n */\n private eachLogicalBlock(root: Element, visit: (el: Element) => void): void {\n for (const node of Array.from(root.childNodes)) {\n if (node.nodeType !== 1) continue; // elements only (skips code placeholders)\n const el = node as Element;\n const tag = el.tagName;\n if (tag === 'UL' || tag === 'OL') {\n for (const li of Array.from(el.children)) {\n if (li.tagName === 'LI') visit(li);\n }\n } else if (tag === 'TABLE' || el.querySelector('table')) {\n // Tables (incl. the scroll wrapper div) diff per row.\n for (const tr of Array.from(el.querySelectorAll('tr'))) {\n visit(tr);\n }\n } else {\n visit(el);\n }\n }\n }\n\n private blockTextKey(text: string): string {\n return text.replace(/\\s+/g, ' ').trim().toLowerCase();\n }\n\n private htmlToElement(html: string): HTMLElement | null {\n if (typeof document === 'undefined') return null;\n const div = document.createElement('div');\n div.innerHTML = html;\n return div;\n }\n\n private loadFromUrl(url: string): void {\n this.httpSub?.unsubscribe();\n this.httpSub = this.http.get(url, { responseType: 'text' }).subscribe({\n next: (text) => this.resolvedMarkdown.set(text),\n error: () => this.resolvedMarkdown.set(`*Failed to load ${url}*`),\n });\n }\n\n private splitIntoParts(\n html: string,\n blocks: { kind: 'code' | 'mermaid'; lang: string; code: string }[]\n ): RenderPart[] {\n const parts: RenderPart[] = [];\n const regex = new RegExp(BLOCK_PLACEHOLDER.source, 'g');\n const cursor = { next: 0 };\n let lastIndex = 0;\n let partIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(html)) !== null) {\n const before = html.slice(lastIndex, match.index);\n if (before.trim()) {\n parts.push({\n type: 'html',\n index: partIndex++,\n safeHtml: this.sanitizer.bypassSecurityTrustHtml(this.sanitizeHtml(before, cursor)),\n });\n }\n const block = blocks[parseInt(match[1], 10)];\n lastIndex = match.index + match[0].length;\n // Unforgeable placeholder, so this can only be a bug — but never a crash.\n if (!block) continue;\n if (block.kind === 'mermaid') {\n parts.push({\n type: 'mermaid',\n index: partIndex++,\n code: block.code,\n });\n } else {\n parts.push({\n type: 'code',\n index: partIndex++,\n code: block.code,\n lang: block.lang as CodeBlockLanguage,\n });\n }\n }\n\n const remaining = html.slice(lastIndex);\n if (remaining.trim()) {\n parts.push({\n type: 'html',\n index: partIndex++,\n safeHtml: this.sanitizer.bypassSecurityTrustHtml(this.sanitizeHtml(remaining, cursor)),\n });\n }\n\n return parts;\n }\n\n private parseMarkdown(md: string): {\n html: string;\n blocks: { kind: 'code' | 'mermaid'; lang: string; code: string }[];\n headings: MarkdownHeading[];\n } {\n const blocks: { kind: 'code' | 'mermaid'; lang: string; code: string }[] = [];\n const headings: MarkdownHeading[] = [];\n const baseUrl = this.baseUrl();\n const linkTarget = this.linkTarget();\n const showAnchors = this.showHeadingAnchors();\n\n // 1. Extract fenced code blocks → placeholders (see BLOCK_MARK: the source\n // is stripped of the marker character first, so it can't forge one)\n // eslint-disable-next-line no-control-regex -- strip the placeholder marker (see BLOCK_MARK)\n let processed = md.replace(/\\u0000/g, '').replace(\n /```(\\w*)\\n([\\s\\S]*?)```/g,\n (_match, lang: string, code: string) => {\n const idx = blocks.length;\n const normalizedLang = (lang || 'text').toLowerCase();\n blocks.push({\n kind: normalizedLang === 'mermaid' ? 'mermaid' : 'code',\n lang: normalizedLang,\n code: code.trimEnd(),\n });\n return blockPlaceholder(idx);\n }\n );\n\n // 2. Escape HTML entities — quotes included, so nothing the parser later\n // interpolates into an attribute (link/image URLs, alt text) can close it.\n processed = this.escapeHtml(processed);\n\n // 3. Headings (# to ######)\n processed = processed.replace(\n /^(#{1,6})\\s+(.+)$/gm,\n (_match, hashes: string, text: string) => {\n const level = hashes.length;\n const id = text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-');\n headings.push({ level, text, id });\n const anchor = showAnchors\n ? `<a href=\"#${id}\" class=\"lc-markdown__anchor\" aria-hidden=\"true\">#</a>`\n : '';\n return `<h${level} id=\"${id}\">${anchor}${text}</h${level}>`;\n }\n );\n\n // 4. Horizontal rules\n processed = processed.replace(/^---+$/gm, '<hr>');\n\n // 5. Blockquotes\n processed = processed.replace(\n /^(?:&gt;)\\s?(.*)$/gm,\n '<blockquote>$1</blockquote>'\n );\n // Merge consecutive blockquotes\n processed = processed.replace(\n /<\\/blockquote>\\n<blockquote>/g,\n '\\n'\n );\n\n // 6. Tables (GFM)\n processed = this.parseTables(processed);\n\n // 7. Task lists. Angular's HTML sanitizer strips <input>, so the checkbox is\n // rendered as a styled <span role=\"checkbox\"> (which survives sanitization and\n // carries accessible checked/disabled state) rather than a form control.\n processed = processed.replace(\n /^[-*]\\s+\\[[xX]\\]\\s+(.*)$/gm,\n '<li class=\"lc-markdown__task-item\">' +\n '<span class=\"lc-markdown__checkbox lc-markdown__checkbox--checked\" role=\"checkbox\" aria-checked=\"true\" aria-disabled=\"true\"></span>' +\n '<span class=\"lc-markdown__task-label\">$1</span></li>'\n );\n processed = processed.replace(\n /^[-*]\\s+\\[ \\]\\s+(.*)$/gm,\n '<li class=\"lc-markdown__task-item\">' +\n '<span class=\"lc-markdown__checkbox\" role=\"checkbox\" aria-checked=\"false\" aria-disabled=\"true\"></span>' +\n '<span class=\"lc-markdown__task-label\">$1</span></li>'\n );\n\n // 8. Unordered lists\n processed = processed.replace(\n /^[-*]\\s+(.*)$/gm,\n '<li>$1</li>'\n );\n // Wrap consecutive <li> in <ul>\n processed = processed.replace(\n /((?:<li[^>]*>.*<\\/li>\\n?)+)/g,\n '<ul>$1</ul>'\n );\n\n // 9. Ordered lists — marked while wrapping, so a run of them gets its <ol>\n // and isn't confused with the <li>s already inside <ul>s above.\n processed = processed.replace(\n /^\\d+\\.\\s+(.*)$/gm,\n '<li data-ol>$1</li>'\n );\n processed = processed.replace(\n /((?:<li data-ol>.*<\\/li>\\n?)+)/g,\n (run) => `<ol>${run.replace(/ data-ol/g, '')}</ol>`\n );\n\n // 10. Inline code\n processed = processed.replace(\n /`([^`]+)`/g,\n '<code>$1</code>'\n );\n\n // 11. Bold + italic combos\n processed = processed.replace(\n /\\*\\*\\*(.+?)\\*\\*\\*/g,\n '<strong><em>$1</em></strong>'\n );\n processed = processed.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n processed = processed.replace(/\\*(.+?)\\*/g, '<em>$1</em>');\n\n // 12. Strikethrough (GFM)\n processed = processed.replace(/~~(.+?)~~/g, '<del>$1</del>');\n\n // 13. Images. Only allow-listed URL schemes become a `src` (see\n // safe-url.ts); anything else — `javascript:`, unknown schemes — is\n // rendered as its alt text. This holds regardless of `sanitize`.\n processed = processed.replace(\n /!\\[([^\\]]*)\\]\\(([^)]+)\\)/g,\n (_match, alt: string, src: string) => {\n if (!isSafeImageUrl(src)) return alt;\n const resolvedSrc = baseUrl && !src.startsWith('http') ? `${baseUrl}/${src}` : src;\n return `<img src=\"${resolvedSrc}\" alt=\"${alt}\" class=\"lc-markdown__img\">`;\n }\n );\n\n // 14. Links — same rule: an unsafe URL leaves plain text, not a link.\n processed = processed.replace(\n /\\[([^\\]]+)\\]\\(([^)]+)\\)/g,\n (_match, text: string, href: string) => {\n if (!isSafeLinkUrl(href)) return text;\n const resolvedHref = baseUrl && !href.startsWith('http') && !href.startsWith('#')\n ? `${baseUrl}/${href}`\n : href;\n return `<a href=\"${resolvedHref}\" target=\"${linkTarget}\" rel=\"noopener\">${text}</a>`;\n }\n );\n\n // 14b. Autolinks — linkify bare URLs / www / emails in plain text. Runs after\n // explicit links and inline code so it can skip anything already inside an\n // <a> or <code> (and never touches tag attributes).\n processed = this.autolink(processed, linkTarget);\n\n // 15. Paragraphs — wrap standalone lines\n processed = processed\n .split('\\n\\n')\n .map((block) => {\n const trimmed = block.trim();\n if (!trimmed) return '';\n if (\n trimmed.startsWith('<h') ||\n trimmed.startsWith('<ul') ||\n trimmed.startsWith('<ol') ||\n trimmed.startsWith('<blockquote') ||\n trimmed.startsWith('<hr') ||\n trimmed.startsWith('<table') ||\n trimmed.startsWith('<div') ||\n trimmed.startsWith(BLOCK_MARK)\n ) {\n return trimmed;\n }\n return `<p>${trimmed.replace(/\\n/g, '<br>')}</p>`;\n })\n .join('\\n');\n\n // 16. Restore code block placeholders\n if (this.syntaxHighlight()) {\n // Leave placeholders — the template will render <lc-code-block> for each\n // No replacement needed here\n } else {\n processed = processed.replace(\n new RegExp(BLOCK_PLACEHOLDER.source, 'g'),\n (_match, idx: string) => {\n const block = blocks[parseInt(idx, 10)];\n if (!block) return '';\n return `<pre><code class=\"language-${block.lang}\">${this.escapeHtml(block.code)}</code></pre>`;\n }\n );\n }\n\n return { html: processed, blocks, headings };\n }\n\n private parseTables(text: string): string {\n return text.replace(\n /^(\\|.+\\|)\\n(\\|[-| :]+\\|)\\n((?:\\|.+\\|\\n?)*)/gm,\n (_match, header: string, separator: string, body: string) => {\n // Per-column alignment from the delimiter row: :--- left, :--: center,\n // ---: right. Surfaced as classes (the sanitizer strips inline `style`).\n const aligns = this.splitTableRow(separator).map((cell) => {\n const left = cell.startsWith(':');\n const right = cell.endsWith(':');\n if (left && right) return 'center';\n if (right) return 'right';\n if (left) return 'left';\n return '';\n });\n const alignClass = (col: number): string =>\n aligns[col] ? ` class=\"lc-markdown__cell--${aligns[col]}\"` : '';\n\n const headers = this.splitTableRow(header)\n .map((cell, i) => `<th scope=\"col\"${alignClass(i)}>${cell.trim()}</th>`)\n .join('');\n\n const rows = body\n .trim()\n .split('\\n')\n .map((row: string) => {\n const cells = this.splitTableRow(row)\n .map((cell, i) => `<td${alignClass(i)}>${cell.trim()}</td>`)\n .join('');\n return `<tr>${cells}</tr>`;\n })\n .join('');\n\n // Wrapped so wide tables scroll horizontally instead of breaking layout.\n return (\n `<div class=\"lc-markdown__table-wrap\">` +\n `<table class=\"lc-markdown__table\"><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>` +\n `</div>`\n );\n }\n );\n }\n\n /** Splits a pipe-table row into trimmed cells, dropping the leading/trailing edges. */\n private splitTableRow(row: string): string[] {\n return row\n .trim()\n .replace(/^\\|/, '')\n .replace(/\\|$/, '')\n .split('|')\n .map((cell) => cell.trim());\n }\n\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n\n /**\n * Linkifies bare URLs (`http(s)://…`, `www.…`) and email addresses in text,\n * leaving anything inside an existing `<a>` or `<code>` (and all tag\n * attributes) untouched. The HTML is tokenized into tags vs. text so only\n * top-level text runs are linkified.\n */\n private autolink(html: string, linkTarget: string): string {\n const url = /(?:https?:\\/\\/|www\\.)[^\\s<]*[^\\s<.,:;!?)\\]}'\"]/;\n const email = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/;\n const pattern = new RegExp(`${url.source}|${email.source}`, 'g');\n\n let depth = 0; // inside <a> or <code>\n return html\n .split(/(<[^>]+>)/g)\n .map((segment) => {\n if (segment.startsWith('<')) {\n if (/^<(a|code)[\\s>]/i.test(segment)) depth++;\n else if (/^<\\/(a|code)>/i.test(segment)) depth = Math.max(0, depth - 1);\n return segment;\n }\n if (depth > 0 || !segment) return segment;\n return segment.replace(pattern, (token) => {\n const isEmail = token.includes('@') && !/^(?:https?:\\/\\/|www\\.)/.test(token);\n const href = isEmail\n ? `mailto:${token}`\n : token.startsWith('www.')\n ? `https://${token}`\n : token;\n return `<a href=\"${href}\" target=\"${linkTarget}\" rel=\"noopener\">${token}</a>`;\n });\n })\n .join('');\n }\n\n protected mermaidSvgFor(index: number): SafeHtml | null {\n return this.mermaidSvgs()[index] ?? null;\n }\n\n protected mermaidErrorFor(index: number): string | null {\n return this.mermaidErrors()[index] ?? null;\n }\n\n private async renderMermaidParts(parts: RenderPart[]): Promise<void> {\n const mermaidParts = parts.filter((part) => part.type === 'mermaid' && !!part.code);\n this.mermaidSvgs.set({});\n this.mermaidErrors.set({});\n\n if (mermaidParts.length === 0) {\n return;\n }\n\n const runId = ++this.mermaidRenderRun;\n\n try {\n const mermaidApi = await this.getMermaidApi();\n if (runId !== this.mermaidRenderRun) return;\n\n const nextSvgs: Record<number, SafeHtml> = {};\n const nextErrors: Record<number, string> = {};\n\n for (const part of mermaidParts) {\n try {\n // Per instance, per run, per part: mermaid's render() removes any\n // element already carrying the id it is given, so an id shared with\n // another <lc-markdown> on the page would delete that one's diagram.\n const renderId = `lc-md-mermaid-${this.instanceId}-${runId}-${part.index}`;\n const { svg } = await mermaidApi.render(renderId, part.code!);\n if (runId !== this.mermaidRenderRun) return;\n // Mermaid emits self-contained SVG with internal <style> rules.\n // Angular HTML sanitization strips critical style declarations,\n // which leaves only labels visible (no edges/shapes). We trust\n // Mermaid output here and rely on Mermaid's strict security mode.\n nextSvgs[part.index] = this.sanitizer.bypassSecurityTrustHtml(svg);\n } catch {\n nextErrors[part.index] = this.mermaidErrorLabel();\n }\n }\n\n if (runId !== this.mermaidRenderRun) return;\n this.mermaidSvgs.set(nextSvgs);\n this.mermaidErrors.set(nextErrors);\n } catch {\n if (runId !== this.mermaidRenderRun) return;\n const fallbackErrors: Record<number, string> = {};\n for (const part of mermaidParts) {\n fallbackErrors[part.index] = this.mermaidUnavailableLabel();\n }\n this.mermaidErrors.set(fallbackErrors);\n }\n }\n\n private async getMermaidApi(): Promise<any> {\n if (!this.mermaidApiPromise) {\n this.mermaidApiPromise = import('mermaid').then((module) => {\n const api = module.default ?? module;\n if (!this.mermaidInitialized) {\n api.initialize({\n startOnLoad: false,\n securityLevel: 'strict',\n });\n this.mermaidInitialized = true;\n }\n return api;\n });\n }\n return this.mermaidApiPromise;\n }\n}\n","<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -- delegated handler for clicks on rendered links, which are focusable anchors -->\n<div [class]=\"containerClasses()\" (click)=\"onLinkClick($event)\">\n @if (highlightChanges()) {\n <div class=\"lc-markdown__sr-only\" role=\"status\" aria-live=\"polite\">{{ changeSummary() }}</div>\n }\n @for (part of renderParts(); track part.index) {\n @if (part.type === 'html') {\n <div [innerHTML]=\"part.safeHtml\"></div>\n } @else if (part.type === 'code') {\n <lc-code-block\n [code]=\"part.code!\"\n [language]=\"part.lang ?? 'text'\"\n [showLineNumbers]=\"true\"\n [showCopy]=\"true\"\n />\n } @else {\n @if (mermaidSvgFor(part.index); as svg) {\n <div class=\"lc-markdown__mermaid\" [innerHTML]=\"svg\"></div>\n } @else if (mermaidErrorFor(part.index); as errorMessage) {\n <div class=\"lc-markdown__mermaid-fallback\">\n <p class=\"lc-markdown__mermaid-error\">{{ errorMessage }}</p>\n <lc-code-block\n [code]=\"part.code!\"\n [language]=\"'text'\"\n [showLineNumbers]=\"true\"\n [showCopy]=\"true\"\n />\n </div>\n } @else {\n <div class=\"lc-markdown__mermaid-loading\">{{ mermaidLoadingLabel() }}</div>\n }\n }\n }\n</div>\n"],"mappings":";;;;;;AAwDA,MAAM,aAAa;AACnB,MAAM,oBAAoB,QAAwB,GAAG,WAAU,SAAU,MAAM;AAE/E,MAAM,oBAAoB;AAG1B,IAAI,oBAAoB;AAqCxB,IAAa,oBAAb,MAAa,kBAAiB;CACX,YAAY,OAAO,YAAY;CAC/B,aAAa,EAAE;CACf,OAAO,OAAO,UAAU;CACxB,OAAO,OAAO,UAAuB;CAC9C;CACA;CACA;CACA;CACA,qBAAqB;CACrB,mBAAmB;CAGlB,MAAM,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,MAAA,CAAA,+BAAA,CAAA,CAAA;CAGrB,UAAU,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAGzB,UAAU,MAAsC,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAG1D,aAAa,MAA0B,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAG/C,WAAW,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAG/B,kBAAkB,MAAe,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAGtC,qBAAqB,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;CAG1C,UAAU,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAIzB,mBAAmB,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAMxC,kBAAkB,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAGjC,wBAAwB,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,wBAAA,CAAA,+BAAA,CAAA,CAAA;CAGvC,sBAAsB,MAAe,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,sBAAA,CAAA,+BAAA,CAAA,CAAA;CAG3C,qBAAqB,MAAM,cAAa,GAAA,YAAA,CAAA,EAAA,WAAA,qBAAA,CAAA,+BAAA,CAAA,CAAA;CAGxC,yBAAyB,MAAM,sBAAqB,GAAA,YAAA,CAAA,EAAA,WAAA,yBAAA,CAAA,+BAAA,CAAA,CAAA;CAGpD,0BAA0B,MAAM,uBAAsB,GAAA,YAAA,CAAA,EAAA,WAAA,0BAAA,CAAA,+BAAA,CAAA,CAAA;CAGtD,sBAAsB,MAAM,gCAA+B,GAAA,YAAA,CAAA,EAAA,WAAA,sBAAA,CAAA,+BAAA,CAAA,CAAA;CAG3D,oBAAoB,MAAM,0CAAyC,GAAA,YAAA,CAAA,EAAA,WAAA,oBAAA,CAAA,+BAAA,CAAA,CAAA;CAGnE,0BAA0B,MAAM,qCAAoC,GAAA,YAAA,CAAA,EAAA,WAAA,0BAAA,CAAA,+BAAA,CAAA,CAAA;CAGpE,YAAY,OAAM;CAGlB,WAAW,OAAM;CAGjB,qBAAqB,OAAM;CAG1B,mBAAmB,OAAe,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAGvC,SAAS,eAAc;EAC7B,MAAM,KAAK,KAAK,iBAAgB;EAChC,IAAI,CAAC,IACH,OAAO;GACL,MAAM;GACN,QAAQ,CAAA;GACR,UAAU,CAAA;EACX;EAEH,OAAO,KAAK,cAAc,EAAE;CAC9B,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAOe,kBAAkB,eAA+C;EAChF,MAAM,EAAE,SAAS,KAAK,OAAM;EAC5B,MAAM,OAAO,KAAK,gBAAe;EACjC,IACE,CAAC,QACD,CAAC,KAAK,iBAAgB,KACtB,QAAQ,QACR,SAAS,KAAK,iBAAgB,GAE9B,OAAO;GAAE;GAAM,OAAO;EAAC;EAEzB,OAAO,KAAK,sBAAsB,MAAM,IAAI;CAC9C,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAGiB,eAAe,eAAe,KAAK,gBAAe,CAAE,CAAC,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAG3D,gBAAgB,eAAc;EAC/C,MAAM,IAAI,KAAK,aAAY;EAC3B,IAAI,KAAK,GAAG,OAAO;EACnB,OAAO,GAAG,EAAC,GAAI,MAAM,IAAI,KAAK,uBAAsB,IAAK,KAAK,wBAAuB;CACvF,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAGiB,kBAAkB,OAAO,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,kBAAA,CAAA,+BAAA,CAAA,CAAA;CAGxC,cAAc,eAA4B;EAClD,MAAM,OAAO,KAAK,gBAAe,CAAE,CAAC;EACpC,MAAM,EAAE,WAAW,KAAK,OAAM;EAC9B,IAAI,CAAC,MAAM,OAAO,CAAA;EAElB,IAAI,KAAK,gBAAe,KAAM,OAAO,SAAS,GAC5C,OAAO,KAAK,eAAe,MAAM,MAAM;EAIzC,OAAO,CAAC;GACN,MAAM;GACN,OAAO;GACP,UAAU,KAAK,UAAU,wBAAwB,KAAK,aAAa,MAAM,EAAE,MAAM,EAAC,CAAE,CAAC;EACtF,CAAA;CACH,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAUM,aAAa,MAAc,QAAwB;EACzD,MAAM,WAAW,KAAK,SAAQ;EAC9B,IAAI,CAAC,KAAK,SAAQ,GAAI;GACpB,OAAO,SAAS,KAAK,MAAM,YAAY,KAAK,CAAA,EAAA,CAAI;GAChD,OAAO;EACT;EAEA,QADkB,KAAK,UAAU,SAAS,gBAAgB,MAAM,IAAI,KAAK,GAClE,CAAU,QAAQ,0BAA0B,KAAK,OAAe,QAAQ,OAAM;GACnF,MAAM,UAAU,SAAS,OAAO;GAChC,IAAI,CAAC,WAAW,QAAQ,KAAK,KAAK,GAAG,OAAO;GAC5C,OAAO,KAAK,MAAK,OAAQ,QAAQ,GAAE,GAAI,MAAK;EAC9C,CAAC;CACH;CAEU,mBAAmB,eAAc;EACzC,IAAI,UAAU,4BAA4B,KAAK,QAAO;EACtD,IAAI,KAAK,gBAAe,GAAI,WAAW;EACvC,OAAO;CACT,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAEQ,cAAc,OAAiC,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAClD,gBAAgB,OAA+B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAEpD,WAAW,eAAe,KAAK,OAAM,CAAE,CAAC,UAAS,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAEzD,cAAA;EAEE,aAAY;GACV,MAAM,MAAM,KAAK,IAAG;GACpB,IAAI,KACF,KAAK,YAAY,GAAG;EAExB,CAAC;EAGD,aAAY;GACV,MAAM,UAAU,KAAK,QAAO;GAC5B,IAAI,YAAY,QACd,KAAK,iBAAiB,IAAI,OAAO;EAErC,CAAC;EAGD,aAAY;GACV,MAAM,IAAI,KAAK,SAAQ;GACvB,gBAAgB,KAAK,SAAS,KAAK,EAAE,UAAU,EAAC,CAAE,CAAC;EACrD,CAAC;EAGD,aAAY;GACV,AAAK,KAAK,mBAAmB,KAAK,YAAW,CAAE;EACjD,CAAC;EAID,aAAY;GACV,MAAM,QAAQ,KAAK,gBAAe,CAAE,CAAC;GACrC,gBAAgB,KAAK,kBAAkB,KAAK,CAAC;EAC/C,CAAC;CACH;CAEA,cAAW;EACT,KAAK,SAAS,YAAW;EACzB,aAAa,KAAK,SAAS;EAC3B,aAAa,KAAK,WAAW;CAC/B;CAEU,YAAY,OAAiB;EAErC,MAAM,SADS,MAAM,OACC,QAAQ,GAAG;EACjC,IAAI,QACF,KAAK,UAAU,KAAK;GAAE,MAAM,OAAO;GAAM;EAAK,CAAE;CAEpD;CAKQ,kBAAkB,OAAa;EACrC,aAAa,KAAK,SAAS;EAC3B,aAAa,KAAK,WAAW;EAE7B,KAAK,gBAAgB,IAAI,KAAK;EAC9B,IAAI,SAAS,GAAG;EAEhB,KAAK,mBAAmB,KAAK,EAAE,eAAe,MAAK,CAAE;EAErD,MAAM,SAAS,KAAK,sBAAqB;EACzC,IAAI,UAAU,SAAS,GACrB,KAAK,YAAY,iBAAiB,KAAK,gBAAgB,IAAI,IAAI,GAAG,MAAM;EAE1E,IAAI,KAAK,oBAAmB,GAE1B,KAAK,cAAc,iBAAiB,KAAK,qBAAoB,GAAI,CAAC;CAEtE;CAEQ,uBAAoB;EAC1B,IAAI,OAAO,aAAa,aAAa;EAErC,AADW,KAAK,KAAK,cAAc,cAAc,8BACjD,CAAA,EAAI,eAAe;GAAE,UAAU;GAAU,OAAO;EAAS,CAAE;CAC7D;CAQQ,sBACN,MACA,cAAoB;EAEpB,IAAI,OAAO,aAAa,aAAa,OAAO;GAAE;GAAM,OAAO;EAAC;EAE5D,MAAM,WAAW,KAAK,iBAAiB,KAAK,cAAc,YAAY,CAAC,CAAC,IAAI;EAC5E,MAAM,OAAO,KAAK,cAAc,IAAI;EACpC,IAAI,CAAC,MAAM,OAAO;GAAE;GAAM,OAAO;EAAC;EAElC,IAAI,QAAQ;EACZ,MAAM,iBAAiB,OAAqB;GAC1C,MAAM,MAAM,KAAK,aAAa,GAAG,eAAe,EAAE;GAClD,IAAI,CAAC,OAAO,SAAS,IAAI,GAAG,GAAG;GAC/B,GAAG,UAAU,IAAI,6BAA6B;GAC9C,MAAM,KAAK,SAAS,cAAc,MAAM;GACxC,GAAG,YAAY;GACf,GAAG,cAAc,GAAG,KAAK,mBAAkB,EAAE;GAC7C,GAAG,aAAa,IAAI,GAAG,UAAU;GACjC;EACF;EAEA,KAAK,iBAAiB,MAAM,aAAa;EACzC,OAAO;GAAE,MAAM,KAAK;GAAW;EAAK;CACtC;CAGQ,iBAAiB,MAAY;EACnC,MAAM,uBAAO,IAAI,IAAG;EACpB,MAAM,OAAO,KAAK,cAAc,IAAI;EACpC,IAAI,CAAC,MAAM,OAAO;EAClB,KAAK,iBAAiB,OAAO,OAAM;GACjC,MAAM,MAAM,KAAK,aAAa,GAAG,eAAe,EAAE;GAClD,IAAI,KAAK,KAAK,IAAI,GAAG;EACvB,CAAC;EACD,OAAO;CACT;CAMQ,iBAAiB,MAAe,OAA4B;EAClE,KAAK,MAAM,QAAQ,MAAM,KAAK,KAAK,UAAU,GAAG;GAC9C,IAAI,KAAK,aAAa,GAAG;GACzB,MAAM,KAAK;GACX,MAAM,MAAM,GAAG;GACf,IAAI,QAAQ,QAAQ,QAAQ,MAC1B;SAAK,MAAM,MAAM,MAAM,KAAK,GAAG,QAAQ,GACrC,IAAI,GAAG,YAAY,MAAM,MAAM,EAAE;GACnC,OACK,IAAI,QAAQ,WAAW,GAAG,cAAc,OAAO,GAEpD,KAAK,MAAM,MAAM,MAAM,KAAK,GAAG,iBAAiB,IAAI,CAAC,GACnD,MAAM,EAAE;QAGV,MAAM,EAAE;EAEZ;CACF;CAEQ,aAAa,MAAY;EAC/B,OAAO,KAAK,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAI,CAAE,CAAC,YAAW;CACrD;CAEQ,cAAc,MAAY;EAChC,IAAI,OAAO,aAAa,aAAa,OAAO;EAC5C,MAAM,MAAM,SAAS,cAAc,KAAK;EACxC,IAAI,YAAY;EAChB,OAAO;CACT;CAEQ,YAAY,KAAW;EAC7B,KAAK,SAAS,YAAW;EACzB,KAAK,UAAU,KAAK,KAAK,IAAI,KAAK,EAAE,cAAc,OAAM,CAAE,CAAC,CAAC,UAAU;GACpE,OAAO,SAAS,KAAK,iBAAiB,IAAI,IAAI;GAC9C,aAAa,KAAK,iBAAiB,IAAI,mBAAmB,IAAG,EAAG;EACjE,CAAA;CACH;CAEQ,eACN,MACA,QAAkE;EAElE,MAAM,QAAsB,CAAA;EAC5B,MAAM,QAAQ,IAAI,OAAO,kBAAkB,QAAQ,GAAG;EACtD,MAAM,SAAS,EAAE,MAAM,EAAC;EACxB,IAAI,YAAY;EAChB,IAAI,YAAY;EAChB,IAAI;EAEJ,QAAQ,QAAQ,MAAM,KAAK,IAAI,OAAO,MAAM;GAC1C,MAAM,SAAS,KAAK,MAAM,WAAW,MAAM,KAAK;GAChD,IAAI,OAAO,KAAI,GACb,MAAM,KAAK;IACT,MAAM;IACN,OAAO;IACP,UAAU,KAAK,UAAU,wBAAwB,KAAK,aAAa,QAAQ,MAAM,CAAC;GACnF,CAAA;GAEH,MAAM,QAAQ,OAAO,SAAS,MAAM,IAAI,EAAE;GAC1C,YAAY,MAAM,QAAQ,MAAM,EAAE,CAAC;GAEnC,IAAI,CAAC,OAAO;GACZ,IAAI,MAAM,SAAS,WACjB,MAAM,KAAK;IACT,MAAM;IACN,OAAO;IACP,MAAM,MAAM;GACb,CAAA;QAED,MAAM,KAAK;IACT,MAAM;IACN,OAAO;IACP,MAAM,MAAM;IACZ,MAAM,MAAM;GACb,CAAA;EAEL;EAEA,MAAM,YAAY,KAAK,MAAM,SAAS;EACtC,IAAI,UAAU,KAAI,GAChB,MAAM,KAAK;GACT,MAAM;GACN,OAAO;GACP,UAAU,KAAK,UAAU,wBAAwB,KAAK,aAAa,WAAW,MAAM,CAAC;EACtF,CAAA;EAGH,OAAO;CACT;CAEQ,cAAc,IAAU;EAK9B,MAAM,SAAqE,CAAA;EAC3E,MAAM,WAA8B,CAAA;EACpC,MAAM,UAAU,KAAK,QAAO;EAC5B,MAAM,aAAa,KAAK,WAAU;EAClC,MAAM,cAAc,KAAK,mBAAkB;EAK3C,IAAI,YAAY,GAAG,QAAQ,WAAW,EAAE,CAAC,CAAC,QACxC,6BACC,QAAQ,MAAc,SAAgB;GACrC,MAAM,MAAM,OAAO;GACnB,MAAM,kBAAkB,QAAQ,OAAA,CAAQ,YAAW;GACnD,OAAO,KAAK;IACV,MAAM,mBAAmB,YAAY,YAAY;IACjD,MAAM;IACN,MAAM,KAAK,QAAO;GACnB,CAAA;GACD,OAAO,iBAAiB,GAAG;EAC7B,CAAC;EAKH,YAAY,KAAK,WAAW,SAAS;EAGrC,YAAY,UAAU,QACpB,wBACC,QAAQ,QAAgB,SAAgB;GACvC,MAAM,QAAQ,OAAO;GACrB,MAAM,KAAK,KACR,YAAW,CAAE,CACb,QAAQ,aAAa,EAAE,CAAC,CACxB,QAAQ,QAAQ,GAAG;GACtB,SAAS,KAAK;IAAE;IAAO;IAAM;GAAE,CAAE;GAIjC,OAAO,KAAK,MAAK,OAAQ,GAAE,IAHZ,cACX,aAAa,GAAE,0DACf,KACqC,KAAI,KAAM,MAAK;EAC1D,CAAC;EAIH,YAAY,UAAU,QAAQ,YAAY,MAAM;EAGhD,YAAY,UAAU,QACpB,uBACA,6BAA6B;EAG/B,YAAY,UAAU,QACpB,iCACA,IAAI;EAIN,YAAY,KAAK,YAAY,SAAS;EAKtC,YAAY,UAAU,QACpB,8BACA,wOAEwD;EAE1D,YAAY,UAAU,QACpB,2BACA,0MAEwD;EAI1D,YAAY,UAAU,QACpB,mBACA,aAAa;EAGf,YAAY,UAAU,QACpB,gCACA,aAAa;EAKf,YAAY,UAAU,QACpB,oBACA,qBAAqB;EAEvB,YAAY,UAAU,QACpB,oCACC,QAAQ,OAAO,IAAI,QAAQ,aAAa,EAAE,EAAC,MAAO;EAIrD,YAAY,UAAU,QACpB,cACA,iBAAiB;EAInB,YAAY,UAAU,QACpB,sBACA,8BAA8B;EAEhC,YAAY,UAAU,QAAQ,kBAAkB,qBAAqB;EACrE,YAAY,UAAU,QAAQ,cAAc,aAAa;EAGzD,YAAY,UAAU,QAAQ,cAAc,eAAe;EAK3D,YAAY,UAAU,QACpB,8BACC,QAAQ,KAAa,QAAe;GACnC,IAAI,CAAC,eAAe,GAAG,GAAG,OAAO;GAEjC,OAAO,aADa,WAAW,CAAC,IAAI,WAAW,MAAM,IAAI,GAAG,QAAO,GAAI,QAAQ,IAChD,SAAU,IAAG;EAC9C,CAAC;EAIH,YAAY,UAAU,QACpB,6BACC,QAAQ,MAAc,SAAgB;GACrC,IAAI,CAAC,cAAc,IAAI,GAAG,OAAO;GAIjC,OAAO,YAHc,WAAW,CAAC,KAAK,WAAW,MAAM,KAAK,CAAC,KAAK,WAAW,GAAG,IAC5E,GAAG,QAAO,GAAI,SACd,KAC2B,YAAa,WAAU,mBAAoB,KAAI;EAChF,CAAC;EAMH,YAAY,KAAK,SAAS,WAAW,UAAU;EAG/C,YAAY,UACT,MAAM,MAAM,CAAC,CACb,KAAK,UAAS;GACb,MAAM,UAAU,MAAM,KAAI;GAC1B,IAAI,CAAC,SAAS,OAAO;GACrB,IACE,QAAQ,WAAW,IAAI,KACvB,QAAQ,WAAW,KAAK,KACxB,QAAQ,WAAW,KAAK,KACxB,QAAQ,WAAW,aAAa,KAChC,QAAQ,WAAW,KAAK,KACxB,QAAQ,WAAW,QAAQ,KAC3B,QAAQ,WAAW,MAAM,KACzB,QAAQ,WAAW,UAAU,GAE7B,OAAO;GAET,OAAO,MAAM,QAAQ,QAAQ,OAAO,MAAM,EAAC;EAC7C,CAAC,CAAC,CACD,KAAK,IAAI;EAGZ,IAAI,KAAK,gBAAe,GAAI,CAG5B,OACE,YAAY,UAAU,QACpB,IAAI,OAAO,kBAAkB,QAAQ,GAAG,IACvC,QAAQ,QAAe;GACtB,MAAM,QAAQ,OAAO,SAAS,KAAK,EAAE;GACrC,IAAI,CAAC,OAAO,OAAO;GACnB,OAAO,8BAA8B,MAAM,KAAI,IAAK,KAAK,WAAW,MAAM,IAAI,EAAC;EACjF,CAAC;EAIL,OAAO;GAAE,MAAM;GAAW;GAAQ;EAAQ;CAC5C;CAEQ,YAAY,MAAY;EAC9B,OAAO,KAAK,QACV,iDACC,QAAQ,QAAgB,WAAmB,SAAgB;GAG1D,MAAM,SAAS,KAAK,cAAc,SAAS,CAAC,CAAC,KAAK,SAAQ;IACxD,MAAM,OAAO,KAAK,WAAW,GAAG;IAChC,MAAM,QAAQ,KAAK,SAAS,GAAG;IAC/B,IAAI,QAAQ,OAAO,OAAO;IAC1B,IAAI,OAAO,OAAO;IAClB,IAAI,MAAM,OAAO;IACjB,OAAO;GACT,CAAC;GACD,MAAM,cAAc,QAClB,OAAO,OAAO,8BAA8B,OAAO,KAAI,KAAM;GAkB/D,OACE,qFAjBc,KAAK,cAAc,MAAM,CAAC,CACvC,KAAK,MAAM,MAAM,kBAAkB,WAAW,CAAC,EAAC,GAAI,KAAK,KAAI,EAAE,MAAO,CAAC,CACvE,KAAK,EAgB0C,EAAO,sBAd5C,KACV,KAAI,CAAE,CACN,MAAM,IAAI,CAAC,CACX,KAAK,QAAe;IAInB,OAAO,OAHO,KAAK,cAAc,GAAG,CAAC,CAClC,KAAK,MAAM,MAAM,MAAM,WAAW,CAAC,EAAC,GAAI,KAAK,KAAI,EAAE,MAAO,CAAC,CAC3D,KAAK,EACM,EAAK;GACrB,CAAC,CAAC,CACD,KAAK,EAKwE,EAAI;EAGtF,CAAC;CAEL;CAGQ,cAAc,KAAW;EAC/B,OAAO,IACJ,KAAI,CAAE,CACN,QAAQ,OAAO,EAAE,CAAC,CAClB,QAAQ,OAAO,EAAE,CAAC,CAClB,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,KAAI,CAAE;CAC9B;CAEQ,WAAW,MAAY;EAC7B,OAAO,KACJ,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,QAAQ;CAC3B;CAQQ,SAAS,MAAc,YAAkB;EAG/C,MAAM,UAAU,IAAI,OAAO,GAAG,iDAAI,OAAM,GAAI,iDAAM,UAAU,GAAG;EAE/D,IAAI,QAAQ;EACZ,OAAO,KACJ,MAAM,YAAY,CAAC,CACnB,KAAK,YAAW;GACf,IAAI,QAAQ,WAAW,GAAG,GAAG;IAC3B,IAAI,mBAAmB,KAAK,OAAO,GAAG;SACjC,IAAI,iBAAiB,KAAK,OAAO,GAAG,QAAQ,KAAK,IAAI,GAAG,QAAQ,CAAC;IACtE,OAAO;GACT;GACA,IAAI,QAAQ,KAAK,CAAC,SAAS,OAAO;GAClC,OAAO,QAAQ,QAAQ,UAAU,UAAS;IAOxC,OAAO,YANS,MAAM,SAAS,GAAG,KAAK,CAAC,yBAAyB,KAAK,KAAK,IAEvE,UAAU,UACV,MAAM,WAAW,MAAM,IACrB,WAAW,UACX,MACiB,YAAa,WAAU,mBAAoB,MAAK;GACzE,CAAC;EACH,CAAC,CAAC,CACD,KAAK,EAAE;CACZ;CAEU,cAAc,OAAa;EACnC,OAAO,KAAK,YAAW,CAAE,CAAC,UAAU;CACtC;CAEU,gBAAgB,OAAa;EACrC,OAAO,KAAK,cAAa,CAAE,CAAC,UAAU;CACxC;CAEQ,MAAM,mBAAmB,OAAmB;EAClD,MAAM,eAAe,MAAM,QAAQ,SAAS,KAAK,SAAS,aAAa,CAAC,CAAC,KAAK,IAAI;EAClF,KAAK,YAAY,IAAI,CAAA,CAAE;EACvB,KAAK,cAAc,IAAI,CAAA,CAAE;EAEzB,IAAI,aAAa,WAAW,GAC1B;EAGF,MAAM,QAAQ,EAAE,KAAK;EAErB,IAAI;GACF,MAAM,aAAa,MAAM,KAAK,cAAa;GAC3C,IAAI,UAAU,KAAK,kBAAkB;GAErC,MAAM,WAAqC,CAAA;GAC3C,MAAM,aAAqC,CAAA;GAE3C,KAAK,MAAM,QAAQ,cACjB,IAAI;IAIF,MAAM,WAAW,iBAAiB,KAAK,WAAU,GAAI,MAAK,GAAI,KAAK;IACnE,MAAM,EAAE,QAAQ,MAAM,WAAW,OAAO,UAAU,KAAK,IAAK;IAC5D,IAAI,UAAU,KAAK,kBAAkB;IAKrC,SAAS,KAAK,SAAS,KAAK,UAAU,wBAAwB,GAAG;GACnE,QAAQ;IACN,WAAW,KAAK,SAAS,KAAK,kBAAiB;GACjD;GAGF,IAAI,UAAU,KAAK,kBAAkB;GACrC,KAAK,YAAY,IAAI,QAAQ;GAC7B,KAAK,cAAc,IAAI,UAAU;EACnC,QAAQ;GACN,IAAI,UAAU,KAAK,kBAAkB;GACrC,MAAM,iBAAyC,CAAA;GAC/C,KAAK,MAAM,QAAQ,cACjB,eAAe,KAAK,SAAS,KAAK,wBAAuB;GAE3D,KAAK,cAAc,IAAI,cAAc;EACvC;CACF;CAEQ,MAAM,gBAAa;EACzB,IAAI,CAAC,KAAK,mBACR,KAAK,oBAAoB,OAAO,UAAU,CAAC,MAAM,WAAU;GACzD,MAAM,MAAM,OAAO,WAAW;GAC9B,IAAI,CAAC,KAAK,oBAAoB;IAC5B,IAAI,WAAW;KACb,aAAa;KACb,eAAe;IAChB,CAAA;IACD,KAAK,qBAAqB;GAC5B;GACA,OAAO;EACT,CAAC;EAEH,OAAO,KAAK;CACd;CA/uBW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAiB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAjB,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAiB,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,KAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,oBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,SAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,kBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,iBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,uBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,qBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,oBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,wBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,yBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,qBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,mBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,yBAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,SAAA;GAAA,WAAA;GAAA,UAAA;GAAA,oBAAA;EAAA;EAAA,UAAA;EAAA,UCnG9B;EAkCA,QAAA,CAAA,whPAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MD2DY;GAAkB,UAAA;GAAA,QAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;IAAA;GAAA;EAAA,CAAA;EAAA,iBAAA,GAAA,wBAAA;EAAA,eAAA,GAAA,kBAAA;CAAA,CAAA;;AAMjB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAiB,YAAA,CAAA;EAT7B,MAAA;EACW,MAAA,CAAA;GAAA,UAAA;GAAa,YACX;GAAI,SACP,CAAC,kBAAkB;GAAC,iBAGZ,wBAAwB;GAAM,eAChC,kBAAkB;GAAI,UAAA;GAAA,QAAA,CAAA,whPAAA;EAAA,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifecorner/angular-ui-kit",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "LifeCorner Design System – Angular UI component library",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,12 @@
1
- import { SafeHtml } from "@angular/platform-browser";
2
1
  import * as i0 from "@angular/core";
3
2
  export type CodeBlockLanguage = 'typescript' | 'javascript' | 'html' | 'css' | 'scss' | 'json' | 'bash' | 'python' | 'java' | 'text';
3
+ /** A run of source text; `className` is one `token-*` class, or `null` for plain text. */
4
+ interface CodeSegment {
5
+ readonly text: string;
6
+ readonly className: string | null;
7
+ }
8
+ /** One rendered line: its segments in order. */
9
+ type CodeLine = readonly CodeSegment[];
4
10
  export declare class CodeBlockComponent {
5
11
  /** Code content to display */
6
12
  code: import("@angular/core").InputSignal<string>;
@@ -14,6 +20,21 @@ export declare class CodeBlockComponent {
14
20
  showCopy: import("@angular/core").InputSignal<boolean>;
15
21
  /** Whether the code block has a header bar */
16
22
  showHeader: import("@angular/core").InputSignal<boolean>;
23
+ /**
24
+ * Accessible name of the scrollable code region (`role="region"`). When it
25
+ * is unset or empty, the `filename` is used, then the `language`. Set a
26
+ * short, localised name that is unique on the page, such as "Befehl für
27
+ * Claude Code". Not used when `wrap` is set.
28
+ * @default undefined
29
+ */
30
+ regionLabel: import("@angular/core").InputSignal<string | undefined>;
31
+ /**
32
+ * Wraps long lines (`white-space: pre-wrap`, breaking anywhere if needed)
33
+ * instead of scrolling sideways. A wrapped block doesn't scroll, so its body
34
+ * is neither a tab stop nor a region.
35
+ * @default false
36
+ */
37
+ wrap: import("@angular/core").InputSignal<boolean>;
17
38
  /** Visible label of the copy button. */
18
39
  copyLabel: import("@angular/core").InputSignal<string>;
19
40
  /** Accessible name of the copy button. */
@@ -23,10 +44,11 @@ export declare class CodeBlockComponent {
23
44
  /** Announced to screen readers after copying. */
24
45
  copiedAnnouncement: import("@angular/core").InputSignal<string>;
25
46
  protected copied: import("@angular/core").WritableSignal<boolean>;
26
- private readonly sanitizer;
27
- protected highlightedLines: import("@angular/core").Signal<SafeHtml[]>;
47
+ protected highlightedLines: import("@angular/core").Signal<CodeLine[]>;
28
48
  protected lineNumbers: import("@angular/core").Signal<number[]>;
29
49
  protected headerLabel: import("@angular/core").Signal<string>;
50
+ /** The region's name: `regionLabel`, then `filename`, then `language` (`''` falls through). */
51
+ protected regionName: import("@angular/core").Signal<string>;
30
52
  protected codeClasses: import("@angular/core").Signal<string>;
31
53
  protected copyToClipboard(): Promise<void>;
32
54
  static ɵfac: i0.ɵɵFactoryDeclaration<CodeBlockComponent, never>;
@@ -61,6 +83,16 @@ export declare class CodeBlockComponent {
61
83
  "required": false;
62
84
  "isSignal": true;
63
85
  };
86
+ "regionLabel": {
87
+ "alias": "regionLabel";
88
+ "required": false;
89
+ "isSignal": true;
90
+ };
91
+ "wrap": {
92
+ "alias": "wrap";
93
+ "required": false;
94
+ "isSignal": true;
95
+ };
64
96
  "copyLabel": {
65
97
  "alias": "copyLabel";
66
98
  "required": false;
@@ -82,4 +114,5 @@ export declare class CodeBlockComponent {
82
114
  "isSignal": true;
83
115
  };
84
116
  }, {}, never, never, true, never>;
85
- }
117
+ }
118
+ export {};