vitepress-linkcard 1.3.0 → 2.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.
@@ -1,9 +1,37 @@
1
1
  import { parserMetadata, xhr } from '.';
2
2
  import LocalFileCache from './local-file-cache';
3
+ /**
4
+ * Local file cache instance for storing fetched URL metadata.
5
+ * This cache persists metadata to disk to avoid repeated network requests.
6
+ * @internal
7
+ */
3
8
  const cache = new LocalFileCache();
4
9
  /**
5
- * @param url
6
- * @returns
10
+ * Retrieves metadata for a given URL, using cache when available.
11
+ *
12
+ * This function first checks if the metadata is already cached. If not, it fetches
13
+ * the HTML content from the URL, parses the metadata, and caches the result for
14
+ * future use.
15
+ *
16
+ * The metadata includes:
17
+ * - Title (from `<title>` or OGP tags)
18
+ * - Description (from meta description or OGP tags)
19
+ * - Logo/icon (from OGP image or favicon)
20
+ *
21
+ * @param url - The URL to fetch metadata from
22
+ * @returns The parsed URL metadata, or null if the URL cannot be fetched or parsed
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const metadata = getUrlMetadata('https://example.com')
27
+ * if (metadata) {
28
+ * console.log(metadata.title) // "Example Domain"
29
+ * console.log(metadata.description) // "Example website description"
30
+ * }
31
+ * ```
32
+ *
33
+ * @see {@link parserMetadata} for details on metadata extraction
34
+ * @see {@link LocalFileCache} for caching implementation
7
35
  */
