@uniweb/kit 0.1.1
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/LICENSE +201 -0
- package/README.md +345 -0
- package/package.json +47 -0
- package/src/components/Asset/Asset.jsx +161 -0
- package/src/components/Asset/index.js +1 -0
- package/src/components/Disclaimer/Disclaimer.jsx +198 -0
- package/src/components/Disclaimer/index.js +1 -0
- package/src/components/FileLogo/FileLogo.jsx +148 -0
- package/src/components/FileLogo/index.js +1 -0
- package/src/components/Icon/Icon.jsx +214 -0
- package/src/components/Icon/index.js +1 -0
- package/src/components/Image/Image.jsx +194 -0
- package/src/components/Image/index.js +1 -0
- package/src/components/Link/Link.jsx +261 -0
- package/src/components/Link/index.js +1 -0
- package/src/components/Media/Media.jsx +322 -0
- package/src/components/Media/index.js +1 -0
- package/src/components/MediaIcon/MediaIcon.jsx +95 -0
- package/src/components/MediaIcon/index.js +1 -0
- package/src/components/SafeHtml/SafeHtml.jsx +93 -0
- package/src/components/SafeHtml/index.js +1 -0
- package/src/components/Section/Render.jsx +245 -0
- package/src/components/Section/Section.jsx +131 -0
- package/src/components/Section/index.js +3 -0
- package/src/components/Section/renderers/Alert.jsx +101 -0
- package/src/components/Section/renderers/Code.jsx +70 -0
- package/src/components/Section/renderers/Details.jsx +77 -0
- package/src/components/Section/renderers/Divider.jsx +42 -0
- package/src/components/Section/renderers/Table.jsx +55 -0
- package/src/components/Section/renderers/index.js +11 -0
- package/src/components/Text/Text.jsx +207 -0
- package/src/components/Text/index.js +14 -0
- package/src/hooks/index.js +1 -0
- package/src/hooks/useWebsite.js +77 -0
- package/src/index.js +69 -0
- package/src/styles/index.css +8 -0
- package/src/utils/index.js +104 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders HTML tables from structured content.
|
|
5
|
+
*
|
|
6
|
+
* @module @uniweb/kit/Section/renderers/Table
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import { cn } from '../../../utils/index.js'
|
|
11
|
+
import { SafeHtml } from '../../SafeHtml/index.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Table - Table renderer
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} props
|
|
17
|
+
* @param {Array} props.content - Table content as rows/cells array
|
|
18
|
+
* @param {string} [props.className] - Additional CSS classes
|
|
19
|
+
*/
|
|
20
|
+
export function Table({ content, className, ...props }) {
|
|
21
|
+
if (!content || !Array.isArray(content)) return null
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn('overflow-x-auto', className)} {...props}>
|
|
25
|
+
<table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
|
26
|
+
<tbody className="divide-y divide-gray-200 bg-white">
|
|
27
|
+
{content.map((row, rowIndex) => (
|
|
28
|
+
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
|
29
|
+
{row.content?.map((cell, cellIndex) => {
|
|
30
|
+
const CellTag = cell.type === 'tableHeader' ? 'th' : 'td'
|
|
31
|
+
const cellContent = cell.content?.[0]?.content?.[0]?.text || ''
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<CellTag
|
|
35
|
+
key={cellIndex}
|
|
36
|
+
className={cn(
|
|
37
|
+
'px-4 py-2 text-sm',
|
|
38
|
+
cell.type === 'tableHeader'
|
|
39
|
+
? 'font-medium text-gray-900 bg-gray-100'
|
|
40
|
+
: 'text-gray-600'
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<SafeHtml value={cellContent} as="span" />
|
|
44
|
+
</CellTag>
|
|
45
|
+
)
|
|
46
|
+
})}
|
|
47
|
+
</tr>
|
|
48
|
+
))}
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default Table
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section Renderers
|
|
3
|
+
*
|
|
4
|
+
* Individual content type renderers for the Section component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { Code } from './Code.jsx'
|
|
8
|
+
export { Alert, Warning } from './Alert.jsx'
|
|
9
|
+
export { Table } from './Table.jsx'
|
|
10
|
+
export { Details } from './Details.jsx'
|
|
11
|
+
export { Divider } from './Divider.jsx'
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Component
|
|
3
|
+
*
|
|
4
|
+
* Smart typography component for rendering content from semantic-parser.
|
|
5
|
+
* Handles single strings or arrays of paragraphs with automatic semantic
|
|
6
|
+
* tag selection and empty content filtering.
|
|
7
|
+
*
|
|
8
|
+
* Security Model:
|
|
9
|
+
* - Assumes content is ALREADY SANITIZED at the engine level
|
|
10
|
+
* - Does NOT sanitize HTML (that's the engine's responsibility)
|
|
11
|
+
* - Trusts the data it receives and renders it as-is
|
|
12
|
+
*
|
|
13
|
+
* @module @uniweb/kit/Text
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { memo } from 'react'
|
|
17
|
+
import { cn } from '../../utils/index.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Text - Smart typography component
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} props
|
|
23
|
+
* @param {string|string[]} props.text - Content to render (string or array of strings)
|
|
24
|
+
* @param {string} [props.as='p'] - HTML tag: 'h1'-'h6', 'p', 'div', 'span'
|
|
25
|
+
* @param {boolean} [props.html=true] - Render as HTML (true) or plain text (false)
|
|
26
|
+
* @param {string} [props.className] - CSS classes for wrapper/elements
|
|
27
|
+
* @param {string} [props.lineAs] - Tag for each line in array. Defaults: 'div' for headings, 'p' for others
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Simple paragraph
|
|
31
|
+
* <Text text="Hello World" />
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Heading
|
|
35
|
+
* <Text text="Welcome" as="h1" />
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Multi-line heading (wrapped in single h1)
|
|
39
|
+
* <Text text={["Welcome to", "Our Platform"]} as="h1" />
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Multiple paragraphs (each gets its own <p>)
|
|
43
|
+
* <Text text={["First paragraph", "Second paragraph"]} />
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Rich HTML content (assumes pre-sanitized)
|
|
47
|
+
* <Text text="Hello <strong>World</strong>" />
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Plain text (HTML tags shown as text)
|
|
51
|
+
* <Text text="Show <strong>tags</strong>" html={false} />
|
|
52
|
+
*/
|
|
53
|
+
export const Text = memo(function Text({
|
|
54
|
+
text,
|
|
55
|
+
as = 'p',
|
|
56
|
+
html = true,
|
|
57
|
+
className,
|
|
58
|
+
lineAs,
|
|
59
|
+
...props
|
|
60
|
+
}) {
|
|
61
|
+
const isArray = Array.isArray(text)
|
|
62
|
+
const Tag = as
|
|
63
|
+
const isHeading = /^h[1-6]$/.test(as)
|
|
64
|
+
|
|
65
|
+
// Single string input
|
|
66
|
+
if (!isArray) {
|
|
67
|
+
if (!text || (typeof text === 'string' && text.trim() === '')) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (html) {
|
|
72
|
+
return (
|
|
73
|
+
<Tag
|
|
74
|
+
className={className}
|
|
75
|
+
dangerouslySetInnerHTML={{ __html: text }}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Tag className={className} {...props}>
|
|
83
|
+
{text}
|
|
84
|
+
</Tag>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Array input - filter empty content
|
|
89
|
+
const filteredText = text.filter(
|
|
90
|
+
(item) => typeof item === 'string' && item.trim() !== ''
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (filteredText.length === 0) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Determine line wrapper tag with smart defaults
|
|
98
|
+
const LineTag = lineAs || (isHeading ? 'div' : 'p')
|
|
99
|
+
|
|
100
|
+
// Multi-line heading: wrap all lines in single heading tag
|
|
101
|
+
if (isHeading) {
|
|
102
|
+
return (
|
|
103
|
+
<Tag className={className} {...props}>
|
|
104
|
+
{filteredText.map((line, i) => {
|
|
105
|
+
if (html) {
|
|
106
|
+
return (
|
|
107
|
+
<LineTag
|
|
108
|
+
key={i}
|
|
109
|
+
dangerouslySetInnerHTML={{ __html: line }}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
return <LineTag key={i}>{line}</LineTag>
|
|
114
|
+
})}
|
|
115
|
+
</Tag>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Non-heading arrays: render each line as separate element
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
{filteredText.map((line, i) => {
|
|
123
|
+
if (html) {
|
|
124
|
+
return (
|
|
125
|
+
<LineTag
|
|
126
|
+
key={i}
|
|
127
|
+
className={className}
|
|
128
|
+
dangerouslySetInnerHTML={{ __html: line }}
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return (
|
|
134
|
+
<LineTag key={i} className={className} {...props}>
|
|
135
|
+
{line}
|
|
136
|
+
</LineTag>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
</>
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Semantic Wrapper Components
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* H1 - Heading level 1
|
|
149
|
+
* @example
|
|
150
|
+
* <H1 text="Main Title" />
|
|
151
|
+
* <H1 text={["Multi-line", "Title"]} />
|
|
152
|
+
*/
|
|
153
|
+
export const H1 = (props) => <Text {...props} as="h1" />
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* H2 - Heading level 2
|
|
157
|
+
*/
|
|
158
|
+
export const H2 = (props) => <Text {...props} as="h2" />
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* H3 - Heading level 3
|
|
162
|
+
*/
|
|
163
|
+
export const H3 = (props) => <Text {...props} as="h3" />
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* H4 - Heading level 4
|
|
167
|
+
*/
|
|
168
|
+
export const H4 = (props) => <Text {...props} as="h4" />
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* H5 - Heading level 5
|
|
172
|
+
*/
|
|
173
|
+
export const H5 = (props) => <Text {...props} as="h5" />
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* H6 - Heading level 6
|
|
177
|
+
*/
|
|
178
|
+
export const H6 = (props) => <Text {...props} as="h6" />
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* P - Paragraph
|
|
182
|
+
* @example
|
|
183
|
+
* <P text="A paragraph of content" />
|
|
184
|
+
* <P text={["First paragraph", "Second paragraph"]} />
|
|
185
|
+
*/
|
|
186
|
+
export const P = (props) => <Text {...props} as="p" />
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Span - Inline text
|
|
190
|
+
*/
|
|
191
|
+
export const Span = (props) => <Text {...props} as="span" />
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Div - Block container
|
|
195
|
+
* @example
|
|
196
|
+
* <Div text={["Item 1", "Item 2"]} lineAs="span" />
|
|
197
|
+
*/
|
|
198
|
+
export const Div = (props) => <Text {...props} as="div" />
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* PlainText - Text with HTML rendering disabled
|
|
202
|
+
* @example
|
|
203
|
+
* <PlainText text="Show <strong>tags</strong> as text" />
|
|
204
|
+
*/
|
|
205
|
+
export const PlainText = (props) => <Text {...props} html={false} />
|
|
206
|
+
|
|
207
|
+
export default Text
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useWebsite, default } from './useWebsite.js'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWebsite Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides access to the current website instance and common utilities.
|
|
5
|
+
* This is the primary hook for kit components to interact with the runtime.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* function MyComponent() {
|
|
9
|
+
* const { website, localize, makeHref } = useWebsite()
|
|
10
|
+
* return <a href={makeHref('/about')}>{localize({ en: 'About', fr: 'À propos' })}</a>
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getUniweb } from '@uniweb/core'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the current website instance and utilities
|
|
18
|
+
* @returns {Object} Website utilities
|
|
19
|
+
*/
|
|
20
|
+
export function useWebsite() {
|
|
21
|
+
const uniweb = getUniweb()
|
|
22
|
+
|
|
23
|
+
if (!uniweb) {
|
|
24
|
+
console.warn('[Kit] Runtime not initialized. Components require @uniweb/runtime.')
|
|
25
|
+
return {
|
|
26
|
+
website: null,
|
|
27
|
+
localize: (val, defaultVal = '') => defaultVal,
|
|
28
|
+
makeHref: (href) => href,
|
|
29
|
+
getLanguage: () => 'en',
|
|
30
|
+
getLanguages: () => []
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const website = uniweb.activeWebsite
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
/**
|
|
38
|
+
* The active Website instance
|
|
39
|
+
*/
|
|
40
|
+
website,
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Localize a multilingual value
|
|
44
|
+
* @param {Object|string} value - Object with language keys or string
|
|
45
|
+
* @param {string} defaultVal - Fallback value
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
localize: (value, defaultVal = '') => website.localize(value, defaultVal),
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Transform a href (handles topic: protocol, etc.)
|
|
52
|
+
* @param {string} href
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
makeHref: (href) => website.makeHref(href),
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get current language code
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
getLanguage: () => website.getLanguage(),
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get available languages
|
|
65
|
+
* @returns {Array<{label: string, value: string}>}
|
|
66
|
+
*/
|
|
67
|
+
getLanguages: () => website.getLanguages(),
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get routing components (Link, useNavigate, etc.)
|
|
71
|
+
* @returns {Object}
|
|
72
|
+
*/
|
|
73
|
+
getRoutingComponents: () => website.getRoutingComponents()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default useWebsite
|
package/src/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uniweb/kit
|
|
3
|
+
*
|
|
4
|
+
* Standard component library for Uniweb foundations.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { Link, Image, useWebsite } from '@uniweb/kit'
|
|
8
|
+
*
|
|
9
|
+
* function MyComponent() {
|
|
10
|
+
* const { localize } = useWebsite()
|
|
11
|
+
* return (
|
|
12
|
+
* <Link to="/about">
|
|
13
|
+
* <Image src="/logo.png" alt="Logo" />
|
|
14
|
+
* {localize({ en: 'About', fr: 'À propos' })}
|
|
15
|
+
* </Link>
|
|
16
|
+
* )
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Components
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
// Primitives
|
|
25
|
+
export { Link } from './components/Link/index.js'
|
|
26
|
+
export { Image } from './components/Image/index.js'
|
|
27
|
+
export { SafeHtml } from './components/SafeHtml/index.js'
|
|
28
|
+
export { Icon } from './components/Icon/index.js'
|
|
29
|
+
|
|
30
|
+
// Typography
|
|
31
|
+
export {
|
|
32
|
+
Text,
|
|
33
|
+
H1, H2, H3, H4, H5, H6,
|
|
34
|
+
P, Span, Div,
|
|
35
|
+
PlainText
|
|
36
|
+
} from './components/Text/index.js'
|
|
37
|
+
|
|
38
|
+
// Media
|
|
39
|
+
export { Media } from './components/Media/index.js'
|
|
40
|
+
export { FileLogo } from './components/FileLogo/index.js'
|
|
41
|
+
export { MediaIcon } from './components/MediaIcon/index.js'
|
|
42
|
+
|
|
43
|
+
// Content
|
|
44
|
+
export { Section, Render } from './components/Section/index.js'
|
|
45
|
+
export { Code, Alert, Warning, Table, Details, Divider } from './components/Section/renderers/index.js'
|
|
46
|
+
|
|
47
|
+
// Utilities
|
|
48
|
+
export { Asset } from './components/Asset/index.js'
|
|
49
|
+
export { Disclaimer } from './components/Disclaimer/index.js'
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Hooks
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
export { useWebsite } from './hooks/index.js'
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Utilities
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
cn,
|
|
63
|
+
twMerge,
|
|
64
|
+
twJoin,
|
|
65
|
+
stripTags,
|
|
66
|
+
isExternalUrl,
|
|
67
|
+
isFileUrl,
|
|
68
|
+
detectMediaType
|
|
69
|
+
} from './utils/index.js'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit Utilities
|
|
3
|
+
*
|
|
4
|
+
* Common utility functions for kit components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { twMerge, twJoin } from 'tailwind-merge'
|
|
8
|
+
|
|
9
|
+
// Re-export tailwind-merge utilities
|
|
10
|
+
export { twMerge, twJoin }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Merge class names with Tailwind CSS conflict resolution
|
|
14
|
+
* @param {...string} classes - Class names to merge
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function cn(...classes) {
|
|
18
|
+
return twMerge(twJoin(classes.filter(Boolean)))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip HTML tags from a string
|
|
23
|
+
* @param {string} html - HTML string
|
|
24
|
+
* @returns {string} Plain text
|
|
25
|
+
*/
|
|
26
|
+
export function stripTags(html) {
|
|
27
|
+
if (!html || typeof html !== 'string') return ''
|
|
28
|
+
|
|
29
|
+
// Use DOMParser for safe HTML entity decoding
|
|
30
|
+
if (typeof DOMParser !== 'undefined') {
|
|
31
|
+
const doc = new DOMParser().parseFromString(html, 'text/html')
|
|
32
|
+
return doc.body.textContent || ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fallback: simple regex (less accurate but works in Node)
|
|
36
|
+
return html.replace(/<[^>]*>/g, '')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a URL is external (different origin)
|
|
41
|
+
* @param {string} url
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isExternalUrl(url) {
|
|
45
|
+
if (!url || typeof url !== 'string') return false
|
|
46
|
+
if (url.startsWith('/') || url.startsWith('#')) return false
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const urlObj = new URL(url, window.location.origin)
|
|
50
|
+
return urlObj.origin !== window.location.origin
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a URL points to a downloadable file
|
|
58
|
+
* @param {string} url
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
export function isFileUrl(url) {
|
|
62
|
+
if (!url || typeof url !== 'string') return false
|
|
63
|
+
|
|
64
|
+
const fileExtensions = [
|
|
65
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
66
|
+
'.zip', '.rar', '.7z', '.tar', '.gz',
|
|
67
|
+
'.mp3', '.wav', '.ogg', '.flac',
|
|
68
|
+
'.mp4', '.avi', '.mov', '.wmv', '.webm',
|
|
69
|
+
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp',
|
|
70
|
+
'.txt', '.csv', '.json', '.xml'
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const lowerUrl = url.toLowerCase()
|
|
74
|
+
return fileExtensions.some(ext => lowerUrl.includes(ext))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect media type from URL
|
|
79
|
+
* @param {string} url
|
|
80
|
+
* @returns {'youtube'|'vimeo'|'video'|'image'|'audio'|'unknown'}
|
|
81
|
+
*/
|
|
82
|
+
export function detectMediaType(url) {
|
|
83
|
+
if (!url) return 'unknown'
|
|
84
|
+
|
|
85
|
+
const lowerUrl = url.toLowerCase()
|
|
86
|
+
|
|
87
|
+
if (lowerUrl.includes('youtube.com') || lowerUrl.includes('youtu.be')) {
|
|
88
|
+
return 'youtube'
|
|
89
|
+
}
|
|
90
|
+
if (lowerUrl.includes('vimeo.com')) {
|
|
91
|
+
return 'vimeo'
|
|
92
|
+
}
|
|
93
|
+
if (/\.(mp4|webm|ogg|mov|avi)/.test(lowerUrl)) {
|
|
94
|
+
return 'video'
|
|
95
|
+
}
|
|
96
|
+
if (/\.(mp3|wav|ogg|flac|aac)/.test(lowerUrl)) {
|
|
97
|
+
return 'audio'
|
|
98
|
+
}
|
|
99
|
+
if (/\.(jpg|jpeg|png|gif|svg|webp|avif)/.test(lowerUrl)) {
|
|
100
|
+
return 'image'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return 'unknown'
|
|
104
|
+
}
|