@bendyline/squisq-react 2.2.0 → 2.3.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,396 @@
1
+ import {
2
+ MermaidDiagram
3
+ } from "./chunk-WLUZTUNZ.js";
4
+ import {
5
+ useMediaUrl
6
+ } from "./chunk-LR3AIGDD.js";
7
+
8
+ // src/InlineVideoPlayer.tsx
9
+ import { jsx } from "react/jsx-runtime";
10
+ function InlineVideoPlayer({
11
+ src,
12
+ basePath = "",
13
+ width,
14
+ height,
15
+ poster,
16
+ controls = true,
17
+ preload = "metadata",
18
+ className
19
+ }) {
20
+ const resolvedSrc = useMediaUrl(src, basePath);
21
+ const resolvedPoster = useMediaUrl(poster ?? "", basePath);
22
+ const posterUrl = poster ? resolvedPoster : void 0;
23
+ if (!resolvedSrc) return null;
24
+ return /* @__PURE__ */ jsx("span", { className: `squisq-inline-video-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx(
25
+ "video",
26
+ {
27
+ src: resolvedSrc,
28
+ poster: posterUrl,
29
+ controls,
30
+ preload,
31
+ width,
32
+ height,
33
+ playsInline: true
34
+ }
35
+ ) });
36
+ }
37
+
38
+ // src/InlineAudioPlayer.tsx
39
+ import { jsx as jsx2 } from "react/jsx-runtime";
40
+ function InlineAudioPlayer({
41
+ src,
42
+ basePath = "",
43
+ controls = true,
44
+ preload = "metadata",
45
+ className
46
+ }) {
47
+ const resolvedSrc = useMediaUrl(src, basePath);
48
+ if (!resolvedSrc) return null;
49
+ return /* @__PURE__ */ jsx2("span", { className: `squisq-inline-audio-player ${className ?? ""}`.trim(), children: /* @__PURE__ */ jsx2("audio", { src: resolvedSrc, controls, preload }) });
50
+ }
51
+
52
+ // src/MarkdownRenderer.tsx
53
+ import { Fragment } from "react";
54
+ import {
55
+ sanitizeHtmlNodes,
56
+ sanitizeUrl
57
+ } from "@bendyline/squisq/markdown";
58
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
59
+ var DEFAULT_CTX = { htmlPolicy: "sanitize" };
60
+ function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
61
+ return nodes.map((node, i) => {
62
+ const key = `${keyPrefix}i${i}`;
63
+ switch (node.type) {
64
+ case "text": {
65
+ if (!node.value.includes("\n")) {
66
+ return /* @__PURE__ */ jsx3(Fragment, { children: node.value }, key);
67
+ }
68
+ const parts = node.value.split("\n");
69
+ return /* @__PURE__ */ jsx3(Fragment, { children: parts.map((part, j) => /* @__PURE__ */ jsxs(Fragment, { children: [
70
+ j > 0 && /* @__PURE__ */ jsx3("br", {}),
71
+ part
72
+ ] }, j)) }, key);
73
+ }
74
+ case "emphasis":
75
+ return /* @__PURE__ */ jsx3("em", { className: "squisq-md-em", children: renderInline(node.children, key, ctx) }, key);
76
+ case "strong":
77
+ return /* @__PURE__ */ jsx3("strong", { className: "squisq-md-strong", children: renderInline(node.children, key, ctx) }, key);
78
+ case "delete":
79
+ return /* @__PURE__ */ jsx3("del", { className: "squisq-md-del", children: renderInline(node.children, key, ctx) }, key);
80
+ case "inlineCode":
81
+ return /* @__PURE__ */ jsx3("code", { className: "squisq-md-inline-code", children: node.value }, key);
82
+ case "link": {
83
+ const href = sanitizeUrl(node.url, "link", { extraLinkSchemes: ctx.linkSchemes });
84
+ if (!href) {
85
+ return /* @__PURE__ */ jsx3("span", { className: "squisq-md-link squisq-md-link--blocked", children: renderInline(node.children, key, ctx) }, key);
86
+ }
87
+ return /* @__PURE__ */ jsx3(
88
+ "a",
89
+ {
90
+ className: "squisq-md-link",
91
+ href,
92
+ title: node.title ?? void 0,
93
+ target: "_blank",
94
+ rel: "noopener noreferrer",
95
+ children: renderInline(node.children, key, ctx)
96
+ },
97
+ key
98
+ );
99
+ }
100
+ case "image":
101
+ return /* @__PURE__ */ jsx3(MdImage, { src: node.url, alt: node.alt ?? "", title: node.title ?? void 0 }, key);
102
+ case "break":
103
+ return /* @__PURE__ */ jsx3("br", {}, key);
104
+ case "inlineMath":
105
+ return /* @__PURE__ */ jsx3("code", { className: "squisq-md-inline-math", children: node.value }, key);
106
+ case "htmlInline":
107
+ if (ctx.htmlPolicy === "strip") return null;
108
+ return /* @__PURE__ */ jsx3("span", { className: "squisq-md-html-inline", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
109
+ case "footnoteReference":
110
+ return /* @__PURE__ */ jsx3("sup", { className: "squisq-md-footnote-ref", children: /* @__PURE__ */ jsxs("a", { href: `#fn-${node.identifier}`, children: [
111
+ "[",
112
+ node.label ?? node.identifier,
113
+ "]"
114
+ ] }) }, key);
115
+ case "linkReference":
116
+ return /* @__PURE__ */ jsx3("span", { className: "squisq-md-link-ref", children: renderInline(node.children, key, ctx) }, key);
117
+ case "imageReference":
118
+ return /* @__PURE__ */ jsxs("span", { className: "squisq-md-image-ref", children: [
119
+ "[",
120
+ node.alt ?? node.identifier,
121
+ "]"
122
+ ] }, key);
123
+ case "textDirective":
124
+ return /* @__PURE__ */ jsx3("span", { className: "squisq-md-text-directive", "data-directive": node.name, children: renderInline(node.children, key, ctx) }, key);
125
+ case "mention":
126
+ return /* @__PURE__ */ jsxs(
127
+ "span",
128
+ {
129
+ className: "squisq-md-mention mention",
130
+ "data-mention": "true",
131
+ "data-kind": node.targetKind,
132
+ "data-id": node.targetId,
133
+ "data-label": node.displayName,
134
+ children: [
135
+ "@",
136
+ node.displayName
137
+ ]
138
+ },
139
+ key
140
+ );
141
+ default:
142
+ return null;
143
+ }
144
+ });
145
+ }
146
+ function renderBlock(node, key, ctx = DEFAULT_CTX) {
147
+ switch (node.type) {
148
+ case "paragraph":
149
+ return /* @__PURE__ */ jsx3("p", { className: "squisq-md-p", children: renderInline(node.children, key, ctx) }, key);
150
+ case "heading": {
151
+ const Tag = `h${node.depth}`;
152
+ return /* @__PURE__ */ jsx3(Tag, { className: `squisq-md-heading squisq-md-h${node.depth}`, children: renderInline(node.children, key, ctx) }, key);
153
+ }
154
+ case "blockquote":
155
+ return /* @__PURE__ */ jsx3("blockquote", { className: "squisq-md-blockquote", children: renderBlocks(node.children, key, ctx) }, key);
156
+ case "list":
157
+ if (node.ordered) {
158
+ return /* @__PURE__ */ jsx3("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
159
+ }
160
+ return /* @__PURE__ */ jsx3("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
161
+ case "code":
162
+ if (node.lang?.trim().toLowerCase() === "mermaid") {
163
+ return /* @__PURE__ */ jsx3(
164
+ MermaidDiagram,
165
+ {
166
+ source: node.value,
167
+ className: "squisq-md-mermaid",
168
+ ariaLabel: "Mermaid diagram",
169
+ theme: ctx.theme
170
+ },
171
+ key
172
+ );
173
+ }
174
+ return /* @__PURE__ */ jsx3("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx3("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
175
+ case "thematicBreak":
176
+ return /* @__PURE__ */ jsx3("hr", { className: "squisq-md-hr" }, key);
177
+ case "table":
178
+ return renderTable(node.children, node.align, key, ctx);
179
+ case "htmlBlock":
180
+ if (ctx.htmlPolicy === "strip") return null;
181
+ return /* @__PURE__ */ jsx3("div", { className: "squisq-md-html-block", children: renderHtmlNodes(resolveHtmlNodes(node.htmlChildren, ctx.htmlPolicy), `${key}h`, ctx) }, key);
182
+ case "math":
183
+ return /* @__PURE__ */ jsx3("pre", { className: "squisq-md-math-block", children: /* @__PURE__ */ jsx3("code", { children: node.value }) }, key);
184
+ case "definition":
185
+ return null;
186
+ case "footnoteDefinition":
187
+ return /* @__PURE__ */ jsxs("div", { className: "squisq-md-footnote-def", id: `fn-${node.identifier}`, children: [
188
+ /* @__PURE__ */ jsx3("sup", { children: node.label ?? node.identifier }),
189
+ renderBlocks(node.children, key, ctx)
190
+ ] }, key);
191
+ case "containerDirective":
192
+ return /* @__PURE__ */ jsxs(
193
+ "div",
194
+ {
195
+ className: `squisq-md-directive squisq-md-directive-${node.name}`,
196
+ "data-directive": node.name,
197
+ children: [
198
+ node.label && /* @__PURE__ */ jsx3("div", { className: "squisq-md-directive-label", children: node.label }),
199
+ renderBlocks(node.children, key, ctx)
200
+ ]
201
+ },
202
+ key
203
+ );
204
+ case "leafDirective":
205
+ return /* @__PURE__ */ jsx3(
206
+ "div",
207
+ {
208
+ className: `squisq-md-directive squisq-md-directive-${node.name}`,
209
+ "data-directive": node.name,
210
+ children: renderInline(node.children, key, ctx)
211
+ },
212
+ key
213
+ );
214
+ case "definitionList":
215
+ return /* @__PURE__ */ jsx3("dl", { className: "squisq-md-dl", children: node.children.map((child, i) => {
216
+ if (child.type === "definitionTerm") {
217
+ return /* @__PURE__ */ jsx3("dt", { className: "squisq-md-dt", children: renderInline(child.children, `${key}dt${i}`, ctx) }, `${key}dt${i}`);
218
+ }
219
+ return /* @__PURE__ */ jsx3("dd", { className: "squisq-md-dd", children: renderBlocks(child.children, `${key}dd${i}`, ctx) }, `${key}dd${i}`);
220
+ }) }, key);
221
+ default:
222
+ return null;
223
+ }
224
+ }
225
+ function renderListItem(item, key, ctx = DEFAULT_CTX) {
226
+ const isTask = item.checked !== null && item.checked !== void 0;
227
+ return /* @__PURE__ */ jsxs("li", { className: `squisq-md-li${isTask ? " squisq-md-task" : ""}`, children: [
228
+ isTask && /* @__PURE__ */ jsx3("input", { type: "checkbox", checked: !!item.checked, readOnly: true, className: "squisq-md-checkbox" }),
229
+ renderBlocks(item.children, key, ctx)
230
+ ] }, key);
231
+ }
232
+ function renderTable(rows, align, key, ctx = DEFAULT_CTX) {
233
+ const [headerRow, ...bodyRows] = rows;
234
+ return /* @__PURE__ */ jsxs("table", { className: "squisq-md-table", children: [
235
+ headerRow && /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsx3("tr", { children: headerRow.children.map((cell, ci) => /* @__PURE__ */ jsx3(
236
+ "th",
237
+ {
238
+ className: "squisq-md-th",
239
+ style: align?.[ci] ? { textAlign: align[ci] } : void 0,
240
+ children: renderInline(cell.children, `${key}th${ci}`, ctx)
241
+ },
242
+ `${key}th${ci}`
243
+ )) }) }),
244
+ bodyRows.length > 0 && /* @__PURE__ */ jsx3("tbody", { children: bodyRows.map((row, ri) => /* @__PURE__ */ jsx3("tr", { children: row.children.map((cell, ci) => /* @__PURE__ */ jsx3(
245
+ "td",
246
+ {
247
+ className: "squisq-md-td",
248
+ style: align?.[ci] ? { textAlign: align[ci] } : void 0,
249
+ children: renderInline(cell.children, `${key}td${ri}-${ci}`, ctx)
250
+ },
251
+ `${key}td${ri}-${ci}`
252
+ )) }, `${key}tr${ri}`)) })
253
+ ] }, key);
254
+ }
255
+ function renderBlocks(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
256
+ return nodes.map((node, i) => renderBlock(node, `${keyPrefix}b${i}`, ctx));
257
+ }
258
+ function MdImage({ src, alt, title }) {
259
+ const safeSrc = sanitizeUrl(src, "media");
260
+ const resolved = useMediaUrl(safeSrc ?? "", ".");
261
+ if (!safeSrc) return null;
262
+ return /* @__PURE__ */ jsx3("img", { className: "squisq-md-image", src: resolved, alt, title });
263
+ }
264
+ function resolveHtmlNodes(nodes, htmlPolicy) {
265
+ if (htmlPolicy === "strip") return [];
266
+ if (htmlPolicy === "trusted") return nodes;
267
+ return sanitizeHtmlNodes(nodes);
268
+ }
269
+ var DANGEROUS_HTML_TAGS = /* @__PURE__ */ new Set([
270
+ "base",
271
+ "embed",
272
+ "iframe",
273
+ "link",
274
+ "meta",
275
+ "object",
276
+ "script",
277
+ "style",
278
+ "title"
279
+ ]);
280
+ var PASSTHROUGH_ATTRS = {
281
+ // common
282
+ class: "className",
283
+ id: "id",
284
+ title: "title",
285
+ style: "style",
286
+ // media-adjacent (used when video/audio appear inside other wrappers)
287
+ width: "width",
288
+ height: "height",
289
+ src: "src",
290
+ alt: "alt",
291
+ loading: "loading",
292
+ decoding: "decoding",
293
+ // anchor
294
+ href: "href",
295
+ target: "target",
296
+ rel: "rel"
297
+ };
298
+ var RE_USED_TARGETS = /* @__PURE__ */ new Set(["", "_self", "_parent", "_top"]);
299
+ function reactPropsFromAttrs(attrs, ctx) {
300
+ const out = {};
301
+ for (const [name, value] of Object.entries(attrs)) {
302
+ const propName = PASSTHROUGH_ATTRS[name];
303
+ if (!propName) continue;
304
+ if (propName === "style") {
305
+ out["data-style"] = value;
306
+ continue;
307
+ }
308
+ if (propName === "href") {
309
+ const href = sanitizeUrl(value, "link", { extraLinkSchemes: ctx.linkSchemes });
310
+ if (href) out.href = href;
311
+ continue;
312
+ }
313
+ if (propName === "src") {
314
+ const src = sanitizeUrl(value, "media");
315
+ if (src) out.src = src;
316
+ continue;
317
+ }
318
+ out[propName] = value;
319
+ }
320
+ if (typeof out.target === "string" && !RE_USED_TARGETS.has(out.target.toLowerCase())) {
321
+ const rel = typeof out.rel === "string" ? out.rel : "";
322
+ const tokens = rel.split(/\s+/).filter((t) => t && t.toLowerCase() !== "opener");
323
+ if (!tokens.some((t) => t.toLowerCase() === "noopener")) tokens.push("noopener");
324
+ out.rel = tokens.join(" ");
325
+ }
326
+ return out;
327
+ }
328
+ function renderHtmlElement(el, key, ctx) {
329
+ const tagName = el.tagName.toLowerCase();
330
+ if (DANGEROUS_HTML_TAGS.has(tagName)) return null;
331
+ if (tagName === "video") {
332
+ const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
333
+ const poster = sanitizeUrl(el.attributes.poster ?? "", "media") ?? void 0;
334
+ return /* @__PURE__ */ jsx3(
335
+ InlineVideoPlayer,
336
+ {
337
+ src,
338
+ width: el.attributes.width,
339
+ height: el.attributes.height,
340
+ poster,
341
+ controls: "controls" in el.attributes,
342
+ preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
343
+ },
344
+ key
345
+ );
346
+ }
347
+ if (tagName === "audio") {
348
+ const src = sanitizeUrl(el.attributes.src ?? "", "media") ?? "";
349
+ return /* @__PURE__ */ jsx3(
350
+ InlineAudioPlayer,
351
+ {
352
+ src,
353
+ controls: "controls" in el.attributes,
354
+ preload: el.attributes.preload === "none" || el.attributes.preload === "metadata" || el.attributes.preload === "auto" ? el.attributes.preload : void 0
355
+ },
356
+ key
357
+ );
358
+ }
359
+ const Tag = tagName;
360
+ const props = reactPropsFromAttrs(el.attributes, ctx);
361
+ if (el.selfClosing) {
362
+ return /* @__PURE__ */ jsx3(Tag, { ...props }, key);
363
+ }
364
+ return /* @__PURE__ */ jsx3(Tag, { ...props, children: renderHtmlNodes(el.children, `${key}c`, ctx) }, key);
365
+ }
366
+ function renderHtmlNodes(nodes, keyPrefix, ctx = DEFAULT_CTX) {
367
+ return nodes.map((node, i) => {
368
+ const key = `${keyPrefix}${i}`;
369
+ switch (node.type) {
370
+ case "htmlElement":
371
+ return renderHtmlElement(node, key, ctx);
372
+ case "htmlText":
373
+ return /* @__PURE__ */ jsx3(Fragment, { children: node.value }, key);
374
+ case "htmlComment":
375
+ return null;
376
+ default:
377
+ return null;
378
+ }
379
+ });
380
+ }
381
+ function MarkdownRenderer({
382
+ nodes,
383
+ className,
384
+ htmlPolicy = "sanitize",
385
+ linkSchemes,
386
+ theme
387
+ }) {
388
+ if (!nodes || nodes.length === 0) return null;
389
+ return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes, theme }) });
390
+ }
391
+
392
+ export {
393
+ InlineVideoPlayer,
394
+ InlineAudioPlayer,
395
+ MarkdownRenderer
396
+ };
@@ -0,0 +1,58 @@
1
+ // src/hooks/MediaContext.tsx
2
+ import { createContext, useContext, useState, useEffect, useMemo } from "react";
3
+ import {
4
+ DEFAULT_INTERACTIVE_RESOURCE_POLICY,
5
+ isResourceUrlAllowed
6
+ } from "@bendyline/squisq/markdown";
7
+ var MediaContext = createContext(null);
8
+ var ResourcePolicyContext = createContext(
9
+ DEFAULT_INTERACTIVE_RESOURCE_POLICY
10
+ );
11
+ function useResourcePolicy() {
12
+ return useContext(ResourcePolicyContext);
13
+ }
14
+ function useMediaProvider() {
15
+ return useContext(MediaContext);
16
+ }
17
+ function useMediaUrl(relativePath, basePath) {
18
+ const provider = useMediaProvider();
19
+ const resourcePolicy = useResourcePolicy();
20
+ const safePath = typeof relativePath === "string" ? relativePath : "";
21
+ const isAbsolute = !safePath || /^(?:[a-z][a-z0-9+.-]*:|\/\/|\/)/i.test(safePath);
22
+ const fallback = useMemo(() => {
23
+ const candidate = isAbsolute ? safePath : `${basePath.replace(/\/$/, "")}/${safePath}`;
24
+ return isResourceUrlAllowed(candidate, resourcePolicy) ? candidate : "";
25
+ }, [isAbsolute, safePath, basePath, resourcePolicy]);
26
+ const needsProvider = !isAbsolute && !!provider;
27
+ const [url, setUrl] = useState(fallback);
28
+ useEffect(() => {
29
+ if (!needsProvider) {
30
+ setUrl(fallback);
31
+ return;
32
+ }
33
+ let cancelled = false;
34
+ setUrl(fallback);
35
+ provider.resolveUrl(safePath).then(
36
+ (resolved) => {
37
+ if (!cancelled) {
38
+ setUrl(isResourceUrlAllowed(resolved, resourcePolicy) ? resolved : "");
39
+ }
40
+ },
41
+ () => {
42
+ if (!cancelled) setUrl(fallback);
43
+ }
44
+ );
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [needsProvider, provider, safePath, fallback, resourcePolicy]);
49
+ return needsProvider ? url : fallback;
50
+ }
51
+
52
+ export {
53
+ MediaContext,
54
+ ResourcePolicyContext,
55
+ useResourcePolicy,
56
+ useMediaProvider,
57
+ useMediaUrl
58
+ };