valtech-components 2.0.810 → 2.0.813

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.
@@ -53,7 +53,7 @@ import 'prismjs/components/prism-json';
53
53
  * Current version of valtech-components.
54
54
  * This is automatically updated during the publish process.
55
55
  */
56
- const VERSION = '2.0.810';
56
+ const VERSION = '2.0.813';
57
57
 
58
58
  /**
59
59
  * Servicio para gestionar presets de componentes.
@@ -35849,6 +35849,308 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
35849
35849
  type: Input
35850
35850
  }] } });
35851
35851
 
35852
+ /** Built-in label sets for the three platform locales. */
35853
+ const CALLOUT_LABELS = {
35854
+ es: {
35855
+ NOTE: 'Nota',
35856
+ TIP: 'Tip',
35857
+ INFO: 'Info',
35858
+ IMPORTANT: 'Importante',
35859
+ WARNING: 'Atención',
35860
+ CAUTION: 'Precaución',
35861
+ },
35862
+ en: {
35863
+ NOTE: 'Note',
35864
+ TIP: 'Tip',
35865
+ INFO: 'Info',
35866
+ IMPORTANT: 'Important',
35867
+ WARNING: 'Warning',
35868
+ CAUTION: 'Caution',
35869
+ },
35870
+ pt: {
35871
+ NOTE: 'Nota',
35872
+ TIP: 'Dica',
35873
+ INFO: 'Info',
35874
+ IMPORTANT: 'Importante',
35875
+ WARNING: 'Atenção',
35876
+ CAUTION: 'Cuidado',
35877
+ },
35878
+ };
35879
+ /**
35880
+ * Per-callout-kind color (Ionic semantic color). Fixed across locales since
35881
+ * red = danger no matter what language you speak.
35882
+ */
35883
+ const CALLOUT_COLORS = {
35884
+ NOTE: 'primary',
35885
+ TIP: 'success',
35886
+ INFO: 'tertiary',
35887
+ IMPORTANT: 'warning',
35888
+ WARNING: 'warning',
35889
+ CAUTION: 'danger',
35890
+ };
35891
+ const BOX_DRAWING = /[┌┐└┘├┤┬┴┼─│╔╗╚╝═║]/;
35892
+ const CALLOUT_RE = /^>\s*\[!(NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT)\]\s*(.*)$/i;
35893
+ const FENCE_RE = /^```([\w-]*)\s*$/;
35894
+ const HEADING_RE = /^(#{1,6})\s+(.+)$/;
35895
+ const SEPARATOR_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
35896
+ const UNORDERED_RE = /^\s*[-*+]\s+(.+)$/;
35897
+ const CHECKLIST_RE = /^\s*[-*+]\s+\[([ xX])\]\s+(.+)$/;
35898
+ const ORDERED_RE = /^\s*\d+\.\s+(.+)$/;
35899
+ const TABLE_DIVIDER_RE = /^\s*\|?[\s:|-]+\|?\s*$/;
35900
+ const TABLE_ROW_RE = /^\s*\|(.+)\|\s*$/;
35901
+ /**
35902
+ * Pure Markdown → ArticleMetadata parser. No Angular deps — usable from Node scripts
35903
+ * (build-time generation) and from the Angular `MarkdownArticleParserService` wrapper.
35904
+ *
35905
+ * Supported syntax:
35906
+ * - Headings (#, ##, ### …) → title / subtitle
35907
+ * - Paragraphs → paragraph (with `processLinks` + `allowPartialBold`)
35908
+ * - Lists (-, *, 1.) → unordered / ordered list (- [ ] / - [x] for checklist)
35909
+ * - Blockquotes (>) → quote, with GitHub callouts `> [!NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT]` mapped to notes
35910
+ * - Code fences (```lang) → code
35911
+ * - Box-drawing ASCII art (┌┐└┘│─) auto-wrapped as code
35912
+ * - Tables → flattened into paragraphs with bold keys
35913
+ * - Horizontal rules (---, ***, ___) → separator
35914
+ */
35915
+ function parseMarkdownArticle(markdown, options) {
35916
+ const lines = normalize(markdown).split('\n');
35917
+ const elements = [];
35918
+ const labels = options?.calloutLabels ??
35919
+ (options?.locale ? CALLOUT_LABELS[options.locale] : CALLOUT_LABELS.es);
35920
+ let i = 0;
35921
+ while (i < lines.length) {
35922
+ const line = lines[i];
35923
+ if (line.trim() === '') {
35924
+ i++;
35925
+ continue;
35926
+ }
35927
+ const fence = line.match(FENCE_RE);
35928
+ if (fence) {
35929
+ const lang = fence[1] || undefined;
35930
+ const body = [];
35931
+ i++;
35932
+ while (i < lines.length && !FENCE_RE.test(lines[i])) {
35933
+ body.push(lines[i]);
35934
+ i++;
35935
+ }
35936
+ i++;
35937
+ elements.push({
35938
+ type: 'code',
35939
+ props: { code: body.join('\n'), language: lang, theme: 'dark' },
35940
+ });
35941
+ continue;
35942
+ }
35943
+ if (BOX_DRAWING.test(line)) {
35944
+ const body = [];
35945
+ while (i < lines.length && (lines[i].trim() === '' || BOX_DRAWING.test(lines[i]))) {
35946
+ body.push(lines[i]);
35947
+ i++;
35948
+ }
35949
+ while (body.length && body[body.length - 1].trim() === '')
35950
+ body.pop();
35951
+ elements.push({
35952
+ type: 'code',
35953
+ props: { code: body.join('\n'), language: 'text', theme: 'dark' },
35954
+ });
35955
+ continue;
35956
+ }
35957
+ if (SEPARATOR_RE.test(line)) {
35958
+ elements.push({ type: 'separator', props: { style: 'line' } });
35959
+ i++;
35960
+ continue;
35961
+ }
35962
+ const heading = line.match(HEADING_RE);
35963
+ if (heading) {
35964
+ elements.push(makeHeading(heading[1].length, heading[2].trim()));
35965
+ i++;
35966
+ continue;
35967
+ }
35968
+ if (line.startsWith('>')) {
35969
+ const block = [];
35970
+ while (i < lines.length && lines[i].startsWith('>')) {
35971
+ block.push(lines[i]);
35972
+ i++;
35973
+ }
35974
+ elements.push(makeQuoteOrCallout(block, labels));
35975
+ continue;
35976
+ }
35977
+ if (TABLE_ROW_RE.test(line)) {
35978
+ const rows = [];
35979
+ while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
35980
+ rows.push(lines[i]);
35981
+ i++;
35982
+ }
35983
+ elements.push(...makeTable(rows));
35984
+ continue;
35985
+ }
35986
+ if (CHECKLIST_RE.test(line) || UNORDERED_RE.test(line) || ORDERED_RE.test(line)) {
35987
+ const listType = CHECKLIST_RE.test(line)
35988
+ ? 'checklist'
35989
+ : ORDERED_RE.test(line)
35990
+ ? 'ordered'
35991
+ : 'unordered';
35992
+ const items = [];
35993
+ while (i < lines.length && lines[i].trim() !== '') {
35994
+ const cur = lines[i];
35995
+ const check = cur.match(CHECKLIST_RE);
35996
+ const unord = cur.match(UNORDERED_RE);
35997
+ const ord = cur.match(ORDERED_RE);
35998
+ if (check)
35999
+ items.push({ text: check[2].trim() });
36000
+ else if (ord && listType === 'ordered')
36001
+ items.push({ text: ord[1].trim() });
36002
+ else if (unord && listType !== 'ordered')
36003
+ items.push({ text: unord[1].trim() });
36004
+ else
36005
+ break;
36006
+ i++;
36007
+ }
36008
+ elements.push({ type: 'list', props: { items, listType } });
36009
+ continue;
36010
+ }
36011
+ const paragraph = [];
36012
+ while (i < lines.length && lines[i].trim() !== '' && !startsNewBlock(lines[i])) {
36013
+ paragraph.push(lines[i]);
36014
+ i++;
36015
+ }
36016
+ const text = paragraph.join(' ').trim();
36017
+ if (text) {
36018
+ elements.push({
36019
+ type: 'paragraph',
36020
+ props: {
36021
+ content: text,
36022
+ size: 'medium',
36023
+ color: 'dark',
36024
+ bold: false,
36025
+ processLinks: true,
36026
+ allowPartialBold: true,
36027
+ },
36028
+ });
36029
+ }
36030
+ }
36031
+ // Strip parser-only options before merging into ArticleMetadata
36032
+ const { locale: _l, calloutLabels: _c, ...metadataOverrides } = options ?? {};
36033
+ return {
36034
+ elements,
36035
+ maxWidth: '900px',
36036
+ centered: true,
36037
+ theme: 'auto',
36038
+ ...metadataOverrides,
36039
+ };
36040
+ }
36041
+ function normalize(md) {
36042
+ return md.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
36043
+ }
36044
+ function startsNewBlock(line) {
36045
+ return (HEADING_RE.test(line) ||
36046
+ FENCE_RE.test(line) ||
36047
+ SEPARATOR_RE.test(line) ||
36048
+ TABLE_ROW_RE.test(line) ||
36049
+ UNORDERED_RE.test(line) ||
36050
+ ORDERED_RE.test(line) ||
36051
+ CHECKLIST_RE.test(line) ||
36052
+ line.startsWith('>') ||
36053
+ BOX_DRAWING.test(line));
36054
+ }
36055
+ function makeHeading(level, content) {
36056
+ if (level === 1) {
36057
+ return {
36058
+ type: 'title',
36059
+ props: { content, size: 'xlarge', color: 'dark', bold: true },
36060
+ };
36061
+ }
36062
+ const size = level === 2 ? 'large' : level === 3 ? 'medium' : 'small';
36063
+ return {
36064
+ type: 'subtitle',
36065
+ props: { content, size, color: 'dark', bold: true },
36066
+ };
36067
+ }
36068
+ function makeQuoteOrCallout(block, labels) {
36069
+ const first = block[0];
36070
+ const callout = first.match(CALLOUT_RE);
36071
+ const lines = block.map(l => l.replace(/^>\s?/, ''));
36072
+ if (callout) {
36073
+ const type = callout[1].toUpperCase();
36074
+ const firstLineRest = callout[2] || '';
36075
+ const rest = lines.slice(1).join(' ').trim();
36076
+ const text = [firstLineRest, rest].filter(Boolean).join(' ').trim();
36077
+ return makeNote(type, text, labels);
36078
+ }
36079
+ const text = lines.join(' ').trim();
36080
+ return {
36081
+ type: 'quote',
36082
+ props: {
36083
+ content: text,
36084
+ size: 'medium',
36085
+ color: 'medium',
36086
+ bold: false,
36087
+ showQuoteMark: true,
36088
+ alignment: 'left',
36089
+ },
36090
+ };
36091
+ }
36092
+ function makeNote(kind, text, labels) {
36093
+ return {
36094
+ type: 'note',
36095
+ props: {
36096
+ text,
36097
+ prefix: `${labels[kind]}:`,
36098
+ color: CALLOUT_COLORS[kind],
36099
+ textColor: 'dark',
36100
+ size: 'medium',
36101
+ rounded: true,
36102
+ allowPartialBold: true,
36103
+ processLinks: true,
36104
+ },
36105
+ };
36106
+ }
36107
+ function makeTable(rows) {
36108
+ const parsed = rows
36109
+ .filter(r => !TABLE_DIVIDER_RE.test(r))
36110
+ .map(r => r
36111
+ .trim()
36112
+ .replace(/^\|/, '')
36113
+ .replace(/\|$/, '')
36114
+ .split('|')
36115
+ .map(c => c.trim()));
36116
+ if (parsed.length === 0)
36117
+ return [];
36118
+ const header = parsed[0];
36119
+ const dataRows = parsed.slice(1);
36120
+ if (dataRows.length === 0) {
36121
+ return [
36122
+ {
36123
+ type: 'paragraph',
36124
+ props: {
36125
+ content: header.join(' · '),
36126
+ size: 'medium',
36127
+ color: 'dark',
36128
+ bold: false,
36129
+ processLinks: true,
36130
+ allowPartialBold: true,
36131
+ },
36132
+ },
36133
+ ];
36134
+ }
36135
+ return dataRows.map(row => {
36136
+ const pairs = row.map((cell, idx) => {
36137
+ const key = header[idx] ?? '';
36138
+ return key ? `**${key}:** ${cell}` : cell;
36139
+ });
36140
+ return {
36141
+ type: 'paragraph',
36142
+ props: {
36143
+ content: pairs.join(' · '),
36144
+ size: 'medium',
36145
+ color: 'dark',
36146
+ bold: false,
36147
+ processLinks: true,
36148
+ allowPartialBold: true,
36149
+ },
36150
+ };
36151
+ });
36152
+ }
36153
+
35852
36154
  /**
35853
36155
  * `val-faq`
35854
36156
  *
@@ -35857,7 +36159,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
35857
36159
  * reusar la misma data (`FaqMetadata.categories`) para emitir el structured
35858
36160
  * data `FAQPage` (JSON-LD) y ganar rich results en buscadores.
35859
36161
  *
35860
- * Compone `val-accordion` (una por categoría) + `val-searchbar`.
36162
+ * Las respuestas (`FaqItem.answer`) son **Markdown** se parsean con
36163
+ * `parseMarkdownArticle` y se renderizan vía `val-article`. Soportan links,
36164
+ * listas, negritas, tablas, etc.
35861
36165
  *
35862
36166
  * @example
35863
36167
  * ```html
@@ -35867,12 +36171,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
35867
36171
  class FaqComponent {
35868
36172
  constructor() {
35869
36173
  this.props_ = signal({ categories: [] });
35870
- /** Término de búsqueda actual (lowercase). */
36174
+ /** Término de búsqueda actual. */
35871
36175
  this.query = signal('');
