@globalbrain/sefirot 4.23.1 → 4.24.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/config/vite.js CHANGED
@@ -46,8 +46,10 @@ export const baseConfig = {
46
46
  'sefirot/': fileURLToPath(new URL('../lib/', import.meta.url))
47
47
  },
48
48
 
49
+ // list the client-side direct dependencies/peerDependencies which get bundled
49
50
  dedupe: [
50
51
  '@sentry/browser',
52
+ '@sentry/vue',
51
53
  '@tanstack/vue-virtual',
52
54
  '@tinyhttp/content-disposition',
53
55
  '@tinyhttp/cookie',
@@ -56,10 +58,12 @@ export const baseConfig = {
56
58
  '@vuelidate/validators',
57
59
  '@vueuse/core',
58
60
  'body-scroll-lock',
59
- 'dayjs',
60
61
  'd3',
62
+ 'dayjs',
63
+ 'dompurify',
61
64
  'file-saver',
62
65
  'fuse.js',
66
+ 'html2canvas',
63
67
  'lodash-es',
64
68
  'markdown-it',
65
69
  'normalize.css',
@@ -67,8 +71,8 @@ export const baseConfig = {
67
71
  'pinia',
68
72
  'qs',
69
73
  'v-calendar',
70
- 'vue-router',
71
- 'vue'
74
+ 'vue',
75
+ 'vue-router'
72
76
  ]
73
77
  },
74
78
 
@@ -1,52 +1,24 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, shallowRef, watch } from 'vue'
3
- import { type LinkCallback, type LinkSubscriberPayload, useLink, useMarkdown } from '../composables/Markdown'
2
+ import { computed } from 'vue'
3
+ import { useMarkdown } from '../composables/Markdown'
4
4
 