8
36
  export function getUrlMetadata(url) {
9
37
  if (cache.has(url))
@@ -1,22 +1,75 @@
1
1
  /**
2
- * TODO: Refactor
2
+ * HTML parser module for extracting metadata from web pages.
3
+ *
4
+ * This module provides functions to parse HTML strings and extract metadata such as
5
+ * title, description, and logo/icon URLs. It supports both standard HTML meta tags
6
+ * and Open Graph Protocol (OGP) tags.
7
+ *
8
+ * @module parser
9
+ * @todo Refactor to improve maintainability and add support for more meta tag formats
3
10
  */
4
11
  import { isString } from '@luckrya/utility';
5
12
  import { cleanPath, extractUrl } from './url';
13
+ /**
14
+ * Default logo URL used when no logo can be extracted from the page.
15
+ */
6
16
  const DEFAULT_LOGO = 'https://resources.whatwg.org/logo-url.svg';
17
+ /**
18
+ * Regular expression to match HTML tag content between opening and closing tags.
19
+ * @example Matches: `<title>Page Title</title>` → captures "Page Title"
20
+ */
7
21
  const HtmlTagContentReg = /(<[A-Za-z]+\s*[^>]*>(.*)<\/[A-Za-z]+>)/;
22
+ /**
23
+ * Regular expression to extract the `content` attribute value from HTML meta tags.
24
+ * @example Matches: `content="value"` or `content='value'`
25
+ */
8
26
  const ContentAttrValueHtmlMetaTagReg = /content=["|']([^>]*)["|']/;
27
+ /**
28
+ * Regular expression to extract the `href` attribute value from HTML link tags.
29
+ * @example Matches: `href="value"` or `href='value'`
30
+ */
9
31
  const HrefAttrValueHtmlLinkTagReg = /href=["|']([^>]*)["|']/;
32
+ /**
33
+ * Regular expression to match HTML title tags.
34
+ * @example Matches: `<title>Page Title</title>`
35
+ */
10
36
  const HtmlTitleTagReg = /(<title\s*[^>]*>(.*)<\/title>)/g;
11
- // const HtmlMetaTagReg = /<meta\s[^>]*\/?>/g;
12
- // const HtmlLinkTagReg = /<link\s[^>]*\/?>/g;
37
+ /**
38
+ * Creates a regular expression to match self-closing or non-closing HTML tags with a specific attribute.
39
+ *
40
+ * This function generates patterns to find meta or link tags that contain a specific attribute,
41
+ * which is useful for finding OGP tags like `og:title`, `og:description`, etc.
42
+ *
43
+ * @param attr - The attribute to search for (e.g., 'title', 'description', 'image')
44
+ * @param tag - The HTML tag name to match (default: 'meta')
45
+ * @returns A RegExp that matches tags containing the specified attribute
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const reg = containArrSelfLosingHtmlTagReg('og:title')
50
+ * // Matches: <meta property="og:title" content="...">
51
+ * ```
52
+ *
53
+ * @internal
54
+ */
13
55
  const containArrSelfLosingHtmlTagReg = (attr, tag = 'meta') => new RegExp(`<${tag}\\s[^>]*\\w+=['|"]([a-zA-Z]|:|\\s)*${attr}['|"][^>]*\\/?>`);
14
56
  /**
15
- * @param htmlString
16
- * @returns
17
- * <title>$value</title>
18
- * <meta property="og:title" content="$value" />
19
- * <link data-react-helmet="true" href="https://s.xml" rel="search" title="$value" type="applon+xml">
57
+ * Extracts the page title from HTML string.
58
+ *
59
+ * Attempts to find the title in the following order:
60
+ * 1. Meta tags with 'title' attribute (e.g., `<meta property="og:title" content="...">`)
61
+ * 2. Standard HTML `<title>` tag
62
+ *
63
+ * @param htmlString - The HTML content to parse
64
+ * @returns The extracted title, or undefined if not found
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const title = matchTitleByMetaTag('<title>Example Page</title>')
69
+ * // Returns: "Example Page"
70
+ * ```
71
+ *
72
+ * @internal
20
73
  */
21
74
  function matchTitleByMetaTag(htmlString) {
22
75
  let title;
@@ -37,9 +90,22 @@ function matchTitleByMetaTag(htmlString) {
37
90
  return title;
38
91
  }
39
92
  /**
40
- * @returns
41
- * <meta name="description" content="$value" />
42
- * <meta property="og:description" content="$value" />
93
+ * Extracts the page description from HTML string.
94
+ *
95
+ * Searches for description in meta tags, supporting both standard and OGP formats:
96
+ * - `<meta name="description" content="...">`
97
+ * - `<meta property="og:description" content="...">`
98
+ *
99
+ * @param htmlString - The HTML content to parse
100
+ * @returns The extracted description, or undefined if not found
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const desc = matchDescriptionByMetaTag('<meta name="description" content="A great site">')
105
+ * // Returns: "A great site"
106
+ * ```
107
+ *
108
+ * @internal
43
109
  */
44
110
  function matchDescriptionByMetaTag(htmlString) {
45
111
  let description;
@@ -52,9 +118,22 @@ function matchDescriptionByMetaTag(htmlString) {
52
118
  return description;
53
119
  }
54
120
  /**
55
- * @returns
56
- * <meta property="og:image" content="$value" />
57
- * <link rel="icon" href="$value">
121
+ * Extracts the page logo/icon URL from HTML string.
122
+ *
123
+ * Attempts to find the logo in the following order:
124
+ * 1. OGP image tag: `<meta property="og:image" content="...">`
125
+ * 2. Icon link tag: `<link rel="icon" href="...">`
126
+ *
127
+ * @param htmlString - The HTML content to parse
128
+ * @returns The extracted logo URL (may be relative), or undefined if not found
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const logo = matchLogoByLinkOrMetaTag('<link rel="icon" href="/favicon.ico">')
133
+ * // Returns: "/favicon.ico"
134
+ * ```
135
+ *
136
+ * @internal
58
137
  */
59
138
  function matchLogoByLinkOrMetaTag(htmlString) {
60
139
  let logo;
@@ -76,11 +155,39 @@ function matchLogoByLinkOrMetaTag(htmlString) {
76
155
  return logo;
77
156
  }
78
157
  /**
79
- * @param htmlString
80
- * @param url
81
- * @returns
158
+ * Parses HTML string to extract structured metadata for link card generation.
159
+ *
160
+ * This is the main parsing function that extracts title, description, and logo
161
+ * from an HTML page. It handles both absolute and relative URLs, converting
162
+ * relative logo paths to absolute URLs when necessary.
163
+ *
164
+ * @param htmlString - The HTML content to parse
165
+ * @param url - The URL of the page (used to resolve relative logo URLs)
166
+ * @returns Parsed metadata object, or null if no valid metadata could be extracted
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * const html = '<title>Example</title><meta name="description" content="Test">'
171
+ * const metadata = parserMetadata(html, 'https://example.com')
172
+ * // Returns: { title: 'Example', description: 'Test', logo: '...' }
173
+ * ```
174
+ *
175
+ * @remarks
176
+ * - Returns null if all metadata fields (title, description, logo) are empty
177
+ * - Relative logo URLs are converted to absolute URLs using the page's origin
178
+ * - Falls back to a default logo if no logo can be found
179
+ *
180
+ * @todo Handle protocol-relative URLs like `//img.example.com/logo.png`
82
181
  */
83
182
  export function parserMetadata(htmlString, url) {
183
+ /**
184
+ * Converts a potentially relative logo URL to an absolute URL.
185
+ *
186
+ * @param logo - The logo URL (may be relative or absolute)
187
+ * @returns Absolute URL for the logo, or default logo if input is invalid
188
+ *
189
+ * @internal
190
+ */
84
191
  function absolute(logo) {
85
192
  if (!logo)
86
193
  return DEFAULT_LOGO;
@@ -99,8 +206,15 @@ export function parserMetadata(htmlString, url) {
99
206
  return metadata;
100
207
  }
101
208
  /**
102
- * @param obj
103
- * @returns
209
+ * Checks if an object contains only undefined or empty string values.
210
+ *
211
+ * This utility function is used to determine if any valid metadata was extracted
212
+ * from a page. If all fields are empty, the metadata is considered invalid.
213
+ *
214
+ * @param obj - Object to check (typically a metadata object)
215
+ * @returns true if the object has no non-empty string values, false otherwise
216
+ *
217
+ * @internal
104
218
  */
105
219
  function isEmptyStringObject(obj) {
106
220
  return !Object.values(obj).filter((v) => isString(v)).length;
@@ -1,13 +1,48 @@
1
1
  /**
2
- * @param str
3
- * @returns
2
+ * Style generation utilities for link card rendering.
3
+ *
4
+ * This module provides functions to generate inline CSS styles and class names
5
+ * for the link card components. The styles support customizable colors and
6
+ * responsive design with text ellipsis for long content.
7
+ *
8
+ * @module style
9
+ */
10
+ /**
11
+ * Converts a camelCase string to hyphenated kebab-case.
12
+ *
13
+ * This is used to convert JavaScript style property names (e.g., `backgroundColor`)
14
+ * to CSS property names (e.g., `background-color`).
15
+ *
16
+ * @param str - The camelCase string to convert
17
+ * @returns The hyphenated kebab-case string
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * hyphenate('backgroundColor') // Returns: 'background-color'
22
+ * hyphenate('fontSize') // Returns: 'font-size'
23
+ * ```
24
+ *
25
+ * @internal
4
26
  */
5
27
  function hyphenate(str) {
6
28
  return str.replace(/\B([A-Z])/g, '-$1').toLowerCase();
7
29
  }
8
30
  /**
9
- * @param style
10
- * @returns
31
+ * Joins style properties into a CSS string.
32
+ *
33
+ * Converts a JavaScript object of style properties into a semicolon-separated
34
+ * CSS string suitable for inline styles.
35
+ *
36
+ * @param style - Object containing CSS properties and values
37
+ * @returns A string of CSS declarations
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * join({ fontSize: '16px', color: 'red' })
42
+ * // Returns: 'font-size: 16px; color: red;'
43
+ * ```
44
+ *
45
+ * @internal
11
46
  */
12
47
  function join(style) {
13
48
  return Object.entries(style)
@@ -19,15 +54,38 @@ function join(style) {
19
54
  .join(' ');
20
55
  }
21
56
  /**
22
- * @param style
23
- * @returns
57
+ * Wraps CSS declarations in an inline style attribute.
58
+ *
59
+ * @param style - Object containing CSS properties and values
60
+ * @returns A complete HTML style attribute string
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * inlineStyle({ color: 'red', fontSize: '16px' })
65
+ * // Returns: 'style="color: red; font-size: 16px;"'
66
+ * ```
67
+ *
68
+ * @internal
24
69
  */
25
70
  function inlineStyle(style) {
26
71
  return `style="${join(style)}"`;
27
72
  }
28
73
  /**
29
- * @param line
30
- * @returns
74
+ * Generates CSS properties for text ellipsis with line clamping.
75
+ *
76
+ * Creates styles that truncate text after a specified number of lines with
77
+ * an ellipsis. Uses both WebKit-specific properties and standard line-clamp.
78
+ *
79
+ * @param line - Maximum number of lines to display before truncation
80
+ * @returns CSS properties object for ellipsis effect
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * ellipsisStyle(2)
85
+ * // Returns styles that truncate text after 2 lines with "..."
86
+ * ```
87
+ *
88
+ * @internal
31
89
  */
32
90
  const ellipsisStyle = (line) => ({
33
91
  '-webkit-box-orient': 'vertical',
@@ -41,12 +99,33 @@ const ellipsisStyle = (line) => ({
41
99
  wordBreak: 'break-word'
42
100
  });
43
101
  /**
44
- See: * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/components/VPFeature.vue
45
- * @param borderColor
46
- * @param bgColor
47
- * @returns
102
+ * Generates complete inline styles for all link card components.
103
+ *
104
+ * Creates a set of inline style strings for each part of the link card:
105
+ * - Container: main card box with border and background
106
+ * - Image: logo/icon display
107
+ * - Texts: wrapper for text content
108
+ * - Title: card title (2-line ellipsis)
109
+ * - Domain: domain name with underline
110
+ * - Description: description text (2-line ellipsis)
111
+ *
112
+ * The styles are inspired by VitePress's VPFeature component design.
113
+ *
114
+ * The container uses CSS custom properties for theming:
115
+ * - `--vitepress-linkcard-border-color`: Border color (default: #7d7d7dff)
116
+ * - `--vitepress-linkcard-bg-color`: Background color (default: transparent)
117
+ *
118
+ * @returns Object containing style attribute strings for each card component
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * const styles = STYLE()
123
+ * // Use in HTML: <div ${styles.container}>...</div>
124
+ * ```
125
+ *
126
+ * @see {@link https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/components/VPFeature.vue | VPFeature component}
48
127
  */
49
- export const STYLE = (borderColor, bgColor) => ({
128
+ export const STYLE = () => ({
50
129
  a: inlineStyle({
51
130
  color: 'unset !important',
52
131
  display: 'block',
@@ -59,11 +138,12 @@ export const STYLE = (borderColor, bgColor) => ({
59
138
  flexWrap: 'wrap',
60
139
  gap: '10px',
61
140
  borderRadius: '12px',
62
- border: `1px solid ${borderColor}`,
63
- backgroundColor: bgColor,
141
+ border: `1px solid var(--vp-c-bg-soft)`,
142
+ backgroundColor: `var(--vp-c-bg-soft)`,
64
143
  boxSizing: 'border-box',
65
144
  width: '100%',
66
- height: '130px'
145
+ height: '130px',
146
+ transition: 'border-color 0.25s, background-color 0.25s'
67
147
  }),
68
148
  img: inlineStyle({
69
149
  borderRadius: '0px 12px 12px 0px',
@@ -102,8 +182,27 @@ export const STYLE = (borderColor, bgColor) => ({
102
182
  })
103
183
  });
104
184
  /**
105
- * @param prefix
106
- * @returns
185
+ * Generates CSS class names with a custom prefix.
186
+ *
187
+ * When using the `classPrefix` option, this function creates consistent
188
+ * class names for all card components following a BEM-like naming convention.
189
+ *
190
+ * @param prefix - The prefix to prepend to all class names
191
+ * @returns Object mapping component names to their class names
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const classes = classNames('my-card')
196
+ * // Returns: {
197
+ * // container: 'my-card__container',
198
+ * // title: 'my-card__texts--title',
199
+ * // ...
200
+ * // }
201
+ * ```
202
+ *
203
+ * @remarks
204
+ * When class names are used instead of inline styles, you must provide
205
+ * your own CSS definitions for these classes.
107
206
  */
108
207
  export const classNames = (prefix) => ({
109
208
  container: `${prefix}__container`,
@@ -1,13 +1,44 @@
1
1
  /**
2
- * @param url
3
- * @returns
2
+ * URL utility functions for parsing and manipulating URLs.
3
+ *
4
+ * @module url
5
+ */
6
+ /**
7
+ * Parses a URL string and returns a URL object.
8
+ *
9
+ * This is a wrapper around the native URL constructor that provides
10
+ * a consistent interface for URL parsing throughout the codebase.
11
+ *
12
+ * @param url - The URL string to parse
13
+ * @returns A URL object containing the parsed components
14
+ * @throws {TypeError} If the URL string is invalid
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const urlObj = extractUrl('https://example.com/path')
19
+ * console.log(urlObj.origin) // "https://example.com"
20
+ * console.log(urlObj.pathname) // "/path"
21
+ * ```
22
+ *
23
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL | MDN - URL}
4
24
  */
5
25
  export function extractUrl(url) {
6
26
  return new URL(url);
7
27
  }
8
28
  /**
9
- * @param path
10
- * @returns
29
+ * Removes duplicate consecutive slashes from a path string.
30
+ *
31
+ * This function normalizes paths by replacing multiple consecutive slashes
32
+ * with a single slash. Useful for constructing clean URLs from path segments.
33
+ *
34
+ * @param path - The path string to clean
35
+ * @returns The cleaned path with no consecutive slashes
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * cleanPath('/path//to///file') // Returns: '/path/to/file'
40
+ * cleanPath('//images//logo.png') // Returns: '/images/logo.png'
41
+ * ```
11
42
  */
12
43
  export function cleanPath(path) {
13
44
  return path.replace(/\/\//g, '/');
@@ -1,13 +1,52 @@
1
+ /**
2
+ * XHR module for fetching remote URL content.
3
+ *
4
+ * This module provides both synchronous and asynchronous HTTP GET request functionality
5
+ * with built-in caching. It works in both browser and Node.js environments by using
6
+ * the appropriate XMLHttpRequest implementation.
7
+ *
8
+ * @module xhr
9
+ * @todo Replace xmlhttprequest with a modern alternative (e.g., node-fetch or axios)
10
+ */
1
11
  // Refactor: xmlhttprequest will be replaced later
2
12
  // @ts-expect-error: xmlhttprequest has no types
3
13
  import xhrForNode from 'xmlhttprequest';
4
14
  import { inBrowser, isString } from '@luckrya/utility';
5
- // TODO: Local File Cache
15
+ /**
16
+ * In-memory cache for storing fetched HTML content by URL.
17
+ * Prevents redundant network requests for the same URLs.
18
+ * @internal
19
+ */
6
20
  const cache = new Map();
21
+ /**
22
+ * XMLHttpRequest implementation that works in both browser and Node.js.
23
+ * @internal
24
+ */
7
25
  const XHR = inBrowser ? window.XMLHttpRequest : xhrForNode.XMLHttpRequest;
8
26
  /**
9
- * @param url
10
- * @returns
27
+ * Performs a synchronous HTTP GET request to fetch HTML content from a URL.
28
+ *
29
+ * This function fetches the content synchronously, which means it blocks execution
30
+ * until the request completes. The response is cached to avoid redundant requests.
31
+ *
32
+ * @param url - The URL to fetch content from
33
+ * @returns The HTML content as a string, or undefined if the request fails
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const html = sync('https://example.com')
38
+ * if (html) {
39
+ * console.log('Fetched HTML content')
40
+ * }
41
+ * ```
42
+ *
43
+ * @remarks
44
+ * - Returns cached content if available
45
+ * - Only returns content if HTTP status is 200
46
+ * - Errors are logged to console but not thrown
47
+ * - Synchronous requests block the event loop - use with caution
48
+ *
49
+ * @see {@link async} for an asynchronous alternative
11
50
  */
12
51
  export function sync(url) {
13
52
  if (cache.has(url))
@@ -29,8 +68,35 @@ export function sync(url) {
29
68
  return result;
30
69
  }
31
70
  /**
32
- * @param url
33
- * @returns
71
+ * Performs an asynchronous HTTP GET request to fetch HTML content from a URL.
72
+ *
73
+ * This function fetches content asynchronously using Promises. The response is
74
+ * cached to avoid redundant requests.
75
+ *
76
+ * @param url - The URL to fetch content from
77
+ * @returns A Promise that resolves to the HTML content string, or undefined if the request fails
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * async function fetchPage() {
82
+ * try {
83
+ * const html = await async('https://example.com')
84
+ * if (html) {
85
+ * console.log('Fetched HTML content')
86
+ * }
87
+ * } catch (error) {
88
+ * console.error('Failed to fetch:', error)
89
+ * }
90
+ * }
91
+ * ```
92
+ *
93
+ * @remarks
94
+ * - Returns cached content if available
95
+ * - Only resolves with content if HTTP status is 200 and readyState is 4
96
+ * - Promise is rejected if the request throws an error
97
+ * - Preferred over `sync()` for non-blocking operations
98
+ *
99
+ * @see {@link sync} for a synchronous alternative
34
100
  */
35
101
  export function async(url) {
36
102
  return new Promise((resolve, reject) => {