@coffer-org/web-ui 4.0.0 → 5.0.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/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from './host-services.ts';
5
5
  export * from './list-view.ts';
6
6
  export * from './plugin-ui.ts';
7
7
  export * from './site-links.ts';
8
+ export * from './internal-link.ts';
8
9
  export * from './lib/utils.ts';
9
10
  export * from './lib/date-format.ts';
10
11
  export * from './hooks/use-as-ref.ts';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./host-services.js";
5
5
  export * from "./list-view.js";
6
6
  export * from "./plugin-ui.js";
7
7
  export * from "./site-links.js";
8
+ export * from "./internal-link.js";
8
9
  export * from "./lib/utils.js";
9
10
  export * from "./lib/date-format.js";
10
11
  export * from "./hooks/use-as-ref.js";
@@ -0,0 +1,15 @@
1
+ import { type SiteShelf } from './site-links.ts';
2
+ export type HrefKind = {
3
+ kind: 'internal';
4
+ to: string;
5
+ } | {
6
+ kind: 'external';
7
+ href: string;
8
+ } | {
9
+ kind: 'text';
10
+ };
11
+ export type SiteShelfPair = SiteShelf;
12
+ export declare function classifyHref(href: string, origin: string, routes: {
13
+ pattern: string;
14
+ }[], pairs?: SiteShelfPair[]): HrefKind;
15
+ export declare function classifySiteHref(href: string): HrefKind;
@@ -0,0 +1,66 @@
1
+ import { getSiteLinks, getSiteShelves } from "./site-links.js";
2
+ import { getPluginRoutes } from "./plugin-ui.js";
3
+ const SEGMENT = /^[A-Za-z0-9._~%-]+$/;
4
+ function matchesPattern(pathname, pattern) {
5
+ const got = pathname.split('/').filter(Boolean);
6
+ const want = pattern.split('/').filter(Boolean);
7
+ const captured = {};
8
+ for (let i = 0; i < want.length; i++) {
9
+ const w = want[i];
10
+ if (w === '*')
11
+ return got.length >= i ? captured : null;
12
+ const g = got[i];
13
+ if (g === undefined)
14
+ return null;
15
+ if (w.startsWith(':')) {
16
+ if (!SEGMENT.test(g))
17
+ return null;
18
+ captured[w.slice(1)] = g;
19
+ }
20
+ else if (w !== g)
21
+ return null;
22
+ }
23
+ return got.length === want.length ? captured : null;
24
+ }
25
+ function hostOf(origin) {
26
+ try {
27
+ return new URL(origin).hostname;
28
+ }
29
+ catch {
30
+ return '';
31
+ }
32
+ }
33
+ export function classifyHref(href, origin, routes, pairs = []) {
34
+ const raw = href.trim();
35
+ if (!raw || raw.startsWith('//'))
36
+ return { kind: 'text' };
37
+ const isPath = raw.startsWith('/');
38
+ if (!isPath && !/^https?:\/\//i.test(raw))
39
+ return { kind: 'text' };
40
+ let url;
41
+ try {
42
+ url = new URL(raw, origin);
43
+ }
44
+ catch {
45
+ return { kind: 'text' };
46
+ }
47
+ if (url.hostname !== hostOf(origin))
48
+ return { kind: 'external', href: url.href };
49
+ for (const r of routes) {
50
+ const captured = matchesPattern(url.pathname, r.pattern);
51
+ if (!captured)
52
+ continue;
53
+ if (pairs.length && captured['library'] !== undefined && captured['shelf'] !== undefined) {
54
+ const known = pairs.some((p) => p.library === captured['library'] && p.shelf === captured['shelf']);
55
+ if (!known)
56
+ return { kind: 'text' };
57
+ }
58
+ return { kind: 'internal', to: `${url.pathname}${url.search}${url.hash}` };
59
+ }
60
+ return { kind: 'text' };
61
+ }
62
+ export function classifySiteHref(href) {
63
+ const routes = [...getSiteLinks(), ...getPluginRoutes().map((r) => ({ pattern: `/${r.path}` }))];
64
+ const origin = typeof window === 'undefined' ? '' : window.location.origin;
65
+ return classifyHref(href, origin, routes, getSiteShelves());
66
+ }
@@ -1,17 +1,46 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { marked } from 'marked';
3
3
  import DOMPurify from 'dompurify';
4
+ import { useInRouterContext, useNavigate } from 'react-router-dom';
5
+ import { classifySiteHref } from "../internal-link.js";
6
+ const INTERNAL_ATTR = 'data-site-link';
4
7
  DOMPurify.addHook('afterSanitizeAttributes', (node) => {
5
- if (node.tagName === 'A') {
6
- node.setAttribute('target', '_blank');
7
- node.setAttribute('rel', 'noopener noreferrer');
8
+ if (node.tagName !== 'A')
9
+ return;
10
+ const kind = classifySiteHref(node.getAttribute('href') ?? '');
11
+ if (kind.kind === 'internal') {
12
+ node.setAttribute('href', kind.to);
13
+ node.setAttribute(INTERNAL_ATTR, '');
14
+ node.removeAttribute('target');
15
+ node.removeAttribute('rel');
16
+ return;
8
17
  }
18
+ node.setAttribute('target', '_blank');
19
+ node.setAttribute('rel', 'noopener noreferrer');
9
20
  });
10
21
  export function markdownToSafeHtml(src) {
11
22
  const raw = marked.parse(src, { async: false });
12
23
  return DOMPurify.sanitize(raw);
13
24
  }
25
+ function Prose({ value, onClick }) {
26
+ return (_jsx("div", { className: "md-static", onClick: onClick, dangerouslySetInnerHTML: { __html: markdownToSafeHtml(value) } }));
27
+ }
28
+ function RoutedProse({ value }) {
29
+ const navigate = useNavigate();
30
+ return (_jsx(Prose, { value: value, onClick: (event) => {
31
+ const anchor = event.target.closest?.(`a[${INTERNAL_ATTR}]`);
32
+ if (!anchor)
33
+ return;
34
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
35
+ return;
36
+ const to = anchor.getAttribute('href');
37
+ if (!to)
38
+ return;
39
+ event.preventDefault();
40
+ navigate(to);
41
+ } }));
42
+ }
14
43
  export function MarkdownView({ value }) {
15
- return _jsx("div", { className: "md-static", dangerouslySetInnerHTML: { __html: markdownToSafeHtml(value) } });
44
+ return useInRouterContext() ? _jsx(RoutedProse, { value: value }) : _jsx(Prose, { value: value });
16
45
  }
17
46
  export default MarkdownView;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/web-ui",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -25,7 +25,7 @@
25
25
  "postpack": "node ../../scripts/swap-exports.mjs src"
26
26
  },
27
27
  "dependencies": {
28
- "@coffer-org/sdk": "^4.0.0",
28
+ "@coffer-org/sdk": "^5.0.0",
29
29
  "@ark-ui/react": "^5.37.2",
30
30
  "@base-ui/react": "^1.5.0",
31
31
  "@icons-pack/react-simple-icons": "^13.13.0",