5
- const props = defineProps<{
5
+ const props = withDefaults(defineProps<{
6
6
  tag?: string
7
7
  content: string
8
- callbacks?: LinkCallback[]
8
+ html?: boolean
9
9
  inline?: boolean
10
- }>()
11
-
12
- const emit = defineEmits<{
13
- clicked: [payload: LinkSubscriberPayload]
14
- }>()
15
-
16
- const container = shallowRef<Element | null>(null)
17
-
18
- const { addListeners, subscribe } = useLink({
19
- container,
20
- callbacks: props.callbacks
10
+ }>(), {
11
+ tag: 'div',
12
+ html: true
21
13
  })
22
14
 
23
- const markdown = useMarkdown()
24
- const rendered = computed(() => markdown(props.content, props.inline ?? false))
25
-
26
- watch(
27
- rendered,
28
- () => nextTick(() => addListeners()),
29
- { immediate: true }
30
- )
31
-
32
- subscribe((payload) => emit('clicked', payload))
15
+ const markdown = useMarkdown({ html: props.html, inline: props.inline })
16
+ const rendered = computed(() => markdown(props.content))
33
17
  </script>
34
18
 
35
19
  <template>
36
- <component
37
- v-if="$slots.default"
38
- :is="tag ?? 'div'"
39
- class="SMarkdown-container"
40
- ref="container"
41
- >
20
+ <component v-if="$slots.default" :is="tag" class="SMarkdown-container">
42
21
  <slot v-bind="{ rendered }" />
43
22
  </component>
44
-
45
- <component
46
- v-else
47
- :is="tag ?? 'div'"
48
- class="SMarkdown-container"
49
- ref="container"
50
- v-html="rendered"
51
- />
23
+ <component v-else :is="tag" class="SMarkdown-container" v-html="rendered" />
52
24
  </template>
@@ -1,34 +1,54 @@
1
+ import DOMPurify, { type Config } from 'dompurify'
1
2
  import MarkdownIt, { type Options as MarkdownItOptions } from 'markdown-it'
2
- import { type Ref, onUnmounted } from 'vue'
3
- import { useRouter } from 'vue-router'
4
- import { type LinkAttrs, isCallbackUrl, isExternalUrl, linkPlugin } from './markdown/LinkPlugin'
5
3
 
6
- export type UseMarkdown = (source: string, inline: boolean) => string
4
+ export type UseMarkdown = (source: string, inline?: boolean) => string
7
5
 
8
6
  export interface UseMarkdownOptions extends MarkdownItOptions {
9
- linkAttrs?: LinkAttrs
10
7
  config?: (md: MarkdownIt) => void
8
+ /** @default false */
9
+ inline?: boolean
10
+ domPurifyOptions?: Config
11
11
  }
12
12
 
13
- export function useMarkdown(options: UseMarkdownOptions = {}): UseMarkdown {
14
- const md = new MarkdownIt({
15
- html: true,
16
- linkify: true,
17
- ...options
18
- })
13
+ const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i
19
14
 
20
- md.use(linkPlugin, {
21
- target: '_blank',
22
- rel: 'noopener noreferrer',
23
- ...options.linkAttrs
24
- })
15
+ if (typeof document !== 'undefined') {
16
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
17
+ if (node.tagName === 'A') {
18
+ const target = node.getAttribute('target')
19
+ if (target && target !== '_blank' && target !== '_self') {
20
+ node.removeAttribute('target')
21
+ }
25
22
 
26
- if (options.config) {
27
- options.config(md)
28
- }
23
+ const href = node.getAttribute('href')
24
+ if (href && EXTERNAL_URL_RE.test(href)) {
25
+ node.setAttribute('target', '_blank')
26
+ node.setAttribute('rel', 'noreferrer')
27
+ }
29
28
 
30
- return (source, inline) => {
31
- return inline ? md.renderInline(source) : md.render(source)
29
+ node.classList.add('SMarkdown-link')
30
+ }
31
+ })
32
+ }
33
+
34
+ export function useMarkdown({
35
+ config,
36
+ inline: _inline,
37
+ domPurifyOptions,
38
+ ...options
39
+ }: UseMarkdownOptions = {}): UseMarkdown {
40
+ //
41
+
42
+ const md = new MarkdownIt({ html: true, linkify: true, ...options })
43
+ config?.(md)
44
+
45
+ return (source, inline = _inline) => {
46
+ const html = inline ? md.renderInline(source) : md.render(source)
47
+ return DOMPurify.sanitize(html, {
48
+ USE_PROFILES: { html: true },
49
+ ADD_ATTR: ['target'],
50
+ ...domPurifyOptions
51
+ })
32
52
  }
33
53
  }
34
54
 
@@ -45,109 +65,3 @@ export function useLinkifyIt() {
45
65
 
46
66
  return (source: string) => md.renderInline(source)
47
67
  }
48
-
49
- export interface UseLink {
50
- addListeners(): void
51
- removeListeners(): void
52
- subscribe(cb: LinkSubscriber): () => void
53
- }
54
-
55
- export interface UseLinkOptions {
56
- container: Ref<Element | null>
57
- callbacks?: LinkCallback[]
58
- }
59
-
60
- export interface LinkSubscriberPayload {
61
- event: Event
62
- target: HTMLAnchorElement
63
- isExternal: boolean
64
- isCallback: boolean
65
- }
66
-
67
- export type LinkSubscriber = (payload: LinkSubscriberPayload) => void
68
-
69
- export type LinkCallback = () => void
70
-
71
- export function useLink({ container, callbacks }: UseLinkOptions): UseLink {
72
- const router = useRouter()
73
- const subscribers: LinkSubscriber[] = []
74
-
75
- onUnmounted(() => removeListeners())
76
-
77
- function handler(event: Event): void {
78
- const target = event.target as HTMLAnchorElement
79
- const href = target.getAttribute('href')!
80
-
81
- if (!href) {
82
- return
83
- }
84
-
85
- const isExternal = isExternalUrl(href)
86
- const isCallback = isCallbackUrl(href)
87
-
88
- subscribers.forEach((sub) => sub({
89
- event,
90
- target,
91
- isExternal,
92
- isCallback
93
- }))
94
-
95
- if (isExternal) {
96
- return
97
- }
98
-
99
- if (!event.defaultPrevented) {
100
- event.preventDefault()
101
- }
102
-
103
- if (isCallback) {
104
- const idx = Number.parseInt(target.dataset.callbackId || '')
105
- const callback = (callbacks ?? [])[idx]
106
-
107
- if (!callback) {
108
- throw new Error(`Callback not found at index: ${idx}`)
109
- }
110
-
111
- return callback()
112
- }
113
-
114
- router.push(href)
115
- }
116
-
117
- function addListeners(): void {
118
- removeListeners()
119
-
120
- if (container.value) {
121
- findLinks(container.value).forEach((element) => {
122
- element.addEventListener('click', handler)
123
- })
124
- }
125
- }
126
-
127
- function removeListeners(): void {
128
- if (container.value) {
129
- findLinks(container.value).forEach((element) => {
130
- element.removeEventListener('click', handler)
131
- })
132
- }
133
- }
134
-
135
- function subscribe(fn: LinkSubscriber): () => void {
136
- subscribers.push(fn)
137
-
138
- return () => {
139
- const idx = subscribers.indexOf(fn)
140
- idx > -1 && subscribers.splice(idx, 1)
141
- }
142
- }
143
-
144
- return {
145
- addListeners,
146
- removeListeners,
147
- subscribe
148
- }
149
- }
150
-
151
- function findLinks(target: Element) {
152
- return target.querySelectorAll('a.SMarkdown-link')
153
- }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
3
  "type": "module",
4
- "version": "4.23.1",
4
+ "version": "4.24.0",
5
5
  "packageManager": "pnpm@9.15.4",
6
6
  "description": "Vue Components for Global Brain Design System.",
7
7
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",
@@ -48,7 +48,7 @@
48
48
  "@types/body-scroll-lock": "^3.1.2",
49
49
  "@types/lodash-es": "^4.17.12",
50
50
  "@types/markdown-it": "^14.1.2",
51
- "@vue/reactivity": "^3.5.13",
51
+ "@vue/reactivity": "^3.5.16",
52
52
  "@vuelidate/core": "^2.0.3",
53
53
  "@vuelidate/validators": "^2.0.4",
54
54
  "@vueuse/core": "^12 || ^13",
@@ -58,23 +58,24 @@
58
58
  "lodash-es": "^4.17.21",
59
59
  "markdown-it": "^14.1.0",
60
60
  "normalize.css": "^8.0.1",
61
- "pinia": "^3.0.2",
62
- "postcss": "^8.5.3",
61
+ "pinia": "^3.0.3",
62
+ "postcss": "^8.5.4",
63
63
  "postcss-nested": "^7.0.2",
64
64
  "v-calendar": "3.0.1",
65
- "vue": "^3.5.13",
66
- "vue-router": "^4.5.0"
65
+ "vue": "^3.5.16",
66
+ "vue-router": "^4.5.1"
67
67
  },
68
68
  "dependencies": {
69
- "@sentry/browser": "^9.14.0",
70
- "@sentry/vue": "^9.14.0",
69
+ "@sentry/browser": "^9.26.0",
70
+ "@sentry/vue": "^9.26.0",
71
71
  "@tanstack/vue-virtual": "3.0.0-beta.62",
72
72
  "@tinyhttp/content-disposition": "^2.2.2",
73
73
  "@tinyhttp/cookie": "^2.1.1",
74
74
  "@types/d3": "^7.4.3",
75
75
  "@types/file-saver": "^2.0.7",
76
- "@types/qs": "^6.9.18",
76
+ "@types/qs": "^6.14.0",
77
77
  "d3": "^7.9.0",
78
+ "dompurify": "^3.2.6",
78
79
  "file-saver": "^2.0.5",
79
80
  "html2canvas": "^1.4.1",
80
81
  "magic-string": "^0.30.17",
@@ -91,10 +92,10 @@
91
92
  "@types/body-scroll-lock": "^3.1.2",
92
93
  "@types/lodash-es": "^4.17.12",
93
94
  "@types/markdown-it": "^14.1.2",
94
- "@types/node": "^22.14.1",
95
- "@vitejs/plugin-vue": "^5.2.3",
96
- "@vitest/coverage-v8": "^3.1.2",
97
- "@vue/reactivity": "^3.5.13",
95
+ "@types/node": "^22.15.29",
96
+ "@vitejs/plugin-vue": "^5.2.4",
97
+ "@vitest/coverage-v8": "^3.2.1",
98
+ "@vue/reactivity": "^3.5.16",
98
99
  "@vue/test-utils": "^2.4.6",
99
100
  "@vuelidate/core": "^2.0.3",
100
101
  "@vuelidate/validators": "^2.0.4",
@@ -103,23 +104,23 @@
103
104
  "dayjs": "^1.11.13",
104
105
  "eslint": "8.57.0",
105
106
  "fuse.js": "^7.1.0",
106
- "happy-dom": "^17.4.4",
107
+ "happy-dom": "^17.6.3",
107
108
  "histoire": "0.16.5",
108
109
  "lodash-es": "^4.17.21",
109
110
  "markdown-it": "^14.1.0",
110
111
  "normalize.css": "^8.0.1",
111
- "pinia": "^3.0.2",
112
- "postcss": "^8.5.3",
112
+ "pinia": "^3.0.3",
113
+ "postcss": "^8.5.4",
113
114
  "postcss-nested": "^7.0.2",
114
115
  "punycode": "^2.3.1",
115
- "release-it": "^19.0.1",
116
+ "release-it": "^19.0.3",
116
117
  "typescript": "~5.8.3",
117
118
  "v-calendar": "3.0.1",
118
- "vite": "^6.3.3",
119
- "vitepress": ">=2.0.0-alpha.3",
120
- "vitest": "^3.1.2",
121
- "vue": "^3.5.13",
122
- "vue-router": "^4.5.0",
119
+ "vite": "^6.3.5",
120
+ "vitepress": "^2.0.0-alpha.5",
121
+ "vitest": "^3.2.1",
122
+ "vue": "^3.5.16",
123
+ "vue-router": "^4.5.1",
123
124
  "vue-tsc": "^2.2.10"
124
125
  }
125
126
  }
