@wix/himalaya-cli 0.999.0 → 0.1001.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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +39 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validator.d.ts +50 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +46 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +46 -0
- package/dist/cli.mjs +205 -22
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/component-reference.json +2 -2
- package/guides/component-reference.md +32 -0
- package/package.json +2 -2
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/community-club-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/events-meetups-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/owner-lite/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/owner-lite/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/restaurant-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/retail-store-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +11 -31
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-model.ts +31 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/aria-widget-view.ts +48 -13
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type ConversationSummary,
|
|
43
43
|
} from "@himalaya/wix-genie-chat";
|
|
44
44
|
import { proseOfParts, scanEnvelopes, stripHidden } from "./aria-widgets.js";
|
|
45
|
-
import { isWebUrl } from "./aria-widget-model.js";
|
|
45
|
+
import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
46
46
|
import { NO_WIDGET, activeWidgetOf, asyncAdvanced, type SubtreeSlot, type WidgetOptionRow, type WidgetView }
|
|
47
47
|
from "./aria-widget-view.js";
|
|
48
48
|
import { substituteSelection } from "./aria-widget-model.js";
|
|
@@ -528,36 +528,16 @@ export function disclosedLabel(link: { label: string; url: string }): string {
|
|
|
528
528
|
return `${link.label} (${host || UNKNOWN_HOST_LABEL})`;
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// inline code `x` -> x
|
|
542
|
-
t = t.replace(/`([^`]+)`/g, "$1");
|
|
543
|
-
// images  -> alt ; links [text](url) -> text
|
|
544
|
-
t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
545
|
-
t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
546
|
-
// bold/italic **x** *x* __x__ _x_ -> x
|
|
547
|
-
t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
|
|
548
|
-
t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
|
|
549
|
-
// ATX headings: '### Title' -> 'Title'
|
|
550
|
-
t = t.replace(/^#{1,6}\s+/gm, "");
|
|
551
|
-
// blockquotes '> ' -> ''
|
|
552
|
-
t = t.replace(/^>\s?/gm, "");
|
|
553
|
-
// unordered list markers '- ' / '* ' / '+ ' -> bullet
|
|
554
|
-
t = t.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
555
|
-
// horizontal rules
|
|
556
|
-
t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
|
|
557
|
-
// collapse 3+ newlines to a double
|
|
558
|
-
t = t.replace(/\n{3,}/g, "\n\n");
|
|
559
|
-
return t.trim();
|
|
560
|
-
}
|
|
531
|
+
/** `stripMarkdown` used to live here. It moved to `aria-widget-model.ts` —
|
|
532
|
+
* which imports NOTHING, so the widget view can reach it without closing a
|
|
533
|
+
* cycle back through this file. Re-exported rather than relocated silently,
|
|
534
|
+
* so every consumer and test that names `aria-chat.ts` still resolves.
|
|
535
|
+
*
|
|
536
|
+
* Imported AND re-exported, and that is not redundant: `export { x } from "y"`
|
|
537
|
+
* does not bind `x` in this module's scope, and `toBubbles` below calls it —
|
|
538
|
+
* written that way the identifier is unbound at runtime and the whole turn
|
|
539
|
+
* machinery dies. (Measured: 20 tests, every one of them about polling.) */
|
|
540
|
+
export { stripMarkdown };
|
|
561
541
|
|
|
562
542
|
/** A greeting or optimistic bubble: no links, all slots defaulted. */
|
|
563
543
|
export function plainBubble(id: string, role: string, who: string, content: string): AriaBubble {
|
|
@@ -167,6 +167,37 @@ export function objectAt(v: unknown): Record<string, unknown> | null {
|
|
|
167
167
|
return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/** Aria replies come back as markdown. There is no native markdown renderer
|
|
171
|
+
* (adding one is a CI-blocking parity change across both renderers), so
|
|
172
|
+
* normalize to clean plain text on-device: drop code fences, unwrap
|
|
173
|
+
* links/emphasis/headings, turn bullet markers into '•', collapse blank runs.
|
|
174
|
+
* Safe and idempotent. */
|
|
175
|
+
export function stripMarkdown(src: string): string {
|
|
176
|
+
if (!src) return "";
|
|
177
|
+
let t = src;
|
|
178
|
+
// fenced code blocks -> keep inner text, drop the ``` fences
|
|
179
|
+
t = t.replace(/```[a-zA-Z0-9]*\n?/g, "").replace(/```/g, "");
|
|
180
|
+
// inline code `x` -> x
|
|
181
|
+
t = t.replace(/`([^`]+)`/g, "$1");
|
|
182
|
+
// images  -> alt ; links [text](url) -> text
|
|
183
|
+
t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
184
|
+
t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
185
|
+
// bold/italic **x** *x* __x__ _x_ -> x
|
|
186
|
+
t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
|
|
187
|
+
t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
|
|
188
|
+
// ATX headings: '### Title' -> 'Title'
|
|
189
|
+
t = t.replace(/^#{1,6}\s+/gm, "");
|
|
190
|
+
// blockquotes '> ' -> ''
|
|
191
|
+
t = t.replace(/^>\s?/gm, "");
|
|
192
|
+
// unordered list markers '- ' / '* ' / '+ ' -> bullet
|
|
193
|
+
t = t.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
194
|
+
// horizontal rules
|
|
195
|
+
t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
|
|
196
|
+
// collapse 3+ newlines to a double
|
|
197
|
+
t = t.replace(/\n{3,}/g, "\n\n");
|
|
198
|
+
return t.trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
170
201
|
/** A display string, whether the wire wrote it bare or wrapped.
|
|
171
202
|
*
|
|
172
203
|
* QUESTIONNAIRE writes every label as `{ text: "…" }` — question titles,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// the transcript bubbles already use for their three link slots.
|
|
14
14
|
|
|
15
15
|
import type { NormalizedWidget, WidgetKind } from "./aria-widget-model.js";
|
|
16
|
+
import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
16
17
|
import { composeGenericCard } from "./aria-generic-widget.js";
|
|
17
18
|
import { normalizeWidget } from "./aria-widget-kinds.js";
|
|
18
19
|
import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
|
|
@@ -333,7 +334,22 @@ export const NO_WIDGET: WidgetView = {
|
|
|
333
334
|
*
|
|
334
335
|
* A button with no url is left alone: there is no destination to disclose. */
|
|
335
336
|
function discloseButton(disclose: Disclose, label: string, url: string): string {
|
|
336
|
-
|
|
337
|
+
const text = stripMarkdown(label);
|
|
338
|
+
return url ? disclose({ label: text, url }) : text;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** A destination this region is willing to EXPOSE.
|
|
342
|
+
*
|
|
343
|
+
* `isWebUrl` guards the sink in `aria-chat.ts`, which stops a non-web scheme
|
|
344
|
+
* being opened. It does not stop it being PAINTED: a button whose url is
|
|
345
|
+
* `tel:` still has a label and a non-empty url, so it renders, and the owner
|
|
346
|
+
* taps a control that silently does nothing. Asking the same question here
|
|
347
|
+
* collapses the button instead — which is this screen's own rule, and the
|
|
348
|
+
* reason the two checks are not redundant. Same predicate, so they cannot
|
|
349
|
+
* drift into disagreeing about what a web url is. */
|
|
350
|
+
function webOnly(url: string | undefined): string {
|
|
351
|
+
const t = (url ?? "").trim();
|
|
352
|
+
return isWebUrl(t) ? t : "";
|
|
337
353
|
}
|
|
338
354
|
|
|
339
355
|
/** One option, flattened. `selected` is passed in because it is worker state
|
|
@@ -451,7 +467,12 @@ export function viewOf(
|
|
|
451
467
|
allowPicture: (url: string) => boolean = (url) => isDisclosedSame(disclose, url),
|
|
452
468
|
): WidgetView {
|
|
453
469
|
const extra = w.extra as Record<string, unknown>;
|
|
454
|
-
|
|
470
|
+
// Markdown, flattened. Aria replies are markdown off the wire and there is no
|
|
471
|
+
// native markdown renderer, so a marker reaching a `Text` is painted
|
|
472
|
+
// literally — the catalogue sends `Upgrade to **Business Basic**`, and the
|
|
473
|
+
// card showed the asterisks while the bubble beside it did not. Same
|
|
474
|
+
// `stripMarkdown` the transcript uses, so the two cannot disagree.
|
|
475
|
+
const title = stripMarkdown(typeof extra.title === "string" ? extra.title : "");
|
|
455
476
|
// Through the SAME media check as an option's picture. Counting `imageUrl`
|
|
456
477
|
// towards renderability made an image-only VISUAL_DISPLAY reachable, and the
|
|
457
478
|
// descriptor binds this path straight into an `Image` — so an off-Wix url
|
|
@@ -464,13 +485,13 @@ export function viewOf(
|
|
|
464
485
|
|
|
465
486
|
const sections = Array.isArray(extra.sections) ? (extra.sections as Record<string, unknown>[]) : [];
|
|
466
487
|
const rows: WidgetRow[] = sections.map((sec, i) => {
|
|
467
|
-
const subtitle = typeof sec.subtitle === "string" ? sec.subtitle : "";
|
|
488
|
+
const subtitle = stripMarkdown(typeof sec.subtitle === "string" ? sec.subtitle : "");
|
|
468
489
|
return {
|
|
469
490
|
// A stable id per row. Index-based on purpose: a section has no id of its
|
|
470
491
|
// own, and the list is rebuilt wholesale on every tick, so there is
|
|
471
492
|
// nothing for a content-derived key to preserve.
|
|
472
493
|
id: `s${i}`,
|
|
473
|
-
title: typeof sec.title === "string" ? sec.title : "",
|
|
494
|
+
title: stripMarkdown(typeof sec.title === "string" ? sec.title : ""),
|
|
474
495
|
subtitle,
|
|
475
496
|
subtitleHidden: !subtitle,
|
|
476
497
|
};
|
|
@@ -540,8 +561,11 @@ export function viewOf(
|
|
|
540
561
|
|
|
541
562
|
const providerKey = typeof extra.providerKey === "string" ? extra.providerKey : "";
|
|
542
563
|
|
|
543
|
-
const primaryUrl = w.primary?.externalUrl
|
|
544
|
-
const secondaryUrl = w.secondary?.externalUrl
|
|
564
|
+
const primaryUrl = webOnly(w.primary?.externalUrl);
|
|
565
|
+
const secondaryUrl = webOnly(w.secondary?.externalUrl);
|
|
566
|
+
const prose = stripMarkdown(w.text);
|
|
567
|
+
const bottom = stripMarkdown(w.bottomText);
|
|
568
|
+
const badgeText = stripMarkdown(w.badge);
|
|
545
569
|
// An UNKNOWN kind is the one case where a MISSING `respondText` must not
|
|
546
570
|
// make a control inert.
|
|
547
571
|
//
|
|
@@ -563,6 +587,15 @@ export function viewOf(
|
|
|
563
587
|
|| (generic ? (w.secondary?.label ?? "") : "");
|
|
564
588
|
const primaryLabel = w.primary ? discloseButton(disclose, w.primary.label, primaryUrl) : "";
|
|
565
589
|
const secondaryLabel = w.secondary ? discloseButton(disclose, w.secondary.label, secondaryUrl) : "";
|
|
590
|
+
// Whether each footer button is actually SHOWN — a label to read AND
|
|
591
|
+
// something to do (a web destination, or text to send). Defined once and
|
|
592
|
+
// used twice, by the `*Hidden` flags and by `renderable` below, because the
|
|
593
|
+
// two disagreeing is exactly the blank-card bug: `renderable` counted a bare
|
|
594
|
+
// label, so a widget whose only content was a button `webOnly` had just
|
|
595
|
+
// stripped (a `tel:` link with no `respondText`) stayed visible as an empty
|
|
596
|
+
// card with nothing inside it.
|
|
597
|
+
const primaryShown = !!(primaryLabel && (primaryUrl || primaryRespondText));
|
|
598
|
+
const secondaryShown = !!(secondaryLabel && (secondaryUrl || secondaryRespondText));
|
|
566
599
|
|
|
567
600
|
// Hoisted out of the object literal below so `renderable` can see it. It
|
|
568
601
|
// was computed inline, so naming it in the guard was a ReferenceError that
|
|
@@ -587,8 +620,10 @@ export function viewOf(
|
|
|
587
620
|
// though they had something to show. Hiding a widget that has content is a
|
|
588
621
|
// worse failure than the blank card this guard exists to prevent, so the
|
|
589
622
|
// list has to match what the descriptor renders, not what I remembered.
|
|
590
|
-
|
|
591
|
-
|
|
623
|
+
// The STRIPPED prose, not the raw fields: those are what the card draws, and
|
|
624
|
+
// a reply of pure markup (`***`, an image-only ``) strips to nothing.
|
|
625
|
+
const renderable = !!(prose || bottom || badgeText || title || imageUrl
|
|
626
|
+
|| description || options.length || primaryShown || secondaryShown
|
|
592
627
|
|| sections.length || slices.length || steps.length || questions.length
|
|
593
628
|
|| asyncMessage || treeJson);
|
|
594
629
|
|
|
@@ -651,10 +686,10 @@ export function viewOf(
|
|
|
651
686
|
Object.entries(msgs).filter((kv): kv is [string, string] => typeof kv[1] === "string"),
|
|
652
687
|
),
|
|
653
688
|
submitting: false,
|
|
654
|
-
text:
|
|
689
|
+
text: prose, textHidden: !prose,
|
|
655
690
|
title, titleHidden: !title,
|
|
656
|
-
bottomText:
|
|
657
|
-
badge:
|
|
691
|
+
bottomText: bottom, bottomTextHidden: !bottom,
|
|
692
|
+
badge: badgeText, badgeHidden: !badgeText,
|
|
658
693
|
imageUrl, imageHidden: !imageUrl,
|
|
659
694
|
// Shown only when there is a LABEL to read and a DESTINATION to go to.
|
|
660
695
|
//
|
|
@@ -671,9 +706,9 @@ export function viewOf(
|
|
|
671
706
|
// #2605 hid a button that had nothing to DO. Now that `respondText` can be
|
|
672
707
|
// sent, having something to SAY counts too — which is the `&& url` that
|
|
673
708
|
// issue said this one would remove.
|
|
674
|
-
primaryLabel, primaryHidden: !
|
|
709
|
+
primaryLabel, primaryHidden: !primaryShown, primaryUrl,
|
|
675
710
|
primaryRespondText,
|
|
676
|
-
secondaryLabel, secondaryHidden: !
|
|
711
|
+
secondaryLabel, secondaryHidden: !secondaryShown, secondaryUrl,
|
|
677
712
|
secondaryRespondText,
|
|
678
713
|
rows,
|
|
679
714
|
};
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type ConversationSummary,
|
|
43
43
|
} from "@himalaya/wix-genie-chat";
|
|
44
44
|
import { proseOfParts, scanEnvelopes, stripHidden } from "./aria-widgets.js";
|
|
45
|
-
import { isWebUrl } from "./aria-widget-model.js";
|
|
45
|
+
import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
46
46
|
import { NO_WIDGET, activeWidgetOf, asyncAdvanced, type SubtreeSlot, type WidgetOptionRow, type WidgetView }
|
|
47
47
|
from "./aria-widget-view.js";
|
|
48
48
|
import { substituteSelection } from "./aria-widget-model.js";
|
|
@@ -528,36 +528,16 @@ export function disclosedLabel(link: { label: string; url: string }): string {
|
|
|
528
528
|
return `${link.label} (${host || UNKNOWN_HOST_LABEL})`;
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// inline code `x` -> x
|
|
542
|
-
t = t.replace(/`([^`]+)`/g, "$1");
|
|
543
|
-
// images  -> alt ; links [text](url) -> text
|
|
544
|
-
t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
545
|
-
t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
546
|
-
// bold/italic **x** *x* __x__ _x_ -> x
|
|
547
|
-
t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
|
|
548
|
-
t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
|
|
549
|
-
// ATX headings: '### Title' -> 'Title'
|
|
550
|
-
t = t.replace(/^#{1,6}\s+/gm, "");
|
|
551
|
-
// blockquotes '> ' -> ''
|
|
552
|
-
t = t.replace(/^>\s?/gm, "");
|
|
553
|
-
// unordered list markers '- ' / '* ' / '+ ' -> bullet
|
|
554
|
-
t = t.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
555
|
-
// horizontal rules
|
|
556
|
-
t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
|
|
557
|
-
// collapse 3+ newlines to a double
|
|
558
|
-
t = t.replace(/\n{3,}/g, "\n\n");
|
|
559
|
-
return t.trim();
|
|
560
|
-
}
|
|
531
|
+
/** `stripMarkdown` used to live here. It moved to `aria-widget-model.ts` —
|
|
532
|
+
* which imports NOTHING, so the widget view can reach it without closing a
|
|
533
|
+
* cycle back through this file. Re-exported rather than relocated silently,
|
|
534
|
+
* so every consumer and test that names `aria-chat.ts` still resolves.
|
|
535
|
+
*
|
|
536
|
+
* Imported AND re-exported, and that is not redundant: `export { x } from "y"`
|
|
537
|
+
* does not bind `x` in this module's scope, and `toBubbles` below calls it —
|
|
538
|
+
* written that way the identifier is unbound at runtime and the whole turn
|
|
539
|
+
* machinery dies. (Measured: 20 tests, every one of them about polling.) */
|
|
540
|
+
export { stripMarkdown };
|
|
561
541
|
|
|
562
542
|
/** A greeting or optimistic bubble: no links, all slots defaulted. */
|
|
563
543
|
export function plainBubble(id: string, role: string, who: string, content: string): AriaBubble {
|
|
@@ -167,6 +167,37 @@ export function objectAt(v: unknown): Record<string, unknown> | null {
|
|
|
167
167
|
return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/** Aria replies come back as markdown. There is no native markdown renderer
|
|
171
|
+
* (adding one is a CI-blocking parity change across both renderers), so
|
|
172
|
+
* normalize to clean plain text on-device: drop code fences, unwrap
|
|
173
|
+
* links/emphasis/headings, turn bullet markers into '•', collapse blank runs.
|
|
174
|
+
* Safe and idempotent. */
|
|
175
|
+
export function stripMarkdown(src: string): string {
|
|
176
|
+
if (!src) return "";
|
|
177
|
+
let t = src;
|
|
178
|
+
// fenced code blocks -> keep inner text, drop the ``` fences
|
|
179
|
+
t = t.replace(/```[a-zA-Z0-9]*\n?/g, "").replace(/```/g, "");
|
|
180
|
+
// inline code `x` -> x
|
|
181
|
+
t = t.replace(/`([^`]+)`/g, "$1");
|
|
182
|
+
// images  -> alt ; links [text](url) -> text
|
|
183
|
+
t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
184
|
+
t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
185
|
+
// bold/italic **x** *x* __x__ _x_ -> x
|
|
186
|
+
t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
|
|
187
|
+
t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
|
|
188
|
+
// ATX headings: '### Title' -> 'Title'
|
|
189
|
+
t = t.replace(/^#{1,6}\s+/gm, "");
|
|
190
|
+
// blockquotes '> ' -> ''
|
|
191
|
+
t = t.replace(/^>\s?/gm, "");
|
|
192
|
+
// unordered list markers '- ' / '* ' / '+ ' -> bullet
|
|
193
|
+
t = t.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
194
|
+
// horizontal rules
|
|
195
|
+
t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
|
|
196
|
+
// collapse 3+ newlines to a double
|
|
197
|
+
t = t.replace(/\n{3,}/g, "\n\n");
|
|
198
|
+
return t.trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
170
201
|
/** A display string, whether the wire wrote it bare or wrapped.
|
|
171
202
|
*
|
|
172
203
|
* QUESTIONNAIRE writes every label as `{ text: "…" }` — question titles,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// the transcript bubbles already use for their three link slots.
|
|
14
14
|
|
|
15
15
|
import type { NormalizedWidget, WidgetKind } from "./aria-widget-model.js";
|
|
16
|
+
import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
16
17
|
import { composeGenericCard } from "./aria-generic-widget.js";
|
|
17
18
|
import { normalizeWidget } from "./aria-widget-kinds.js";
|
|
18
19
|
import { scanEnvelopes, widgetsOfParts } from "./aria-widgets.js";
|
|
@@ -333,7 +334,22 @@ export const NO_WIDGET: WidgetView = {
|
|
|
333
334
|
*
|
|
334
335
|
* A button with no url is left alone: there is no destination to disclose. */
|
|
335
336
|
function discloseButton(disclose: Disclose, label: string, url: string): string {
|
|
336
|
-
|
|
337
|
+
const text = stripMarkdown(label);
|
|
338
|
+
return url ? disclose({ label: text, url }) : text;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** A destination this region is willing to EXPOSE.
|
|
342
|
+
*
|
|
343
|
+
* `isWebUrl` guards the sink in `aria-chat.ts`, which stops a non-web scheme
|
|
344
|
+
* being opened. It does not stop it being PAINTED: a button whose url is
|
|
345
|
+
* `tel:` still has a label and a non-empty url, so it renders, and the owner
|
|
346
|
+
* taps a control that silently does nothing. Asking the same question here
|
|
347
|
+
* collapses the button instead — which is this screen's own rule, and the
|
|
348
|
+
* reason the two checks are not redundant. Same predicate, so they cannot
|
|
349
|
+
* drift into disagreeing about what a web url is. */
|
|
350
|
+
function webOnly(url: string | undefined): string {
|
|
351
|
+
const t = (url ?? "").trim();
|
|
352
|
+
return isWebUrl(t) ? t : "";
|
|
337
353
|
}
|
|
338
354
|
|
|
339
355
|
/** One option, flattened. `selected` is passed in because it is worker state
|
|
@@ -451,7 +467,12 @@ export function viewOf(
|
|
|
451
467
|
allowPicture: (url: string) => boolean = (url) => isDisclosedSame(disclose, url),
|
|
452
468
|
): WidgetView {
|
|
453
469
|
const extra = w.extra as Record<string, unknown>;
|
|
454
|
-
|
|
470
|
+
// Markdown, flattened. Aria replies are markdown off the wire and there is no
|
|
471
|
+
// native markdown renderer, so a marker reaching a `Text` is painted
|
|
472
|
+
// literally — the catalogue sends `Upgrade to **Business Basic**`, and the
|
|
473
|
+
// card showed the asterisks while the bubble beside it did not. Same
|
|
474
|
+
// `stripMarkdown` the transcript uses, so the two cannot disagree.
|
|
475
|
+
const title = stripMarkdown(typeof extra.title === "string" ? extra.title : "");
|
|
455
476
|
// Through the SAME media check as an option's picture. Counting `imageUrl`
|
|
456
477
|
// towards renderability made an image-only VISUAL_DISPLAY reachable, and the
|
|
457
478
|
// descriptor binds this path straight into an `Image` — so an off-Wix url
|
|
@@ -464,13 +485,13 @@ export function viewOf(
|
|
|
464
485
|
|
|
465
486
|
const sections = Array.isArray(extra.sections) ? (extra.sections as Record<string, unknown>[]) : [];
|
|
466
487
|
const rows: WidgetRow[] = sections.map((sec, i) => {
|
|
467
|
-
const subtitle = typeof sec.subtitle === "string" ? sec.subtitle : "";
|
|
488
|
+
const subtitle = stripMarkdown(typeof sec.subtitle === "string" ? sec.subtitle : "");
|
|
468
489
|
return {
|
|
469
490
|
// A stable id per row. Index-based on purpose: a section has no id of its
|
|
470
491
|
// own, and the list is rebuilt wholesale on every tick, so there is
|
|
471
492
|
// nothing for a content-derived key to preserve.
|
|
472
493
|
id: `s${i}`,
|
|
473
|
-
title: typeof sec.title === "string" ? sec.title : "",
|
|
494
|
+
title: stripMarkdown(typeof sec.title === "string" ? sec.title : ""),
|
|
474
495
|
subtitle,
|
|
475
496
|
subtitleHidden: !subtitle,
|
|
476
497
|
};
|
|
@@ -540,8 +561,11 @@ export function viewOf(
|
|
|
540
561
|
|
|
541
562
|
const providerKey = typeof extra.providerKey === "string" ? extra.providerKey : "";
|
|
542
563
|
|
|
543
|
-
const primaryUrl = w.primary?.externalUrl
|
|
544
|
-
const secondaryUrl = w.secondary?.externalUrl
|
|
564
|
+
const primaryUrl = webOnly(w.primary?.externalUrl);
|
|
565
|
+
const secondaryUrl = webOnly(w.secondary?.externalUrl);
|
|
566
|
+
const prose = stripMarkdown(w.text);
|
|
567
|
+
const bottom = stripMarkdown(w.bottomText);
|
|
568
|
+
const badgeText = stripMarkdown(w.badge);
|
|
545
569
|
// An UNKNOWN kind is the one case where a MISSING `respondText` must not
|
|
546
570
|
// make a control inert.
|
|
547
571
|
//
|
|
@@ -563,6 +587,15 @@ export function viewOf(
|
|
|
563
587
|
|| (generic ? (w.secondary?.label ?? "") : "");
|
|
564
588
|
const primaryLabel = w.primary ? discloseButton(disclose, w.primary.label, primaryUrl) : "";
|
|
565
589
|
const secondaryLabel = w.secondary ? discloseButton(disclose, w.secondary.label, secondaryUrl) : "";
|
|
590
|
+
// Whether each footer button is actually SHOWN — a label to read AND
|
|
591
|
+
// something to do (a web destination, or text to send). Defined once and
|
|
592
|
+
// used twice, by the `*Hidden` flags and by `renderable` below, because the
|
|
593
|
+
// two disagreeing is exactly the blank-card bug: `renderable` counted a bare
|
|
594
|
+
// label, so a widget whose only content was a button `webOnly` had just
|
|
595
|
+
// stripped (a `tel:` link with no `respondText`) stayed visible as an empty
|
|
596
|
+
// card with nothing inside it.
|
|
597
|
+
const primaryShown = !!(primaryLabel && (primaryUrl || primaryRespondText));
|
|
598
|
+
const secondaryShown = !!(secondaryLabel && (secondaryUrl || secondaryRespondText));
|
|
566
599
|
|
|
567
600
|
// Hoisted out of the object literal below so `renderable` can see it. It
|
|
568
601
|
// was computed inline, so naming it in the guard was a ReferenceError that
|
|
@@ -587,8 +620,10 @@ export function viewOf(
|
|
|
587
620
|
// though they had something to show. Hiding a widget that has content is a
|
|
588
621
|
// worse failure than the blank card this guard exists to prevent, so the
|
|
589
622
|
// list has to match what the descriptor renders, not what I remembered.
|
|
590
|
-
|
|
591
|
-
|
|
623
|
+
// The STRIPPED prose, not the raw fields: those are what the card draws, and
|
|
624
|
+
// a reply of pure markup (`***`, an image-only ``) strips to nothing.
|
|
625
|
+
const renderable = !!(prose || bottom || badgeText || title || imageUrl
|
|
626
|
+
|| description || options.length || primaryShown || secondaryShown
|
|
592
627
|
|| sections.length || slices.length || steps.length || questions.length
|
|
593
628
|
|| asyncMessage || treeJson);
|
|
594
629
|
|
|
@@ -651,10 +686,10 @@ export function viewOf(
|
|
|
651
686
|
Object.entries(msgs).filter((kv): kv is [string, string] => typeof kv[1] === "string"),
|
|
652
687
|
),
|
|
653
688
|
submitting: false,
|
|
654
|
-
text:
|
|
689
|
+
text: prose, textHidden: !prose,
|
|
655
690
|
title, titleHidden: !title,
|
|
656
|
-
bottomText:
|
|
657
|
-
badge:
|
|
691
|
+
bottomText: bottom, bottomTextHidden: !bottom,
|
|
692
|
+
badge: badgeText, badgeHidden: !badgeText,
|
|
658
693
|
imageUrl, imageHidden: !imageUrl,
|
|
659
694
|
// Shown only when there is a LABEL to read and a DESTINATION to go to.
|
|
660
695
|
//
|
|
@@ -671,9 +706,9 @@ export function viewOf(
|
|
|
671
706
|
// #2605 hid a button that had nothing to DO. Now that `respondText` can be
|
|
672
707
|
// sent, having something to SAY counts too — which is the `&& url` that
|
|
673
708
|
// issue said this one would remove.
|
|
674
|
-
primaryLabel, primaryHidden: !
|
|
709
|
+
primaryLabel, primaryHidden: !primaryShown, primaryUrl,
|
|
675
710
|
primaryRespondText,
|
|
676
|
-
secondaryLabel, secondaryHidden: !
|
|
711
|
+
secondaryLabel, secondaryHidden: !secondaryShown, secondaryUrl,
|
|
677
712
|
secondaryRespondText,
|
|
678
713
|
rows,
|
|
679
714
|
};
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type ConversationSummary,
|
|
43
43
|
} from "@himalaya/wix-genie-chat";
|
|
44
44
|
import { proseOfParts, scanEnvelopes, stripHidden } from "./aria-widgets.js";
|
|
45
|
-
import { isWebUrl } from "./aria-widget-model.js";
|
|
45
|
+
import { isWebUrl, stripMarkdown } from "./aria-widget-model.js";
|
|
46
46
|
import { NO_WIDGET, activeWidgetOf, asyncAdvanced, type SubtreeSlot, type WidgetOptionRow, type WidgetView }
|
|
47
47
|
from "./aria-widget-view.js";
|
|
48
48
|
import { substituteSelection } from "./aria-widget-model.js";
|
|
@@ -528,36 +528,16 @@ export function disclosedLabel(link: { label: string; url: string }): string {
|
|
|
528
528
|
return `${link.label} (${host || UNKNOWN_HOST_LABEL})`;
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// inline code `x` -> x
|
|
542
|
-
t = t.replace(/`([^`]+)`/g, "$1");
|
|
543
|
-
// images  -> alt ; links [text](url) -> text
|
|
544
|
-
t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
545
|
-
t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
546
|
-
// bold/italic **x** *x* __x__ _x_ -> x
|
|
547
|
-
t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
|
|
548
|
-
t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
|
|
549
|
-
// ATX headings: '### Title' -> 'Title'
|
|
550
|
-
t = t.replace(/^#{1,6}\s+/gm, "");
|
|
551
|
-
// blockquotes '> ' -> ''
|
|
552
|
-
t = t.replace(/^>\s?/gm, "");
|
|
553
|
-
// unordered list markers '- ' / '* ' / '+ ' -> bullet
|
|
554
|
-
t = t.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
555
|
-
// horizontal rules
|
|
556
|
-
t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
|
|
557
|
-
// collapse 3+ newlines to a double
|
|
558
|
-
t = t.replace(/\n{3,}/g, "\n\n");
|
|
559
|
-
return t.trim();
|
|
560
|
-
}
|
|
531
|
+
/** `stripMarkdown` used to live here. It moved to `aria-widget-model.ts` —
|
|
532
|
+
* which imports NOTHING, so the widget view can reach it without closing a
|
|
533
|
+
* cycle back through this file. Re-exported rather than relocated silently,
|
|
534
|
+
* so every consumer and test that names `aria-chat.ts` still resolves.
|
|
535
|
+
*
|
|
536
|
+
* Imported AND re-exported, and that is not redundant: `export { x } from "y"`
|
|
537
|
+
* does not bind `x` in this module's scope, and `toBubbles` below calls it —
|
|
538
|
+
* written that way the identifier is unbound at runtime and the whole turn
|
|
539
|
+
* machinery dies. (Measured: 20 tests, every one of them about polling.) */
|
|
540
|
+
export { stripMarkdown };
|
|
561
541
|
|
|
562
542
|
/** A greeting or optimistic bubble: no links, all slots defaulted. */
|
|
563
543
|
export function plainBubble(id: string, role: string, who: string, content: string): AriaBubble {
|
|
@@ -167,6 +167,37 @@ export function objectAt(v: unknown): Record<string, unknown> | null {
|
|
|
167
167
|
return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record<string, unknown>) : null;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/** Aria replies come back as markdown. There is no native markdown renderer
|
|
171
|
+
* (adding one is a CI-blocking parity change across both renderers), so
|
|
172
|
+
* normalize to clean plain text on-device: drop code fences, unwrap
|
|
173
|
+
* links/emphasis/headings, turn bullet markers into '•', collapse blank runs.
|
|
174
|
+
* Safe and idempotent. */
|
|
175
|
+
export function stripMarkdown(src: string): string {
|
|
176
|
+
if (!src) return "";
|
|
177
|
+
let t = src;
|
|
178
|
+
// fenced code blocks -> keep inner text, drop the ``` fences
|
|
179
|
+
t = t.replace(/```[a-zA-Z0-9]*\n?/g, "").replace(/```/g, "");
|
|
180
|
+
// inline code `x` -> x
|
|
181
|
+
t = t.replace(/`([^`]+)`/g, "$1");
|
|
182
|
+
// images  -> alt ; links [text](url) -> text
|
|
183
|
+
t = t.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
|
|
184
|
+
t = t.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
185
|
+
// bold/italic **x** *x* __x__ _x_ -> x
|
|
186
|
+
t = t.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/\*([^*]+)\*/g, "$1");
|
|
187
|
+
t = t.replace(/__([^_]+)__/g, "$1").replace(/_([^_]+)_/g, "$1");
|
|
188
|
+
// ATX headings: '### Title' -> 'Title'
|
|
189
|
+
t = t.replace(/^#{1,6}\s+/gm, "");
|
|
190
|
+
// blockquotes '> ' -> ''
|
|
191
|
+
t = t.replace(/^>\s?/gm, "");
|
|
192
|
+
// unordered list markers '- ' / '* ' / '+ ' -> bullet
|
|
193
|
+
t = t.replace(/^\s*[-*+]\s+/gm, "• ");
|
|
194
|
+
// horizontal rules
|
|
195
|
+
t = t.replace(/^\s*([-*_])\1{2,}\s*$/gm, "");
|
|
196
|
+
// collapse 3+ newlines to a double
|
|
197
|
+
t = t.replace(/\n{3,}/g, "\n\n");
|
|
198
|
+
return t.trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
170
201
|
/** A display string, whether the wire wrote it bare or wrapped.
|
|
171
202
|
*
|
|
172
203
|
* QUESTIONNAIRE writes every label as `{ text: "…" }` — question titles,
|