@guidelytech/guide-runtime 0.4.1 → 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.
@@ -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: () => import_runtime4.isChecklistComplete,
39
+ isChecklistComplete: () => import_runtime5.isChecklistComplete,
40
40
  isGuidelyElement: () => isGuidelyElement,
41
- isQuestionVisible: () => import_runtime5.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", "iframe", "object", "embed", "form", "input", "textarea", "select", "link", "meta", "base", "template", "svg", "math", "frame", "frameset", "noscript"]);
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 import_runtime3 = require("@guidelytech/shared/runtime");
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, import_runtime3.evaluateRules)(branch.when, ctx)) {
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, import_runtime3.evaluateRules)(step.targeting, ctx)) {
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 import_runtime4 = require("@guidelytech/shared/runtime");
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, import_runtime4.isChecklistComplete)(config, progress);
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 import_runtime5 = require("@guidelytech/shared/runtime");
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, import_runtime5.isQuestionVisible)(q, answers);
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, import_runtime5.isQuestionVisible)(q, answers) && answers[q.id] === void 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) => body.appendChild(el(doc, "div", "g-rc-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 local = data.articles.filter((a) => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q));
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 merged = [...local, ...serverResults.filter((a) => !seen.has(a.id))];
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 collections = [...data.collections].sort((a, b) => a.order - b.order);
2023
- const uncollected = data.articles.filter((a) => !a.collectionId || !collections.some((c) => c.id === a.collectionId));
2024
- collections.forEach((c) => {
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,