@wix/web5-core 1.63.15 → 1.63.17

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.
Files changed (82) hide show
  1. package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js +36 -32
  2. package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
  3. package/dist/cjs/component/componentDefinitions/index.js +5 -4
  4. package/dist/cjs/component/componentDefinitions/index.js.map +1 -1
  5. package/dist/cjs/component/componentDefinitions/parse-utils.js +43 -6
  6. package/dist/cjs/component/componentDefinitions/parse-utils.js.map +1 -1
  7. package/dist/cjs/index.js +32 -3
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/match/astToParts.js +14 -0
  10. package/dist/cjs/match/astToParts.js.map +1 -1
  11. package/dist/cjs/privacy/consentGate.js +275 -0
  12. package/dist/cjs/privacy/consentGate.js.map +1 -0
  13. package/dist/cjs/privacy/consentOverride.js +94 -0
  14. package/dist/cjs/privacy/consentOverride.js.map +1 -0
  15. package/dist/cjs/privacy/detectProvider.js +71 -0
  16. package/dist/cjs/privacy/detectProvider.js.map +1 -0
  17. package/dist/cjs/privacy/index.js +40 -0
  18. package/dist/cjs/privacy/index.js.map +1 -0
  19. package/dist/cjs/privacy/providers/hostSuppliedProvider.js +89 -0
  20. package/dist/cjs/privacy/providers/hostSuppliedProvider.js.map +1 -0
  21. package/dist/cjs/privacy/providers/oneTrustProvider.js +72 -0
  22. package/dist/cjs/privacy/providers/oneTrustProvider.js.map +1 -0
  23. package/dist/cjs/privacy/providers/shopifyProvider.js +177 -0
  24. package/dist/cjs/privacy/providers/shopifyProvider.js.map +1 -0
  25. package/dist/cjs/privacy/types.js +34 -0
  26. package/dist/cjs/privacy/types.js.map +1 -0
  27. package/dist/cjs/utils/analyticsEvents.js +30 -25
  28. package/dist/cjs/utils/analyticsEvents.js.map +1 -1
  29. package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js +37 -33
  30. package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
  31. package/dist/esm/component/componentDefinitions/index.js +5 -4
  32. package/dist/esm/component/componentDefinitions/index.js.map +1 -1
  33. package/dist/esm/component/componentDefinitions/parse-utils.js +42 -6
  34. package/dist/esm/component/componentDefinitions/parse-utils.js.map +1 -1
  35. package/dist/esm/index.js +3 -0
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/match/astToParts.js +14 -0
  38. package/dist/esm/match/astToParts.js.map +1 -1
  39. package/dist/esm/privacy/consentGate.js +265 -0
  40. package/dist/esm/privacy/consentGate.js.map +1 -0
  41. package/dist/esm/privacy/consentOverride.js +87 -0
  42. package/dist/esm/privacy/consentOverride.js.map +1 -0
  43. package/dist/esm/privacy/detectProvider.js +65 -0
  44. package/dist/esm/privacy/detectProvider.js.map +1 -0
  45. package/dist/esm/privacy/index.js +8 -0
  46. package/dist/esm/privacy/index.js.map +1 -0
  47. package/dist/esm/privacy/providers/hostSuppliedProvider.js +81 -0
  48. package/dist/esm/privacy/providers/hostSuppliedProvider.js.map +1 -0
  49. package/dist/esm/privacy/providers/oneTrustProvider.js +66 -0
  50. package/dist/esm/privacy/providers/oneTrustProvider.js.map +1 -0
  51. package/dist/esm/privacy/providers/shopifyProvider.js +174 -0
  52. package/dist/esm/privacy/providers/shopifyProvider.js.map +1 -0
  53. package/dist/esm/privacy/types.js +30 -0
  54. package/dist/esm/privacy/types.js.map +1 -0
  55. package/dist/esm/utils/analyticsEvents.js +30 -25
  56. package/dist/esm/utils/analyticsEvents.js.map +1 -1
  57. package/dist/types/component/componentDefinitions/TextBlockSectionDefinition.d.ts.map +1 -1
  58. package/dist/types/component/componentDefinitions/index.d.ts.map +1 -1
  59. package/dist/types/component/componentDefinitions/parse-utils.d.ts +21 -0
  60. package/dist/types/component/componentDefinitions/parse-utils.d.ts.map +1 -1
  61. package/dist/types/index.d.ts +1 -0
  62. package/dist/types/index.d.ts.map +1 -1
  63. package/dist/types/match/astToParts.d.ts.map +1 -1
  64. package/dist/types/privacy/consentGate.d.ts +85 -0
  65. package/dist/types/privacy/consentGate.d.ts.map +1 -0
  66. package/dist/types/privacy/consentOverride.d.ts +33 -0
  67. package/dist/types/privacy/consentOverride.d.ts.map +1 -0
  68. package/dist/types/privacy/detectProvider.d.ts +25 -0
  69. package/dist/types/privacy/detectProvider.d.ts.map +1 -0
  70. package/dist/types/privacy/index.d.ts +8 -0
  71. package/dist/types/privacy/index.d.ts.map +1 -0
  72. package/dist/types/privacy/providers/hostSuppliedProvider.d.ts +29 -0
  73. package/dist/types/privacy/providers/hostSuppliedProvider.d.ts.map +1 -0
  74. package/dist/types/privacy/providers/oneTrustProvider.d.ts +15 -0
  75. package/dist/types/privacy/providers/oneTrustProvider.d.ts.map +1 -0
  76. package/dist/types/privacy/providers/shopifyProvider.d.ts +26 -0
  77. package/dist/types/privacy/providers/shopifyProvider.d.ts.map +1 -0
  78. package/dist/types/privacy/types.d.ts +32 -0
  79. package/dist/types/privacy/types.d.ts.map +1 -0
  80. package/dist/types/utils/analyticsEvents.d.ts +9 -0
  81. package/dist/types/utils/analyticsEvents.d.ts.map +1 -1
  82. package/package.json +2 -2
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.isOneTrustHost = exports.createOneTrustConsentProvider = void 0;
5
+ /**
6
+ * OneTrust provider — DL #218.
7
+ *
8
+ * Lifted from the consent check that used to live inside
9
+ * `utils/analyticsEvents.ts`, with one deliberate behaviour change: that
10
+ * function returned `true` when OneTrust was absent ("host is responsible for
11
+ * loading OneTrust"), which on a Shopify storefront meant unconditional
12
+ * default-allow. Absence is no longer this provider's problem — it is only
13
+ * selected when OneTrust is actually on the page, and absence is handled by
14
+ * the gate's own default.
15
+ */
16
+
17
+ /** OneTrust's default taxonomy: C0002 is the performance/analytics category. */
18
+ const ANALYTICS_CATEGORY = 'C0002';
19
+ /** C0004 is targeting/advertising. */
20
+ const MARKETING_CATEGORY = 'C0004';
21
+ const readGroups = () => {
22
+ if (typeof window === 'undefined') {
23
+ return null;
24
+ }
25
+ const groups = window.OnetrustActiveGroups;
26
+ return typeof groups === 'string' ? groups : null;
27
+ };
28
+ const isOneTrustHost = () => readGroups() !== null;
29
+ exports.isOneTrustHost = isOneTrustHost;
30
+ const readSnapshot = () => {
31
+ const groups = readGroups();
32
+ if (groups !== null) {
33
+ try {
34
+ console.log(`[w5-consent] onetrust: active groups "${groups}"`);
35
+ } catch {
36
+ /* never break on a console */
37
+ }
38
+ }
39
+ if (groups === null) {
40
+ return {
41
+ analytics: 'unknown',
42
+ marketing: 'unknown',
43
+ performance: 'unknown'
44
+ };
45
+ }
46
+ // OneTrust publishes the *active* groups, so a category that is absent from
47
+ // a string OneTrust has written is a refusal, not silence.
48
+ const analytics = groups.includes(ANALYTICS_CATEGORY) ? 'granted' : 'denied';
49
+ const marketing = groups.includes(MARKETING_CATEGORY) ? 'granted' : 'denied';
50
+ return {
51
+ analytics,
52
+ marketing,
53
+ performance: analytics
54
+ };
55
+ };
56
+ const createOneTrustConsentProvider = () => ({
57
+ name: 'onetrust',
58
+ read: readSnapshot,
59
+ subscribe(onChange) {
60
+ const publish = () => onChange(readSnapshot());
61
+ if (typeof window === 'undefined') {
62
+ return () => {};
63
+ }
64
+ // OneTrust fires this on every banner interaction.
65
+ window.addEventListener('OneTrustGroupsUpdated', publish);
66
+ return () => {
67
+ window.removeEventListener('OneTrustGroupsUpdated', publish);
68
+ };
69
+ }
70
+ });
71
+ exports.createOneTrustConsentProvider = createOneTrustConsentProvider;
72
+ //# sourceMappingURL=oneTrustProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ANALYTICS_CATEGORY","MARKETING_CATEGORY","readGroups","window","groups","OnetrustActiveGroups","isOneTrustHost","exports","readSnapshot","console","log","analytics","marketing","performance","includes","createOneTrustConsentProvider","name","read","subscribe","onChange","publish","addEventListener","removeEventListener"],"sources":["../../../../src/privacy/providers/oneTrustProvider.ts"],"sourcesContent":["/**\n * OneTrust provider — DL #218.\n *\n * Lifted from the consent check that used to live inside\n * `utils/analyticsEvents.ts`, with one deliberate behaviour change: that\n * function returned `true` when OneTrust was absent (\"host is responsible for\n * loading OneTrust\"), which on a Shopify storefront meant unconditional\n * default-allow. Absence is no longer this provider's problem — it is only\n * selected when OneTrust is actually on the page, and absence is handled by\n * the gate's own default.\n */\n\nimport type { ConsentProvider, ConsentSnapshot } from '../types';\n\n/** OneTrust's default taxonomy: C0002 is the performance/analytics category. */\nconst ANALYTICS_CATEGORY = 'C0002';\n/** C0004 is targeting/advertising. */\nconst MARKETING_CATEGORY = 'C0004';\n\nconst readGroups = (): string | null => {\n if (typeof window === 'undefined') {\n return null;\n }\n const groups = (window as unknown as { OnetrustActiveGroups?: unknown })\n .OnetrustActiveGroups;\n return typeof groups === 'string' ? groups : null;\n};\n\nexport const isOneTrustHost = (): boolean => readGroups() !== null;\n\nconst readSnapshot = (): ConsentSnapshot => {\n const groups = readGroups();\n if (groups !== null) {\n try {\n console.log(`[w5-consent] onetrust: active groups \"${groups}\"`);\n } catch {\n /* never break on a console */\n }\n }\n if (groups === null) {\n return { analytics: 'unknown', marketing: 'unknown', performance: 'unknown' };\n }\n // OneTrust publishes the *active* groups, so a category that is absent from\n // a string OneTrust has written is a refusal, not silence.\n const analytics = groups.includes(ANALYTICS_CATEGORY) ? 'granted' : 'denied';\n const marketing = groups.includes(MARKETING_CATEGORY) ? 'granted' : 'denied';\n return { analytics, marketing, performance: analytics };\n};\n\nexport const createOneTrustConsentProvider = (): ConsentProvider => ({\n name: 'onetrust',\n\n read: readSnapshot,\n\n subscribe(onChange) {\n const publish = () => onChange(readSnapshot());\n if (typeof window === 'undefined') {\n return () => {};\n }\n // OneTrust fires this on every banner interaction.\n window.addEventListener('OneTrustGroupsUpdated', publish);\n return () => {\n window.removeEventListener('OneTrustGroupsUpdated', publish);\n };\n },\n});\n"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA;AACA,MAAMA,kBAAkB,GAAG,OAAO;AAClC;AACA,MAAMC,kBAAkB,GAAG,OAAO;AAElC,MAAMC,UAAU,GAAGA,CAAA,KAAqB;EACtC,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;IACjC,OAAO,IAAI;EACb;EACA,MAAMC,MAAM,GAAID,MAAM,CACnBE,oBAAoB;EACvB,OAAO,OAAOD,MAAM,KAAK,QAAQ,GAAGA,MAAM,GAAG,IAAI;AACnD,CAAC;AAEM,MAAME,cAAc,GAAGA,CAAA,KAAeJ,UAAU,CAAC,CAAC,KAAK,IAAI;AAACK,OAAA,CAAAD,cAAA,GAAAA,cAAA;AAEnE,MAAME,YAAY,GAAGA,CAAA,KAAuB;EAC1C,MAAMJ,MAAM,GAAGF,UAAU,CAAC,CAAC;EAC3B,IAAIE,MAAM,KAAK,IAAI,EAAE;IACnB,IAAI;MACFK,OAAO,CAACC,GAAG,CAAC,yCAAyCN,MAAM,GAAG,CAAC;IACjE,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ;EACA,IAAIA,MAAM,KAAK,IAAI,EAAE;IACnB,OAAO;MAAEO,SAAS,EAAE,SAAS;MAAEC,SAAS,EAAE,SAAS;MAAEC,WAAW,EAAE;IAAU,CAAC;EAC/E;EACA;EACA;EACA,MAAMF,SAAS,GAAGP,MAAM,CAACU,QAAQ,CAACd,kBAAkB,CAAC,GAAG,SAAS,GAAG,QAAQ;EAC5E,MAAMY,SAAS,GAAGR,MAAM,CAACU,QAAQ,CAACb,kBAAkB,CAAC,GAAG,SAAS,GAAG,QAAQ;EAC5E,OAAO;IAAEU,SAAS;IAAEC,SAAS;IAAEC,WAAW,EAAEF;EAAU,CAAC;AACzD,CAAC;AAEM,MAAMI,6BAA6B,GAAGA,CAAA,MAAwB;EACnEC,IAAI,EAAE,UAAU;EAEhBC,IAAI,EAAET,YAAY;EAElBU,SAASA,CAACC,QAAQ,EAAE;IAClB,MAAMC,OAAO,GAAGA,CAAA,KAAMD,QAAQ,CAACX,YAAY,CAAC,CAAC,CAAC;IAC9C,IAAI,OAAOL,MAAM,KAAK,WAAW,EAAE;MACjC,OAAO,MAAM,CAAC,CAAC;IACjB;IACA;IACAA,MAAM,CAACkB,gBAAgB,CAAC,uBAAuB,EAAED,OAAO,CAAC;IACzD,OAAO,MAAM;MACXjB,MAAM,CAACmB,mBAAmB,CAAC,uBAAuB,EAAEF,OAAO,CAAC;IAC9D,CAAC;EACH;AACF,CAAC,CAAC;AAACb,OAAA,CAAAQ,6BAAA,GAAAA,6BAAA","ignoreList":[]}
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.isShopifyHost = exports.createShopifyConsentProvider = void 0;
5
+ var _types = require("../types");
6
+ /**
7
+ * Shopify Customer Privacy API provider — DL #218.
8
+ *
9
+ * Two things about this API shape the provider:
10
+ *
11
+ * 1. **It is not on the page by default.** `window.Shopify.customerPrivacy` is
12
+ * `undefined` until somebody asks for it with `Shopify.loadFeatures`. The
13
+ * theme app extension warms it so it is ready before the bundle mounts;
14
+ * this provider still requests it, because the extension may not be
15
+ * installed and the request is idempotent.
16
+ *
17
+ * 2. **`visitorConsentCollected` may have already fired.** The bundle executes
18
+ * into an already-rendered storefront page (DL #217), so `read()` answers
19
+ * from current state and the event is only ever an update.
20
+ *
21
+ * The region rule is the one judgement encoded here, and it keys off
22
+ * `isRegulationEnforced()` rather than `shouldShowBanner()`. The two are not
23
+ * interchangeable: the banner reflects what the *merchant* configured, while
24
+ * enforcement reflects what the *visitor's jurisdiction* requires. A store with
25
+ * an empty consent configuration reports "no banner needed" everywhere,
26
+ * including inside the EU.
27
+ */
28
+
29
+ const CONSENT_FEATURE = {
30
+ name: 'consent-tracking-api',
31
+ version: '0.1'
32
+ };
33
+ const log = (...args) => {
34
+ try {
35
+ console.log('[w5-consent] shopify:', ...args);
36
+ } catch {
37
+ /* never break on a console */
38
+ }
39
+ };
40
+ const getShopify = () => {
41
+ if (typeof window === 'undefined') {
42
+ return null;
43
+ }
44
+ return window.Shopify ?? null;
45
+ };
46
+ const isShopifyHost = () => getShopify() !== null;
47
+ exports.isShopifyHost = isShopifyHost;
48
+ const toState = value => {
49
+ if (value === 'yes') return 'granted';
50
+ if (value === 'no') return 'denied';
51
+ return null;
52
+ };
53
+ const readSnapshot = () => {
54
+ var _getShopify;
55
+ const privacy = (_getShopify = getShopify()) == null ? void 0 : _getShopify.customerPrivacy;
56
+ if (!privacy) {
57
+ log('customerPrivacy API not on the page — no answer available (store has no privacy configuration, or loadFeatures has not resolved yet)');
58
+ // The API has not loaded (or the store has no privacy configuration at
59
+ // all). Not an answer — the gate holds, and the visitor's own action is
60
+ // what unlocks the session.
61
+ return _types.UNKNOWN_CONSENT;
62
+ }
63
+ let consent = {};
64
+ try {
65
+ consent = (privacy.currentVisitorConsent == null ? void 0 : privacy.currentVisitorConsent()) ?? {};
66
+ } catch {
67
+ // Present but not ready — treat as no answer yet.
68
+ }
69
+ const explicitAnalytics = toState(consent.analytics);
70
+ const explicitMarketing = toState(consent.marketing);
71
+ let allowed = false;
72
+ try {
73
+ allowed = (privacy.analyticsProcessingAllowed == null ? void 0 : privacy.analyticsProcessingAllowed()) === true;
74
+ } catch {
75
+ allowed = false;
76
+ }
77
+
78
+ // Is a privacy regulation in force for *this visitor's* region? This is the
79
+ // question that matters, and it is not the same question as "is a banner
80
+ // being shown" — see the comment on `enforced` below.
81
+ let enforced = null;
82
+ let regulation = '';
83
+ let region = '';
84
+ try {
85
+ if (typeof privacy.isRegulationEnforced === 'function') {
86
+ enforced = privacy.isRegulationEnforced() === true;
87
+ }
88
+ regulation = (privacy.getRegulation == null ? void 0 : privacy.getRegulation()) ?? '';
89
+ region = (privacy.getRegion == null ? void 0 : privacy.getRegion()) ?? '';
90
+ } catch {
91
+ enforced = null;
92
+ }
93
+ let bannerRequired = true;
94
+ try {
95
+ bannerRequired = (privacy.shouldShowBanner == null ? void 0 : privacy.shouldShowBanner()) !== false;
96
+ } catch {
97
+ bannerRequired = true;
98
+ }
99
+
100
+ /*
101
+ * Measured on a live store from a Frankfurt IP, with no consent recorded:
102
+ *
103
+ * region 'DEBE' · regulation 'GDPR' · isRegulationEnforced() true
104
+ * shouldShowBanner() false · analyticsProcessingAllowed() TRUE
105
+ * getShopPrefs() { limit: [] }
106
+ *
107
+ * Shopify said tracking was allowed for a GDPR-protected visitor who had
108
+ * never been asked, because the *merchant* had configured no consent
109
+ * preferences. `shouldShowBanner()` and `analyticsProcessingAllowed()` both
110
+ * describe the merchant's setup; neither is a statement about a legal basis.
111
+ * A merchant's misconfiguration must not become our tracking decision, so
112
+ * under an enforced regulation an absent answer stays `unknown` and the gate
113
+ * holds — the visitor's own prompt is then the only thing that unlocks them.
114
+ */
115
+ const permissiveDefault = allowed || !bannerRequired;
116
+ const fallbackWhenUnenforceable = permissiveDefault ? 'granted' : 'unknown';
117
+ const analytics = explicitAnalytics ?? (enforced === true ? 'unknown' : fallbackWhenUnenforceable);
118
+ const marketing = explicitMarketing ?? (enforced === true ? 'unknown' : !bannerRequired ? 'granted' : 'unknown');
119
+ log(`read — visitorConsent.analytics=${consent.analytics ?? '(unset)'} ` + `analyticsProcessingAllowed=${allowed} bannerRequired=${bannerRequired} ` + `region=${region || '?'} regulation=${regulation || '?'} enforced=${enforced === null ? 'unavailable' : enforced}` + (enforced === true && explicitAnalytics === null ? ' → regulated and unanswered, holding' : '') + ` ⇒ ${analytics}`);
120
+ return {
121
+ analytics,
122
+ marketing,
123
+ // Shopify has no separate performance bucket. Load telemetry carries an
124
+ // app name and a session id to a Wix endpoint, so it answers to the same
125
+ // answer analytics does rather than riding for free.
126
+ performance: analytics
127
+ };
128
+ };
129
+
130
+ /**
131
+ * Ask Shopify to load the consent API if it is not already there. Safe to call
132
+ * more than once; the callback re-reads whatever state arrives.
133
+ */
134
+ const requestConsentApi = onReady => {
135
+ const shopify = getShopify();
136
+ if (!shopify || shopify.customerPrivacy || !shopify.loadFeatures) {
137
+ return;
138
+ }
139
+ try {
140
+ log('requesting consent-tracking-api via Shopify.loadFeatures…');
141
+ shopify.loadFeatures([CONSENT_FEATURE], error => {
142
+ if (error) {
143
+ log('loadFeatures failed — gate stays held', error);
144
+ return;
145
+ }
146
+ log('loadFeatures resolved — re-reading consent');
147
+ onReady();
148
+ });
149
+ } catch {
150
+ // Storefronts without the feature simply never resolve; the gate stays
151
+ // held and engagement remains the only unlock.
152
+ }
153
+ };
154
+ const createShopifyConsentProvider = () => ({
155
+ name: 'shopify-customer-privacy',
156
+ read: readSnapshot,
157
+ subscribe(onChange) {
158
+ const publish = () => {
159
+ log('visitorConsentCollected — the visitor answered the banner');
160
+ onChange(readSnapshot());
161
+ };
162
+
163
+ // The visitor may have answered the banner before this bundle existed, so
164
+ // the event is an update — never the first read.
165
+ if (typeof document !== 'undefined') {
166
+ document.addEventListener('visitorConsentCollected', publish);
167
+ }
168
+ requestConsentApi(() => onChange(readSnapshot()));
169
+ return () => {
170
+ if (typeof document !== 'undefined') {
171
+ document.removeEventListener('visitorConsentCollected', publish);
172
+ }
173
+ };
174
+ }
175
+ });
176
+ exports.createShopifyConsentProvider = createShopifyConsentProvider;
177
+ //# sourceMappingURL=shopifyProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_types","require","CONSENT_FEATURE","name","version","log","args","console","getShopify","window","Shopify","isShopifyHost","exports","toState","value","readSnapshot","_getShopify","privacy","customerPrivacy","UNKNOWN_CONSENT","consent","currentVisitorConsent","explicitAnalytics","analytics","explicitMarketing","marketing","allowed","analyticsProcessingAllowed","enforced","regulation","region","isRegulationEnforced","getRegulation","getRegion","bannerRequired","shouldShowBanner","permissiveDefault","fallbackWhenUnenforceable","performance","requestConsentApi","onReady","shopify","loadFeatures","error","createShopifyConsentProvider","read","subscribe","onChange","publish","document","addEventListener","removeEventListener"],"sources":["../../../../src/privacy/providers/shopifyProvider.ts"],"sourcesContent":["/**\n * Shopify Customer Privacy API provider — DL #218.\n *\n * Two things about this API shape the provider:\n *\n * 1. **It is not on the page by default.** `window.Shopify.customerPrivacy` is\n * `undefined` until somebody asks for it with `Shopify.loadFeatures`. The\n * theme app extension warms it so it is ready before the bundle mounts;\n * this provider still requests it, because the extension may not be\n * installed and the request is idempotent.\n *\n * 2. **`visitorConsentCollected` may have already fired.** The bundle executes\n * into an already-rendered storefront page (DL #217), so `read()` answers\n * from current state and the event is only ever an update.\n *\n * The region rule is the one judgement encoded here, and it keys off\n * `isRegulationEnforced()` rather than `shouldShowBanner()`. The two are not\n * interchangeable: the banner reflects what the *merchant* configured, while\n * enforcement reflects what the *visitor's jurisdiction* requires. A store with\n * an empty consent configuration reports \"no banner needed\" everywhere,\n * including inside the EU.\n */\n\nimport {\n UNKNOWN_CONSENT,\n type ConsentProvider,\n type ConsentSnapshot,\n type ConsentState,\n} from '../types';\n\ntype ShopifyConsentValue = 'yes' | 'no' | '' | undefined;\n\ninterface ShopifyVisitorConsent {\n analytics?: ShopifyConsentValue;\n marketing?: ShopifyConsentValue;\n preferences?: ShopifyConsentValue;\n sale_of_data?: ShopifyConsentValue;\n}\n\ninterface ShopifyCustomerPrivacy {\n analyticsProcessingAllowed?: () => boolean;\n marketingAllowed?: () => boolean;\n currentVisitorConsent?: () => ShopifyVisitorConsent;\n shouldShowBanner?: () => boolean;\n /** Whether a privacy regulation applies to this visitor's region. */\n isRegulationEnforced?: () => boolean;\n /** e.g. 'GDPR', 'CCPA'. */\n getRegulation?: () => string;\n /** e.g. 'DEBE' — country + subdivision. */\n getRegion?: () => string;\n}\n\ninterface ShopifyGlobal {\n customerPrivacy?: ShopifyCustomerPrivacy;\n loadFeatures?: (\n features: { name: string; version: string }[],\n callback: (error?: unknown) => void,\n ) => void;\n}\n\nconst CONSENT_FEATURE = { name: 'consent-tracking-api', version: '0.1' };\n\nconst log = (...args: unknown[]): void => {\n try {\n console.log('[w5-consent] shopify:', ...args);\n } catch {\n /* never break on a console */\n }\n};\n\nconst getShopify = (): ShopifyGlobal | null => {\n if (typeof window === 'undefined') {\n return null;\n }\n return (window as unknown as { Shopify?: ShopifyGlobal }).Shopify ?? null;\n};\n\nexport const isShopifyHost = (): boolean => getShopify() !== null;\n\nconst toState = (value: ShopifyConsentValue): ConsentState | null => {\n if (value === 'yes') return 'granted';\n if (value === 'no') return 'denied';\n return null;\n};\n\nconst readSnapshot = (): ConsentSnapshot => {\n const privacy = getShopify()?.customerPrivacy;\n if (!privacy) {\n log(\n 'customerPrivacy API not on the page — no answer available (store has no privacy configuration, or loadFeatures has not resolved yet)',\n );\n // The API has not loaded (or the store has no privacy configuration at\n // all). Not an answer — the gate holds, and the visitor's own action is\n // what unlocks the session.\n return UNKNOWN_CONSENT;\n }\n\n let consent: ShopifyVisitorConsent = {};\n try {\n consent = privacy.currentVisitorConsent?.() ?? {};\n } catch {\n // Present but not ready — treat as no answer yet.\n }\n\n const explicitAnalytics = toState(consent.analytics);\n const explicitMarketing = toState(consent.marketing);\n\n let allowed = false;\n try {\n allowed = privacy.analyticsProcessingAllowed?.() === true;\n } catch {\n allowed = false;\n }\n\n // Is a privacy regulation in force for *this visitor's* region? This is the\n // question that matters, and it is not the same question as \"is a banner\n // being shown\" — see the comment on `enforced` below.\n let enforced: boolean | null = null;\n let regulation = '';\n let region = '';\n try {\n if (typeof privacy.isRegulationEnforced === 'function') {\n enforced = privacy.isRegulationEnforced() === true;\n }\n regulation = privacy.getRegulation?.() ?? '';\n region = privacy.getRegion?.() ?? '';\n } catch {\n enforced = null;\n }\n\n let bannerRequired = true;\n try {\n bannerRequired = privacy.shouldShowBanner?.() !== false;\n } catch {\n bannerRequired = true;\n }\n\n /*\n * Measured on a live store from a Frankfurt IP, with no consent recorded:\n *\n * region 'DEBE' · regulation 'GDPR' · isRegulationEnforced() true\n * shouldShowBanner() false · analyticsProcessingAllowed() TRUE\n * getShopPrefs() { limit: [] }\n *\n * Shopify said tracking was allowed for a GDPR-protected visitor who had\n * never been asked, because the *merchant* had configured no consent\n * preferences. `shouldShowBanner()` and `analyticsProcessingAllowed()` both\n * describe the merchant's setup; neither is a statement about a legal basis.\n * A merchant's misconfiguration must not become our tracking decision, so\n * under an enforced regulation an absent answer stays `unknown` and the gate\n * holds — the visitor's own prompt is then the only thing that unlocks them.\n */\n const permissiveDefault = allowed || !bannerRequired;\n const fallbackWhenUnenforceable = permissiveDefault ? 'granted' : 'unknown';\n\n const analytics: ConsentState =\n explicitAnalytics ??\n (enforced === true ? 'unknown' : fallbackWhenUnenforceable);\n\n const marketing: ConsentState =\n explicitMarketing ??\n (enforced === true ? 'unknown' : !bannerRequired ? 'granted' : 'unknown');\n\n log(\n `read — visitorConsent.analytics=${consent.analytics ?? '(unset)'} ` +\n `analyticsProcessingAllowed=${allowed} bannerRequired=${bannerRequired} ` +\n `region=${region || '?'} regulation=${regulation || '?'} enforced=${\n enforced === null ? 'unavailable' : enforced\n }` +\n (enforced === true && explicitAnalytics === null\n ? ' → regulated and unanswered, holding'\n : '') +\n ` ⇒ ${analytics}`,\n );\n\n return {\n analytics,\n marketing,\n // Shopify has no separate performance bucket. Load telemetry carries an\n // app name and a session id to a Wix endpoint, so it answers to the same\n // answer analytics does rather than riding for free.\n performance: analytics,\n };\n};\n\n/**\n * Ask Shopify to load the consent API if it is not already there. Safe to call\n * more than once; the callback re-reads whatever state arrives.\n */\nconst requestConsentApi = (onReady: () => void): void => {\n const shopify = getShopify();\n if (!shopify || shopify.customerPrivacy || !shopify.loadFeatures) {\n return;\n }\n try {\n log('requesting consent-tracking-api via Shopify.loadFeatures…');\n shopify.loadFeatures([CONSENT_FEATURE], (error) => {\n if (error) {\n log('loadFeatures failed — gate stays held', error);\n return;\n }\n log('loadFeatures resolved — re-reading consent');\n onReady();\n });\n } catch {\n // Storefronts without the feature simply never resolve; the gate stays\n // held and engagement remains the only unlock.\n }\n};\n\nexport const createShopifyConsentProvider = (): ConsentProvider => ({\n name: 'shopify-customer-privacy',\n\n read: readSnapshot,\n\n subscribe(onChange) {\n const publish = () => {\n log('visitorConsentCollected — the visitor answered the banner');\n onChange(readSnapshot());\n };\n\n // The visitor may have answered the banner before this bundle existed, so\n // the event is an update — never the first read.\n if (typeof document !== 'undefined') {\n document.addEventListener('visitorConsentCollected', publish);\n }\n requestConsentApi(() => onChange(readSnapshot()));\n\n return () => {\n if (typeof document !== 'undefined') {\n document.removeEventListener('visitorConsentCollected', publish);\n }\n };\n },\n});\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.UNKNOWN_CONSENT = void 0;
5
+ /**
6
+ * Consent vocabulary shared by the gate and every provider.
7
+ *
8
+ * DL #218. Three states, because "we have not been told" is not the same
9
+ * answer as "no" and must not be collapsed into one: a visitor who has not
10
+ * been asked can still be unlocked by their own action, a visitor who
11
+ * refused never can.
12
+ */
13
+
14
+ /**
15
+ * `necessary` is never gated — auth tokens and the configuration fetch are
16
+ * what make the widget work at all, and a visitor who loaded the page asked
17
+ * for that much. Everything else answers to the gate.
18
+ */
19
+
20
+ const UNKNOWN_CONSENT = exports.UNKNOWN_CONSENT = Object.freeze({
21
+ performance: 'unknown',
22
+ analytics: 'unknown',
23
+ marketing: 'unknown'
24
+ });
25
+
26
+ /**
27
+ * A consent provider adapts one host's CMP to the gate.
28
+ *
29
+ * `read()` must answer synchronously with what is known *now*. Providers are
30
+ * installed into an already-rendered storefront page (DL #217), so a provider
31
+ * that only subscribes is inert — the event it waits for may have fired long
32
+ * before the bundle executed. Always read first, then subscribe.
33
+ */
34
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["UNKNOWN_CONSENT","exports","Object","freeze","performance","analytics","marketing"],"sources":["../../../src/privacy/types.ts"],"sourcesContent":["/**\n * Consent vocabulary shared by the gate and every provider.\n *\n * DL #218. Three states, because \"we have not been told\" is not the same\n * answer as \"no\" and must not be collapsed into one: a visitor who has not\n * been asked can still be unlocked by their own action, a visitor who\n * refused never can.\n */\n\nexport type ConsentState = 'granted' | 'denied' | 'unknown';\n\n/**\n * `necessary` is never gated — auth tokens and the configuration fetch are\n * what make the widget work at all, and a visitor who loaded the page asked\n * for that much. Everything else answers to the gate.\n */\nexport type ConsentPurpose =\n | 'necessary'\n | 'performance'\n | 'analytics'\n | 'marketing';\n\nexport type GatedPurpose = Exclude<ConsentPurpose, 'necessary'>;\n\nexport type ConsentSnapshot = Record<GatedPurpose, ConsentState>;\n\nexport const UNKNOWN_CONSENT: ConsentSnapshot = Object.freeze({\n performance: 'unknown',\n analytics: 'unknown',\n marketing: 'unknown',\n});\n\n/**\n * A consent provider adapts one host's CMP to the gate.\n *\n * `read()` must answer synchronously with what is known *now*. Providers are\n * installed into an already-rendered storefront page (DL #217), so a provider\n * that only subscribes is inert — the event it waits for may have fired long\n * before the bundle executed. Always read first, then subscribe.\n */\nexport interface ConsentProvider {\n readonly name: string;\n read(): ConsentSnapshot;\n subscribe(onChange: (snapshot: ConsentSnapshot) => void): () => void;\n}\n"],"mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA;AACA;AACA;AACA;AACA;;AAWO,MAAMA,eAAgC,GAAAC,OAAA,CAAAD,eAAA,GAAGE,MAAM,CAACC,MAAM,CAAC;EAC5DC,WAAW,EAAE,SAAS;EACtBC,SAAS,EAAE,SAAS;EACpBC,SAAS,EAAE;AACb,CAAC,CAAC;;AAEF;AACA;AACA;AACA;AACA;AACA;AACA;AACA","ignoreList":[]}
@@ -8,18 +8,21 @@ exports.pushEvent = pushEvent;
8
8
  exports.pushExit = pushExit;