35872
36176
  /**
35873
- * Categorías visibles tras aplicar el filtro de búsqueda. Una categoría
35874
- * desaparece si ninguna de sus preguntas matchea. El match es sobre
35875
- * pregunta + respuesta, case-insensitive.
36177
+ * Cache de respuestas parseadas (Markdown ArticleMetadata). Se reconstruye
36178
+ * solo cuando cambian las categorías el parseo NO corre por keystroke.
36179
+ * Keyed por id de item (los ids son únicos en toda la FAQ).
36180
+ */
36181
+ this.parsedAnswers = computed(() => {
36182
+ const map = new Map();
36183
+ for (const cat of this.props_().categories ?? []) {
36184
+ for (const item of cat.items) {
36185
+ map.set(item.id, parseMarkdownArticle(item.answer));
36186
+ }
36187
+ }
36188
+ return map;
36189
+ });
36190
+ /**
36191
+ * Categorías visibles tras el filtro. Una categoría desaparece si ninguna
36192
+ * de sus preguntas matchea. Match sobre pregunta + respuesta (raw markdown),
36193
+ * case-insensitive.
35876
36194
  */
35877
36195
  this.visibleCategories = computed(() => {
35878
36196
  const q = this.query().trim().toLowerCase();
@@ -35901,17 +36219,9 @@ class FaqComponent {
35901
36219
  get props() {
35902
36220
  return this.props_();
35903
36221
  }
35904
- /** Mapea una categoría a `AccordionMetadata` para `val-accordion`. */
35905
- accordionFor(cat) {
35906
- return {
35907
- multiple: this.props_().multiple ?? false,
35908
- expandIconSlot: 'end',
35909
- items: cat.items.map(it => ({
35910
- value: it.id,
35911
- header: it.question,
35912
- content: it.answer,
35913
- })),
35914
- };
36222
+ /** Respuesta parseada de un item desde el cache. */
36223
+ answerOf(item) {
36224
+ return this.parsedAnswers().get(item.id) ?? { elements: [] };
35915
36225
  }
35916
36226
  onSearch(term) {
35917
36227
  this.query.set(term ?? '');
@@ -35932,23 +36242,38 @@ class FaqComponent {
35932
36242
  }
35933
36243
 
35934
36244
  @if (visibleCategories().length === 0) {
35935
- <p class="val-faq__empty">{{ props.noResultsText || 'Sin resultados' }}</p>
36245
+ <p class="val-faq__empty">
36246
+ {{ props.noResultsText || 'Sin resultados' }}
36247
+ </p>
35936
36248
  } @else {
35937
36249
  @for (cat of visibleCategories(); track cat.id) {
35938
36250
  <section class="val-faq__category">
35939
36251
  @if (showCategoryLabel()) {
35940
36252
  <h3 class="val-faq__category-label">{{ cat.label }}</h3>
35941
36253
  }
35942
- <val-accordion [props]="accordionFor(cat)" />
36254
+ <ion-accordion-group [multiple]="props.multiple ?? false">
36255
+ @for (item of cat.items; track item.id) {
36256
+ <ion-accordion [value]="item.id" class="val-faq__item">
36257
+ <ion-item slot="header" lines="none" class="val-faq__q">
36258
+ <ion-label class="val-faq__q-label">
36259
+ {{ item.question }}
36260
+ </ion-label>
36261
+ </ion-item>
36262
+ <div slot="content" class="val-faq__a">
36263
+ <val-article [props]="answerOf(item)" />
36264
+ </div>
36265
+ </ion-accordion>
36266
+ }
36267
+ </ion-accordion-group>
35943
36268
  </section>
35944
36269
  }
35945
36270
  }
35946
36271
  </div>
35947
- `, isInline: true, styles: [":host{display:block;width:100%}.val-faq__search{margin-bottom:16px}.val-faq__category+.val-faq__category{margin-top:24px}.val-faq__category-label{font-size:.8rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ion-color-medium, #92949c);margin:0 0 8px;padding-inline:4px}.val-faq__empty{text-align:center;padding:32px 16px;color:var(--ion-color-medium, #92949c);font-size:.95rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: AccordionComponent, selector: "val-accordion", inputs: ["preset", "props"], outputs: ["accordionChange"] }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["preset", "props"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }] }); }
36272
+ `, isInline: true, styles: [":host{display:block;width:100%}.val-faq__search{margin-bottom:16px}.val-faq__category+.val-faq__category{margin-top:24px}.val-faq__category-label{font-size:.8rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ion-color-medium, #92949c);margin:0 0 8px;padding-inline:4px}ion-accordion-group{display:flex;flex-direction:column;gap:8px}.val-faq__item{border-radius:12px;overflow:hidden;background:var(--ion-color-light, #f4f5f8)}.val-faq__q{--background: transparent;--min-height: 56px}.val-faq__q-label{font-weight:600;white-space:normal}.val-faq__a{padding:4px 16px 16px}.val-faq__empty{text-align:center;padding:32px 16px;color:var(--ion-color-medium, #92949c);font-size:.95rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonAccordion, selector: "ion-accordion", inputs: ["disabled", "mode", "readonly", "toggleIcon", "toggleIconSlot", "value"] }, { kind: "component", type: IonAccordionGroup, selector: "ion-accordion-group", inputs: ["animated", "disabled", "expand", "mode", "multiple", "readonly", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["preset", "props"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "component", type: ArticleComponent, selector: "val-article", inputs: ["props"] }] }); }
35948
36273
  }
35949
36274
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FaqComponent, decorators: [{
35950
36275
  type: Component,
35951
- args: [{ selector: 'val-faq', standalone: true, imports: [CommonModule, AccordionComponent, SearchbarComponent], template: `
36276
+ args: [{ selector: 'val-faq', standalone: true, imports: [CommonModule, IonAccordion, IonAccordionGroup, IonItem, IonLabel, SearchbarComponent, ArticleComponent], template: `
35952
36277
  <div class="val-faq">
35953
36278
  @if (props.searchable !== false) {
35954
36279
  <div class="val-faq__search">
@@ -35963,19 +36288,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
35963
36288
  }
35964
36289
 
35965
36290
  @if (visibleCategories().length === 0) {
35966
- <p class="val-faq__empty">{{ props.noResultsText || 'Sin resultados' }}</p>
36291
+ <p class="val-faq__empty">
36292
+ {{ props.noResultsText || 'Sin resultados' }}
36293
+ </p>
35967
36294
  } @else {
35968
36295
  @for (cat of visibleCategories(); track cat.id) {
35969
36296
  <section class="val-faq__category">
35970
36297
  @if (showCategoryLabel()) {
35971
36298
  <h3 class="val-faq__category-label">{{ cat.label }}</h3>
35972
36299
  }
35973
- <val-accordion [props]="accordionFor(cat)" />
36300
+ <ion-accordion-group [multiple]="props.multiple ?? false">
36301
+ @for (item of cat.items; track item.id) {
36302
+ <ion-accordion [value]="item.id" class="val-faq__item">
36303
+ <ion-item slot="header" lines="none" class="val-faq__q">
36304
+ <ion-label class="val-faq__q-label">
36305
+ {{ item.question }}
36306
+ </ion-label>
36307
+ </ion-item>
36308
+ <div slot="content" class="val-faq__a">
36309
+ <val-article [props]="answerOf(item)" />
36310
+ </div>
36311
+ </ion-accordion>
36312
+ }
36313
+ </ion-accordion-group>
35974
36314
  </section>
35975
36315
  }
35976
36316
  }
35977
36317
  </div>
35978
- `, styles: [":host{display:block;width:100%}.val-faq__search{margin-bottom:16px}.val-faq__category+.val-faq__category{margin-top:24px}.val-faq__category-label{font-size:.8rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ion-color-medium, #92949c);margin:0 0 8px;padding-inline:4px}.val-faq__empty{text-align:center;padding:32px 16px;color:var(--ion-color-medium, #92949c);font-size:.95rem}\n"] }]
36318
+ `, styles: [":host{display:block;width:100%}.val-faq__search{margin-bottom:16px}.val-faq__category+.val-faq__category{margin-top:24px}.val-faq__category-label{font-size:.8rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ion-color-medium, #92949c);margin:0 0 8px;padding-inline:4px}ion-accordion-group{display:flex;flex-direction:column;gap:8px}.val-faq__item{border-radius:12px;overflow:hidden;background:var(--ion-color-light, #f4f5f8)}.val-faq__q{--background: transparent;--min-height: 56px}.val-faq__q-label{font-weight:600;white-space:normal}.val-faq__a{padding:4px 16px 16px}.val-faq__empty{text-align:center;padding:32px 16px;color:var(--ion-color-medium, #92949c);font-size:.95rem}\n"] }]
35979
36319
  }], propDecorators: { props: [{
35980
36320
  type: Input
35981
36321
  }] } });
@@ -37555,308 +37895,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
37555
37895
  }]
37556
37896
  }] });