@@ -1,45 +0,0 @@
1
- import type MarkdownIt from 'markdown-it'
2
-
3
- export type LinkAttrs = Record<string, string>
4
-
5
- const EXTERNAL_REGEX = /^https?:/
6
- const CALLBACK_REGEX = /\{([\d}]+)\}/
7
- const CALLBACK_HREF = '#callback'
8
-
9
- export function linkPlugin(md: MarkdownIt, linkAttrs: LinkAttrs = {}): void {
10
- md.renderer.rules.link_open = (tokens, idx, options, _env, self) => {
11
- const token = tokens[idx]
12
- const hrefIndex = token.attrIndex('href')
13
-
14
- if (hrefIndex >= 0) {
15
- const hrefAttr = token.attrs![hrefIndex]
16
- const url = decodeURIComponent(hrefAttr[1])
17
-
18
- if (isExternalUrl(url)) {
19
- Object.entries(linkAttrs).forEach(([key, val]) => {
20
- token.attrSet(key, val)
21
- })
22
- }
23
-
24
- if (isCallbackUrl(url)) {
25
- const matched = url.match(CALLBACK_REGEX)![1]
26
-
27
- token.attrSet('data-callback-id', matched)
28
-
29
- hrefAttr[1] = CALLBACK_HREF
30
- }
31
-
32
- token.attrSet('class', 'SMarkdown-link')
33
- }
34
-
35
- return self.renderToken(tokens, idx, options)
36
- }
37
- }
38
-
39
- export function isExternalUrl(url: string): boolean {
40
- return EXTERNAL_REGEX.test(url)
41
- }
42
-
43
- export function isCallbackUrl(url: string): boolean {
44
- return url === CALLBACK_HREF || CALLBACK_REGEX.test(decodeURIComponent(url))
45
- }