@guidelytech/guide-runtime 0.4.0 → 0.5.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/chunk-SGFC66YD.js +322 -0
- package/dist/chunk-SGFC66YD.js.map +1 -0
- package/dist/index.cjs +59 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/messenger.cjs +47 -15
- package/dist/messenger.cjs.map +1 -1
- package/dist/messenger.d.cts +2 -0
- package/dist/messenger.d.ts +2 -0
- package/dist/messenger.js +2 -1
- package/dist/messenger.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/sanitize.ts
|
|
6
|
+
import { EMBED_IFRAME_ATTRS, VIDEO_ATTRS, isAllowedEmbedSrc, isSafeMediaUrl } from "@guidelytech/shared/runtime";
|
|
7
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set(["p", "br", "strong", "b", "em", "i", "u", "a", "ul", "ol", "li", "img", "h1", "h2", "h3", "h4", "code", "pre", "blockquote", "span", "div", "hr", "button"]);
|
|
8
|
+
var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
|
|
9
|
+
var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "data-g-action", "data-g-value"]);
|
|
10
|
+
function isSafeUrl(value, attr, tag) {
|
|
11
|
+
const v = value.trim().replace(/[\u0000-\u001f\s]/g, "").toLowerCase();
|
|
12
|
+
if (!v) return false;
|
|
13
|
+
const m = /^([a-z][a-z0-9+.-]*):/.exec(v);
|
|
14
|
+
if (!m) return true;
|
|
15
|
+
const scheme = m[1];
|
|
16
|
+
if (scheme === "data") return tag === "img" && attr === "src" && v.startsWith("data:image/");
|
|
17
|
+
return scheme === "http" || scheme === "https" || scheme === "mailto" || scheme === "tel";
|
|
18
|
+
}
|
|
19
|
+
function sanitizeElement(el2) {
|
|
20
|
+
const tag = el2.tagName.toLowerCase();
|
|
21
|
+
for (const attr of Array.from(el2.attributes)) {
|
|
22
|
+
const name = attr.name.toLowerCase();
|
|
23
|
+
if (name.startsWith("on") || !ALLOWED_ATTRS.has(name)) {
|
|
24
|
+
el2.removeAttribute(attr.name);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if ((name === "href" || name === "src") && !isSafeUrl(attr.value, name, tag)) el2.removeAttribute(attr.name);
|
|
28
|
+
}
|
|
29
|
+
if (tag === "a") {
|
|
30
|
+
const target = el2.getAttribute("target");
|
|
31
|
+
if (target && target !== "_blank" && target !== "_self") el2.removeAttribute("target");
|
|
32
|
+
if (el2.getAttribute("target") === "_blank") el2.setAttribute("rel", "noopener noreferrer");
|
|
33
|
+
}
|
|
34
|
+
if (tag === "button") el2.setAttribute("type", "button");
|
|
35
|
+
if (tag === "img") {
|
|
36
|
+
el2.setAttribute("loading", "lazy");
|
|
37
|
+
if (!el2.getAttribute("alt")) el2.setAttribute("alt", "");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function rebuildMedia(el2) {
|
|
41
|
+
const doc = el2.ownerDocument;
|
|
42
|
+
const tag = el2.tagName.toLowerCase();
|
|
43
|
+
const src = el2.getAttribute("src");
|
|
44
|
+
let out;
|
|
45
|
+
if (tag === "iframe") {
|
|
46
|
+
if (!isAllowedEmbedSrc(src)) return null;
|
|
47
|
+
out = doc.createElement("iframe");
|
|
48
|
+
out.setAttribute("src", src);
|
|
49
|
+
out.setAttribute("title", el2.getAttribute("title") || "Embedded video");
|
|
50
|
+
for (const [k, v] of Object.entries(EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
|
|
51
|
+
} else {
|
|
52
|
+
if (!isSafeMediaUrl(src)) return null;
|
|
53
|
+
out = doc.createElement("video");
|
|
54
|
+
out.setAttribute("src", src);
|
|
55
|
+
const poster = el2.getAttribute("poster");
|
|
56
|
+
if (isSafeMediaUrl(poster)) out.setAttribute("poster", poster);
|
|
57
|
+
for (const [k, v] of Object.entries(VIDEO_ATTRS)) out.setAttribute(k, v);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function walk(node) {
|
|
62
|
+
const children = Array.from(node.childNodes);
|
|
63
|
+
for (const child of children) {
|
|
64
|
+
if (child.nodeType === 8) {
|
|
65
|
+
node.removeChild(child);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (child.nodeType !== 1) continue;
|
|
69
|
+
const el2 = child;
|
|
70
|
+
const tag = el2.tagName.toLowerCase();
|
|
71
|
+
if (DROP_TAGS.has(tag)) {
|
|
72
|
+
node.removeChild(el2);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (tag === "iframe" || tag === "video") {
|
|
76
|
+
const media = rebuildMedia(el2);
|
|
77
|
+
if (media) node.replaceChild(media, el2);
|
|
78
|
+
else node.removeChild(el2);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
82
|
+
while (el2.firstChild) node.insertBefore(el2.firstChild, el2);
|
|
83
|
+
node.removeChild(el2);
|
|
84
|
+
walk(node);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
sanitizeElement(el2);
|
|
88
|
+
walk(el2);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function sanitizeToFragment(html, doc = document) {
|
|
92
|
+
const tpl = doc.createElement("template");
|
|
93
|
+
tpl.innerHTML = html || "";
|
|
94
|
+
walk(tpl.content);
|
|
95
|
+
return tpl.content;
|
|
96
|
+
}
|
|
97
|
+
function sanitizeHtml(html, doc = document) {
|
|
98
|
+
const frag = sanitizeToFragment(html, doc);
|
|
99
|
+
const div = doc.createElement("div");
|
|
100
|
+
div.appendChild(frag);
|
|
101
|
+
return div.innerHTML;
|
|
102
|
+
}
|
|
103
|
+
function setSanitizedHtml(el2, html) {
|
|
104
|
+
el2.textContent = "";
|
|
105
|
+
el2.appendChild(sanitizeToFragment(html, el2.ownerDocument));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/help-view.ts
|
|
109
|
+
import { isArticleListed, resourceCenterSections } from "@guidelytech/shared/runtime";
|
|
110
|
+
function el(doc, tag, cls, text) {
|
|
111
|
+
const e = doc.createElement(tag);
|
|
112
|
+
if (cls) e.className = cls;
|
|
113
|
+
if (text != null) e.textContent = text;
|
|
114
|
+
return e;
|
|
115
|
+
}
|
|
116
|
+
function listItem(doc, icon, title, summary) {
|
|
117
|
+
const b = el(doc, "button", "g-rc-item");
|
|
118
|
+
b.type = "button";
|
|
119
|
+
const i = el(doc, "span", "g-rc-icon", icon);
|
|
120
|
+
i.setAttribute("aria-hidden", "true");
|
|
121
|
+
const text = el(doc, "span");
|
|
122
|
+
text.appendChild(el(doc, "div", "g-rc-title", title));
|
|
123
|
+
if (summary) text.appendChild(el(doc, "div", "g-rc-sum", summary));
|
|
124
|
+
b.append(i, text);
|
|
125
|
+
return b;
|
|
126
|
+
}
|
|
127
|
+
function createHelpView(options) {
|
|
128
|
+
const { doc } = options;
|
|
129
|
+
let query = "";
|
|
130
|
+
let serverResults = [];
|
|
131
|
+
let searchSeq = 0;
|
|
132
|
+
let openSeq = 0;
|
|
133
|
+
let active = null;
|
|
134
|
+
const root = el(doc, "div", "g-help");
|
|
135
|
+
const searchWrap = el(doc, "div", "g-rc-search");
|
|
136
|
+
const input = el(doc, "input", "g-input");
|
|
137
|
+
input.type = "search";
|
|
138
|
+
input.placeholder = "Search help articles\u2026";
|
|
139
|
+
input.setAttribute("aria-label", "Search help articles");
|
|
140
|
+
searchWrap.appendChild(input);
|
|
141
|
+
const body = el(doc, "div", "g-panel-body");
|
|
142
|
+
root.append(searchWrap, body);
|
|
143
|
+
const hasBody = (a) => typeof a.body === "string" && !a.bodyOmitted;
|
|
144
|
+
const openArticle = (article) => {
|
|
145
|
+
if (article.externalUrl) {
|
|
146
|
+
window.open(article.externalUrl, "_blank", "noopener,noreferrer");
|
|
147
|
+
if (hasBody(article)) options.onArticleView?.(article);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const seq = ++openSeq;
|
|
151
|
+
if (hasBody(article) || !options.loadArticle) {
|
|
152
|
+
active = { article, loading: false };
|
|
153
|
+
if (hasBody(article)) options.onArticleView?.(article);
|
|
154
|
+
return render();
|
|
155
|
+
}
|
|
156
|
+
active = { article, loading: true };
|
|
157
|
+
render();
|
|
158
|
+
options.loadArticle(article.id).then((full) => {
|
|
159
|
+
if (seq !== openSeq || !active) return;
|
|
160
|
+
active = { article: full ?? { ...article, body: "" }, loading: false };
|
|
161
|
+
if (full) options.onArticleView?.(full);
|
|
162
|
+
render();
|
|
163
|
+
}).catch(() => {
|
|
164
|
+
if (seq !== openSeq || !active) return;
|
|
165
|
+
active = { article: { ...article, body: "" }, loading: false };
|
|
166
|
+
render();
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
const renderArticle = ({ article, loading }) => {
|
|
170
|
+
const wrap = el(doc, "div", "g-rc-article");
|
|
171
|
+
const back = el(doc, "button", "g-btn g-btn-link g-rc-back", "\u2190 Back");
|
|
172
|
+
back.type = "button";
|
|
173
|
+
back.addEventListener("click", () => {
|
|
174
|
+
openSeq++;
|
|
175
|
+
active = null;
|
|
176
|
+
render();
|
|
177
|
+
});
|
|
178
|
+
const title = el(doc, "h3", "g-title", article.title);
|
|
179
|
+
const content = el(doc, "div", "g-body");
|
|
180
|
+
if (loading) content.appendChild(el(doc, "div", "g-rc-empty", "Loading\u2026"));
|
|
181
|
+
else if (article.body) setSanitizedHtml(content, article.body);
|
|
182
|
+
else content.appendChild(el(doc, "div", "g-rc-empty", "This article couldn't be loaded."));
|
|
183
|
+
wrap.append(back, title, content);
|
|
184
|
+
body.appendChild(wrap);
|
|
185
|
+
back.focus?.({ preventScroll: true });
|
|
186
|
+
};
|
|
187
|
+
const section = (label, depth = 0) => {
|
|
188
|
+
const s = body.appendChild(el(doc, "div", "g-rc-section", label));
|
|
189
|
+
if (depth) s.dataset.depth = String(depth);
|
|
190
|
+
return s;
|
|
191
|
+
};
|
|
192
|
+
const articleButton = (a) => {
|
|
193
|
+
const b = listItem(doc, "\u{1F4C4}", a.title, a.summary);
|
|
194
|
+
b.addEventListener("click", () => openArticle(a));
|
|
195
|
+
body.appendChild(b);
|
|
196
|
+
};
|
|
197
|
+
function render() {
|
|
198
|
+
body.textContent = "";
|
|
199
|
+
if (active) return renderArticle(active);
|
|
200
|
+
const data = options.data();
|
|
201
|
+
const member = options.audienceIds?.() ?? [];
|
|
202
|
+
const sections = resourceCenterSections(data.collections, data.articles, member);
|
|
203
|
+
const q = query.trim().toLowerCase();
|
|
204
|
+
if (q) {
|
|
205
|
+
const visible = sections.flatMap((s) => s.articles);
|
|
206
|
+
const local = visible.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
|
|
207
|
+
const seen = new Set(local.map((a) => a.id));
|
|
208
|
+
const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
|
|
209
|
+
const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && isArticleListed(data.collections, a, member))];
|
|
210
|
+
section(`Results (${merged.length})`);
|
|
211
|
+
if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
|
|
212
|
+
merged.forEach(articleButton);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (data.recommended?.length) {
|
|
216
|
+
section("Recommended");
|
|
217
|
+
data.recommended.forEach((g) => {
|
|
218
|
+
const b = listItem(doc, "\u25B6", g.name, "Start guide");
|
|
219
|
+
b.addEventListener("click", () => options.onStartGuide?.(g.id));
|
|
220
|
+
body.appendChild(b);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (data.checklists?.length) {
|
|
224
|
+
section("Checklists");
|
|
225
|
+
data.checklists.forEach((g) => {
|
|
226
|
+
const b = listItem(doc, "\u2611", g.name, "Open checklist");
|
|
227
|
+
b.addEventListener("click", () => options.onOpenChecklist?.(g.id));
|
|
228
|
+
body.appendChild(b);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
for (const s of sections) {
|
|
232
|
+
section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
|
|
233
|
+
s.articles.forEach(articleButton);
|
|
234
|
+
}
|
|
235
|
+
if (data.faqs?.length) {
|
|
236
|
+
section("FAQ");
|
|
237
|
+
data.faqs.forEach((f) => {
|
|
238
|
+
const b = listItem(doc, "?", f.question);
|
|
239
|
+
b.setAttribute("aria-expanded", "false");
|
|
240
|
+
const a = el(doc, "div", "g-faq-a");
|
|
241
|
+
setSanitizedHtml(a, f.answer);
|
|
242
|
+
a.hidden = true;
|
|
243
|
+
b.addEventListener("click", () => {
|
|
244
|
+
a.hidden = !a.hidden;
|
|
245
|
+
b.setAttribute("aria-expanded", String(!a.hidden));
|
|
246
|
+
});
|
|
247
|
+
body.append(b, a);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (data.links?.length) {
|
|
251
|
+
section("Links");
|
|
252
|
+
data.links.forEach((l) => {
|
|
253
|
+
const a = el(doc, "a", "g-rc-item");
|
|
254
|
+
a.href = l.url;
|
|
255
|
+
a.target = "_blank";
|
|
256
|
+
a.rel = "noopener noreferrer";
|
|
257
|
+
const i = el(doc, "span", "g-rc-icon", "\u2197");
|
|
258
|
+
i.setAttribute("aria-hidden", "true");
|
|
259
|
+
a.append(i, el(doc, "span", "g-rc-title", l.label));
|
|
260
|
+
body.appendChild(a);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (!body.childNodes.length) body.appendChild(el(doc, "div", "g-rc-empty", "Nothing here yet."));
|
|
264
|
+
}
|
|
265
|
+
let debounce = null;
|
|
266
|
+
const onInput = () => {
|
|
267
|
+
query = input.value;
|
|
268
|
+
openSeq++;
|
|
269
|
+
active = null;
|
|
270
|
+
render();
|
|
271
|
+
if (debounce) clearTimeout(debounce);
|
|
272
|
+
if (!options.search || !query.trim()) {
|
|
273
|
+
serverResults = [];
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const seq = ++searchSeq;
|
|
277
|
+
debounce = setTimeout(() => {
|
|
278
|
+
options.search?.(query.trim()).then((results) => {
|
|
279
|
+
if (seq !== searchSeq) return;
|
|
280
|
+
serverResults = Array.isArray(results) ? results : [];
|
|
281
|
+
render();
|
|
282
|
+
}).catch(() => {
|
|
283
|
+
});
|
|
284
|
+
}, 250);
|
|
285
|
+
};
|
|
286
|
+
input.addEventListener("input", onInput);
|
|
287
|
+
return {
|
|
288
|
+
el: root,
|
|
289
|
+
render,
|
|
290
|
+
reset() {
|
|
291
|
+
openSeq++;
|
|
292
|
+
active = null;
|
|
293
|
+
query = "";
|
|
294
|
+
input.value = "";
|
|
295
|
+
serverResults = [];
|
|
296
|
+
},
|
|
297
|
+
openArticle(id) {
|
|
298
|
+
const listed = options.data().articles.find((a) => a.id === id) ?? { id, slug: id, title: "Article", summary: "", updatedAt: "", bodyOmitted: true };
|
|
299
|
+
openArticle(listed);
|
|
300
|
+
},
|
|
301
|
+
focusSearch: () => input.focus?.({ preventScroll: true }),
|
|
302
|
+
setQuery(q) {
|
|
303
|
+
input.value = q;
|
|
304
|
+
onInput();
|
|
305
|
+
},
|
|
306
|
+
destroy() {
|
|
307
|
+
if (debounce) clearTimeout(debounce);
|
|
308
|
+
input.removeEventListener("input", onInput);
|
|
309
|
+
root.remove();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export {
|
|
315
|
+
__publicField,
|
|
316
|
+
sanitizeToFragment,
|
|
317
|
+
sanitizeHtml,
|
|
318
|
+
setSanitizedHtml,
|
|
319
|
+
el,
|
|
320
|
+
createHelpView
|
|
321
|
+
};
|
|
322
|
+
//# sourceMappingURL=chunk-SGFC66YD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sanitize.ts","../src/help-view.ts"],"sourcesContent":["import { EMBED_IFRAME_ATTRS, VIDEO_ATTRS, isAllowedEmbedSrc, isSafeMediaUrl } from \"@guidelytech/shared/runtime\";\n\nconst ALLOWED_TAGS = new Set([\"p\", \"br\", \"strong\", \"b\", \"em\", \"i\", \"u\", \"a\", \"ul\", \"ol\", \"li\", \"img\", \"h1\", \"h2\", \"h3\", \"h4\", \"code\", \"pre\", \"blockquote\", \"span\", \"div\", \"hr\", \"button\"]);\nconst DROP_TAGS = new Set([\"script\", \"style\", \"object\", \"embed\", \"form\", \"input\", \"textarea\", \"select\", \"link\", \"meta\", \"base\", \"template\", \"svg\", \"math\", \"frame\", \"frameset\", \"noscript\"]);\nconst ALLOWED_ATTRS = new Set([\"href\", \"src\", \"alt\", \"title\", \"target\", \"rel\", \"class\", \"data-g-action\", \"data-g-value\"]);\nfunction isSafeUrl(value: string, attr: string, tag: string): boolean {\n // eslint-disable-next-line no-control-regex\n const v = value.trim().replace(/[\\u0000-\\u001f\\s]/g, \"\").toLowerCase();\n if (!v) return false;\n const m = /^([a-z][a-z0-9+.-]*):/.exec(v);\n if (!m) return true; // relative URL\n const scheme = m[1];\n if (scheme === \"data\") return tag === \"img\" && attr === \"src\" && v.startsWith(\"data:image/\");\n return scheme === \"http\" || scheme === \"https\" || scheme === \"mailto\" || scheme === \"tel\";\n}\n\nfunction sanitizeElement(el: Element): void {\n const tag = el.tagName.toLowerCase();\n for (const attr of Array.from(el.attributes)) {\n const name = attr.name.toLowerCase();\n if (name.startsWith(\"on\") || !ALLOWED_ATTRS.has(name)) {\n el.removeAttribute(attr.name);\n continue;\n }\n if ((name === \"href\" || name === \"src\") && !isSafeUrl(attr.value, name, tag)) el.removeAttribute(attr.name);\n }\n if (tag === \"a\") {\n const target = el.getAttribute(\"target\");\n if (target && target !== \"_blank\" && target !== \"_self\") el.removeAttribute(\"target\");\n if (el.getAttribute(\"target\") === \"_blank\") el.setAttribute(\"rel\", \"noopener noreferrer\");\n }\n if (tag === \"button\") el.setAttribute(\"type\", \"button\");\n if (tag === \"img\") {\n el.setAttribute(\"loading\", \"lazy\");\n if (!el.getAttribute(\"alt\")) el.setAttribute(\"alt\", \"\");\n }\n}\n\n/**\n * Media elements are rebuilt from scratch rather than filtered: an `<iframe>` only for a canonical\n * YouTube/Vimeo/Loom/Wistia embed, a `<video>` only for an absolute http(s) file, each with fixed\n * attributes. Returns null when the element must go.\n */\nfunction rebuildMedia(el: Element): Element | null {\n const doc = el.ownerDocument;\n const tag = el.tagName.toLowerCase();\n const src = el.getAttribute(\"src\");\n let out: Element;\n if (tag === \"iframe\") {\n if (!isAllowedEmbedSrc(src)) return null;\n out = doc.createElement(\"iframe\");\n out.setAttribute(\"src\", src!);\n out.setAttribute(\"title\", el.getAttribute(\"title\") || \"Embedded video\");\n for (const [k, v] of Object.entries(EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);\n } else {\n if (!isSafeMediaUrl(src)) return null;\n out = doc.createElement(\"video\");\n out.setAttribute(\"src\", src!);\n const poster = el.getAttribute(\"poster\");\n if (isSafeMediaUrl(poster)) out.setAttribute(\"poster\", poster!);\n for (const [k, v] of Object.entries(VIDEO_ATTRS)) out.setAttribute(k, v);\n }\n return out;\n}\n\nfunction walk(node: Node): void {\n const children = Array.from(node.childNodes);\n for (const child of children) {\n if (child.nodeType === 8 /* comment */) {\n node.removeChild(child);\n continue;\n }\n if (child.nodeType !== 1) continue;\n const el = child as Element;\n const tag = el.tagName.toLowerCase();\n if (DROP_TAGS.has(tag)) {\n node.removeChild(el);\n continue;\n }\n if (tag === \"iframe\" || tag === \"video\") {\n const media = rebuildMedia(el);\n if (media) node.replaceChild(media, el);\n else node.removeChild(el);\n continue;\n }\n if (!ALLOWED_TAGS.has(tag)) {\n // Unwrap: keep text/children, drop the element itself.\n while (el.firstChild) node.insertBefore(el.firstChild, el);\n node.removeChild(el);\n // Re-walk the parent since children moved up.\n walk(node);\n return;\n }\n sanitizeElement(el);\n walk(el);\n }\n}\n\n/**\n * Parses HTML into a sanitized DocumentFragment using a strict allowlist. No external\n * dependency: a `<template>` element parses inertly and the tree is walked element by element.\n */\nexport function sanitizeToFragment(html: string, doc: Document = document): DocumentFragment {\n const tpl = doc.createElement(\"template\");\n tpl.innerHTML = html || \"\";\n walk(tpl.content);\n return tpl.content;\n}\n\n/** Returns sanitized HTML as a string. */\nexport function sanitizeHtml(html: string, doc: Document = document): string {\n const frag = sanitizeToFragment(html, doc);\n const div = doc.createElement(\"div\");\n div.appendChild(frag);\n return div.innerHTML;\n}\n\n/** Replaces the content of `el` with the sanitized HTML. */\nexport function setSanitizedHtml(el: Element, html: string): void {\n el.textContent = \"\";\n el.appendChild(sanitizeToFragment(html, el.ownerDocument));\n}\n","import type { ResourceCenterArticle, ResourceCenterConfig } from \"@guidelytech/shared\";\nimport { isArticleListed, resourceCenterSections } from \"@guidelytech/shared/runtime\";\nimport { setSanitizedHtml } from \"./sanitize\";\n\nexport interface HelpGuideRef {\n id: string;\n key: string;\n name: string;\n kind: string;\n}\n\n/** What the help view lists. Article bodies may be missing (`bodyOmitted` or summaries only): they load on open. */\nexport interface HelpData {\n collections: ResourceCenterConfig[\"collections\"];\n articles: Array<Omit<ResourceCenterArticle, \"body\"> & { body?: string }>;\n recommended?: HelpGuideRef[];\n checklists?: HelpGuideRef[];\n faqs?: ResourceCenterConfig[\"faqs\"];\n links?: ResourceCenterConfig[\"links\"];\n}\n\nexport interface HelpViewOptions {\n doc: Document;\n data: () => HelpData;\n /** Server search. Results are merged with the client-side title/summary filter. */\n search?: (q: string) => Promise<ResourceCenterArticle[]>;\n /** The visitor's audiences; articles and collections targeted at others aren't listed. */\n audienceIds?: () => ReadonlySet<string> | readonly string[];\n /** Loads an article body that isn't in `data()`. */\n loadArticle?: (id: string) => Promise<ResourceCenterArticle | null>;\n onStartGuide?: (guideId: string) => void;\n onOpenChecklist?: (guideId: string) => void;\n onArticleView?: (article: ResourceCenterArticle) => void;\n}\n\nexport interface HelpView {\n /** Search box + scrollable body. */\n el: HTMLElement;\n render(): void;\n /** Back to the article list and clear the search. */\n reset(): void;\n openArticle(id: string): void;\n focusSearch(): void;\n setQuery(q: string): void;\n destroy(): void;\n}\n\nexport function el<K extends keyof HTMLElementTagNameMap>(doc: Document, tag: K, cls?: string, text?: string): HTMLElementTagNameMap[K] {\n const e = doc.createElement(tag);\n if (cls) e.className = cls;\n if (text != null) e.textContent = text;\n return e;\n}\n\nexport function listItem(doc: Document, icon: string, title: string, summary?: string): HTMLButtonElement {\n const b = el(doc, \"button\", \"g-rc-item\");\n b.type = \"button\";\n const i = el(doc, \"span\", \"g-rc-icon\", icon);\n i.setAttribute(\"aria-hidden\", \"true\");\n const text = el(doc, \"span\");\n text.appendChild(el(doc, \"div\", \"g-rc-title\", title));\n if (summary) text.appendChild(el(doc, \"div\", \"g-rc-sum\", summary));\n b.append(i, text);\n return b;\n}\n\ntype ListedArticle = HelpData[\"articles\"][number];\n\n/** Article browser shared by the resource center and the messenger Help tab. */\nexport function createHelpView(options: HelpViewOptions): HelpView {\n const { doc } = options;\n let query = \"\";\n let serverResults: ResourceCenterArticle[] = [];\n let searchSeq = 0;\n let openSeq = 0;\n let active: { article: ListedArticle; loading: boolean } | null = null;\n\n const root = el(doc, \"div\", \"g-help\");\n const searchWrap = el(doc, \"div\", \"g-rc-search\");\n const input = el(doc, \"input\", \"g-input\");\n input.type = \"search\";\n input.placeholder = \"Search help articles…\";\n input.setAttribute(\"aria-label\", \"Search help articles\");\n searchWrap.appendChild(input);\n const body = el(doc, \"div\", \"g-panel-body\");\n root.append(searchWrap, body);\n\n const hasBody = (a: ListedArticle): a is ResourceCenterArticle => typeof a.body === \"string\" && !a.bodyOmitted;\n\n const openArticle = (article: ListedArticle) => {\n if (article.externalUrl) {\n window.open(article.externalUrl, \"_blank\", \"noopener,noreferrer\");\n if (hasBody(article)) options.onArticleView?.(article);\n return;\n }\n const seq = ++openSeq;\n if (hasBody(article) || !options.loadArticle) {\n active = { article, loading: false };\n if (hasBody(article)) options.onArticleView?.(article);\n return render();\n }\n active = { article, loading: true };\n render();\n options\n .loadArticle(article.id)\n .then((full) => {\n if (seq !== openSeq || !active) return;\n active = { article: full ?? { ...article, body: \"\" }, loading: false };\n if (full) options.onArticleView?.(full);\n render();\n })\n .catch(() => {\n if (seq !== openSeq || !active) return;\n active = { article: { ...article, body: \"\" }, loading: false };\n render();\n });\n };\n\n const renderArticle = ({ article, loading }: NonNullable<typeof active>) => {\n const wrap = el(doc, \"div\", \"g-rc-article\");\n const back = el(doc, \"button\", \"g-btn g-btn-link g-rc-back\", \"← Back\");\n back.type = \"button\";\n back.addEventListener(\"click\", () => {\n openSeq++;\n active = null;\n render();\n });\n const title = el(doc, \"h3\", \"g-title\", article.title);\n const content = el(doc, \"div\", \"g-body\");\n if (loading) content.appendChild(el(doc, \"div\", \"g-rc-empty\", \"Loading…\"));\n else if (article.body) setSanitizedHtml(content, article.body);\n else content.appendChild(el(doc, \"div\", \"g-rc-empty\", \"This article couldn't be loaded.\"));\n wrap.append(back, title, content);\n body.appendChild(wrap);\n back.focus?.({ preventScroll: true });\n };\n\n const section = (label: string, depth = 0) => {\n const s = body.appendChild(el(doc, \"div\", \"g-rc-section\", label));\n if (depth) s.dataset.depth = String(depth);\n return s;\n };\n const articleButton = (a: ListedArticle) => {\n const b = listItem(doc, \"📄\", a.title, a.summary);\n b.addEventListener(\"click\", () => openArticle(a));\n body.appendChild(b);\n };\n\n function render() {\n body.textContent = \"\";\n if (active) return renderArticle(active);\n const data = options.data();\n const member = options.audienceIds?.() ?? [];\n const sections = resourceCenterSections(data.collections, data.articles, member);\n const q = query.trim().toLowerCase();\n if (q) {\n const visible = sections.flatMap((s) => s.articles);\n const local = visible.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));\n const seen = new Set(local.map((a) => a.id));\n // Server search covers every published article: keep only the ones listed for this visitor.\n const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));\n const merged: ListedArticle[] = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && isArticleListed(data.collections, a, member))];\n section(`Results (${merged.length})`);\n if (!merged.length) body.appendChild(el(doc, \"div\", \"g-rc-empty\", \"No articles match your search.\"));\n merged.forEach(articleButton);\n return;\n }\n if (data.recommended?.length) {\n section(\"Recommended\");\n data.recommended.forEach((g) => {\n const b = listItem(doc, \"▶\", g.name, \"Start guide\");\n b.addEventListener(\"click\", () => options.onStartGuide?.(g.id));\n body.appendChild(b);\n });\n }\n if (data.checklists?.length) {\n section(\"Checklists\");\n data.checklists.forEach((g) => {\n const b = listItem(doc, \"☑\", g.name, \"Open checklist\");\n b.addEventListener(\"click\", () => options.onOpenChecklist?.(g.id));\n body.appendChild(b);\n });\n }\n // Nested collections read as \"Billing › Invoices\", depth first.\n for (const s of sections) {\n section(s.collection ? s.path.join(\" › \") : \"Articles\", s.depth);\n s.articles.forEach(articleButton);\n }\n if (data.faqs?.length) {\n section(\"FAQ\");\n data.faqs.forEach((f) => {\n const b = listItem(doc, \"?\", f.question);\n b.setAttribute(\"aria-expanded\", \"false\");\n const a = el(doc, \"div\", \"g-faq-a\");\n setSanitizedHtml(a, f.answer);\n a.hidden = true;\n b.addEventListener(\"click\", () => {\n a.hidden = !a.hidden;\n b.setAttribute(\"aria-expanded\", String(!a.hidden));\n });\n body.append(b, a);\n });\n }\n if (data.links?.length) {\n section(\"Links\");\n data.links.forEach((l) => {\n const a = el(doc, \"a\", \"g-rc-item\");\n a.href = l.url;\n a.target = \"_blank\";\n a.rel = \"noopener noreferrer\";\n const i = el(doc, \"span\", \"g-rc-icon\", \"↗\");\n i.setAttribute(\"aria-hidden\", \"true\");\n a.append(i, el(doc, \"span\", \"g-rc-title\", l.label));\n body.appendChild(a);\n });\n }\n if (!body.childNodes.length) body.appendChild(el(doc, \"div\", \"g-rc-empty\", \"Nothing here yet.\"));\n }\n\n let debounce: ReturnType<typeof setTimeout> | null = null;\n const onInput = () => {\n query = input.value;\n openSeq++;\n active = null;\n render();\n if (debounce) clearTimeout(debounce);\n if (!options.search || !query.trim()) {\n serverResults = [];\n return;\n }\n const seq = ++searchSeq;\n debounce = setTimeout(() => {\n options\n .search?.(query.trim())\n .then((results) => {\n if (seq !== searchSeq) return;\n serverResults = Array.isArray(results) ? results : [];\n render();\n })\n .catch(() => {});\n }, 250);\n };\n input.addEventListener(\"input\", onInput);\n\n return {\n el: root,\n render,\n reset() {\n openSeq++;\n active = null;\n query = \"\";\n input.value = \"\";\n serverResults = [];\n },\n openArticle(id: string) {\n const listed = options.data().articles.find((a) => a.id === id) ?? { id, slug: id, title: \"Article\", summary: \"\", updatedAt: \"\", bodyOmitted: true };\n openArticle(listed);\n },\n focusSearch: () => input.focus?.({ preventScroll: true }),\n setQuery(q: string) {\n input.value = q;\n onInput();\n },\n destroy() {\n if (debounce) clearTimeout(debounce);\n input.removeEventListener(\"input\", onInput);\n root.remove();\n },\n };\n}\n"],"mappings":"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","names":["el"]}
|
package/dist/index.cjs
CHANGED
|
@@ -36,9 +36,9 @@ __export(src_exports, {
|
|
|
36
36
|
generateSelectors: () => generateSelectors,
|
|
37
37
|
highlightSelectors: () => highlightSelectors,
|
|
38
38
|
isAnchoredStep: () => isAnchoredStep,
|
|
39
|
-
isChecklistComplete: () =>
|
|
39
|
+
isChecklistComplete: () => import_runtime5.isChecklistComplete,
|
|
40
40
|
isGuidelyElement: () => isGuidelyElement,
|
|
41
|
-
isQuestionVisible: () =>
|
|
41
|
+
isQuestionVisible: () => import_runtime6.isQuestionVisible,
|
|
42
42
|
isStableId: () => isStableId,
|
|
43
43
|
isVisible: () => isVisible,
|
|
44
44
|
normalizeText: () => normalizeText,
|
|
@@ -353,8 +353,9 @@ function describeElement(el2) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
// src/sanitize.ts
|
|
356
|
+
var import_runtime3 = require("@guidelytech/shared/runtime");
|
|
356
357
|
var ALLOWED_TAGS = /* @__PURE__ */ new Set(["p", "br", "strong", "b", "em", "i", "u", "a", "ul", "ol", "li", "img", "h1", "h2", "h3", "h4", "code", "pre", "blockquote", "span", "div", "hr", "button"]);
|
|
357
|
-
var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "
|
|
358
|
+
var DROP_TAGS = /* @__PURE__ */ new Set(["script", "style", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
|
|
358
359
|
var ALLOWED_ATTRS = /* @__PURE__ */ new Set(["href", "src", "alt", "title", "target", "rel", "class", "data-g-action", "data-g-value"]);
|
|
359
360
|
function isSafeUrl(value, attr, tag) {
|
|
360
361
|
const v = value.trim().replace(/[\u0000-\u001f\s]/g, "").toLowerCase();
|
|
@@ -386,6 +387,27 @@ function sanitizeElement(el2) {
|
|
|
386
387
|
if (!el2.getAttribute("alt")) el2.setAttribute("alt", "");
|
|
387
388
|
}
|
|
388
389
|
}
|
|
390
|
+
function rebuildMedia(el2) {
|
|
391
|
+
const doc = el2.ownerDocument;
|
|
392
|
+
const tag = el2.tagName.toLowerCase();
|
|
393
|
+
const src = el2.getAttribute("src");
|
|
394
|
+
let out;
|
|
395
|
+
if (tag === "iframe") {
|
|
396
|
+
if (!(0, import_runtime3.isAllowedEmbedSrc)(src)) return null;
|
|
397
|
+
out = doc.createElement("iframe");
|
|
398
|
+
out.setAttribute("src", src);
|
|
399
|
+
out.setAttribute("title", el2.getAttribute("title") || "Embedded video");
|
|
400
|
+
for (const [k, v] of Object.entries(import_runtime3.EMBED_IFRAME_ATTRS)) out.setAttribute(k, v);
|
|
401
|
+
} else {
|
|
402
|
+
if (!(0, import_runtime3.isSafeMediaUrl)(src)) return null;
|
|
403
|
+
out = doc.createElement("video");
|
|
404
|
+
out.setAttribute("src", src);
|
|
405
|
+
const poster = el2.getAttribute("poster");
|
|
406
|
+
if ((0, import_runtime3.isSafeMediaUrl)(poster)) out.setAttribute("poster", poster);
|
|
407
|
+
for (const [k, v] of Object.entries(import_runtime3.VIDEO_ATTRS)) out.setAttribute(k, v);
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
389
411
|
function walk(node) {
|
|
390
412
|
const children = Array.from(node.childNodes);
|
|
391
413
|
for (const child of children) {
|
|
@@ -400,6 +422,12 @@ function walk(node) {
|
|
|
400
422
|
node.removeChild(el2);
|
|
401
423
|
continue;
|
|
402
424
|
}
|
|
425
|
+
if (tag === "iframe" || tag === "video") {
|
|
426
|
+
const media = rebuildMedia(el2);
|
|
427
|
+
if (media) node.replaceChild(media, el2);
|
|
428
|
+
else node.removeChild(el2);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
403
431
|
if (!ALLOWED_TAGS.has(tag)) {
|
|
404
432
|
while (el2.firstChild) node.insertBefore(el2.firstChild, el2);
|
|
405
433
|
node.removeChild(el2);
|
|
@@ -473,6 +501,9 @@ var STYLES = `
|
|
|
473
501
|
.g-body ul,.g-body ol{margin:6px 0;padding-left:20px}
|
|
474
502
|
.g-body a{color:var(--g-primary)}
|
|
475
503
|
.g-body img{max-width:100%;height:auto;border-radius:6px}
|
|
504
|
+
.g-body video,.g-body iframe{display:block;width:100%;max-width:100%;border:0;border-radius:6px;margin:8px 0;background:#000}
|
|
505
|
+
.g-body video{height:auto;max-height:60vh}
|
|
506
|
+
.g-body iframe{aspect-ratio:16/9;height:auto}
|
|
476
507
|
.g-body code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;background:var(--g-subtle);padding:1px 4px;border-radius:4px}
|
|
477
508
|
.g-body pre{background:var(--g-subtle);padding:10px;border-radius:6px;overflow:auto}
|
|
478
509
|
.g-body blockquote{margin:6px 0;padding-left:10px;border-left:3px solid var(--g-border);color:var(--g-muted)}
|
|
@@ -547,6 +578,7 @@ var STYLES = `
|
|
|
547
578
|
/* resource center */
|
|
548
579
|
.g-rc-search{padding:12px 20px 4px}
|
|
549
580
|
.g-rc-section{padding:8px 20px 4px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--g-muted);font-weight:600}
|
|
581
|
+
.g-rc-section[data-depth]{text-transform:none;letter-spacing:0;font-size:12px}
|
|
550
582
|
.g-rc-item{display:flex;align-items:flex-start;gap:10px;width:100%;padding:10px 20px;border:0;background:transparent;font:inherit;color:inherit;text-align:left;cursor:pointer;min-height:44px;text-decoration:none}
|
|
551
583
|
.g-rc-item:hover{background:var(--g-subtle)}
|
|
552
584
|
.g-rc-item .g-rc-title{font-weight:500}
|
|
@@ -1165,7 +1197,7 @@ function isAnchoredStep(step) {
|
|
|
1165
1197
|
}
|
|
1166
1198
|
|
|
1167
1199
|
// src/controller.ts
|
|
1168
|
-
var
|
|
1200
|
+
var import_runtime4 = require("@guidelytech/shared/runtime");
|
|
1169
1201
|
function noop() {
|
|
1170
1202
|
}
|
|
1171
1203
|
function runGuide(guide, options = {}) {
|
|
@@ -1247,7 +1279,7 @@ function runGuide(guide, options = {}) {
|
|
|
1247
1279
|
const ctx = getCtx();
|
|
1248
1280
|
if (step && ctx) {
|
|
1249
1281
|
for (const branch of step.branches) {
|
|
1250
|
-
if ((0,
|
|
1282
|
+
if ((0, import_runtime4.evaluateRules)(branch.when, ctx)) {
|
|
1251
1283
|
target = steps.findIndex((s) => s.id === branch.goTo);
|
|
1252
1284
|
if (target >= 0) break;
|
|
1253
1285
|
}
|
|
@@ -1401,7 +1433,7 @@ function runGuide(guide, options = {}) {
|
|
|
1401
1433
|
index = i;
|
|
1402
1434
|
const step = steps[i];
|
|
1403
1435
|
const ctx = getCtx();
|
|
1404
|
-
if (step.targeting && step.targeting.rules.length && ctx && !(0,
|
|
1436
|
+
if (step.targeting && step.targeting.rules.length && ctx && !(0, import_runtime4.evaluateRules)(step.targeting, ctx)) {
|
|
1405
1437
|
const nextIndex = i + direction;
|
|
1406
1438
|
if (nextIndex >= total) return end("completed");
|
|
1407
1439
|
if (nextIndex < 0) return;
|
|
@@ -1495,7 +1527,7 @@ function runGuide(guide, options = {}) {
|
|
|
1495
1527
|
}
|
|
1496
1528
|
|
|
1497
1529
|
// src/checklist.ts
|
|
1498
|
-
var
|
|
1530
|
+
var import_runtime5 = require("@guidelytech/shared/runtime");
|
|
1499
1531
|
function renderChecklist(options) {
|
|
1500
1532
|
const { scope, config } = options;
|
|
1501
1533
|
const doc = scope.ownerDocument;
|
|
@@ -1570,7 +1602,7 @@ function renderChecklist(options) {
|
|
|
1570
1602
|
bar.setAttribute("aria-valuemax", "100");
|
|
1571
1603
|
body.textContent = "";
|
|
1572
1604
|
foot.textContent = "";
|
|
1573
|
-
const complete = (0,
|
|
1605
|
+
const complete = (0, import_runtime5.isChecklistComplete)(config, progress);
|
|
1574
1606
|
if (complete) {
|
|
1575
1607
|
const wrap = doc.createElement("div");
|
|
1576
1608
|
wrap.className = "g-complete";
|
|
@@ -1658,7 +1690,7 @@ function renderChecklist(options) {
|
|
|
1658
1690
|
}
|
|
1659
1691
|
|
|
1660
1692
|
// src/survey.ts
|
|
1661
|
-
var
|
|
1693
|
+
var import_runtime6 = require("@guidelytech/shared/runtime");
|
|
1662
1694
|
function renderSurvey(options) {
|
|
1663
1695
|
const { scope, config } = options;
|
|
1664
1696
|
const doc = scope.ownerDocument;
|
|
@@ -1850,14 +1882,14 @@ function renderSurvey(options) {
|
|
|
1850
1882
|
config.questions.forEach((q) => {
|
|
1851
1883
|
const el2 = questionEls.get(q.id);
|
|
1852
1884
|
if (!el2) return;
|
|
1853
|
-
const visible = (0,
|
|
1885
|
+
const visible = (0, import_runtime6.isQuestionVisible)(q, answers);
|
|
1854
1886
|
el2.hidden = !visible;
|
|
1855
1887
|
if (!visible && answers[q.id] !== void 0) delete answers[q.id];
|
|
1856
1888
|
});
|
|
1857
1889
|
}
|
|
1858
1890
|
form.addEventListener("submit", (e) => {
|
|
1859
1891
|
e.preventDefault();
|
|
1860
|
-
const missing = config.questions.filter((q) => q.required && (0,
|
|
1892
|
+
const missing = config.questions.filter((q) => q.required && (0, import_runtime6.isQuestionVisible)(q, answers) && answers[q.id] === void 0);
|
|
1861
1893
|
if (missing.length) {
|
|
1862
1894
|
errorEl.textContent = `Please answer: ${missing.map((q) => q.title).join(", ")}`;
|
|
1863
1895
|
questionEls.get(missing[0].id)?.querySelector("button,textarea")?.focus();
|
|
@@ -1906,6 +1938,7 @@ function renderSurvey(options) {
|
|
|
1906
1938
|
}
|
|
1907
1939
|
|
|
1908
1940
|
// src/help-view.ts
|
|
1941
|
+
var import_runtime7 = require("@guidelytech/shared/runtime");
|
|
1909
1942
|
function el(doc, tag, cls, text) {
|
|
1910
1943
|
const e = doc.createElement(tag);
|
|
1911
1944
|
if (cls) e.className = cls;
|
|
@@ -1983,7 +2016,11 @@ function createHelpView(options) {
|
|
|
1983
2016
|
body.appendChild(wrap);
|
|
1984
2017
|
back.focus?.({ preventScroll: true });
|
|
1985
2018
|
};
|
|
1986
|
-
const section = (label
|
|
2019
|
+
const section = (label, depth = 0) => {
|
|
2020
|
+
const s = body.appendChild(el(doc, "div", "g-rc-section", label));
|
|
2021
|
+
if (depth) s.dataset.depth = String(depth);
|
|
2022
|
+
return s;
|
|
2023
|
+
};
|
|
1987
2024
|
const articleButton = (a) => {
|
|
1988
2025
|
const b = listItem(doc, "\u{1F4C4}", a.title, a.summary);
|
|
1989
2026
|
b.addEventListener("click", () => openArticle(a));
|
|
@@ -1993,11 +2030,15 @@ function createHelpView(options) {
|
|
|
1993
2030
|
body.textContent = "";
|
|
1994
2031
|
if (active) return renderArticle(active);
|
|
1995
2032
|
const data = options.data();
|
|
2033
|
+
const member = options.audienceIds?.() ?? [];
|
|
2034
|
+
const sections = (0, import_runtime7.resourceCenterSections)(data.collections, data.articles, member);
|
|
1996
2035
|
const q = query.trim().toLowerCase();
|
|
1997
2036
|
if (q) {
|
|
1998
|
-
const
|
|
2037
|
+
const visible = sections.flatMap((s) => s.articles);
|
|
2038
|
+
const local = visible.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
|
|
1999
2039
|
const seen = new Set(local.map((a) => a.id));
|
|
2000
|
-
const
|
|
2040
|
+
const hidden = new Set(data.articles.map((a) => a.id).filter((id) => !visible.some((v) => v.id === id)));
|
|
2041
|
+
const merged = [...local, ...serverResults.filter((a) => !seen.has(a.id) && !hidden.has(a.id) && (0, import_runtime7.isArticleListed)(data.collections, a, member))];
|
|
2001
2042
|
section(`Results (${merged.length})`);
|
|
2002
2043
|
if (!merged.length) body.appendChild(el(doc, "div", "g-rc-empty", "No articles match your search."));
|
|
2003
2044
|
merged.forEach(articleButton);
|
|
@@ -2019,17 +2060,9 @@ function createHelpView(options) {
|
|
|
2019
2060
|
body.appendChild(b);
|
|
2020
2061
|
});
|
|
2021
2062
|
}
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
const articles = data.articles.filter((a) => a.collectionId === c.id);
|
|
2026
|
-
if (!articles.length) return;
|
|
2027
|
-
section(c.name);
|
|
2028
|
-
articles.forEach(articleButton);
|
|
2029
|
-
});
|
|
2030
|
-
if (uncollected.length) {
|
|
2031
|
-
section("Articles");
|
|
2032
|
-
uncollected.forEach(articleButton);
|
|
2063
|
+
for (const s of sections) {
|
|
2064
|
+
section(s.collection ? s.path.join(" \u203A ") : "Articles", s.depth);
|
|
2065
|
+
s.articles.forEach(articleButton);
|
|
2033
2066
|
}
|
|
2034
2067
|
if (data.faqs?.length) {
|
|
2035
2068
|
section("FAQ");
|
|
@@ -2144,6 +2177,7 @@ function renderResourceCenter(options) {
|
|
|
2144
2177
|
doc,
|
|
2145
2178
|
data: () => ({ collections: config.collections, articles: config.articles, recommended: byId(config.recommendedGuideIds), checklists: byId(config.checklistGuideIds), faqs: config.faqs, links: config.links }),
|
|
2146
2179
|
search: options.search,
|
|
2180
|
+
audienceIds: options.audienceIds,
|
|
2147
2181
|
loadArticle: options.loadArticle,
|
|
2148
2182
|
onStartGuide: options.onStartGuide,
|
|
2149
2183
|
onOpenChecklist: options.onOpenChecklist,
|