37557
37897
 
37558
- /** Built-in label sets for the three platform locales. */
37559
- const CALLOUT_LABELS = {
37560
- es: {
37561
- NOTE: 'Nota',
37562
- TIP: 'Tip',
37563
- INFO: 'Info',
37564
- IMPORTANT: 'Importante',
37565
- WARNING: 'Atención',
37566
- CAUTION: 'Precaución',
37567
- },
37568
- en: {
37569
- NOTE: 'Note',
37570
- TIP: 'Tip',
37571
- INFO: 'Info',
37572
- IMPORTANT: 'Important',
37573
- WARNING: 'Warning',
37574
- CAUTION: 'Caution',
37575
- },
37576
- pt: {
37577
- NOTE: 'Nota',
37578
- TIP: 'Dica',
37579
- INFO: 'Info',
37580
- IMPORTANT: 'Importante',
37581
- WARNING: 'Atenção',
37582
- CAUTION: 'Cuidado',
37583
- },
37584
- };
37585
- /**
37586
- * Per-callout-kind color (Ionic semantic color). Fixed across locales since
37587
- * red = danger no matter what language you speak.
37588
- */
37589
- const CALLOUT_COLORS = {
37590
- NOTE: 'primary',
37591
- TIP: 'success',
37592
- INFO: 'tertiary',
37593
- IMPORTANT: 'warning',
37594
- WARNING: 'warning',
37595
- CAUTION: 'danger',
37596
- };
37597
- const BOX_DRAWING = /[┌┐└┘├┤┬┴┼─│╔╗╚╝═║]/;
37598
- const CALLOUT_RE = /^>\s*\[!(NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT)\]\s*(.*)$/i;
37599
- const FENCE_RE = /^```([\w-]*)\s*$/;
37600
- const HEADING_RE = /^(#{1,6})\s+(.+)$/;
37601
- const SEPARATOR_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
37602
- const UNORDERED_RE = /^\s*[-*+]\s+(.+)$/;
37603
- const CHECKLIST_RE = /^\s*[-*+]\s+\[([ xX])\]\s+(.+)$/;
37604
- const ORDERED_RE = /^\s*\d+\.\s+(.+)$/;
37605
- const TABLE_DIVIDER_RE = /^\s*\|?[\s:|-]+\|?\s*$/;
37606
- const TABLE_ROW_RE = /^\s*\|(.+)\|\s*$/;
37607
- /**
37608
- * Pure Markdown → ArticleMetadata parser. No Angular deps — usable from Node scripts
37609
- * (build-time generation) and from the Angular `MarkdownArticleParserService` wrapper.
37610
- *
37611
- * Supported syntax:
37612
- * - Headings (#, ##, ### …) → title / subtitle
37613
- * - Paragraphs → paragraph (with `processLinks` + `allowPartialBold`)
37614
- * - Lists (-, *, 1.) → unordered / ordered list (- [ ] / - [x] for checklist)
37615
- * - Blockquotes (>) → quote, with GitHub callouts `> [!NOTE|TIP|INFO|WARNING|CAUTION|IMPORTANT]` mapped to notes
37616
- * - Code fences (```lang) → code
37617
- * - Box-drawing ASCII art (┌┐└┘│─) auto-wrapped as code
37618
- * - Tables → flattened into paragraphs with bold keys
37619
- * - Horizontal rules (---, ***, ___) → separator
37620
- */
37621
- function parseMarkdownArticle(markdown, options) {
37622
- const lines = normalize(markdown).split('\n');
37623
- const elements = [];
37624
- const labels = options?.calloutLabels ??
37625
- (options?.locale ? CALLOUT_LABELS[options.locale] : CALLOUT_LABELS.es);
37626
- let i = 0;
37627
- while (i < lines.length) {
37628
- const line = lines[i];
37629
- if (line.trim() === '') {
37630
- i++;
37631
- continue;
37632
- }
37633
- const fence = line.match(FENCE_RE);
37634
- if (fence) {
37635
- const lang = fence[1] || undefined;
37636
- const body = [];
37637
- i++;
37638
- while (i < lines.length && !FENCE_RE.test(lines[i])) {
37639
- body.push(lines[i]);
37640
- i++;
37641
- }
37642
- i++;
37643
- elements.push({
37644
- type: 'code',
37645
- props: { code: body.join('\n'), language: lang, theme: 'dark' },
37646
- });
37647
- continue;
37648
- }
37649
- if (BOX_DRAWING.test(line)) {
37650
- const body = [];
37651
- while (i < lines.length && (lines[i].trim() === '' || BOX_DRAWING.test(lines[i]))) {
37652
- body.push(lines[i]);
37653
- i++;
37654
- }
37655
- while (body.length && body[body.length - 1].trim() === '')
37656
- body.pop();
37657
- elements.push({
37658
- type: 'code',
37659
- props: { code: body.join('\n'), language: 'text', theme: 'dark' },
37660
- });
37661
- continue;
37662
- }
37663
- if (SEPARATOR_RE.test(line)) {
37664
- elements.push({ type: 'separator', props: { style: 'line' } });
37665
- i++;
37666
- continue;
37667
- }
37668
- const heading = line.match(HEADING_RE);
37669
- if (heading) {
37670
- elements.push(makeHeading(heading[1].length, heading[2].trim()));
37671
- i++;
37672
- continue;
37673
- }
37674
- if (line.startsWith('>')) {
37675
- const block = [];
37676
- while (i < lines.length && lines[i].startsWith('>')) {
37677
- block.push(lines[i]);
37678
- i++;
37679
- }
37680
- elements.push(makeQuoteOrCallout(block, labels));
37681
- continue;
37682
- }
37683
- if (TABLE_ROW_RE.test(line)) {
37684
- const rows = [];
37685
- while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
37686
- rows.push(lines[i]);
37687
- i++;
37688
- }
37689
- elements.push(...makeTable(rows));
37690
- continue;
37691
- }
37692
- if (CHECKLIST_RE.test(line) || UNORDERED_RE.test(line) || ORDERED_RE.test(line)) {
37693
- const listType = CHECKLIST_RE.test(line)
37694
- ? 'checklist'
37695
- : ORDERED_RE.test(line)
37696
- ? 'ordered'
37697
- : 'unordered';
37698
- const items = [];
37699
- while (i < lines.length && lines[i].trim() !== '') {
37700
- const cur = lines[i];
37701
- const check = cur.match(CHECKLIST_RE);
37702
- const unord = cur.match(UNORDERED_RE);
37703
- const ord = cur.match(ORDERED_RE);
37704
- if (check)
37705
- items.push({ text: check[2].trim() });
37706
- else if (ord && listType === 'ordered')
37707
- items.push({ text: ord[1].trim() });
37708
- else if (unord && listType !== 'ordered')
37709
- items.push({ text: unord[1].trim() });
37710
- else
37711
- break;
37712
- i++;
37713
- }
37714
- elements.push({ type: 'list', props: { items, listType } });
37715
- continue;
37716
- }
37717
- const paragraph = [];
37718
- while (i < lines.length && lines[i].trim() !== '' && !startsNewBlock(lines[i])) {
37719
- paragraph.push(lines[i]);
37720
- i++;
37721
- }
37722
- const text = paragraph.join(' ').trim();
37723
- if (text) {
37724
- elements.push({
37725
- type: 'paragraph',
37726
- props: {
37727
- content: text,
37728
- size: 'medium',
37729
- color: 'dark',
37730
- bold: false,
37731
- processLinks: true,
37732
- allowPartialBold: true,
37733
- },
37734
- });
37735
- }
37736
- }
37737
- // Strip parser-only options before merging into ArticleMetadata
37738
- const { locale: _l, calloutLabels: _c, ...metadataOverrides } = options ?? {};
37739
- return {
37740
- elements,
37741
- maxWidth: '900px',
37742
- centered: true,
37743
- theme: 'auto',
37744
- ...metadataOverrides,
37745
- };
37746
- }
37747
- function normalize(md) {
37748
- return md.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
37749
- }
37750
- function startsNewBlock(line) {
37751
- return (HEADING_RE.test(line) ||
37752
- FENCE_RE.test(line) ||
37753
- SEPARATOR_RE.test(line) ||
37754
- TABLE_ROW_RE.test(line) ||
37755
- UNORDERED_RE.test(line) ||
37756
- ORDERED_RE.test(line) ||
37757
- CHECKLIST_RE.test(line) ||
37758
- line.startsWith('>') ||
37759
- BOX_DRAWING.test(line));
37760
- }
37761
- function makeHeading(level, content) {
37762
- if (level === 1) {
37763
- return {
37764
- type: 'title',
37765
- props: { content, size: 'xlarge', color: 'dark', bold: true },
37766
- };
37767
- }
37768
- const size = level === 2 ? 'large' : level === 3 ? 'medium' : 'small';
37769
- return {
37770
- type: 'subtitle',
37771
- props: { content, size, color: 'dark', bold: true },
37772
- };
37773
- }
37774
- function makeQuoteOrCallout(block, labels) {
37775
- const first = block[0];
37776
- const callout = first.match(CALLOUT_RE);
37777
- const lines = block.map(l => l.replace(/^>\s?/, ''));
37778
- if (callout) {
37779
- const type = callout[1].toUpperCase();
37780
- const firstLineRest = callout[2] || '';
37781
- const rest = lines.slice(1).join(' ').trim();
37782
- const text = [firstLineRest, rest].filter(Boolean).join(' ').trim();
37783
- return makeNote(type, text, labels);
37784
- }
37785
- const text = lines.join(' ').trim();
37786
- return {
37787
- type: 'quote',
37788
- props: {
37789
- content: text,
37790
- size: 'medium',
37791
- color: 'medium',
37792
- bold: false,
37793
- showQuoteMark: true,
37794
- alignment: 'left',
37795
- },
37796
- };
37797
- }
37798
- function makeNote(kind, text, labels) {
37799
- return {
37800
- type: 'note',
37801
- props: {
37802
- text,
37803
- prefix: `${labels[kind]}:`,
37804
- color: CALLOUT_COLORS[kind],
37805
- textColor: 'dark',
37806
- size: 'medium',
37807
- rounded: true,
37808
- allowPartialBold: true,
37809
- processLinks: true,
37810
- },
37811
- };
37812
- }
37813
- function makeTable(rows) {
37814
- const parsed = rows
37815
- .filter(r => !TABLE_DIVIDER_RE.test(r))
37816
- .map(r => r
37817
- .trim()
37818
- .replace(/^\|/, '')
37819
- .replace(/\|$/, '')
37820
- .split('|')
37821
- .map(c => c.trim()));
37822
- if (parsed.length === 0)
37823
- return [];
37824
- const header = parsed[0];
37825
- const dataRows = parsed.slice(1);
37826
- if (dataRows.length === 0) {
37827
- return [
37828
- {
37829
- type: 'paragraph',
37830
- props: {
37831
- content: header.join(' · '),
37832
- size: 'medium',
37833
- color: 'dark',
37834
- bold: false,
37835
- processLinks: true,
37836
- allowPartialBold: true,
37837
- },
37838
- },
37839
- ];
37840
- }
37841
- return dataRows.map(row => {
37842
- const pairs = row.map((cell, idx) => {
37843
- const key = header[idx] ?? '';
37844
- return key ? `**${key}:** ${cell}` : cell;
37845
- });
37846
- return {
37847
- type: 'paragraph',
37848
- props: {
37849
- content: pairs.join(' · '),
37850
- size: 'medium',
37851
- color: 'dark',
37852
- bold: false,
37853
- processLinks: true,
37854
- allowPartialBold: true,
37855
- },
37856
- };
37857
- });
37858
- }
37859
-
37860
37898
  /**
37861
37899
  * Angular service wrapper for the pure {@link parseMarkdownArticle} function.
37862
37900
  * Provided in root so it can be injected anywhere. The actual parsing logic lives in
@@ -45600,7 +45638,9 @@ const VALTECH_COMPANY_LINKS = {
45600
45638
  ],
45601
45639
  support: [
45602
45640
  { key: 'contactSupport', url: '/contact', kind: 'support', external: false },
45603
- { key: 'faq', url: '/legal/faq', kind: 'site', external: false },
45641
+ // FAQ página dedicada `/faq` (kind 'site': vive en el sitio corporativo;
45642
+ // en apps satellite se reescribe al main site vía LegalLinkService).
45643
+ { key: 'faq', url: '/faq', kind: 'site', external: false },
45604
45644
  { key: 'feedback', url: '/feedback', kind: 'support', external: false },
45605
45645
  ],
45606
45646
  };