rehype-og-card 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 roboin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # rehype-og-card
2
+
3
+ Rehype plugin to convert text links to link cards inspired by [remark-link-card](https://github.com/gladevise/remark-link-card). Bare links are converted to link cards, but not links in lists or non-bare links.
4
+
5
+ | Features | remark-link-card | rehype-og-card |
6
+ | :--------------------------------------------- | :---------------------------------------------------------------------------------------: | :-----------------------: |
7
+ | Plugin type | Remark | Rehype |
8
+ | TypeScript support | ❌ | ✅ |
9
+ | Convert bare links to link cards | ✅ | ✅ |
10
+ | Shorten URLs displayed on link cards | ✅ | ✅ |
11
+ | Server-side cache for images | ✅ | ✅ |
12
+ | Cache images with long URLs | ❌ ([implemented](https://github.com/gladevise/remark-link-card/pull/16) but not released) | ✅ |
13
+ | Build cache for Open Graph metadata and images | ❌ | Planned to be implemented |
14
+ | Exclude links in lists from conversion | ❌ | ✅ |
15
+ | Exclude specific domains from conversion | ❌ | ✅ |
16
+ | Lazy loading images | ❌ | ✅ |
17
+ | Async decoding images | ❌ | ✅ |
18
+ | Customize the crawler's user agent string | ❌ | ✅ |
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ npm install rehype-og-card
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```js
29
+ import rehypeOGCard, { RehypeOGCardOptions } from "rehype-og-card";
30
+ import remarkParse from "remark-parse";
31
+ import remarkRehype from "remark-rehype";
32
+ import rehypeStringify from "rehype-stringify";
33
+ import { unified } from "unified";
34
+
35
+ const processorFactory = (options?: RehypeOGCardOptions) => {
36
+ return unified().use(remarkParse).use(remarkRehype).use(rehypeOGCard, options).use(rehypeStringify);
37
+ };
38
+
39
+ const main = async () => {
40
+ const processor = processorFactory({ serverCache: false });
41
+
42
+ const input = `
43
+ this is a link: https://blog.google/products/android/world-emoji-day-2024/
44
+
45
+ \`https://blog.google/products/android/world-emoji-day-2024/\`
46
+
47
+ https://blog.google/products/android/world-emoji-day-2024/
48
+
49
+ [https://blog.google/products/android/world-emoji-day-2024/](https://blog.google/products/android/world-emoji-day-2024/)
50
+
51
+ [Google blog](https://blog.google/products/android/world-emoji-day-2024/)
52
+ `.trim();
53
+
54
+ const result = await processor.process(input);
55
+ console.log(result.toString());
56
+ };
57
+
58
+ main();
59
+ ```
60
+
61
+ Output:
62
+
63
+ ```html
64
+ <p>this is a link: https://blog.google/products/android/world-emoji-day-2024/</p>
65
+ <p><code>https://blog.google/products/android/world-emoji-day-2024/</code></p>
66
+ <p><a class="rlc-container" href="https://blog.google/products/android/world-emoji-day-2024/">
67
+ <div class="rlc-info">
68
+ <div class="rlc-title">10 fun facts about emoji for World Emoji Day</div>
69
+ <div class="rlc-description">Celebrate World Emoji Day with Google, and check out what’s new for Emoji Kitchen.</div>
70
+ <div class="rlc-url-container">
71
+ <img class="rlc-favicon" loading="lazy" decoding="async" src="https://www.google.com/s2/favicons?domain=blog.google" alt="favicon" width="16" height="16">
72
+ <span class="rlc-url">blog.google</span>
73
+ </div>
74
+ </div>
75
+ <div class="rlc-image-container">
76
+ <img class="rlc-image" loading="lazy" decoding="async" src="https://storage.googleapis.com/gweb-uniblog-publish-prod/images/world_emoji_day_v2_1.width-1300.png" alt="https://storage.googleapis.com/gweb-uniblog-publish-prod/images/world_emoji_day_v2_1.width-1300.png">
77
+ </div>
78
+ </a></p>
79
+ <p><a href="https://blog.google/products/android/world-emoji-day-2024/">https://blog.google/products/android/world-emoji-day-2024/</a></p>
80
+ <p><a href="https://blog.google/products/android/world-emoji-day-2024/">Google blog</a></p>
81
+ ```
82
+
83
+ ## What will be converted to link cards and what won't?
84
+
85
+ The following links will be converted to link cards:
86
+
87
+ - Bare links
88
+ - Links that have the same URL and text (if you enable the `enableSameTextURLConversion` option)
89
+
90
+ The following links will NOT be converted to link cards:
91
+
92
+ - Links in lists
93
+ - Links in code blocks
94
+ - Links in sentences
95
+ - Non-bare links
96
+ - Links that have the same URL and text (if you disable the `enableSameTextURLConversion` option)
97
+
98
+ ## Options
99
+
100
+ All options are optional.
101
+
102
+ ### `crawlerUserAgent`
103
+
104
+ User agent to use for fetching the OG metadata and images.
105
+
106
+ - Type: `string`
107
+ - Default: `"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"`
108
+
109
+ ### `decoding`
110
+
111
+ `decoding` attribute of the image element.
112
+
113
+ - Type: `"sync" | "async" | "auto"`
114
+ - Default: `"async"`
115
+
116
+ ### `enableSameTextURLConversion`
117
+
118
+ Whether to enable converting the links that have the same URL and text (e.g., `[https://example.com](https://example.com)`).
119
+
120
+ - Type: `boolean`
121
+ - Default: `false`
122
+
123
+ ### `excludeDomains`
124
+
125
+ Domains to exclude from conversion.
126
+
127
+ - Type: `string[]`
128
+ - Default: `[]`
129
+
130
+ ### `loading`
131
+
132
+ `loading` attribute of the image element.
133
+
134
+ - Type: `"eager" | "lazy"`
135
+ - Default: `"lazy"`
136
+
137
+ ### `serverCache`
138
+
139
+ Whether to cache the OG data on the server.
140
+
141
+ - Type: `boolean`
142
+ - Default: `true`
143
+
144
+ ### `serverCachePath`
145
+
146
+ Path to save the server cache.
147
+
148
+ - Type: `string`
149
+ - Default: `"./public/rehype-og-card-cache/"`
150
+
151
+ ### `shortenURL`
152
+
153
+ Whether to shorten the displayed URL.
154
+
155
+ - Type: `boolean`
156
+ - Default: `true`
@@ -0,0 +1,11 @@
1
+ import { RehypeOGCardOptions } from "./util.js";
2
+ import type { Plugin } from "unified";
3
+ import type { Root } from "hast";
4
+ /**
5
+ * Rehype plugin to create OG card from bare links.
6
+ * @param options Plugin options.
7
+ * @returns Transformer function.
8
+ */
9
+ declare const rehypeOGCard: Plugin<[RehypeOGCardOptions | undefined], Root>;
10
+ export default rehypeOGCard;
11
+ export { RehypeOGCardOptions };
package/dist/index.js ADDED
@@ -0,0 +1,111 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { convertTextToAnchorElement, createOGCard, downloadImage, getOGData, isAnchorElement, isTextNode, isValidURL } from "./util.js";
11
+ import { isElement } from "hast-util-is-element";
12
+ import { visitParents } from "unist-util-visit-parents";
13
+ const DEFAULT_OPTIONS = {
14
+ crawlerUserAgent:
15
+ // eslint-disable-next-line max-len
16
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
17
+ decoding: "async",
18
+ enableSameTextURLConversion: false,
19
+ excludeDomains: [],
20
+ loading: "lazy",
21
+ serverCache: true,
22
+ serverCachePath: "./public/rehype-og-card-cache/",
23
+ shortenURL: true
24
+ };
25
+ /**
26
+ * Rehype plugin to create OG card from bare links.
27
+ * @param options Plugin options.
28
+ * @returns Transformer function.
29
+ */
30
+ // eslint-disable-next-line max-lines-per-function
31
+ const rehypeOGCard = (options) => {
32
+ const mergedOptions = Object.assign(Object.assign({}, DEFAULT_OPTIONS), options);
33
+ /**
34
+ * Transform function to create OG card from bare links.
35
+ * @param tree Root node of the HAST tree.
36
+ */
37
+ // eslint-disable-next-line max-lines-per-function
38
+ const transform = (tree) => __awaiter(void 0, void 0, void 0, function* () {
39
+ const linkCardPromises = [];
40
+ // eslint-disable-next-line max-statements, max-lines-per-function
41
+ visitParents(tree, ["element", "text"], (node, ancestors) => {
42
+ var _a;
43
+ let anchorNode = null;
44
+ if (node.type === "text" && isValidURL(node.value.trim())) {
45
+ anchorNode = convertTextToAnchorElement(node);
46
+ }
47
+ else if (mergedOptions.enableSameTextURLConversion &&
48
+ isAnchorElement(node) &&
49
+ isValidURL(node.properties.href)) {
50
+ const isBareLink =
51
+ // eslint-disable-next-line no-magic-numbers
52
+ node.children.length === 1 &&
53
+ isTextNode(node.children[0]) &&
54
+ node.children[0].value === node.properties.href;
55
+ if (!isBareLink)
56
+ return;
57
+ anchorNode = node;
58
+ }
59
+ else {
60
+ return;
61
+ }
62
+ // eslint-disable-next-line no-magic-numbers
63
+ if (!isElement(ancestors[ancestors.length - 1], "p"))
64
+ return;
65
+ const shouldSkip = ancestors.some((ancestor) => isElement(ancestor) && ["ul", "ol"].includes(ancestor.tagName));
66
+ if (shouldSkip)
67
+ return;
68
+ // eslint-disable-next-line no-magic-numbers
69
+ const isTheOnlyChild = ancestors[ancestors.length - 1].children.length === 1;
70
+ if (!isTheOnlyChild)
71
+ return;
72
+ const targetURL = new URL(anchorNode.properties.href);
73
+ if ((_a = mergedOptions.excludeDomains) === null || _a === void 0 ? void 0 : _a.includes(targetURL.hostname))
74
+ return;
75
+ // eslint-disable-next-line jsdoc/require-jsdoc, max-statements
76
+ const linkCardPromise = () => __awaiter(void 0, void 0, void 0, function* () {
77
+ const OGData = yield getOGData(anchorNode.properties.href, mergedOptions.crawlerUserAgent);
78
+ if (!OGData)
79
+ return;
80
+ if (OGData.OGImageURL && mergedOptions.serverCache) {
81
+ const cachePath = yield downloadImage(OGData.OGImageURL, mergedOptions.serverCachePath, mergedOptions.crawlerUserAgent);
82
+ if (cachePath) {
83
+ OGData.OGImageURL = `/${cachePath}`;
84
+ }
85
+ }
86
+ if (OGData.faviconURL && mergedOptions.serverCache) {
87
+ const cachePath = yield downloadImage(OGData.faviconURL, mergedOptions.serverCachePath, mergedOptions.crawlerUserAgent);
88
+ if (cachePath) {
89
+ OGData.faviconURL = `/${cachePath}`;
90
+ }
91
+ }
92
+ if (mergedOptions.shortenURL) {
93
+ const shortenedURL = new URL(anchorNode.properties.href);
94
+ OGData.displayURL = shortenedURL.hostname;
95
+ }
96
+ const OGCard = createOGCard(OGData, mergedOptions);
97
+ // eslint-disable-next-line no-magic-numbers
98
+ const index = ancestors[ancestors.length - 1].children.indexOf(node);
99
+ // eslint-disable-next-line no-magic-numbers
100
+ if (index === -1)
101
+ return;
102
+ // eslint-disable-next-line no-magic-numbers
103
+ ancestors[ancestors.length - 1].children.splice(index, 1, ...OGCard);
104
+ });
105
+ linkCardPromises.push(linkCardPromise());
106
+ });
107
+ yield Promise.all(linkCardPromises);
108
+ });
109
+ return transform;
110
+ };
111
+ export default rehypeOGCard;
package/dist/util.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ import type { Element, ElementContent, RootContent, Text } from "hast";
2
+ interface RehypeOGCardOptions {
3
+ /**
4
+ * Domains to exclude from conversion.
5
+ */
6
+ excludeDomains?: string[];
7
+ /**
8
+ * `decoding` attribute of the image element.
9
+ * @default "async"
10
+ */
11
+ decoding?: HTMLImageElement["decoding"];
12
+ /**
13
+ * `loading` attribute of the image element.
14
+ * @default "lazy"
15
+ */
16
+ loading?: HTMLImageElement["loading"];
17
+ /**
18
+ * Whether to shorten the displayed URL.
19
+ * @default true
20
+ */
21
+ shortenURL?: boolean;
22
+ /**
23
+ * Whether to enable converting the links
24
+ * that have the same URL and text (e.g., `[https://example.com](https://example.com)`).
25
+ * @default false
26
+ */
27
+ enableSameTextURLConversion?: boolean;
28
+ /**
29
+ * Whether to cache the OG data on the server.
30
+ * @default true
31
+ */
32
+ serverCache?: boolean;
33
+ /**
34
+ * Path to save the server cache.
35
+ * @default "./public/rehype-og-card-cache/"
36
+ */
37
+ serverCachePath?: string;
38
+ /**
39
+ * User agent to use for fetching the OG metadata and images.
40
+ * @default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
41
+ */
42
+ crawlerUserAgent?: string;
43
+ }
44
+ interface AnchorElement extends Element {
45
+ tagName: "a";
46
+ properties: {
47
+ href: string;
48
+ };
49
+ }
50
+ /**
51
+ * Check if the node is a text node.
52
+ * @param node Node to check.
53
+ * @returns `true` if the node is a text node, `false` otherwise.
54
+ */
55
+ declare const isTextNode: (node: ElementContent) => node is Text;
56
+ /**
57
+ * Check if the node is an anchor element.
58
+ * @param node Node to check.
59
+ * @returns `true` if the node is an anchor element, `false` otherwise.
60
+ */
61
+ declare const isAnchorElement: (node: unknown) => node is AnchorElement;
62
+ /**
63
+ * Check if the URL is valid.
64
+ * @param url URL to check.
65
+ * @returns `true` if the URL is valid, `false` otherwise.
66
+ */
67
+ declare const isValidURL: (url: string) => boolean;
68
+ interface OGCardData {
69
+ url: string;
70
+ title: string;
71
+ description?: string;
72
+ displayURL: string;
73
+ faviconURL?: string;
74
+ OGImageURL?: string;
75
+ OGImageAlt?: string;
76
+ OGImageWidth?: number;
77
+ OGImageHeight?: number;
78
+ }
79
+ /**
80
+ * Get OG card data from given URL.
81
+ * @param url URL to get OG card data.
82
+ * @param userAgent User agent to use for fetching.
83
+ * @returns OG card data.
84
+ */
85
+ declare const getOGData: (url: string, userAgent: string) => Promise<OGCardData | null>;
86
+ /**
87
+ * Create OG card element from given data.
88
+ * @param data Data to create OG card.
89
+ * @param options Options to create OG card.
90
+ * @returns OG card element.
91
+ */
92
+ declare const createOGCard: (data: OGCardData, options: Required<{
93
+ loading: RehypeOGCardOptions["loading"];
94
+ decoding: RehypeOGCardOptions["decoding"];
95
+ }>) => RootContent[];
96
+ /**
97
+ * Convert text node to anchor element. **This function does not check if the text is a valid URL.**
98
+ * @param text Text node to convert.
99
+ * @returns Anchor element.
100
+ */
101
+ declare const convertTextToAnchorElement: (text: Text) => AnchorElement;
102
+ /**
103
+ * Download image from given URL.
104
+ * @param url URL to download image.
105
+ * @param directly Path to save the image.
106
+ * @param userAgent User agent to use for fetching.
107
+ * @returns Path to the downloaded image.
108
+ */
109
+ declare const downloadImage: (url: string, directly: string, userAgent: string) => Promise<string | null>;
110
+ export { AnchorElement, RehypeOGCardOptions, OGCardData, isTextNode, isAnchorElement, isValidURL, getOGData, createOGCard, convertTextToAnchorElement, downloadImage };
package/dist/util.js ADDED
@@ -0,0 +1,189 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { createHash } from "crypto";
11
+ import { fromHtml } from "hast-util-from-html";
12
+ import fs from "fs/promises";
13
+ import { isElement } from "hast-util-is-element";
14
+ import path from "path";
15
+ import sanitizeHtml from "sanitize-html";
16
+ import scraper from "open-graph-scraper";
17
+ /**
18
+ * Check if the node is a text node.
19
+ * @param node Node to check.
20
+ * @returns `true` if the node is a text node, `false` otherwise.
21
+ */
22
+ const isTextNode = (node) => Boolean(node) && typeof node === "object" && "type" in node && node.type === "text";
23
+ /**
24
+ * Check if the node is an anchor element.
25
+ * @param node Node to check.
26
+ * @returns `true` if the node is an anchor element, `false` otherwise.
27
+ */
28
+ const isAnchorElement = (node) => Boolean(node) && isElement(node, "a") && "href" in node.properties && typeof node.properties.href === "string";
29
+ /**
30
+ * Check if the URL is valid.
31
+ * @param url URL to check.
32
+ * @returns `true` if the URL is valid, `false` otherwise.
33
+ */
34
+ const isValidURL = (url) => {
35
+ try {
36
+ new URL(url);
37
+ return true;
38
+ }
39
+ catch (_a) {
40
+ return false;
41
+ }
42
+ };
43
+ /**
44
+ * Get OG card data from given URL.
45
+ * @param url URL to get OG card data.
46
+ * @param userAgent User agent to use for fetching.
47
+ * @returns OG card data.
48
+ */
49
+ const getOGData = (url, userAgent) => __awaiter(void 0, void 0, void 0, function* () {
50
+ try {
51
+ const { result } = yield scraper({
52
+ fetchOptions: {
53
+ headers: {
54
+ "user-agent": userAgent
55
+ }
56
+ },
57
+ url
58
+ });
59
+ const OGImage = result.ogImage ? result.ogImage[0] : null;
60
+ const faviconURL = result.favicon
61
+ ? `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}`
62
+ : // eslint-disable-next-line no-undefined
63
+ undefined;
64
+ return {
65
+ OGImageAlt: OGImage === null || OGImage === void 0 ? void 0 : OGImage.alt,
66
+ OGImageHeight: OGImage === null || OGImage === void 0 ? void 0 : OGImage.height,
67
+ OGImageURL: OGImage === null || OGImage === void 0 ? void 0 : OGImage.url,
68
+ OGImageWidth: OGImage === null || OGImage === void 0 ? void 0 : OGImage.width,
69
+ // eslint-disable-next-line no-undefined
70
+ description: result.ogDescription ? sanitizeHtml(result.ogDescription) : undefined,
71
+ displayURL: url,
72
+ faviconURL,
73
+ title: result.ogTitle ? sanitizeHtml(result.ogTitle) : url,
74
+ url
75
+ };
76
+ }
77
+ catch (error) {
78
+ // eslint-disable-next-line no-console
79
+ console.error("[rehype-og-card] Error fetching OG data:", error);
80
+ return null;
81
+ }
82
+ });
83
+ /**
84
+ * Sanitize HTML.
85
+ * @param html HTML to sanitize.
86
+ * @returns Sanitized HTML.
87
+ */
88
+ const sanitize = (html) => {
89
+ const options = {
90
+ allowedAttributes: {},
91
+ allowedTags: [],
92
+ disallowedTagsMode: "recursiveEscape"
93
+ // eslint-disable-next-line no-magic-numbers
94
+ };
95
+ return sanitizeHtml(html, options);
96
+ };
97
+ /**
98
+ * Create OG card element from given data.
99
+ * @param data Data to create OG card.
100
+ * @param options Options to create OG card.
101
+ * @returns OG card element.
102
+ */
103
+ const createOGCard = (data, options) => {
104
+ data.title = sanitize(data.title);
105
+ if (data.description) {
106
+ data.description = sanitize(data.description);
107
+ }
108
+ const imageContainerElement = data.OGImageURL
109
+ ? `
110
+ <div class="rlc-image-container">
111
+ <img class="rlc-image"
112
+ loading="${options.loading}"
113
+ decoding="${options.decoding}"
114
+ src="${data.OGImageURL}" alt="${data.OGImageAlt || data.OGImageURL}"
115
+ ${data.OGImageWidth ? `width="${data.OGImageWidth}"` : ""}
116
+ ${data.OGImageHeight ? `height="${data.OGImageHeight}"` : ""} >
117
+ </div>`
118
+ : "";
119
+ const descriptionElement = data.description
120
+ ? `
121
+ <div class="rlc-description">${data.description}</div>`
122
+ : "";
123
+ // The `loading` attribute must be specified before the `src` attribute because of a bug in Firefox.
124
+ // See https://bugzil.la/1647077 for more information.
125
+ const card = `
126
+ <a class="rlc-container" href="${data.url}">
127
+ <div class="rlc-info">
128
+ <div class="rlc-title">${data.title}</div>${descriptionElement}
129
+ <div class="rlc-url-container">
130
+ <img class="rlc-favicon"
131
+ loading="${options.loading}"
132
+ decoding="${options.decoding}"
133
+ src="${data.faviconURL}" alt="favicon" width="16" height="16">
134
+ <span class="rlc-url">${data.displayURL}</span>
135
+ </div>
136
+ </div>${imageContainerElement}
137
+ </a>
138
+ `.trim();
139
+ return fromHtml(card, { fragment: true }).children;
140
+ };
141
+ /**
142
+ * Convert text node to anchor element. **This function does not check if the text is a valid URL.**
143
+ * @param text Text node to convert.
144
+ * @returns Anchor element.
145
+ */
146
+ const convertTextToAnchorElement = (text) => ({
147
+ children: [text],
148
+ properties: {
149
+ href: text.value.trim()
150
+ },
151
+ tagName: "a",
152
+ type: "element"
153
+ });
154
+ /**
155
+ * Download image from given URL.
156
+ * @param url URL to download image.
157
+ * @param directly Path to save the image.
158
+ * @param userAgent User agent to use for fetching.
159
+ * @returns Path to the downloaded image.
160
+ */
161
+ // eslint-disable-next-line max-statements
162
+ const downloadImage = (url, directly, userAgent) => __awaiter(void 0, void 0, void 0, function* () {
163
+ if (!isValidURL(url)) {
164
+ // eslint-disable-next-line no-console
165
+ console.error("[rehype-og-card] Invalid thumbnail URL:", url);
166
+ return null;
167
+ }
168
+ try {
169
+ const response = yield fetch(url, {
170
+ headers: {
171
+ "user-agent": userAgent
172
+ }
173
+ });
174
+ const arrayBuffer = yield response.arrayBuffer();
175
+ const buffer = Buffer.from(arrayBuffer);
176
+ const hash = createHash("sha256").update(url).digest("hex");
177
+ const filename = hash + path.extname(new URL(url).pathname);
178
+ const savePath = path.posix.join(directly, filename);
179
+ yield fs.mkdir(directly, { recursive: true });
180
+ yield fs.writeFile(savePath, buffer);
181
+ return savePath;
182
+ }
183
+ catch (error) {
184
+ // eslint-disable-next-line no-console
185
+ console.error("[rehype-og-card] Error downloading image:", error);
186
+ return null;
187
+ }
188
+ });
189
+ export { isTextNode, isAnchorElement, isValidURL, getOGData, createOGCard, convertTextToAnchorElement, downloadImage };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "rehype-og-card",
3
+ "version": "0.1.0",
4
+ "description": "Rehype plugin to convert text links to link cards",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "./dist"
9
+ ],
10
+ "type": "module",
11
+ "publishConfig": {
12
+ "provenance": true
13
+ },
14
+ "scripts": {
15
+ "test": "npx vitest",
16
+ "lint": "npx eslint ./src/**/*.ts",
17
+ "format": "npx prettier --write ./src/**/*",
18
+ "format:check": "npx prettier --check ./src/**/*",
19
+ "build": "npx tsc",
20
+ "version": "npm run build && git add .",
21
+ "ci:version": "changeset version && npm run version",
22
+ "ci:publish": "npm run build && changeset publish"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Robot-Inventor/rehype-og-card.git"
27
+ },
28
+ "keywords": [
29
+ "markdown",
30
+ "remark",
31
+ "ogp",
32
+ "unified",
33
+ "rehype",
34
+ "rehype-plugin"
35
+ ],
36
+ "author": "Robot-Inventor",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/Robot-Inventor/rehype-og-card/issues"
40
+ },
41
+ "homepage": "https://github.com/Robot-Inventor/rehype-og-card#readme",
42
+ "devDependencies": {
43
+ "@changesets/changelog-github": "^0.5.0",
44
+ "@changesets/cli": "^2.27.7",
45
+ "@robot-inventor/eslint-config": "^0.2.21",
46
+ "@types/node": "^20.14.11",
47
+ "@types/sanitize-html": "^2.11.0",
48
+ "eslint": "^9.7.0",
49
+ "hast-util-to-html": "^9.0.1",
50
+ "prettier": "^3.3.3",
51
+ "rehype-stringify": "^10.0.0",
52
+ "remark-parse": "^11.0.0",
53
+ "remark-rehype": "^11.1.0",
54
+ "typescript": "^5.5.3",
55
+ "unified": "^11.0.5",
56
+ "vitest": "^2.0.3"
57
+ },
58
+ "dependencies": {
59
+ "@types/hast": "^3.0.4",
60
+ "hast-util-from-html": "^2.0.1",
61
+ "hast-util-is-element": "^3.0.0",
62
+ "open-graph-scraper": "^6.6.3",
63
+ "sanitize-html": "^2.13.0",
64
+ "unist-util-visit-parents": "^6.0.1"
65
+ }
66
+ }