9
9
  exports.pushLinkClick = pushLinkClick;
10
10
  exports.pushPromptSubmit = pushPromptSubmit;
11
- const ANALYTICS_CATEGORY = 'C0002';
11
+ var _consentGate = require("../privacy/consentGate");
12
+ /**
13
+ * Whether analytics may go on the wire right now.
14
+ *
15
+ * DL #218: this used to read OneTrust directly and return `true` when OneTrust
16
+ * was absent, which on a Shopify storefront — where no host loads OneTrust —
17
+ * meant unconditional default-allow. It now defers to the consent gate, which
18
+ * reads whichever CMP the host actually has and, in the absence of any answer,
19
+ * holds until the visitor does something deliberate.
20
+ */
12
21
  function hasAnalyticsConsent() {
13
22
  if (typeof window === 'undefined') {
14
23
  return false;
15
24
  }
16
- const groups = window.OnetrustActiveGroups;
17
- if (typeof groups === 'string') {
18
- return groups.includes(ANALYTICS_CATEGORY);
19
- }
20
-
21
- // No OneTrust on page — allow tracking (host is responsible for loading OneTrust)
22
- return true;
25
+ return (0, _consentGate.mayTransmit)('analytics');
23
26
  }
24
27
 
25
28
  /**
@@ -33,26 +36,28 @@ function pushEvent(eventName, params = {}) {
33
36
  if (typeof window === 'undefined') {
34
37
  return;
35
38
  }
36
- if (!hasAnalyticsConsent()) {
37
- return;
38
- }
39
39
 
40
- // GTM mode
41
- if (window.google_tag_manager && window.dataLayer) {
42
- window.dataLayer.push({
43
- event: eventName,
44
- ...params
45
- });
46
- return;
47
- }
40
+ // DL #218: routed through the gate rather than checked against it, so an
41
+ // event raised before consent resolves is held and replayed rather than
42
+ // dropped on the floor.
43
+ (0, _consentGate.transmit)('analytics', () => {
44
+ // GTM mode
45
+ if (window.google_tag_manager && window.dataLayer) {
46
+ window.dataLayer.push({
47
+ event: eventName,
48
+ ...params
49
+ });
50
+ return;
51
+ }
48
52
 
49
- // Standalone gtag.js mode
50
- if (typeof window.gtag === 'function') {
51
- window.gtag('event', eventName, params);
52
- return;
53
- }
53
+ // Standalone gtag.js mode
54
+ if (typeof window.gtag === 'function') {
55
+ window.gtag('event', eventName, params);
56
+ return;
57
+ }
54
58
 
55
- // No analytics SDK loaded — no-op
59
+ // No analytics SDK loaded — no-op
60
+ }, `ga ${eventName}`);
56
61
  }
57
62
  function pushPromptSubmit(promptType, promptText) {
58
63
  pushEvent('web5_prompt_submit', {
@@ -1 +1 @@
1
- {"version":3,"names":["ANALYTICS_CATEGORY","hasAnalyticsConsent","window","groups","OnetrustActiveGroups","includes","pushEvent","eventName","params","google_tag_manager","dataLayer","push","event","gtag","pushPromptSubmit","promptType","promptText","prompt_type","prompt_text","pushLinkClick","linkUrl","linkType","linkText","link_url","link_type","link_text","pushError","errorType","statusCode","errorMessage","error_type","status_code","error_message","pushExit","exitUrl","exitType","exit_url","exit_type","pushEntityFiltered","reason","entityType","entityId","entity_type","entity_id"],"sources":["../../../src/utils/analyticsEvents.ts"],"sourcesContent":["declare global {\n interface Window {\n dataLayer?: Record<string, unknown>[];\n gtag?: (...args: unknown[]) => void;\n google_tag_manager?: Record<string, unknown>;\n OnetrustActiveGroups?: string;\n }\n}\n\nconst ANALYTICS_CATEGORY = 'C0002';\n\nexport function hasAnalyticsConsent(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n\n const groups = window.OnetrustActiveGroups;\n if (typeof groups === 'string') {\n return groups.includes(ANALYTICS_CATEGORY);\n }\n\n // No OneTrust on page — allow tracking (host is responsible for loading OneTrust)\n return true;\n}\n\n/**\n * Universal event dispatch: auto-detects GTM vs standalone gtag.js.\n *\n * GTM is checked first via `google_tag_manager` because when GTM has a GA4\n * tag configured it also creates `window.gtag`. Using dataLayer.push()\n * ensures GTM triggers/tags still process the event.\n */\nexport function pushEvent(\n eventName: string,\n params: Record<string, unknown> = {},\n): void {\n if (typeof window === 'undefined') {\n return;\n }\n if (!hasAnalyticsConsent()) {\n return;\n }\n\n // GTM mode\n if (window.google_tag_manager && window.dataLayer) {\n window.dataLayer.push({ event: eventName, ...params });\n return;\n }\n\n // Standalone gtag.js mode\n if (typeof window.gtag === 'function') {\n window.gtag('event', eventName, params);\n return;\n }\n\n // No analytics SDK loaded — no-op\n}\n\nexport function pushPromptSubmit(\n promptType: 'text' | 'chip',\n promptText: string,\n): void {\n pushEvent('web5_prompt_submit', {\n prompt_type: promptType,\n prompt_text: promptText,\n });\n}\n\nexport function pushLinkClick(\n linkUrl: string,\n linkType: 'web5' | 'external' | 'internal',\n linkText?: string,\n): void {\n pushEvent('web5_link_click', {\n link_url: linkUrl,\n link_type: linkType,\n link_text: linkText ?? '',\n });\n}\n\nexport function pushError(\n errorType: string,\n statusCode: number,\n errorMessage?: string,\n): void {\n pushEvent('web5_error', {\n error_type: errorType,\n status_code: statusCode,\n error_message: errorMessage ?? '',\n });\n}\n\nexport function pushExit(\n exitUrl: string,\n linkText: string,\n exitType: 'external' | 'internal',\n): void {\n pushEvent('web5_exit', {\n exit_url: exitUrl,\n link_text: linkText,\n exit_type: exitType,\n });\n}\n\nexport type EntityFilteredReason = 'no_data' | 'no_url';\n\nexport function pushEntityFiltered(\n reason: EntityFilteredReason,\n entityType: string,\n entityId: string,\n): void {\n pushEvent('web5_entity_filtered', {\n reason,\n entity_type: entityType,\n entity_id: entityId,\n });\n}\n"],"mappings":";;;;;;;;;;AASA,MAAMA,kBAAkB,GAAG,OAAO;AAE3B,SAASC,mBAAmBA,CAAA,EAAY;EAC7C,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;IACjC,OAAO,KAAK;EACd;EAEA,MAAMC,MAAM,GAAGD,MAAM,CAACE,oBAAoB;EAC1C,IAAI,OAAOD,MAAM,KAAK,QAAQ,EAAE;IAC9B,OAAOA,MAAM,CAACE,QAAQ,CAACL,kBAAkB,CAAC;EAC5C;;EAEA;EACA,OAAO,IAAI;AACb;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASM,SAASA,CACvBC,SAAiB,EACjBC,MAA+B,GAAG,CAAC,CAAC,EAC9B;EACN,IAAI,OAAON,MAAM,KAAK,WAAW,EAAE;IACjC;EACF;EACA,IAAI,CAACD,mBAAmB,CAAC,CAAC,EAAE;IAC1B;EACF;;EAEA;EACA,IAAIC,MAAM,CAACO,kBAAkB,IAAIP,MAAM,CAACQ,SAAS,EAAE;IACjDR,MAAM,CAACQ,SAAS,CAACC,IAAI,CAAC;MAAEC,KAAK,EAAEL,SAAS;MAAE,GAAGC;IAAO,CAAC,CAAC;IACtD;EACF;;EAEA;EACA,IAAI,OAAON,MAAM,CAACW,IAAI,KAAK,UAAU,EAAE;IACrCX,MAAM,CAACW,IAAI,CAAC,OAAO,EAAEN,SAAS,EAAEC,MAAM,CAAC;IACvC;EACF;;EAEA;AACF;AAEO,SAASM,gBAAgBA,CAC9BC,UAA2B,EAC3BC,UAAkB,EACZ;EACNV,SAAS,CAAC,oBAAoB,EAAE;IAC9BW,WAAW,EAAEF,UAAU;IACvBG,WAAW,EAAEF;EACf,CAAC,CAAC;AACJ;AAEO,SAASG,aAAaA,CAC3BC,OAAe,EACfC,QAA0C,EAC1CC,QAAiB,EACX;EACNhB,SAAS,CAAC,iBAAiB,EAAE;IAC3BiB,QAAQ,EAAEH,OAAO;IACjBI,SAAS,EAAEH,QAAQ;IACnBI,SAAS,EAAEH,QAAQ,IAAI;EACzB,CAAC,CAAC;AACJ;AAEO,SAASI,SAASA,CACvBC,SAAiB,EACjBC,UAAkB,EAClBC,YAAqB,EACf;EACNvB,SAAS,CAAC,YAAY,EAAE;IACtBwB,UAAU,EAAEH,SAAS;IACrBI,WAAW,EAAEH,UAAU;IACvBI,aAAa,EAAEH,YAAY,IAAI;EACjC,CAAC,CAAC;AACJ;AAEO,SAASI,QAAQA,CACtBC,OAAe,EACfZ,QAAgB,EAChBa,QAAiC,EAC3B;EACN7B,SAAS,CAAC,WAAW,EAAE;IACrB8B,QAAQ,EAAEF,OAAO;IACjBT,SAAS,EAAEH,QAAQ;IACnBe,SAAS,EAAEF;EACb,CAAC,CAAC;AACJ;AAIO,SAASG,kBAAkBA,CAChCC,MAA4B,EAC5BC,UAAkB,EAClBC,QAAgB,EACV;EACNnC,SAAS,CAAC,sBAAsB,EAAE;IAChCiC,MAAM;IACNG,WAAW,EAAEF,UAAU;IACvBG,SAAS,EAAEF;EACb,CAAC,CAAC;AACJ","ignoreList":[]}
1
+ {"version":3,"names":["_consentGate","require","hasAnalyticsConsent","window","mayTransmit","pushEvent","eventName","params","transmit","google_tag_manager","dataLayer","push","event","gtag","pushPromptSubmit","promptType","promptText","prompt_type","prompt_text","pushLinkClick","linkUrl","linkType","linkText","link_url","link_type","link_text","pushError","errorType","statusCode","errorMessage","error_type","status_code","error_message","pushExit","exitUrl","exitType","exit_url","exit_type","pushEntityFiltered","reason","entityType","entityId","entity_type","entity_id"],"sources":["../../../src/utils/analyticsEvents.ts"],"sourcesContent":["import { mayTransmit, transmit } from '../privacy/consentGate';\n\ndeclare global {\n interface Window {\n dataLayer?: Record<string, unknown>[];\n gtag?: (...args: unknown[]) => void;\n google_tag_manager?: Record<string, unknown>;\n OnetrustActiveGroups?: string;\n }\n}\n\n/**\n * Whether analytics may go on the wire right now.\n *\n * DL #218: this used to read OneTrust directly and return `true` when OneTrust\n * was absent, which on a Shopify storefront — where no host loads OneTrust —\n * meant unconditional default-allow. It now defers to the consent gate, which\n * reads whichever CMP the host actually has and, in the absence of any answer,\n * holds until the visitor does something deliberate.\n */\nexport function hasAnalyticsConsent(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n return mayTransmit('analytics');\n}\n\n/**\n * Universal event dispatch: auto-detects GTM vs standalone gtag.js.\n *\n * GTM is checked first via `google_tag_manager` because when GTM has a GA4\n * tag configured it also creates `window.gtag`. Using dataLayer.push()\n * ensures GTM triggers/tags still process the event.\n */\nexport function pushEvent(\n eventName: string,\n params: Record<string, unknown> = {},\n): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n // DL #218: routed through the gate rather than checked against it, so an\n // event raised before consent resolves is held and replayed rather than\n // dropped on the floor.\n transmit(\n 'analytics',\n () => {\n // GTM mode\n if (window.google_tag_manager && window.dataLayer) {\n window.dataLayer.push({ event: eventName, ...params });\n return;\n }\n\n // Standalone gtag.js mode\n if (typeof window.gtag === 'function') {\n window.gtag('event', eventName, params);\n return;\n }\n\n // No analytics SDK loaded — no-op\n },\n `ga ${eventName}`,\n );\n}\n\nexport function pushPromptSubmit(\n promptType: 'text' | 'chip',\n promptText: string,\n): void {\n pushEvent('web5_prompt_submit', {\n prompt_type: promptType,\n prompt_text: promptText,\n });\n}\n\nexport function pushLinkClick(\n linkUrl: string,\n linkType: 'web5' | 'external' | 'internal',\n linkText?: string,\n): void {\n pushEvent('web5_link_click', {\n link_url: linkUrl,\n link_type: linkType,\n link_text: linkText ?? '',\n });\n}\n\nexport function pushError(\n errorType: string,\n statusCode: number,\n errorMessage?: string,\n): void {\n pushEvent('web5_error', {\n error_type: errorType,\n status_code: statusCode,\n error_message: errorMessage ?? '',\n });\n}\n\nexport function pushExit(\n exitUrl: string,\n linkText: string,\n exitType: 'external' | 'internal',\n): void {\n pushEvent('web5_exit', {\n exit_url: exitUrl,\n link_text: linkText,\n exit_type: exitType,\n });\n}\n\nexport type EntityFilteredReason = 'no_data' | 'no_url';\n\nexport function pushEntityFiltered(\n reason: EntityFilteredReason,\n entityType: string,\n entityId: string,\n): void {\n pushEvent('web5_entity_filtered', {\n reason,\n entity_type: entityType,\n entity_id: entityId,\n });\n}\n"],"mappings":";;;;;;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAWA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,mBAAmBA,CAAA,EAAY;EAC7C,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;IACjC,OAAO,KAAK;EACd;EACA,OAAO,IAAAC,wBAAW,EAAC,WAAW,CAAC;AACjC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,SAASA,CACvBC,SAAiB,EACjBC,MAA+B,GAAG,CAAC,CAAC,EAC9B;EACN,IAAI,OAAOJ,MAAM,KAAK,WAAW,EAAE;IACjC;EACF;;EAEA;EACA;EACA;EACA,IAAAK,qBAAQ,EACN,WAAW,EACX,MAAM;IACN;IACA,IAAIL,MAAM,CAACM,kBAAkB,IAAIN,MAAM,CAACO,SAAS,EAAE;MACjDP,MAAM,CAACO,SAAS,CAACC,IAAI,CAAC;QAAEC,KAAK,EAAEN,SAAS;QAAE,GAAGC;MAAO,CAAC,CAAC;MACtD;IACF;;IAEA;IACA,IAAI,OAAOJ,MAAM,CAACU,IAAI,KAAK,UAAU,EAAE;MACrCV,MAAM,CAACU,IAAI,CAAC,OAAO,EAAEP,SAAS,EAAEC,MAAM,CAAC;MACvC;IACF;;IAEA;EACA,CAAC,EACD,MAAMD,SAAS,EACjB,CAAC;AACH;AAEO,SAASQ,gBAAgBA,CAC9BC,UAA2B,EAC3BC,UAAkB,EACZ;EACNX,SAAS,CAAC,oBAAoB,EAAE;IAC9BY,WAAW,EAAEF,UAAU;IACvBG,WAAW,EAAEF;EACf,CAAC,CAAC;AACJ;AAEO,SAASG,aAAaA,CAC3BC,OAAe,EACfC,QAA0C,EAC1CC,QAAiB,EACX;EACNjB,SAAS,CAAC,iBAAiB,EAAE;IAC3BkB,QAAQ,EAAEH,OAAO;IACjBI,SAAS,EAAEH,QAAQ;IACnBI,SAAS,EAAEH,QAAQ,IAAI;EACzB,CAAC,CAAC;AACJ;AAEO,SAASI,SAASA,CACvBC,SAAiB,EACjBC,UAAkB,EAClBC,YAAqB,EACf;EACNxB,SAAS,CAAC,YAAY,EAAE;IACtByB,UAAU,EAAEH,SAAS;IACrBI,WAAW,EAAEH,UAAU;IACvBI,aAAa,EAAEH,YAAY,IAAI;EACjC,CAAC,CAAC;AACJ;AAEO,SAASI,QAAQA,CACtBC,OAAe,EACfZ,QAAgB,EAChBa,QAAiC,EAC3B;EACN9B,SAAS,CAAC,WAAW,EAAE;IACrB+B,QAAQ,EAAEF,OAAO;IACjBT,SAAS,EAAEH,QAAQ;IACnBe,SAAS,EAAEF;EACb,CAAC,CAAC;AACJ;AAIO,SAASG,kBAAkBA,CAChCC,MAA4B,EAC5BC,UAAkB,EAClBC,QAAgB,EACV;EACNpC,SAAS,CAAC,sBAAsB,EAAE;IAChCkC,MAAM;IACNG,WAAW,EAAEF,UAAU;IACvBG,SAAS,EAAEF;EACb,CAAC,CAAC;AACJ","ignoreList":[]}
@@ -1,60 +1,67 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
- import { headingMarkdown, bodyText, calloutFromParts } from './parse-utils.js';
2
+ import { headingMarkdown, bodyMarkdown } from './parse-utils.js';
3
3
  import { DIAGNOSTIC_TYPES } from '../diagnosticTypes.js';
4
4
  export class TextBlockSectionDefinition {
5
5
  constructor() {
6
6
  _defineProperty(this, "sectionType", 'textBlock');
7
- // The trailing callout is OPTIONAL. A heading plus running copy is the shape
8
- // stores emit constantly for explainer content, and requiring the blockquote
9
- // meant those blocks matched no definition at all and fell through to
10
- // `FallbackSectionDefinition` rendered as undifferentiated prose with no
11
- // section treatment. `parse()` already tolerates a missing callout (it only
12
- // rejects when title AND description are empty).
7
+ // A repeated body, not a single `t` a heading owns ALL the running copy
8
+ // beneath it. With one `t` the pattern took the heading and the FIRST
9
+ // paragraph only;
10
+ // because a pattern matches a PREFIX of the block, every later paragraph fell
11
+ // through to `FallbackSectionDefinition` and rendered as undifferentiated
12
+ // prose with no section treatment. Multi-paragraph explainer copy is the
13
+ // shape stores emit constantly, so most of the block was landing unstyled.
13
14
  //
14
- // NOTE for registry authors: with the callout optional this also matches a
15
- // bare `h2 > t`, and a pattern matches a PREFIX of the block. Register
15
+ // No callout in the grammar. A trailing blockquote is
16
+ // `CalloutSectionDefinition`'s (`html_comment? > callout+`) it sits above
17
+ // `textBlock` in `createSdkRegistry` and in every client registry, so once
18
+ // this pattern stops before the quote the parser's next iteration claims it
19
+ // as its own section. Keeping it here only duplicated that ownership.
20
+ //
21
+ // `link` is in the alternation because `convertToBlockElements` unwraps a
22
+ // paragraph into its inline nodes: `Try narrowing to [men](web5://ask/…)
23
+ // instead` emits `t link t`. With `t+` alone the run ended at that `link`,
24
+ // truncating the section at the first paragraph carrying an ask link — the
25
+ // exact shape DL #134 made the prompts emit. `parse()` uses `bodyMarkdown`,
26
+ // which keeps the link inline rather than deleting it the way `bodyText`
27
+ // does. Entity blocks are unaffected: their links are `link[entityType]` and
28
+ // every registry puts the entity definitions above this one.
29
+ //
30
+ // NOTE for registry authors: this still matches a bare `h2 > t`, so the
31
+ // prefix hazard is unchanged — if anything it claims MORE. Register
16
32
  // `textBlock` BELOW every definition whose block opens `h2 > t` — entity,
17
33
  // entityCollection, listItemsSection, featureCards, ctaBanner, comparison,
18
- // kpi, nextSteps. Above them it claims the heading + intro paragraph off the
34
+ // kpi, nextSteps. Above them it claims the heading + intro copy off the
19
35
  // front of the block and strands the list / links / table that follow
20
36
  // (`EntitySectionDefinition` allows `h2 > (t | callout)* > links…`, and
21
37
  // `listItemsSection` / `ctaBanner` are the same shape). `createSdkRegistry`
22
38
  // and the client registries in this repo order it that way; a client
23
39
  // registry living outside this repo has to do the same.
24
- _defineProperty(this, "patterns", ['html_comment? > h2 > t > callout?']);
40
+ _defineProperty(this, "patterns", ['html_comment? > h2 > (t | link)+']);
25
41
  _defineProperty(this, "defaults", {
26
42
  title: 'Text Block Title',
27
- description: 'Description text.',
28
- callout: {
29
- text: 'A key insight or quote.'
30
- }
43
+ description: 'Description text.'
31
44
  });
32
45
  _defineProperty(this, "fixtures", [{
33
46
  markdown: `## Choosing the Right Liquid Data Option
34
47
 
35
48
  Liquid Data Go is designed for brands that want affordable, scalable access to syndicated POS, shopper, and pricing insights with minimal setup and no data expertise required.
36
49
 
37
- > If you mainly need fast, packaged insights in one category, Liquid Data Go is usually the right fit; if you need to integrate many datasets and support multiple enterprise workflows, the broader Liquid Data platform and its solutions become more relevant.`,
50
+ It covers a single category out of the box, so a brand can stand up reporting without a data team and without negotiating a full platform contract first.
51
+
52
+ The broader Liquid Data platform becomes the better fit once you need to integrate many datasets and support multiple enterprise workflows.`,
38
53
  expected: {
39
54
  title: 'Choosing the Right Liquid Data Option',
40
- description: 'Liquid Data Go is designed for brands that want affordable, scalable access to syndicated POS, shopper, and pricing insights with minimal setup and no data expertise required.',
41
- callout: {
42
- text: 'If you mainly need fast, packaged insights in one category, Liquid Data Go is usually the right fit; if you need to integrate many datasets and support multiple enterprise workflows, the broader Liquid Data platform and its solutions become more relevant.'
43
- }
55
+ description: ['Liquid Data Go is designed for brands that want affordable, scalable access to syndicated POS, shopper, and pricing insights with minimal setup and no data expertise required.', 'It covers a single category out of the box, so a brand can stand up reporting without a data team and without negotiating a full platform contract first.', 'The broader Liquid Data platform becomes the better fit once you need to integrate many datasets and support multiple enterprise workflows.'].join('\n\n')
44
56
  }
45
57
  }, {
46
58
  markdown: `<!-- section: textBlock -->
47
59
  ## Key Takeaway
48
60
 
49
- The platform scales with your needs.
50
-
51
- > Start small with Go, then expand to the full suite as your data maturity grows.`,
61
+ The platform scales with your needs.`,
52
62
  expected: {
53
63
  title: 'Key Takeaway',
54
- description: 'The platform scales with your needs.',
55
- callout: {
56
- text: 'Start small with Go, then expand to the full suite as your data maturity grows.'
57
- }
64
+ description: 'The platform scales with your needs.'
58
65
  }
59
66
  }, {
60
67
  markdown: `## How to choose between a clinical serum and a nourishing dry oil
@@ -62,23 +69,20 @@ The platform scales with your needs.
62
69
  Clinically tested serums with patented actives target visible reduction of marks and uneven tone, while dry oils focus on broad nourishment and a satin finish.`,
63
70
  expected: {
64
71
  title: 'How to choose between a clinical serum and a nourishing dry oil',
65
- description: 'Clinically tested serums with patented actives target visible reduction of marks and uneven tone, while dry oils focus on broad nourishment and a satin finish.',
66
- callout: undefined
72
+ description: 'Clinically tested serums with patented actives target visible reduction of marks and uneven tone, while dry oils focus on broad nourishment and a satin finish.'
67
73
  }
68
74
  }]);
69
75
  }
70
76
  parse(parts, context) {
71
77
  const title = headingMarkdown(parts, 2) || '';
72
- const description = bodyText(parts) || '';
73
- const callout = calloutFromParts(parts);
78
+ const description = bodyMarkdown(parts) || '';
74
79
  if (!title && !description) {
75
80
  context == null || context.reportDiagnostic == null || context.reportDiagnostic(DIAGNOSTIC_TYPES.SECTION_REJECTED, 'textBlock: both title and description are empty');
76
81
  return null;
77
82
  }
78
83
  return {
79
84
  title,
80
- description,
81
- callout
85
+ description
82
86
  };
83
87
  }
84
88
  }
@@ -1 +1 @@
1
- {"version":3,"names":["headingMarkdown","bodyText","calloutFromParts","DIAGNOSTIC_TYPES","TextBlockSectionDefinition","constructor","_defineProperty","title","description","callout","text","markdown","expected","undefined","parse","parts","context","reportDiagnostic","SECTION_REJECTED"],"sources":["../../../../src/component/componentDefinitions/TextBlockSectionDefinition.ts"],"sourcesContent":["import type { Part } from '../../parts/parts';\nimport { headingMarkdown, bodyText, calloutFromParts } from './parse-utils';\nimport type {\n TextBlockCompProps,\n TextBlockSectionComponent,\n SectionFixture,\n} from '../types';\nimport type { SectionDefinition, ParseContext } from '../section-definition';\nimport { DIAGNOSTIC_TYPES } from '../diagnosticTypes';\n\nexport class TextBlockSectionDefinition\n implements SectionDefinition<TextBlockSectionComponent>\n{\n sectionType = 'textBlock' as const;\n // The trailing callout is OPTIONAL. A heading plus running copy is the shape\n // stores emit constantly for explainer content, and requiring the blockquote\n // meant those blocks matched no definition at all and fell through to\n // `FallbackSectionDefinition` rendered as undifferentiated prose with no\n // section treatment. `parse()` already tolerates a missing callout (it only\n // rejects when title AND description are empty).\n //\n // NOTE for registry authors: with the callout optional this also matches a\n // bare `h2 > t`, and a pattern matches a PREFIX of the block. Register\n // `textBlock` BELOW every definition whose block opens `h2 > t` — entity,\n // entityCollection, listItemsSection, featureCards, ctaBanner, comparison,\n // kpi, nextSteps. Above them it claims the heading + intro paragraph off the\n // front of the block and strands the list / links / table that follow\n // (`EntitySectionDefinition` allows `h2 > (t | callout)* > links…`, and\n // `listItemsSection` / `ctaBanner` are the same shape). `createSdkRegistry`\n // and the client registries in this repo order it that way; a client\n // registry living outside this repo has to do the same.\n patterns = ['html_comment? > h2 > t > callout?'];\n\n defaults: TextBlockCompProps = {\n title: 'Text Block Title',\n description: 'Description text.',\n callout: { text: 'A key insight or quote.' },\n };\n\n fixtures: SectionFixture<TextBlockCompProps>[] = [\n {\n markdown: `## Choosing the Right Liquid Data Option\n\nLiquid Data Go is designed for brands that want affordable, scalable access to syndicated POS, shopper, and pricing insights with minimal setup and no data expertise required.\n\n> If you mainly need fast, packaged insights in one category, Liquid Data Go is usually the right fit; if you need to integrate many datasets and support multiple enterprise workflows, the broader Liquid Data platform and its solutions become more relevant.`,\n expected: {\n title: 'Choosing the Right Liquid Data Option',\n description:\n 'Liquid Data Go is designed for brands that want affordable, scalable access to syndicated POS, shopper, and pricing insights with minimal setup and no data expertise required.',\n callout: {\n text: 'If you mainly need fast, packaged insights in one category, Liquid Data Go is usually the right fit; if you need to integrate many datasets and support multiple enterprise workflows, the broader Liquid Data platform and its solutions become more relevant.',\n },\n },\n },\n {\n markdown: `<!-- section: textBlock -->\n## Key Takeaway\n\nThe platform scales with your needs.\n\n> Start small with Go, then expand to the full suite as your data maturity grows.`,\n expected: {\n title: 'Key Takeaway',\n description: 'The platform scales with your needs.',\n callout: {\n text: 'Start small with Go, then expand to the full suite as your data maturity grows.',\n },\n },\n },\n {\n markdown: `## How to choose between a clinical serum and a nourishing dry oil\n\nClinically tested serums with patented actives target visible reduction of marks and uneven tone, while dry oils focus on broad nourishment and a satin finish.`,\n expected: {\n title: 'How to choose between a clinical serum and a nourishing dry oil',\n description:\n 'Clinically tested serums with patented actives target visible reduction of marks and uneven tone, while dry oils focus on broad nourishment and a satin finish.',\n callout: undefined,\n },\n },\n ];\n\n parse(parts: Part[], context?: ParseContext): TextBlockCompProps | null {\n const title = headingMarkdown(parts, 2) || '';\n const description = bodyText(parts) || '';\n const callout = calloutFromParts(parts);\n\n if (!title && !description) {\n context?.reportDiagnostic?.(\n DIAGNOSTIC_TYPES.SECTION_REJECTED,\n 'textBlock: both title and description are empty',\n );\n return null;\n }\n\n return { title, description, callout };\n }\n}\n"],"mappings":";AACA,SAASA,eAAe,EAAEC,QAAQ,EAAEC,gBAAgB,QAAQ,eAAe;AAO3E,SAASC,gBAAgB,QAAQ,oBAAoB;AAErD,OAAO,MAAMC,0BAA0B,CAEvC;EAAAC,YAAA;IAAAC,eAAA,sBACgB,WAAW;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAAAA,eAAA,mBACW,CAAC,mCAAmC,CAAC;IAAAA,eAAA,mBAEjB;MAC7BC,KAAK,EAAE,kBAAkB;MACzBC,WAAW,EAAE,mBAAmB;MAChCC,OAAO,EAAE;QAAEC,IAAI,EAAE;MAA0B;IAC7C,CAAC;IAAAJ,eAAA,mBAEgD,CAC/C;MACEK,QAAQ,EAAE;AAChB;AACA;AACA;AACA,kQAAkQ;MAC5PC,QAAQ,EAAE;QACRL,KAAK,EAAE,uCAAuC;QAC9CC,WAAW,EACT,iLAAiL;QACnLC,OAAO,EAAE;UACPC,IAAI,EAAE;QACR;MACF;IACF,CAAC,EACD;MACEC,QAAQ,EAAE;AAChB;AACA;AACA;AACA;AACA,kFAAkF;MAC5EC,QAAQ,EAAE;QACRL,KAAK,EAAE,cAAc;QACrBC,WAAW,EAAE,sCAAsC;QACnDC,OAAO,EAAE;UACPC,IAAI,EAAE;QACR;MACF;IACF,CAAC,EACD;MACEC,QAAQ,EAAE;AAChB;AACA,gKAAgK;MAC1JC,QAAQ,EAAE;QACRL,KAAK,EAAE,iEAAiE;QACxEC,WAAW,EACT,iKAAiK;QACnKC,OAAO,EAAEI;MACX;IACF,CAAC,CACF;EAAA;EAEDC,KAAKA,CAACC,KAAa,EAAEC,OAAsB,EAA6B;IACtE,MAAMT,KAAK,GAAGP,eAAe,CAACe,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE;IAC7C,MAAMP,WAAW,GAAGP,QAAQ,CAACc,KAAK,CAAC,IAAI,EAAE;IACzC,MAAMN,OAAO,GAAGP,gBAAgB,CAACa,KAAK,CAAC;IAEvC,IAAI,CAACR,KAAK,IAAI,CAACC,WAAW,EAAE;MAC1BQ,OAAO,YAAPA,OAAO,CAAEC,gBAAgB,YAAzBD,OAAO,CAAEC,gBAAgB,CACvBd,gBAAgB,CAACe,gBAAgB,EACjC,iDACF,CAAC;MACD,OAAO,IAAI;IACb;IAEA,OAAO;MAAEX,KAAK;MAAEC,WAAW;MAAEC;IAAQ,CAAC;EACxC;AACF","ignoreList":[]}
1
+ {"version":3,"names":["headingMarkdown","bodyMarkdown","DIAGNOSTIC_TYPES","TextBlockSectionDefinition","constructor","_defineProperty","title","description","markdown","expected","join","parse","parts","context","reportDiagnostic","SECTION_REJECTED"],"sources":["../../../../src/component/componentDefinitions/TextBlockSectionDefinition.ts"],"sourcesContent":["import type { Part } from '../../parts/parts';\nimport { headingMarkdown, bodyMarkdown } from './parse-utils';\nimport type {\n TextBlockCompProps,\n TextBlockSectionComponent,\n SectionFixture,\n} from '../types';\nimport type { SectionDefinition, ParseContext } from '../section-definition';\nimport { DIAGNOSTIC_TYPES } from '../diagnosticTypes';\n\nexport class TextBlockSectionDefinition\n implements SectionDefinition<TextBlockSectionComponent>\n{\n sectionType = 'textBlock' as const;\n // A repeated body, not a single `t` — a heading owns ALL the running copy\n // beneath it. With one `t` the pattern took the heading and the FIRST\n // paragraph only;\n // because a pattern matches a PREFIX of the block, every later paragraph fell\n // through to `FallbackSectionDefinition` and rendered as undifferentiated\n // prose with no section treatment. Multi-paragraph explainer copy is the\n // shape stores emit constantly, so most of the block was landing unstyled.\n //\n // No callout in the grammar. A trailing blockquote is\n // `CalloutSectionDefinition`'s (`html_comment? > callout+`) it sits above\n // `textBlock` in `createSdkRegistry` and in every client registry, so once\n // this pattern stops before the quote the parser's next iteration claims it\n // as its own section. Keeping it here only duplicated that ownership.\n //\n // `link` is in the alternation because `convertToBlockElements` unwraps a\n // paragraph into its inline nodes: `Try narrowing to [men](web5://ask/…)\n // instead` emits `t link t`. With `t+` alone the run ended at that `link`,\n // truncating the section at the first paragraph carrying an ask link — the\n // exact shape DL #134 made the prompts emit. `parse()` uses `bodyMarkdown`,\n // which keeps the link inline rather than deleting it the way `bodyText`\n // does. Entity blocks are unaffected: their links are `link[entityType]` and\n // every registry puts the entity definitions above this one.\n //\n // NOTE for registry authors: this still matches a bare `h2 > t`, so the\n // prefix hazard is unchanged if anything it claims MORE. Register\n // `textBlock` BELOW every definition whose block opens `h2 > t` — entity,\n // entityCollection, listItemsSection, featureCards, ctaBanner, comparison,\n // kpi, nextSteps. Above them it claims the heading + intro copy off the\n // front of the block and strands the list / links / table that follow\n // (`EntitySectionDefinition` allows `h2 > (t | callout)* > links…`, and\n // `listItemsSection` / `ctaBanner` are the same shape). `createSdkRegistry`\n // and the client registries in this repo order it that way; a client\n // registry living outside this repo has to do the same.\n patterns = ['html_comment? > h2 > (t | link)+'];\n\n defaults: TextBlockCompProps = {\n title: 'Text Block Title',\n description: 'Description text.',\n };\n\n fixtures: SectionFixture<TextBlockCompProps>[] = [\n {\n markdown: `## Choosing the Right Liquid Data Option\n\nLiquid Data Go is designed for brands that want affordable, scalable access to syndicated POS, shopper, and pricing insights with minimal setup and no data expertise required.\n\nIt covers a single category out of the box, so a brand can stand up reporting without a data team and without negotiating a full platform contract first.\n\nThe broader Liquid Data platform becomes the better fit once you need to integrate many datasets and support multiple enterprise workflows.`,\n expected: {\n title: 'Choosing the Right Liquid Data Option',\n description: [\n 'Liquid Data Go is designed for brands that want affordable, scalable access to syndicated POS, shopper, and pricing insights with minimal setup and no data expertise required.',\n 'It covers a single category out of the box, so a brand can stand up reporting without a data team and without negotiating a full platform contract first.',\n 'The broader Liquid Data platform becomes the better fit once you need to integrate many datasets and support multiple enterprise workflows.',\n ].join('\\n\\n'),\n },\n },\n {\n markdown: `<!-- section: textBlock -->\n## Key Takeaway\n\nThe platform scales with your needs.`,\n expected: {\n title: 'Key Takeaway',\n description: 'The platform scales with your needs.',\n },\n },\n {\n markdown: `## How to choose between a clinical serum and a nourishing dry oil\n\nClinically tested serums with patented actives target visible reduction of marks and uneven tone, while dry oils focus on broad nourishment and a satin finish.`,\n expected: {\n title:\n 'How to choose between a clinical serum and a nourishing dry oil',\n description:\n 'Clinically tested serums with patented actives target visible reduction of marks and uneven tone, while dry oils focus on broad nourishment and a satin finish.',\n },\n },\n ];\n\n parse(parts: Part[], context?: ParseContext): TextBlockCompProps | null {\n const title = headingMarkdown(parts, 2) || '';\n const description = bodyMarkdown(parts) || '';\n\n if (!title && !description) {\n context?.reportDiagnostic?.(\n DIAGNOSTIC_TYPES.SECTION_REJECTED,\n 'textBlock: both title and description are empty',\n );\n return null;\n }\n\n return { title, description };\n }\n}\n"],"mappings":";AACA,SAASA,eAAe,EAAEC,YAAY,QAAQ,eAAe;AAO7D,SAASC,gBAAgB,QAAQ,oBAAoB;AAErD,OAAO,MAAMC,0BAA0B,CAEvC;EAAAC,YAAA;IAAAC,eAAA,sBACgB,WAAW;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAAAA,eAAA,mBACW,CAAC,kCAAkC,CAAC;IAAAA,eAAA,mBAEhB;MAC7BC,KAAK,EAAE,kBAAkB;MACzBC,WAAW,EAAE;IACf,CAAC;IAAAF,eAAA,mBAEgD,CAC/C;MACEG,QAAQ,EAAE;AAChB;AACA;AACA;AACA;AACA;AACA,4IAA4I;MACtIC,QAAQ,EAAE;QACRH,KAAK,EAAE,uCAAuC;QAC9CC,WAAW,EAAE,CACX,iLAAiL,EACjL,2JAA2J,EAC3J,6IAA6I,CAC9I,CAACG,IAAI,CAAC,MAAM;MACf;IACF,CAAC,EACD;MACEF,QAAQ,EAAE;AAChB;AACA;AACA,qCAAqC;MAC/BC,QAAQ,EAAE;QACRH,KAAK,EAAE,cAAc;QACrBC,WAAW,EAAE;MACf;IACF,CAAC,EACD;MACEC,QAAQ,EAAE;AAChB;AACA,gKAAgK;MAC1JC,QAAQ,EAAE;QACRH,KAAK,EACH,iEAAiE;QACnEC,WAAW,EACT;MACJ;IACF,CAAC,CACF;EAAA;EAEDI,KAAKA,CAACC,KAAa,EAAEC,OAAsB,EAA6B;IACtE,MAAMP,KAAK,GAAGN,eAAe,CAACY,KAAK,EAAE,CAAC,CAAC,IAAI,EAAE;IAC7C,MAAML,WAAW,GAAGN,YAAY,CAACW,KAAK,CAAC,IAAI,EAAE;IAE7C,IAAI,CAACN,KAAK,IAAI,CAACC,WAAW,EAAE;MAC1BM,OAAO,YAAPA,OAAO,CAAEC,gBAAgB,YAAzBD,OAAO,CAAEC,gBAAgB,CACvBZ,gBAAgB,CAACa,gBAAgB,EACjC,iDACF,CAAC;MACD,OAAO,IAAI;IACb;IAEA,OAAO;MAAET,KAAK;MAAEC;IAAY,CAAC;EAC/B;AACF","ignoreList":[]}
@@ -55,10 +55,11 @@ export function createSdkRegistry() {
55
55
  .register(new CalloutSectionDefinition()).register(new HtmlCommentSectionDefinition()).register(new CtaBannerSectionDefinition())
56
56
  // textBlock comes after every definition whose block opens `h2 > t`
57
57
  // (entity, listItems, featureCards, ctaBanner, comparison, …). Its
58
- // trailing callout is optional, so its pattern also matches a bare
59
- // `h2 > t`, and a pattern matches a PREFIX of the block — registered
60
- // above those it would claim the heading + intro paragraph and strand
61
- // the list / links / table that follow in a separate section.
58
+ // `h2 > t+` also matches a bare `h2 > t`, and a pattern matches a PREFIX
59
+ // of the block registered above those it would claim the heading +
60
+ // intro copy and strand the list / links / table that follow in a
61
+ // separate section. It stops before a trailing callout, which Callout
62
+ // (registered above) then claims on the parser's next iteration.
62
63
  .register(new TextBlockSectionDefinition()).register(new FallbackSectionDefinition()).register(new ErrorSectionDefinition());
63
64
  }
64
65
  //# sourceMappingURL=index.js.map