vitepress-linkcards 2.2.2
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 +21 -0
- package/README.md +133 -0
- package/dist/.cjs.min.js +23 -0
- package/dist/.esm.min.js +23 -0
- package/dist/api.js +36 -0
- package/dist/assemble/html.js +60 -0
- package/dist/assemble/index.js +4 -0
- package/dist/assemble/local-file-cache.js +66 -0
- package/dist/assemble/metadata.js +21 -0
- package/dist/assemble/parser.js +82 -0
- package/dist/assemble/style.js +93 -0
- package/dist/assemble/url.js +18 -0
- package/dist/assemble/xhr.js +59 -0
- package/dist/index.js +3 -0
- package/dist/link-to-card-plugin.js +78 -0
- package/dist/types.js +1 -0
- package/package.json +71 -0
- package/types/api.d.ts +16 -0
- package/types/assemble/html.d.ts +9 -0
- package/types/assemble/index.d.ts +4 -0
- package/types/assemble/local-file-cache.d.ts +11 -0
- package/types/assemble/metadata.d.ts +8 -0
- package/types/assemble/parser.d.ts +9 -0
- package/types/assemble/style.d.ts +20 -0
- package/types/assemble/url.d.ts +14 -0
- package/types/assemble/xhr.d.ts +14 -0
- package/types/index.d.ts +3 -0
- package/types/link-to-card-plugin.d.ts +8 -0
- package/types/types.d.ts +124 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 No bug
|
|
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,133 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
vitepress-linkcard
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
|
|
7
|
+
**A VitePress plugin to generate a pretty linkcard with OGP.**
|
|
8
|
+
|
|
9
|
+
You can see: [A blog generated with this plugin](https://asumoranda.com/posts/10-vitepress-linkcard.html) | [Docs by TypeDoc](https://asumo-1xts.github.io/vitepress-linkcard/)
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/vitepress-linkcard)
|
|
12
|
+
[](https://vuejs.github.io/vitepress/v1]/)
|
|
13
|
+
[](/LICENSE)
|
|
14
|
+
|
|
15
|
+
[](https://yarnpkg.com/)
|
|
16
|
+
[](https://oxc.rs/)
|
|
17
|
+
|
|
18
|
+
<img src="https://github.com/asumo-1xts/vitepress-linkcard/blob/main/.github/screen.gif?raw=true" width=90% alt="How it shows" />
|
|
19
|
+
|
|
20
|
+
This plugin was forked from [markdown-it-link-to-card](https://github.com/luckrya/markdown-it-link-to-card).
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
## Getting started
|
|
25
|
+
|
|
26
|
+
### Install
|
|
27
|
+
|
|
28
|
+
```shell
|
|
29
|
+
npm i -D vitepress-linkcard # npm
|
|
30
|
+
yarn add -D vitepress-linkcard # yarn
|
|
31
|
+
pnpm add -D vitepress-linkcard # pnpm
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Usage
|
|
35
|
+
|
|
36
|
+
#### `docs/.vitepress/config.ts`
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { defineConfig } from 'vitepress'
|
|
40
|
+
import { linkToCardPlugin } from 'vitepress-linkcard'
|
|
41
|
+
import type { LinkToCardPluginOptions } from 'vitepress-linkcard'
|
|
42
|
+
|
|
43
|
+
export default defineConfig({
|
|
44
|
+
// ...
|
|
45
|
+
markdown: {
|
|
46
|
+
config: (md) => {
|
|
47
|
+
md.use<LinkToCardPluginOptions>(linkToCardPlugin, {
|
|
48
|
+
// target: "_self" // if needed
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ...
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### `*.md`
|
|
57
|
+
|
|
58
|
+
Generates a linkcard when `@:` appended.
|
|
59
|
+
|
|
60
|
+
```md
|
|
61
|
+
[example](@:https://example.com)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Supported options
|
|
65
|
+
|
|
66
|
+
### Target
|
|
67
|
+
|
|
68
|
+
As shown in [Usage](#usage), you can specify the target window in which to open a link.
|
|
69
|
+
|
|
70
|
+
- `_blank` **(default)**
|
|
71
|
+
- `_self`
|
|
72
|
+
- `_top`
|
|
73
|
+
- `_parent`
|
|
74
|
+
|
|
75
|
+
### Color theme
|
|
76
|
+
|
|
77
|
+
You can customize:
|
|
78
|
+
|
|
79
|
+
| Property | **default** |
|
|
80
|
+
| :---------------------------- | --------------------- |
|
|
81
|
+
| Border color | `var(--vp-c-bg)` |
|
|
82
|
+
| Background color | `var(--vp-c-bg-soft)` |
|
|
83
|
+
| Border color when hovered | none |
|
|
84
|
+
| Background color when hovered | none |
|
|
85
|
+
|
|
86
|
+
#### `docs/.vitepress/theme/custom.css`
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
/* For example: like "Features" in VitePress */
|
|
90
|
+
|
|
91
|
+
.vitepress-linkcard-container {
|
|
92
|
+
border-color: #00000000 !important;
|
|
93
|
+
background-color: var(--vp-c-bg-soft) !important;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.vitepress-linkcard-container:hover {
|
|
97
|
+
border-color: var(--vp-c-brand-1) !important;
|
|
98
|
+
background-color: var(--vp-c-bg-soft) !important;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### `docs/.vitepress/theme/index.ts`
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import DefaultTheme from 'vitepress/theme-without-fonts'
|
|
106
|
+
import type { Theme as ThemeConfig } from 'vitepress'
|
|
107
|
+
import './custom.css'
|
|
108
|
+
|
|
109
|
+
const Theme: ThemeConfig = {
|
|
110
|
+
extends: DefaultTheme
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default {
|
|
114
|
+
...Theme
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Other specifications
|
|
119
|
+
|
|
120
|
+
### `.linkcard_cache.json`
|
|
121
|
+
|
|
122
|
+
It is generated automatically in `docs/` and cache all the parsed metadata.
|
|
123
|
+
|
|
124
|
+
You can move it to root dir if needed.
|
|
125
|
+
|
|
126
|
+
### Special handling for `github.com`
|
|
127
|
+
|
|
128
|
+
When the domain is `github.com`, trimming is performed as shown in the following example to avoid duplication of the title and description.
|
|
129
|
+
|
|
130
|
+
| | Title | Description |
|
|
131
|
+
| ------ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| Before | GitHub - asumo-1xts/vitepress-linkcard: A VitePress plugin to generate a pretty linkcard. | A VitePress plugin to generate a pretty linkcard. Contribute to asumo-1xts/vitepress-linkcard development by creating an account on GitHub. |
|
|
133
|
+
| After | asumo-1xts/vitepress-linkcard | A VitePress plugin to generate a pretty linkcard. |
|
package/dist/.cjs.min.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* vitepress-linkcards v2.2.2
|
|
3
|
+
* (c) 2022 - 2026 luckrya
|
|
4
|
+
* Released under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports,"__esModule",{value:!0});var e=require("url"),t=require("child_process"),r=require("fs"),n=require("http"),o=require("https"),i=require("node:fs");function a(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var s,c,l=a(e),u=a(t),f=a(r),d=a(n),p=a(o),h=a(i);function v(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function g(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,m(n.key),n)}}function y(e,t,r){return(t=m(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function b(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),r.push.apply(r,n)}return r}function E(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?b(Object(r),!0).forEach(function(t){y(e,t,r[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):b(Object(r)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))})}return e}function w(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=r){var n,o,i,a,s=[],c=!0,l=!1;try{if(i=(r=r.call(e)).next,0===t){if(Object(r)!==r)return;c=!1}else for(;!(c=(n=i.call(r)).done)&&(s.push(n.value),s.length!==t);c=!0);}catch(e){l=!0,o=e}finally{try{if(!c&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(l)throw o}}return s}}(e,t)||function(e,t){if(e){if("string"==typeof e)return v(e,t);var r={}.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?v(e,t):void 0}}
|
|
7
|
+
/*!
|
|
8
|
+
* @luckrya/utility v0.1.0
|
|
9
|
+
* (c) 2022 - 2022 Y.R
|
|
10
|
+
* Released under the MIT License.
|
|
11
|
+
*/(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function m(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}(c=s||(s={})).Array="[object Array]",c.Object="[object Object]",c.Function="[object Function]",c.Number="[object Number]",c.String="[object String]",c.Boolean="[object Boolean]",c.Undefined="[object Undefined]",c.Null="[object Null]",c.Error="[object Error]";var x=function(e){return Object.prototype.toString.call(e)===s.String},S=function(e){return Object.prototype.toString.call(e)===s.Function},O="undefined"!=typeof window,k=function(){var e,t="".concat(process.cwd(),"/docs/.linkcard_cache.json"),r="".concat(process.cwd(),"/.linkcard_cache.json");if(h.default.existsSync(t))e=t;else if(h.default.existsSync(r))e=r;else{e=t;h.default.writeFileSync(e,JSON.stringify({"https://example.com/":{description:"Example Website",logo:"https://example.com/example.png",title:"Example Title"}},null,2))}return e},N=function(){return function(e,t,r){return t&&g(e.prototype,t),r&&g(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}(function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e)},[{key:"setFile",value:function(e){var t=e,r=this.readFile();r&&(t=Object.assign(r,t)),h.default.writeFileSync(k(),JSON.stringify(t)),function(){var e=k(),t=h.default.readFileSync(e,"utf-8").trim();if(t){var r=JSON.parse(t),n=JSON.stringify(r,null,2)+"\n";h.default.writeFileSync(e,n)}}()}},{key:"readFile",value:function(){var e=h.default.readFileSync(k(),"utf-8"),t=JSON.parse(e);if(function(e){return Object.prototype.toString.call(e)===s.Object}(t))return t}},{key:"has",value:function(e){return!!this.get(e)}},{key:"get",value:function(e){var t=this.readFile();return null==t?void 0:t[e]}},{key:"set",value:function(e,t){this.setFile(y({},e,t))}}])}(),T=new N;function j(e){return'style="'.concat(function(e){return Object.entries(e).map(function(e){var t,r=w(e,2),n=r[0],o=r[1];if(n&&o)return"".concat((t=n,t.replace(/\B([A-Z])/g,"-$1").toLowerCase()),": ").concat(o,";")}).filter(Boolean).join(" ")}(e),'"')}var C=function(e){return{"-webkit-box-orient":"vertical","-webkit-line-clamp":e,display:"-webkit-box",hyphens:"auto",lineClamp:e,overflow:"hidden",overflowWrap:"anywhere",textOverflow:"ellipsis",wordBreak:"break-word"}},R=function(e,t){var r,n,o={rel:'rel="noopener noreferrer"',target:'target="'.concat(t.target,'"'),href:'href="'.concat(t.href,'"'),title:'title="'.concat(t.linkTitle,'"')},i=function(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'")},a={a:j({color:"unset !important",display:"block",width:"100%",textDecoration:"none"}),container:j({display:"flex",alignItems:"center",flexWrap:"wrap",gap:"10px",borderRadius:"12px",border:"1px solid var(--vp-c-bg)",backgroundColor:"var(--vp-c-bg-soft)",boxSizing:"border-box",width:"100%",height:"130px",transition:"all 0.25s"}),img:j({borderRadius:"0px 12px 12px 0px",maxWidth:"40%",height:"128px",flexShrink:0,objectFit:"contain",overflow:"hidden"}),texts:j({flex:"1 1 0%",minWidth:"0"}),title:j(E(E({},C(2)),{},{opacity:1,fontSize:"16px",lineHeight:"22px",margin:"0 16px 8px 16px",fontWeight:"bold"})),domain:j(E(E({},C(1)),{},{opacity:1,fontSize:"12px",lineHeight:"16px",margin:"8px 16px 8px 16px",textDecoration:"underline"})),description:j(E(E({},C(2)),{},{opacity:.8,fontSize:"12px",lineHeight:"16px",margin:"8px 16px 0px 16px"}))},s=t.href||"",c=new URL(s).origin.replace(/^https?:\/\//,"").replace(/^www\./,"")||"Unknown domain",l=e.title,u=e.description;"github.com"==c?(l=(null===(r=e.title)||void 0===r?void 0:r.split(":")[0].replace("GitHub - ",""))||"No title",u=(null===(n=u)||void 0===n?void 0:n.replace(" - ".concat(l),"").replace("Contribute to ".concat(l," development by creating an account on GitHub."),""))||""):(l=e.title||"No title",u=e.description||"");return'<span style="display:block;">\n <a '.concat(o.rel," ").concat(o.target," ").concat(o.href," ").concat(o.title," ").concat(a.a,'>\n <span class="vitepress-linkcard-container" ').concat(a.container,">\n <span ").concat(a.texts,">\n <span ").concat(a.title,">\n ").concat(i(l),"\n </span>\n <span ").concat(a.domain,">\n ").concat(i(c),"\n </span>\n <span ").concat(a.description,">\n ").concat(i(u),'\n </span>\n </span>\n <img src="').concat(null==e?void 0:e.logo,'" ').concat(a.img,"/>\n </span>\n </a>\n</span>")};function D(e){return new URL(e)}var A="https://resources.whatwg.org/logo-url.svg",L=/(<[A-Za-z]+\s*[^>]*>(.*)<\/[A-Za-z]+>)/,q=/content=["|']([^>]*)["|']/,P=/href=["|']([^>]*)["|']/,H=/(<title\s*[^>]*>(.*)<\/title>)/g,F=function(e){return new RegExp("<".concat(arguments.length>1&&void 0!==arguments[1]?arguments[1]:"meta","\\s[^>]*\\w+=['|\"]([a-zA-Z]|:|\\s)*").concat(e,"['|\"][^>]*\\/?>"))};function _(e){var t,r=e.match(F("title"));if(null!=r&&r.length){var n=r[0].match(q);n&&x(n[1])&&(t=n[1])}else{var o=e.match(H);if(null!=o&&o.length){var i=o[0].match(L);i&&x(i[2])&&(t=i[2])}}return t}function I(e){var t,r=e.match(F("description"));if(null!=r&&r.length){var n=r[0].match(q);n&&x(n[1])&&(t=n[1])}return t}function U(e){var t,r=e.match(F("image"));if(null!=r&&r.length){var n=r[0].match(q);n&&x(n[1])&&(t=n[1])}else{var o=e.match(F("icon","link"));if(null!=o&&o.length){var i=o[0].match(P);i&&x(i[1])&&(t=i[1])}}return t}function M(e,t){var r,n,o,i={title:_(e),description:I(e),logo:(r=U(e),r?D(r)?r:"".concat(null===(n=D(t))||void 0===n?void 0:n.origin).concat("/".concat(r).replace(/\/\//g,"/")):A)};return o=i,Object.values(o).filter(function(e){return x(e)}).length?i:null}function B(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var J,G={};
|
|
12
|
+
/**
|
|
13
|
+
* Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
|
|
14
|
+
*
|
|
15
|
+
* This can be used with JS designed for browsers to improve reuse of code and
|
|
16
|
+
* allow the use of existing libraries.
|
|
17
|
+
*
|
|
18
|
+
* Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs.
|
|
19
|
+
*
|
|
20
|
+
* @author Dan DeFelippi <dan@driverdan.com>
|
|
21
|
+
* @contributor David Ellis <d.f.ellis@ieee.org>
|
|
22
|
+
* @license MIT
|
|
23
|
+
*/var z=function(){if(J)return G;J=1;var e=l.default,t=u.default.spawn,r=f.default;return G.XMLHttpRequest=function(){var n,o,i=this,a=d.default,s=p.default,c={},l=!1,u={"User-Agent":"node-XMLHttpRequest",Accept:"*/*"},f={},h={},v=["accept-charset","accept-encoding","access-control-request-headers","access-control-request-method","connection","content-length","content-transfer-encoding","cookie","cookie2","date","expect","host","keep-alive","origin","referer","te","trailer","transfer-encoding","upgrade","via"],g=["TRACE","TRACK","CONNECT"],y=!1,b=!1,E={};this.UNSENT=0,this.OPENED=1,this.HEADERS_RECEIVED=2,this.LOADING=3,this.DONE=4,this.readyState=this.UNSENT,this.onreadystatechange=null,this.responseText="",this.responseXML="",this.status=null,this.statusText=null,this.withCredentials=!1;this.open=function(e,t,r,n,o){if(this.abort(),b=!1,!function(e){return e&&-1===g.indexOf(e)}(e))throw new Error("SecurityError: Request method not allowed");c={method:e,url:t.toString(),async:"boolean"!=typeof r||r,user:n||null,password:o||null},w(this.OPENED)},this.setDisableHeaderCheck=function(e){l=e},this.setRequestHeader=function(e,t){if(this.readyState!==this.OPENED)throw new Error("INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN");if(function(e){return l||e&&-1===v.indexOf(e.toLowerCase())}(e)){if(y)throw new Error("INVALID_STATE_ERR: send flag is true");e=h[e.toLowerCase()]||e,h[e.toLowerCase()]=e,f[e]=f[e]?f[e]+", "+t:t}else console.warn('Refused to set unsafe header "'+e+'"')},this.getResponseHeader=function(e){return"string"==typeof e&&this.readyState>this.OPENED&&o&&o.headers&&o.headers[e.toLowerCase()]&&!b?o.headers[e.toLowerCase()]:null},this.getAllResponseHeaders=function(){if(this.readyState<this.HEADERS_RECEIVED||b)return"";var e="";for(var t in o.headers)"set-cookie"!==t&&"set-cookie2"!==t&&(e+=t+": "+o.headers[t]+"\r\n");return e.substr(0,e.length-2)},this.getRequestHeader=function(e){return"string"==typeof e&&h[e.toLowerCase()]?f[h[e.toLowerCase()]]:""},this.send=function(l){if(this.readyState!==this.OPENED)throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called");if(y)throw new Error("INVALID_STATE_ERR: send has already been called");var d,p=!1,v=!1,g=e.parse(c.url);switch(g.protocol){case"https:":p=!0;case"http:":d=g.hostname;break;case"file:":v=!0;break;case void 0:case null:case"":d="localhost";break;default:throw new Error("Protocol not supported.")}if(v){if("GET"!==c.method)throw new Error("XMLHttpRequest: Only GET method is supported");if(c.async)r.readFile(g.pathname,"utf8",function(e,t){e?i.handleError(e):(i.status=200,i.responseText=t,w(i.DONE))});else try{this.responseText=r.readFileSync(g.pathname,"utf8"),this.status=200,w(i.DONE)}catch(e){this.handleError(e)}}else{var E=g.port||(p?443:80),m=g.pathname+(g.search?g.search:"");for(var x in u)h[x.toLowerCase()]||(f[x]=u[x]);if(f.Host=d,p&&443===E||80===E||(f.Host+=":"+g.port),c.user){void 0===c.password&&(c.password="");var S=new Buffer(c.user+":"+c.password);f.Authorization="Basic "+S.toString("base64")}"GET"===c.method||"HEAD"===c.method?l=null:l?(f["Content-Length"]=Buffer.isBuffer(l)?l.length:Buffer.byteLength(l),f["Content-Type"]||(f["Content-Type"]="text/plain;charset=UTF-8")):"POST"===c.method&&(f["Content-Length"]=0);var O={host:d,port:E,path:m,method:c.method,headers:f,agent:!1,withCredentials:i.withCredentials};if(b=!1,c.async){var k=p?s.request:a.request;y=!0,i.dispatchEvent("readystatechange");var N=function(e){i.handleError(e)};n=k(O,function t(r){if(301!==(o=r).statusCode&&302!==o.statusCode&&303!==o.statusCode&&307!==o.statusCode)o.setEncoding("utf8"),w(i.HEADERS_RECEIVED),i.status=o.statusCode,o.on("data",function(e){e&&(i.responseText+=e),y&&w(i.LOADING)}),o.on("end",function(){y&&(w(i.DONE),y=!1)}),o.on("error",function(e){i.handleError(e)});else{c.url=o.headers.location;var a=e.parse(c.url);d=a.hostname;var s={hostname:a.hostname,port:a.port,path:a.path,method:303===o.statusCode?"GET":c.method,headers:f,withCredentials:i.withCredentials};(n=k(s,t).on("error",N)).end()}}).on("error",N),l&&n.write(l),n.end(),i.dispatchEvent("loadstart")}else{var T=".node-xmlhttprequest-content-"+process.pid,j=".node-xmlhttprequest-sync-"+process.pid;r.writeFileSync(j,"","utf8");for(var C="var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http"+(p?"s":"")+".request;var options = "+JSON.stringify(O)+";var responseText = '';var req = doRequest(options, function(response) {response.setEncoding('utf8');response.on('data', function(chunk) { responseText += chunk;});response.on('end', function() {fs.writeFileSync('"+T+"', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText}}), 'utf8');fs.unlinkSync('"+j+"');});response.on('error', function(error) {fs.writeFileSync('"+T+"', JSON.stringify({err: error}), 'utf8');fs.unlinkSync('"+j+"');});}).on('error', function(error) {fs.writeFileSync('"+T+"', JSON.stringify({err: error}), 'utf8');fs.unlinkSync('"+j+"');});"+(l?"req.write('"+JSON.stringify(l).slice(1,-1).replace(/'/g,"\\'")+"');":"")+"req.end();",R=t(process.argv[0],["-e",C]);r.existsSync(j););var D=JSON.parse(r.readFileSync(T,"utf8"));R.stdin.end(),r.unlinkSync(T),D.err?i.handleError(D.err):(o=D.data,i.status=D.data.statusCode,i.responseText=D.data.text,w(i.DONE))}}},this.handleError=function(e){this.status=0,this.statusText=e,this.responseText=e.stack,b=!0,w(this.DONE),this.dispatchEvent("error")},this.abort=function(){n&&(n.abort(),n=null),f=u,this.status=0,this.responseText="",this.responseXML="",b=!0,this.readyState===this.UNSENT||this.readyState===this.OPENED&&!y||this.readyState===this.DONE||(y=!1,w(this.DONE)),this.readyState=this.UNSENT,this.dispatchEvent("abort")},this.addEventListener=function(e,t){e in E||(E[e]=[]),E[e].push(t)},this.removeEventListener=function(e,t){e in E&&(E[e]=E[e].filter(function(e){return e!==t}))},this.dispatchEvent=function(e){if("function"==typeof i["on"+e]&&i["on"+e](),e in E)for(var t=0,r=E[e].length;t<r;t++)E[e][t].call(i)};var w=function(e){e!=i.LOADING&&i.readyState===e||(i.readyState=e,(c.async||i.readyState<i.OPENED||i.readyState===i.DONE)&&i.dispatchEvent("readystatechange"),i.readyState!==i.DONE||b||(i.dispatchEvent("load"),i.dispatchEvent("loadend")))}},G}(),X=B(z),V=new Map,W=O?window.XMLHttpRequest:X.XMLHttpRequest;function Z(e){if(V.has(e))return V.get(e);var t;try{var r=new W;r.open("GET",e,!1),r.setRequestHeader("Content-Type","text/html"),r.send(),200===r.status&&r.responseText&&(t=r.responseText,V.set(e,r.responseText))}catch(e){console.error("【XHR Error】:".concat(e instanceof Error?e.message:"get remote URL resource exception!"))}return t}var $=new Map;exports.generateCard=function(e,t){return new Promise(function(r){var n=Z(e);if(n){var o=M(n,e);if(o){var i={linkTitle:t.linkTitle,target:t.target||"_blank",classPrefix:t.classPrefix},a=R(o,E(E({},i),{},{href:e})),s={url:e,data:o,options:i,dom:a};$.set(e,s),r(s)}}})},exports.linkToCardPlugin=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};function r(e){var r,n=function(e){if(T.has(e))return T.get(e);var t=null,r=Z(e);return r&&(t=M(r,e))&&T.set(e,t),t}(e.url);if(n){!function(e,t){e.forEach(function(e,r){r!==t&&(e.hidden=!0)})}(e.tokens,e.i);var o={href:e.url,linkTitle:(r=e.tokens,r.map(function(e){var t=e.hidden,r=e.content;return t?r:""}).filter(Boolean).join("")),target:t.target||"_blank",classPrefix:t.classPrefix};return S(t.render)?t.render(n,o):R(n,o)}}e.renderer.renderInline=function(t,r,n){for(var o="",i=0;i<t.length;i++){var a=t[i],s=e.renderer.rules[a.type];a.hidden?o+="":S(s)?o+=s(t,i,r,n,e.renderer):o+=e.renderer.renderToken(t,i,r)}return o},e.renderer.rules.link_open=function(e,t,n,o,i){var a,s=e[t],c="a"===s.tag&&"link_open"===s.type,l=function(e){var t=new RegExp("^(".concat("@",":)([a-zA-Z0-9]+.*)")),r=null==e?void 0:e.match(t);return{isCardLink:!!r,url:null==r?void 0:r[2]}}(null===(a=s.attrs)||void 0===a||null===(a=a.filter(function(e){return e.includes("href")})[0])||void 0===a?void 0:a[1]),u=l.url,f=l.isCardLink;if(c&&f&&u){var d=r({url:u,tokens:e,i:t});if(d)return d}return i.renderToken(e,t,n)}};
|
package/dist/.esm.min.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* vitepress-linkcards v2.2.2
|
|
3
|
+
* (c) 2022 - 2026 luckrya
|
|
4
|
+
* Released under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
import e from"url";import t from"child_process";import r from"fs";import n from"http";import o from"https";import i from"node:fs";function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function s(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,p(n.key),n)}}function c(e,t,r){return(t=p(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function l(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),r.push.apply(r,n)}return r}function u(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?l(Object(r),!0).forEach(function(t){c(e,t,r[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):l(Object(r)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))})}return e}function f(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=r){var n,o,i,a,s=[],c=!0,l=!1;try{if(i=(r=r.call(e)).next,0===t){if(Object(r)!==r)return;c=!1}else for(;!(c=(n=i.call(r)).done)&&(s.push(n.value),s.length!==t);c=!0);}catch(e){l=!0,o=e}finally{try{if(!c&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(l)throw o}}return s}}(e,t)||function(e,t){if(e){if("string"==typeof e)return a(e,t);var r={}.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?a(e,t):void 0}}
|
|
7
|
+
/*!
|
|
8
|
+
* @luckrya/utility v0.1.0
|
|
9
|
+
* (c) 2022 - 2022 Y.R
|
|
10
|
+
* Released under the MIT License.
|
|
11
|
+
*/(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function p(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}var d,h;(h=d||(d={})).Array="[object Array]",h.Object="[object Object]",h.Function="[object Function]",h.Number="[object Number]",h.String="[object String]",h.Boolean="[object Boolean]",h.Undefined="[object Undefined]",h.Null="[object Null]",h.Error="[object Error]";var v=function(e){return Object.prototype.toString.call(e)===d.String},g=function(e){return Object.prototype.toString.call(e)===d.Function},y="undefined"!=typeof window,m=function(){var e,t="".concat(process.cwd(),"/docs/.linkcard_cache.json"),r="".concat(process.cwd(),"/.linkcard_cache.json");if(i.existsSync(t))e=t;else if(i.existsSync(r))e=r;else{e=t;i.writeFileSync(e,JSON.stringify({"https://example.com/":{description:"Example Website",logo:"https://example.com/example.png",title:"Example Title"}},null,2))}return e},b=function(){return function(e,t,r){return t&&s(e.prototype,t),r&&s(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}(function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e)},[{key:"setFile",value:function(e){var t=e,r=this.readFile();r&&(t=Object.assign(r,t)),i.writeFileSync(m(),JSON.stringify(t)),function(){var e=m(),t=i.readFileSync(e,"utf-8").trim();if(t){var r=JSON.parse(t),n=JSON.stringify(r,null,2)+"\n";i.writeFileSync(e,n)}}()}},{key:"readFile",value:function(){var e=i.readFileSync(m(),"utf-8"),t=JSON.parse(e);if(function(e){return Object.prototype.toString.call(e)===d.Object}(t))return t}},{key:"has",value:function(e){return!!this.get(e)}},{key:"get",value:function(e){var t=this.readFile();return null==t?void 0:t[e]}},{key:"set",value:function(e,t){this.setFile(c({},e,t))}}])}(),E=new b;function w(e){return'style="'.concat(function(e){return Object.entries(e).map(function(e){var t,r=f(e,2),n=r[0],o=r[1];if(n&&o)return"".concat((t=n,t.replace(/\B([A-Z])/g,"-$1").toLowerCase()),": ").concat(o,";")}).filter(Boolean).join(" ")}(e),'"')}var x=function(e){return{"-webkit-box-orient":"vertical","-webkit-line-clamp":e,display:"-webkit-box",hyphens:"auto",lineClamp:e,overflow:"hidden",overflowWrap:"anywhere",textOverflow:"ellipsis",wordBreak:"break-word"}},S=function(e,t){var r,n,o={rel:'rel="noopener noreferrer"',target:'target="'.concat(t.target,'"'),href:'href="'.concat(t.href,'"'),title:'title="'.concat(t.linkTitle,'"')},i=function(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'")},a={a:w({color:"unset !important",display:"block",width:"100%",textDecoration:"none"}),container:w({display:"flex",alignItems:"center",flexWrap:"wrap",gap:"10px",borderRadius:"12px",border:"1px solid var(--vp-c-bg)",backgroundColor:"var(--vp-c-bg-soft)",boxSizing:"border-box",width:"100%",height:"130px",transition:"all 0.25s"}),img:w({borderRadius:"0px 12px 12px 0px",maxWidth:"40%",height:"128px",flexShrink:0,objectFit:"contain",overflow:"hidden"}),texts:w({flex:"1 1 0%",minWidth:"0"}),title:w(u(u({},x(2)),{},{opacity:1,fontSize:"16px",lineHeight:"22px",margin:"0 16px 8px 16px",fontWeight:"bold"})),domain:w(u(u({},x(1)),{},{opacity:1,fontSize:"12px",lineHeight:"16px",margin:"8px 16px 8px 16px",textDecoration:"underline"})),description:w(u(u({},x(2)),{},{opacity:.8,fontSize:"12px",lineHeight:"16px",margin:"8px 16px 0px 16px"}))},s=t.href||"",c=new URL(s).origin.replace(/^https?:\/\//,"").replace(/^www\./,"")||"Unknown domain",l=e.title,f=e.description;"github.com"==c?(l=(null===(r=e.title)||void 0===r?void 0:r.split(":")[0].replace("GitHub - ",""))||"No title",f=(null===(n=f)||void 0===n?void 0:n.replace(" - ".concat(l),"").replace("Contribute to ".concat(l," development by creating an account on GitHub."),""))||""):(l=e.title||"No title",f=e.description||"");return'<span style="display:block;">\n <a '.concat(o.rel," ").concat(o.target," ").concat(o.href," ").concat(o.title," ").concat(a.a,'>\n <span class="vitepress-linkcard-container" ').concat(a.container,">\n <span ").concat(a.texts,">\n <span ").concat(a.title,">\n ").concat(i(l),"\n </span>\n <span ").concat(a.domain,">\n ").concat(i(c),"\n </span>\n <span ").concat(a.description,">\n ").concat(i(f),'\n </span>\n </span>\n <img src="').concat(null==e?void 0:e.logo,'" ').concat(a.img,"/>\n </span>\n </a>\n</span>")};function O(e){return new URL(e)}var N="https://resources.whatwg.org/logo-url.svg",k=/(<[A-Za-z]+\s*[^>]*>(.*)<\/[A-Za-z]+>)/,T=/content=["|']([^>]*)["|']/,j=/href=["|']([^>]*)["|']/,C=/(<title\s*[^>]*>(.*)<\/title>)/g,R=function(e){return new RegExp("<".concat(arguments.length>1&&void 0!==arguments[1]?arguments[1]:"meta","\\s[^>]*\\w+=['|\"]([a-zA-Z]|:|\\s)*").concat(e,"['|\"][^>]*\\/?>"))};function D(e){var t,r=e.match(R("title"));if(null!=r&&r.length){var n=r[0].match(T);n&&v(n[1])&&(t=n[1])}else{var o=e.match(C);if(null!=o&&o.length){var i=o[0].match(k);i&&v(i[2])&&(t=i[2])}}return t}function A(e){var t,r=e.match(R("description"));if(null!=r&&r.length){var n=r[0].match(T);n&&v(n[1])&&(t=n[1])}return t}function L(e){var t,r=e.match(R("image"));if(null!=r&&r.length){var n=r[0].match(T);n&&v(n[1])&&(t=n[1])}else{var o=e.match(R("icon","link"));if(null!=o&&o.length){var i=o[0].match(j);i&&v(i[1])&&(t=i[1])}}return t}function P(e,t){var r,n,o,i={title:D(e),description:A(e),logo:(r=L(e),r?O(r)?r:"".concat(null===(n=O(t))||void 0===n?void 0:n.origin).concat("/".concat(r).replace(/\/\//g,"/")):N)};return o=i,Object.values(o).filter(function(e){return v(e)}).length?i:null}function q(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var H,F={};
|
|
12
|
+
/**
|
|
13
|
+
* Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.
|
|
14
|
+
*
|
|
15
|
+
* This can be used with JS designed for browsers to improve reuse of code and
|
|
16
|
+
* allow the use of existing libraries.
|
|
17
|
+
*
|
|
18
|
+
* Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs.
|
|
19
|
+
*
|
|
20
|
+
* @author Dan DeFelippi <dan@driverdan.com>
|
|
21
|
+
* @contributor David Ellis <d.f.ellis@ieee.org>
|
|
22
|
+
* @license MIT
|
|
23
|
+
*/var _=function(){if(H)return F;H=1;var i=e,a=t.spawn,s=r;return F.XMLHttpRequest=function(){var e,t,r=this,c=n,l=o,u={},f=!1,p={"User-Agent":"node-XMLHttpRequest",Accept:"*/*"},d={},h={},v=["accept-charset","accept-encoding","access-control-request-headers","access-control-request-method","connection","content-length","content-transfer-encoding","cookie","cookie2","date","expect","host","keep-alive","origin","referer","te","trailer","transfer-encoding","upgrade","via"],g=["TRACE","TRACK","CONNECT"],y=!1,m=!1,b={};this.UNSENT=0,this.OPENED=1,this.HEADERS_RECEIVED=2,this.LOADING=3,this.DONE=4,this.readyState=this.UNSENT,this.onreadystatechange=null,this.responseText="",this.responseXML="",this.status=null,this.statusText=null,this.withCredentials=!1;this.open=function(e,t,r,n,o){if(this.abort(),m=!1,!function(e){return e&&-1===g.indexOf(e)}(e))throw new Error("SecurityError: Request method not allowed");u={method:e,url:t.toString(),async:"boolean"!=typeof r||r,user:n||null,password:o||null},E(this.OPENED)},this.setDisableHeaderCheck=function(e){f=e},this.setRequestHeader=function(e,t){if(this.readyState!==this.OPENED)throw new Error("INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN");if(function(e){return f||e&&-1===v.indexOf(e.toLowerCase())}(e)){if(y)throw new Error("INVALID_STATE_ERR: send flag is true");e=h[e.toLowerCase()]||e,h[e.toLowerCase()]=e,d[e]=d[e]?d[e]+", "+t:t}else console.warn('Refused to set unsafe header "'+e+'"')},this.getResponseHeader=function(e){return"string"==typeof e&&this.readyState>this.OPENED&&t&&t.headers&&t.headers[e.toLowerCase()]&&!m?t.headers[e.toLowerCase()]:null},this.getAllResponseHeaders=function(){if(this.readyState<this.HEADERS_RECEIVED||m)return"";var e="";for(var r in t.headers)"set-cookie"!==r&&"set-cookie2"!==r&&(e+=r+": "+t.headers[r]+"\r\n");return e.substr(0,e.length-2)},this.getRequestHeader=function(e){return"string"==typeof e&&h[e.toLowerCase()]?d[h[e.toLowerCase()]]:""},this.send=function(n){if(this.readyState!==this.OPENED)throw new Error("INVALID_STATE_ERR: connection must be opened before send() is called");if(y)throw new Error("INVALID_STATE_ERR: send has already been called");var o,f=!1,v=!1,g=i.parse(u.url);switch(g.protocol){case"https:":f=!0;case"http:":o=g.hostname;break;case"file:":v=!0;break;case void 0:case null:case"":o="localhost";break;default:throw new Error("Protocol not supported.")}if(v){if("GET"!==u.method)throw new Error("XMLHttpRequest: Only GET method is supported");if(u.async)s.readFile(g.pathname,"utf8",function(e,t){e?r.handleError(e):(r.status=200,r.responseText=t,E(r.DONE))});else try{this.responseText=s.readFileSync(g.pathname,"utf8"),this.status=200,E(r.DONE)}catch(e){this.handleError(e)}}else{var b=g.port||(f?443:80),w=g.pathname+(g.search?g.search:"");for(var x in p)h[x.toLowerCase()]||(d[x]=p[x]);if(d.Host=o,f&&443===b||80===b||(d.Host+=":"+g.port),u.user){void 0===u.password&&(u.password="");var S=new Buffer(u.user+":"+u.password);d.Authorization="Basic "+S.toString("base64")}"GET"===u.method||"HEAD"===u.method?n=null:n?(d["Content-Length"]=Buffer.isBuffer(n)?n.length:Buffer.byteLength(n),d["Content-Type"]||(d["Content-Type"]="text/plain;charset=UTF-8")):"POST"===u.method&&(d["Content-Length"]=0);var O={host:o,port:b,path:w,method:u.method,headers:d,agent:!1,withCredentials:r.withCredentials};if(m=!1,u.async){var N=f?l.request:c.request;y=!0,r.dispatchEvent("readystatechange");var k=function(e){r.handleError(e)};e=N(O,function n(a){if(301!==(t=a).statusCode&&302!==t.statusCode&&303!==t.statusCode&&307!==t.statusCode)t.setEncoding("utf8"),E(r.HEADERS_RECEIVED),r.status=t.statusCode,t.on("data",function(e){e&&(r.responseText+=e),y&&E(r.LOADING)}),t.on("end",function(){y&&(E(r.DONE),y=!1)}),t.on("error",function(e){r.handleError(e)});else{u.url=t.headers.location;var s=i.parse(u.url);o=s.hostname;var c={hostname:s.hostname,port:s.port,path:s.path,method:303===t.statusCode?"GET":u.method,headers:d,withCredentials:r.withCredentials};(e=N(c,n).on("error",k)).end()}}).on("error",k),n&&e.write(n),e.end(),r.dispatchEvent("loadstart")}else{var T=".node-xmlhttprequest-content-"+process.pid,j=".node-xmlhttprequest-sync-"+process.pid;s.writeFileSync(j,"","utf8");for(var C="var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http"+(f?"s":"")+".request;var options = "+JSON.stringify(O)+";var responseText = '';var req = doRequest(options, function(response) {response.setEncoding('utf8');response.on('data', function(chunk) { responseText += chunk;});response.on('end', function() {fs.writeFileSync('"+T+"', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText}}), 'utf8');fs.unlinkSync('"+j+"');});response.on('error', function(error) {fs.writeFileSync('"+T+"', JSON.stringify({err: error}), 'utf8');fs.unlinkSync('"+j+"');});}).on('error', function(error) {fs.writeFileSync('"+T+"', JSON.stringify({err: error}), 'utf8');fs.unlinkSync('"+j+"');});"+(n?"req.write('"+JSON.stringify(n).slice(1,-1).replace(/'/g,"\\'")+"');":"")+"req.end();",R=a(process.argv[0],["-e",C]);s.existsSync(j););var D=JSON.parse(s.readFileSync(T,"utf8"));R.stdin.end(),s.unlinkSync(T),D.err?r.handleError(D.err):(t=D.data,r.status=D.data.statusCode,r.responseText=D.data.text,E(r.DONE))}}},this.handleError=function(e){this.status=0,this.statusText=e,this.responseText=e.stack,m=!0,E(this.DONE),this.dispatchEvent("error")},this.abort=function(){e&&(e.abort(),e=null),d=p,this.status=0,this.responseText="",this.responseXML="",m=!0,this.readyState===this.UNSENT||this.readyState===this.OPENED&&!y||this.readyState===this.DONE||(y=!1,E(this.DONE)),this.readyState=this.UNSENT,this.dispatchEvent("abort")},this.addEventListener=function(e,t){e in b||(b[e]=[]),b[e].push(t)},this.removeEventListener=function(e,t){e in b&&(b[e]=b[e].filter(function(e){return e!==t}))},this.dispatchEvent=function(e){if("function"==typeof r["on"+e]&&r["on"+e](),e in b)for(var t=0,n=b[e].length;t<n;t++)b[e][t].call(r)};var E=function(e){e!=r.LOADING&&r.readyState===e||(r.readyState=e,(u.async||r.readyState<r.OPENED||r.readyState===r.DONE)&&r.dispatchEvent("readystatechange"),r.readyState!==r.DONE||m||(r.dispatchEvent("load"),r.dispatchEvent("loadend")))}},F}(),I=q(_),U=new Map,B=y?window.XMLHttpRequest:I.XMLHttpRequest;function J(e){if(U.has(e))return U.get(e);var t;try{var r=new B;r.open("GET",e,!1),r.setRequestHeader("Content-Type","text/html"),r.send(),200===r.status&&r.responseText&&(t=r.responseText,U.set(e,r.responseText))}catch(e){console.error("【XHR Error】:".concat(e instanceof Error?e.message:"get remote URL resource exception!"))}return t}var M=new Map;function G(e,t){return new Promise(function(r){var n=J(e);if(n){var o=P(n,e);if(o){var i={linkTitle:t.linkTitle,target:t.target||"_blank",classPrefix:t.classPrefix},a=S(o,u(u({},i),{},{href:e})),s={url:e,data:o,options:i,dom:a};M.set(e,s),r(s)}}})}var z=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};function r(e){var r,n=function(e){if(E.has(e))return E.get(e);var t=null,r=J(e);return r&&(t=P(r,e))&&E.set(e,t),t}(e.url);if(n){!function(e,t){e.forEach(function(e,r){r!==t&&(e.hidden=!0)})}(e.tokens,e.i);var o={href:e.url,linkTitle:(r=e.tokens,r.map(function(e){var t=e.hidden,r=e.content;return t?r:""}).filter(Boolean).join("")),target:t.target||"_blank",classPrefix:t.classPrefix};return g(t.render)?t.render(n,o):S(n,o)}}e.renderer.renderInline=function(t,r,n){for(var o="",i=0;i<t.length;i++){var a=t[i],s=e.renderer.rules[a.type];a.hidden?o+="":g(s)?o+=s(t,i,r,n,e.renderer):o+=e.renderer.renderToken(t,i,r)}return o},e.renderer.rules.link_open=function(e,t,n,o,i){var a,s=e[t],c="a"===s.tag&&"link_open"===s.type,l=function(e){var t=new RegExp("^(".concat("@",":)([a-zA-Z0-9]+.*)")),r=null==e?void 0:e.match(t);return{isCardLink:!!r,url:null==r?void 0:r[2]}}(null===(a=s.attrs)||void 0===a||null===(a=a.filter(function(e){return e.includes("href")})[0])||void 0===a?void 0:a[1]),u=l.url,f=l.isCardLink;if(c&&f&&u){var p=r({url:u,tokens:e,i:t});if(p)return p}return i.renderToken(e,t,n)}};export{G as generateCard,z as linkToCardPlugin};
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { xhr, generateCardDomFragment, parserMetadata } from './assemble';
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
/**
|
|
4
|
+
* Generates a link card by fetching and parsing metadata from a URL.
|
|
5
|
+
*
|
|
6
|
+
* @param url - The URL to fetch metadata from
|
|
7
|
+
* @param options - Rendering options for the card
|
|
8
|
+
* @returns A promise that resolves to a CardResponse containing the card data and HTML
|
|
9
|
+
*/
|
|
10
|
+
export function generateCard(url, options) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const htmlString = xhr.sync(url);
|
|
13
|
+
if (htmlString) {
|
|
14
|
+
const urlMetadata = parserMetadata(htmlString, url);
|
|
15
|
+
if (urlMetadata) {
|
|
16
|
+
const _options = {
|
|
17
|
+
linkTitle: options.linkTitle,
|
|
18
|
+
target: options.target || '_blank',
|
|
19
|
+
classPrefix: options.classPrefix
|
|
20
|
+
};
|
|
21
|
+
const card = generateCardDomFragment(urlMetadata, {
|
|
22
|
+
..._options,
|
|
23
|
+
href: url
|
|
24
|
+
});
|
|
25
|
+
const response = {
|
|
26
|
+
url,
|
|
27
|
+
data: urlMetadata,
|
|
28
|
+
options: _options,
|
|
29
|
+
dom: card
|
|
30
|
+
};
|
|
31
|
+
cache.set(url, response);
|
|
32
|
+
resolve(response);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { STYLE } from './style';
|
|
2
|
+
/**
|
|
3
|
+
* Generates the HTML DOM fragment for a link card display.
|
|
4
|
+
*
|
|
5
|
+
* @param data - The metadata extracted from the URL
|
|
6
|
+
* @param options - Rendering options including href, target, etc.
|
|
7
|
+
* @returns An HTML string containing the card markup
|
|
8
|
+
*/
|
|
9
|
+
export const generateCardDomFragment = (data, options) => {
|
|
10
|
+
const aa = {
|
|
11
|
+
rel: `rel="noopener noreferrer"`,
|
|
12
|
+
target: `target="${options.target}"`,
|
|
13
|
+
href: `href="${options.href}"`,
|
|
14
|
+
title: `title="${options.linkTitle}"`
|
|
15
|
+
};
|
|
16
|
+
const inject = (s) => {
|
|
17
|
+
return s;
|
|
18
|
+
};
|
|
19
|
+
const escapeHTML = (str) => str
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, "'");
|
|
25
|
+
const style = STYLE();
|
|
26
|
+
const url = options.href || '';
|
|
27
|
+
const domain = new URL(url).origin.replace(/^https?:\/\//, '').replace(/^www\./, '') ||
|
|
28
|
+
'Unknown domain';
|
|
29
|
+
let title = data.title;
|
|
30
|
+
let description = data.description;
|
|
31
|
+
if (domain == 'github.com') {
|
|
32
|
+
title = data.title?.split(':')[0].replace('GitHub - ', '') || 'No title';
|
|
33
|
+
description =
|
|
34
|
+
description
|
|
35
|
+
?.replace(` - ${title}`, '')
|
|
36
|
+
.replace(`Contribute to ${title} development by creating an account on GitHub.`, '') || '';
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
title = data.title || 'No title';
|
|
40
|
+
description = data.description || '';
|
|
41
|
+
}
|
|
42
|
+
return `<span style="display:block;">
|
|
43
|
+
<a ${aa.rel} ${aa.target} ${aa.href} ${aa.title} ${style.a}>
|
|
44
|
+
<span class="vitepress-linkcard-container" ${inject(style.container)}>
|
|
45
|
+
<span ${inject(style.texts)}>
|
|
46
|
+
<span ${inject(style.title)}>
|
|
47
|
+
${escapeHTML(title)}
|
|
48
|
+
</span>
|
|
49
|
+
<span ${inject(style.domain)}>
|
|
50
|
+
${escapeHTML(domain)}
|
|
51
|
+
</span>
|
|
52
|
+
<span ${inject(style.description)}>
|
|
53
|
+
${escapeHTML(description)}
|
|
54
|
+
</span>
|
|
55
|
+
</span>
|
|
56
|
+
<img src="${data?.logo}" ${inject(style.img)}/>
|
|
57
|
+
</span>
|
|
58
|
+
</a>
|
|
59
|
+
</span>`;
|
|
60
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { isPureObject } from '@luckrya/utility';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
const CONFIG_FILE = () => {
|
|
4
|
+
let filePath;
|
|
5
|
+
const defaultPath = `${process.cwd()}/docs/.linkcard_cache.json`;
|
|
6
|
+
const fallbackPath = `${process.cwd()}/.linkcard_cache.json`;
|
|
7
|
+
if (fs.existsSync(defaultPath)) {
|
|
8
|
+
filePath = defaultPath;
|
|
9
|
+
}
|
|
10
|
+
else if (fs.existsSync(fallbackPath)) {
|
|
11
|
+
filePath = fallbackPath;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
filePath = defaultPath;
|
|
15
|
+
const initialData = {
|
|
16
|
+
'https://example.com/': {
|
|
17
|
+
description: 'Example Website',
|
|
18
|
+
logo: 'https://example.com/example.png',
|
|
19
|
+
title: 'Example Title'
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
fs.writeFileSync(filePath, JSON.stringify(initialData, null, 2));
|
|
23
|
+
}
|
|
24
|
+
return filePath;
|
|
25
|
+
};
|
|
26
|
+
const format = () => {
|
|
27
|
+
const filePath = CONFIG_FILE();
|
|
28
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
29
|
+
if (!content)
|
|
30
|
+
return;
|
|
31
|
+
const parsed = JSON.parse(content);
|
|
32
|
+
const formatted = JSON.stringify(parsed, null, 2) + '\n';
|
|
33
|
+
fs.writeFileSync(filePath, formatted);
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A simple file-based cache for storing and retrieving structured data.
|
|
37
|
+
*/
|
|
38
|
+
export default class LocalFileCache {
|
|
39
|
+
constructor() { }
|
|
40
|
+
setFile(data) {
|
|
41
|
+
let content = data;
|
|
42
|
+
const _content = this.readFile();
|
|
43
|
+
if (_content) {
|
|
44
|
+
content = Object.assign(_content, content);
|
|
45
|
+
}
|
|
46
|
+
fs.writeFileSync(CONFIG_FILE(), JSON.stringify(content));
|
|
47
|
+
format();
|
|
48
|
+
}
|
|
49
|
+
readFile() {
|
|
50
|
+
const content = fs.readFileSync(CONFIG_FILE(), 'utf-8');
|
|
51
|
+
const data = JSON.parse(content);
|
|
52
|
+
if (isPureObject(data))
|
|
53
|
+
return data;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
has(url) {
|
|
57
|
+
return !!this.get(url);
|
|
58
|
+
}
|
|
59
|
+
get(url) {
|
|
60
|
+
const cache = this.readFile();
|
|
61
|
+
return cache?.[url];
|
|
62
|
+
}
|
|
63
|
+
set(url, data) {
|
|
64
|
+
this.setFile({ [url]: data });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parserMetadata, xhr } from '.';
|
|
2
|
+
import LocalFileCache from './local-file-cache';
|
|
3
|
+
const cache = new LocalFileCache();
|
|
4
|
+
/**
|
|
5
|
+
* Retrieves metadata for a given URL, using cache when available.
|
|
6
|
+
*
|
|
7
|
+
* @param url - The URL to fetch metadata from
|
|
8
|
+
* @returns The parsed URL metadata, or null if unavailable
|
|
9
|
+
*/
|
|
10
|
+
export function getUrlMetadata(url) {
|
|
11
|
+
if (cache.has(url))
|
|
12
|
+
return cache.get(url);
|
|
13
|
+
let metadata = null;
|
|
14
|
+
const htmlString = xhr.sync(url);
|
|
15
|
+
if (htmlString) {
|
|
16
|
+
metadata = parserMetadata(htmlString, url);
|
|
17
|
+
if (metadata)
|
|
18
|
+
cache.set(url, metadata);
|
|
19
|
+
}
|
|
20
|
+
return metadata;
|
|
21
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isString } from '@luckrya/utility';
|
|
2
|
+
import { cleanPath, extractUrl } from './url';
|
|
3
|
+
const DEFAULT_LOGO = 'https://resources.whatwg.org/logo-url.svg';
|
|
4
|
+
const HtmlTagContentReg = /(<[A-Za-z]+\s*[^>]*>(.*)<\/[A-Za-z]+>)/;
|
|
5
|
+
const ContentAttrValueHtmlMetaTagReg = /content=["|']([^>]*)["|']/;
|
|
6
|
+
const HrefAttrValueHtmlLinkTagReg = /href=["|']([^>]*)["|']/;
|
|
7
|
+
const HtmlTitleTagReg = /(<title\s*[^>]*>(.*)<\/title>)/g;
|
|
8
|
+
const containArrSelfLosingHtmlTagReg = (attr, tag = 'meta') => new RegExp(`<${tag}\\s[^>]*\\w+=['|"]([a-zA-Z]|:|\\s)*${attr}['|"][^>]*\\/?>`);
|
|
9
|
+
function matchTitleByMetaTag(htmlString) {
|
|
10
|
+
let title;
|
|
11
|
+
const metas = htmlString.match(containArrSelfLosingHtmlTagReg('title'));
|
|
12
|
+
if (metas?.length) {
|
|
13
|
+
const content = metas[0].match(ContentAttrValueHtmlMetaTagReg);
|
|
14
|
+
if (content && isString(content[1]))
|
|
15
|
+
title = content[1];
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const titleHtmlTag = htmlString.match(HtmlTitleTagReg);
|
|
19
|
+
if (titleHtmlTag?.length) {
|
|
20
|
+
const content = titleHtmlTag[0].match(HtmlTagContentReg);
|
|
21
|
+
if (content && isString(content[2]))
|
|
22
|
+
title = content[2];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return title;
|
|
26
|
+
}
|
|
27
|
+
function matchDescriptionByMetaTag(htmlString) {
|
|
28
|
+
let description;
|
|
29
|
+
const metas = htmlString.match(containArrSelfLosingHtmlTagReg('description'));
|
|
30
|
+
if (metas?.length) {
|
|
31
|
+
const content = metas[0].match(ContentAttrValueHtmlMetaTagReg);
|
|
32
|
+
if (content && isString(content[1]))
|
|
33
|
+
description = content[1];
|
|
34
|
+
}
|
|
35
|
+
return description;
|
|
36
|
+
}
|
|
37
|
+
function matchLogoByLinkOrMetaTag(htmlString) {
|
|
38
|
+
let logo;
|
|
39
|
+
const metas = htmlString.match(containArrSelfLosingHtmlTagReg('image'));
|
|
40
|
+
if (metas?.length) {
|
|
41
|
+
const content = metas[0].match(ContentAttrValueHtmlMetaTagReg);
|
|
42
|
+
if (content && isString(content[1]))
|
|
43
|
+
logo = content[1];
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const linkHtmlTags = htmlString.match(containArrSelfLosingHtmlTagReg('icon', 'link'));
|
|
47
|
+
if (linkHtmlTags?.length) {
|
|
48
|
+
const content = linkHtmlTags[0].match(HrefAttrValueHtmlLinkTagReg);
|
|
49
|
+
if (content && isString(content[1]))
|
|
50
|
+
logo = content[1];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return logo;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parses HTML string to extract structured metadata for link card generation.
|
|
57
|
+
*
|
|
58
|
+
* @param htmlString - The HTML content to parse
|
|
59
|
+
* @param url - The URL of the page
|
|
60
|
+
* @returns Parsed metadata object, or null if no valid metadata found
|
|
61
|
+
*/
|
|
62
|
+
export function parserMetadata(htmlString, url) {
|
|
63
|
+
function absolute(logo) {
|
|
64
|
+
if (!logo)
|
|
65
|
+
return DEFAULT_LOGO;
|
|
66
|
+
return extractUrl(logo)
|
|
67
|
+
? logo
|
|
68
|
+
: `${extractUrl(url)?.origin}${cleanPath(`/${logo}`)}`;
|
|
69
|
+
}
|
|
70
|
+
const metadata = {
|
|
71
|
+
title: matchTitleByMetaTag(htmlString),
|
|
72
|
+
description: matchDescriptionByMetaTag(htmlString),
|
|
73
|
+
logo: absolute(matchLogoByLinkOrMetaTag(htmlString))
|
|
74
|
+
};
|
|
75
|
+
if (isEmptyStringObject(metadata))
|
|
76
|
+
return null;
|
|
77
|
+
else
|
|
78
|
+
return metadata;
|
|
79
|
+
}
|
|
80
|
+
function isEmptyStringObject(obj) {
|
|
81
|
+
return !Object.values(obj).filter((v) => isString(v)).length;
|
|
82
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
function hyphenate(str) {
|
|
2
|
+
return str.replace(/\B([A-Z])/g, '-$1').toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
function join(style) {
|
|
5
|
+
return Object.entries(style)
|
|
6
|
+
.map(([k, v]) => {
|
|
7
|
+
if (k && v)
|
|
8
|
+
return `${hyphenate(k)}: ${v};`;
|
|
9
|
+
})
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.join(' ');
|
|
12
|
+
}
|
|
13
|
+
function inlineStyle(style) {
|
|
14
|
+
return `style="${join(style)}"`;
|
|
15
|
+
}
|
|
16
|
+
const ellipsisStyle = (line) => ({
|
|
17
|
+
'-webkit-box-orient': 'vertical',
|
|
18
|
+
'-webkit-line-clamp': line,
|
|
19
|
+
display: '-webkit-box',
|
|
20
|
+
hyphens: 'auto',
|
|
21
|
+
lineClamp: line,
|
|
22
|
+
overflow: 'hidden',
|
|
23
|
+
overflowWrap: 'anywhere',
|
|
24
|
+
textOverflow: 'ellipsis',
|
|
25
|
+
wordBreak: 'break-word'
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Generates complete inline styles for all link card components.
|
|
29
|
+
*/
|
|
30
|
+
export const STYLE = () => ({
|
|
31
|
+
a: inlineStyle({
|
|
32
|
+
color: 'unset !important',
|
|
33
|
+
display: 'block',
|
|
34
|
+
width: '100%',
|
|
35
|
+
textDecoration: 'none'
|
|
36
|
+
}),
|
|
37
|
+
container: inlineStyle({
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
flexWrap: 'wrap',
|
|
41
|
+
gap: '10px',
|
|
42
|
+
borderRadius: '12px',
|
|
43
|
+
border: `1px solid var(--vp-c-bg)`,
|
|
44
|
+
backgroundColor: `var(--vp-c-bg-soft)`,
|
|
45
|
+
boxSizing: 'border-box',
|
|
46
|
+
width: '100%',
|
|
47
|
+
height: '130px',
|
|
48
|
+
transition: 'all 0.25s'
|
|
49
|
+
}),
|
|
50
|
+
img: inlineStyle({
|
|
51
|
+
borderRadius: '0px 12px 12px 0px',
|
|
52
|
+
maxWidth: '40%',
|
|
53
|
+
height: '128px',
|
|
54
|
+
flexShrink: 0,
|
|
55
|
+
objectFit: 'contain',
|
|
56
|
+
overflow: 'hidden'
|
|
57
|
+
}),
|
|
58
|
+
texts: inlineStyle({
|
|
59
|
+
flex: '1 1 0%',
|
|
60
|
+
minWidth: '0'
|
|
61
|
+
}),
|
|
62
|
+
title: inlineStyle({
|
|
63
|
+
...ellipsisStyle(2),
|
|
64
|
+
opacity: 1,
|
|
65
|
+
fontSize: '16px',
|
|
66
|
+
lineHeight: '22px',
|
|
67
|
+
margin: '0 16px 8px 16px',
|
|
68
|
+
fontWeight: 'bold'
|
|
69
|
+
}),
|
|
70
|
+
domain: inlineStyle({
|
|
71
|
+
...ellipsisStyle(1),
|
|
72
|
+
opacity: 1,
|
|
73
|
+
fontSize: '12px',
|
|
74
|
+
lineHeight: '16px',
|
|
75
|
+
margin: '8px 16px 8px 16px',
|
|
76
|
+
textDecoration: 'underline'
|
|
77
|
+
}),
|
|
78
|
+
description: inlineStyle({
|
|
79
|
+
...ellipsisStyle(2),
|
|
80
|
+
opacity: 0.8,
|
|
81
|
+
fontSize: '12px',
|
|
82
|
+
lineHeight: '16px',
|
|
83
|
+
margin: '8px 16px 0px 16px'
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
export const classNames = (prefix) => ({
|
|
87
|
+
container: `${prefix}__container`,
|
|
88
|
+
img: `${prefix}__img`,
|
|
89
|
+
texts: `${prefix}__texts`,
|
|
90
|
+
title: `${prefix}__texts--title`,
|
|
91
|
+
domain: `${prefix}__texts--domain`,
|
|
92
|
+
description: `${prefix}__texts--desc`
|
|
93
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a URL string and returns a URL object.
|
|
3
|
+
*
|
|
4
|
+
* @param url - The URL string to parse
|
|
5
|
+
* @returns A URL object containing the parsed components
|
|
6
|
+
*/
|
|
7
|
+
export function extractUrl(url) {
|
|
8
|
+
return new URL(url);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Removes duplicate consecutive slashes from a path string.
|
|
12
|
+
*
|
|
13
|
+
* @param path - The path string to clean
|
|
14
|
+
* @returns The cleaned path with no consecutive slashes
|
|
15
|
+
*/
|
|
16
|
+
export function cleanPath(path) {
|
|
17
|
+
return path.replace(/\/\//g, '/');
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// @ts-expect-error: xmlhttprequest has no types
|
|
2
|
+
import xhrForNode from 'xmlhttprequest';
|
|
3
|
+
import { inBrowser, isString } from '@luckrya/utility';
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
const XHR = inBrowser ? window.XMLHttpRequest : xhrForNode.XMLHttpRequest;
|
|
6
|
+
/**
|
|
7
|
+
* Performs a synchronous HTTP GET request to fetch HTML content from a URL.
|
|
8
|
+
*
|
|
9
|
+
* @param url - The URL to fetch content from
|
|
10
|
+
* @returns The HTML content as a string, or undefined if the request fails
|
|
11
|
+
*/
|
|
12
|
+
export function sync(url) {
|
|
13
|
+
if (cache.has(url))
|
|
14
|
+
return cache.get(url);
|
|
15
|
+
let result;
|
|
16
|
+
try {
|
|
17
|
+
const xhr = new XHR();
|
|
18
|
+
xhr.open('GET', url, false);
|
|
19
|
+
xhr.setRequestHeader('Content-Type', 'text/html');
|
|
20
|
+
xhr.send();
|
|
21
|
+
if (xhr.status === 200 && !!xhr.responseText) {
|
|
22
|
+
result = xhr.responseText;
|
|
23
|
+
cache.set(url, xhr.responseText);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error(`【XHR Error】:${err instanceof Error ? err.message : 'get remote URL resource exception!'}`);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Performs an asynchronous HTTP GET request to fetch HTML content from a URL.
|
|
33
|
+
*
|
|
34
|
+
* @param url - The URL to fetch content from
|
|
35
|
+
* @returns A Promise that resolves to the HTML content string
|
|
36
|
+
*/
|
|
37
|
+
export function async(url) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
try {
|
|
40
|
+
if (cache.has(url))
|
|
41
|
+
return resolve(cache.get(url));
|
|
42
|
+
const xhr = new XHR();
|
|
43
|
+
xhr.open('GET', url, false);
|
|
44
|
+
xhr.setRequestHeader('Content-Type', 'text/html');
|
|
45
|
+
xhr.onreadystatechange = function () {
|
|
46
|
+
if (xhr.readyState === 4 &&
|
|
47
|
+
xhr.status === 200 &&
|
|
48
|
+
isString(xhr.responseText)) {
|
|
49
|
+
cache.set(url, xhr.responseText);
|
|
50
|
+
resolve(xhr.responseText);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
xhr.send();
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
reject(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { isFunction } from '@luckrya/utility';
|
|
2
|
+
import { getUrlMetadata, generateCardDomFragment } from './assemble';
|
|
3
|
+
/**
|
|
4
|
+
* Markdown-it plugin that converts specially-formatted links into rich link preview cards.
|
|
5
|
+
*
|
|
6
|
+
* @param md - The markdown-it instance
|
|
7
|
+
* @param pluginOptions - Configuration options for the plugin
|
|
8
|
+
*/
|
|
9
|
+
export const linkToCardPlugin = (md, pluginOptions = {}) => {
|
|
10
|
+
function parseCardLinkHref(href) {
|
|
11
|
+
const tagRegexp = new RegExp(`^(${'@'}:)([a-zA-Z0-9]+.*)`);
|
|
12
|
+
const match = href?.match(tagRegexp);
|
|
13
|
+
return {
|
|
14
|
+
isCardLink: !!match,
|
|
15
|
+
url: match?.[2]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function assembleCardTpl(options) {
|
|
19
|
+
const urlMetadata = getUrlMetadata(options.url);
|
|
20
|
+
if (urlMetadata) {
|
|
21
|
+
ignoreRestToken(options.tokens, options.i);
|
|
22
|
+
const cardDomOptions = {
|
|
23
|
+
href: options.url,
|
|
24
|
+
linkTitle: joinLinkTitle(options.tokens),
|
|
25
|
+
target: pluginOptions.target || '_blank',
|
|
26
|
+
classPrefix: pluginOptions.classPrefix
|
|
27
|
+
};
|
|
28
|
+
return isFunction(pluginOptions.render)
|
|
29
|
+
? pluginOptions.render(urlMetadata, cardDomOptions)
|
|
30
|
+
: generateCardDomFragment(urlMetadata, cardDomOptions);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
md.renderer.renderInline = (tokens, rootOptions, env) => {
|
|
34
|
+
let result = '';
|
|
35
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
36
|
+
const currentToken = tokens[i];
|
|
37
|
+
const ruleFunction = md.renderer.rules[currentToken.type];
|
|
38
|
+
if (currentToken.hidden) {
|
|
39
|
+
result += '';
|
|
40
|
+
}
|
|
41
|
+
else if (isFunction(ruleFunction)) {
|
|
42
|
+
result += ruleFunction(tokens, i, rootOptions, env, md.renderer);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
result += md.renderer.renderToken(tokens, i, rootOptions);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
md.renderer.rules.link_open = (tokens, i, rootOptions, env, self) => {
|
|
51
|
+
const token = tokens[i];
|
|
52
|
+
const isLinkOpenToken = token.tag === 'a' && token.type === 'link_open';
|
|
53
|
+
const href = token.attrs?.filter((attr) => attr.includes('href'))[0]?.[1];
|
|
54
|
+
const { url, isCardLink } = parseCardLinkHref(href);
|
|
55
|
+
if (isLinkOpenToken && isCardLink && url) {
|
|
56
|
+
const card = assembleCardTpl({ url, tokens, i });
|
|
57
|
+
if (card)
|
|
58
|
+
return card;
|
|
59
|
+
}
|
|
60
|
+
return self.renderToken(tokens, i, rootOptions);
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
function ignoreRestToken(tokens, i) {
|
|
64
|
+
tokens.forEach((token, index) => {
|
|
65
|
+
if (index !== i)
|
|
66
|
+
token.hidden = true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function joinLinkTitle(tokens) {
|
|
70
|
+
return tokens
|
|
71
|
+
.map(({ hidden, content }) => {
|
|
72
|
+
if (hidden)
|
|
73
|
+
return content;
|
|
74
|
+
return '';
|
|
75
|
+
})
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join('');
|
|
78
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vitepress-linkcards",
|
|
3
|
+
"version": "2.2.2",
|
|
4
|
+
"description": "A Vitepress plugin to generate a pretty linkcard with OGP.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"linkcard",
|
|
7
|
+
"markdown-it",
|
|
8
|
+
"ogp",
|
|
9
|
+
"thumbnail",
|
|
10
|
+
"vitepress",
|
|
11
|
+
"vitepress-plugin"
|
|
12
|
+
],
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/asumo-1xts/vitepress-linkcard"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "aSumo",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/asumo-1xts/vitepress-linkcard.git"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"types"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./dist/.esm.min.js",
|
|
28
|
+
"module": "./dist/.esm.min.js",
|
|
29
|
+
"types": "./types/index.d.ts",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc && rollup -c",
|
|
35
|
+
"clean": "rm -rf dist types",
|
|
36
|
+
"dev": "rollup -c -w",
|
|
37
|
+
"docs": "typedoc",
|
|
38
|
+
"format": "oxfmt --write",
|
|
39
|
+
"lint": "oxlint",
|
|
40
|
+
"lint:github": "oxlint --format=github",
|
|
41
|
+
"type:dts": "tsc --emitDeclarationOnly"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@babel/core": "^7.28.5",
|
|
45
|
+
"@babel/preset-env": "^7.28.5",
|
|
46
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
47
|
+
"@luckrya/utility": "^0.1.1",
|
|
48
|
+
"@rollup/plugin-babel": "^6.1.0",
|
|
49
|
+
"@rollup/plugin-commonjs": "^29.0.0",
|
|
50
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
51
|
+
"@types/babel__core": "^7",
|
|
52
|
+
"@types/markdown-it": "^13.0.2",
|
|
53
|
+
"@types/node": "^25.0.3",
|
|
54
|
+
"markdown-it": "^13.0.2",
|
|
55
|
+
"oxfmt": "^0.26.0",
|
|
56
|
+
"oxlint": "^1.41.0",
|
|
57
|
+
"rollup": "^2.79.2",
|
|
58
|
+
"rollup-plugin-terser": "^7.0.2",
|
|
59
|
+
"typedoc": "^0.28.15",
|
|
60
|
+
"typedoc-theme-hierarchy": "^6.0.0",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"xmlhttprequest": "^1.8.0"
|
|
63
|
+
},
|
|
64
|
+
"babel": {
|
|
65
|
+
"presets": [
|
|
66
|
+
"@babel/env",
|
|
67
|
+
"@babel/typescript"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"packageManager": "yarn@4.9.2"
|
|
71
|
+
}
|
package/types/api.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { UrlMetadata, CardDomRenderOptions } from './types';
|
|
2
|
+
interface CardResponse {
|
|
3
|
+
url: string;
|
|
4
|
+
data: UrlMetadata;
|
|
5
|
+
options: Omit<CardDomRenderOptions, 'href'>;
|
|
6
|
+
dom: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Generates a link card by fetching and parsing metadata from a URL.
|
|
10
|
+
*
|
|
11
|
+
* @param url - The URL to fetch metadata from
|
|
12
|
+
* @param options - Rendering options for the card
|
|
13
|
+
* @returns A promise that resolves to a CardResponse containing the card data and HTML
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateCard(url: string, options: Omit<CardDomRenderOptions, 'href'>): Promise<CardResponse>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CardDomRender } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Generates the HTML DOM fragment for a link card display.
|
|
4
|
+
*
|
|
5
|
+
* @param data - The metadata extracted from the URL
|
|
6
|
+
* @param options - Rendering options including href, target, etc.
|
|
7
|
+
* @returns An HTML string containing the card markup
|
|
8
|
+
*/
|
|
9
|
+
export declare const generateCardDomFragment: CardDomRender;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple file-based cache for storing and retrieving structured data.
|
|
3
|
+
*/
|
|
4
|
+
export default class LocalFileCache<V extends Record<string, unknown>> {
|
|
5
|
+
constructor();
|
|
6
|
+
private setFile;
|
|
7
|
+
private readFile;
|
|
8
|
+
has(url: string): boolean;
|
|
9
|
+
get(url: string): V | undefined;
|
|
10
|
+
set(url: string, data: V): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UrlMetadata } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Retrieves metadata for a given URL, using cache when available.
|
|
4
|
+
*
|
|
5
|
+
* @param url - The URL to fetch metadata from
|
|
6
|
+
* @returns The parsed URL metadata, or null if unavailable
|
|
7
|
+
*/
|
|
8
|
+
export declare function getUrlMetadata(url: string): UrlMetadata | null | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UrlMetadata } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Parses HTML string to extract structured metadata for link card generation.
|
|
4
|
+
*
|
|
5
|
+
* @param htmlString - The HTML content to parse
|
|
6
|
+
* @param url - The URL of the page
|
|
7
|
+
* @returns Parsed metadata object, or null if no valid metadata found
|
|
8
|
+
*/
|
|
9
|
+
export declare function parserMetadata(htmlString: string, url: string): UrlMetadata | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates complete inline styles for all link card components.
|
|
3
|
+
*/
|
|
4
|
+
export declare const STYLE: () => {
|
|
5
|
+
a: string;
|
|
6
|
+
container: string;
|
|
7
|
+
img: string;
|
|
8
|
+
texts: string;
|
|
9
|
+
title: string;
|
|
10
|
+
domain: string;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const classNames: (prefix?: string) => {
|
|
14
|
+
container: string;
|
|
15
|
+
img: string;
|
|
16
|
+
texts: string;
|
|
17
|
+
title: string;
|
|
18
|
+
domain: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a URL string and returns a URL object.
|
|
3
|
+
*
|
|
4
|
+
* @param url - The URL string to parse
|
|
5
|
+
* @returns A URL object containing the parsed components
|
|
6
|
+
*/
|
|
7
|
+
export declare function extractUrl(url: string): URL;
|
|
8
|
+
/**
|
|
9
|
+
* Removes duplicate consecutive slashes from a path string.
|
|
10
|
+
*
|
|
11
|
+
* @param path - The path string to clean
|
|
12
|
+
* @returns The cleaned path with no consecutive slashes
|
|
13
|
+
*/
|
|
14
|
+
export declare function cleanPath(path: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performs a synchronous HTTP GET request to fetch HTML content from a URL.
|
|
3
|
+
*
|
|
4
|
+
* @param url - The URL to fetch content from
|
|
5
|
+
* @returns The HTML content as a string, or undefined if the request fails
|
|
6
|
+
*/
|
|
7
|
+
export declare function sync(url: string): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Performs an asynchronous HTTP GET request to fetch HTML content from a URL.
|
|
10
|
+
*
|
|
11
|
+
* @param url - The URL to fetch content from
|
|
12
|
+
* @returns A Promise that resolves to the HTML content string
|
|
13
|
+
*/
|
|
14
|
+
export declare function async(url: string): Promise<string | undefined>;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LinkToCardPlugin } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Markdown-it plugin that converts specially-formatted links into rich link preview cards.
|
|
4
|
+
*
|
|
5
|
+
* @param md - The markdown-it instance
|
|
6
|
+
* @param pluginOptions - Configuration options for the plugin
|
|
7
|
+
*/
|
|
8
|
+
export declare const linkToCardPlugin: LinkToCardPlugin;
|
package/types/types.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
/**
|
|
3
|
+
* Type definition for the link-to-card plugin compatible with markdown-it.
|
|
4
|
+
*
|
|
5
|
+
* @see {@link https://markdown-it.github.io/markdown-it/#MarkdownIt.use | MarkdownIt.use}
|
|
6
|
+
*/
|
|
7
|
+
export type LinkToCardPlugin = MarkdownIt.PluginWithOptions<LinkToCardPluginOptions>;
|
|
8
|
+
/**
|
|
9
|
+
* Configuration options for the link-to-card plugin.
|
|
10
|
+
*
|
|
11
|
+
* This interface defines the customization options available when using the vitepress-linkcard plugin.
|
|
12
|
+
* The plugin converts specially-formatted markdown links (prefixed with `@:`) into rich link preview cards
|
|
13
|
+
* with Open Graph Protocol (OGP) metadata.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { linkToCardPlugin } from 'vitepress-linkcard'
|
|
18
|
+
*
|
|
19
|
+
* markdownIt.use(linkToCardPlugin, {
|
|
20
|
+
* target: '_blank'
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @see {@link https://daringfireball.net/projects/markdown/syntax#link | Markdown Link Syntax}
|
|
25
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target | HTML anchor target attribute}
|
|
26
|
+
*/
|
|
27
|
+
export interface LinkToCardPluginOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Where to display the linked URL.
|
|
30
|
+
* @defaultValue `'_blank'` - Opens the link in a new tab/window
|
|
31
|
+
*/
|
|
32
|
+
target?: ATarget;
|
|
33
|
+
/**
|
|
34
|
+
* CSS class name prefix for the card DOM elements.
|
|
35
|
+
* When set, no inline styles are injected; only the class names are applied.
|
|
36
|
+
*
|
|
37
|
+
* @example `'my-docs__link-card'`
|
|
38
|
+
*/
|
|
39
|
+
classPrefix?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Custom function to render the DOM fragment for the link card.
|
|
42
|
+
* When provided, this function is used instead of the default card renderer.
|
|
43
|
+
*/
|
|
44
|
+
render?: CardDomRender;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Metadata extracted from a URL's HTML page using Open Graph Protocol tags and meta tags.
|
|
48
|
+
*
|
|
49
|
+
* This interface represents the structured data fetched from a web page to populate the link card display.
|
|
50
|
+
* The metadata is typically extracted from `<meta>` tags, OGP tags, and other HTML elements.
|
|
51
|
+
*
|
|
52
|
+
* @see {@link https://ogp.me/ | Open Graph Protocol}
|
|
53
|
+
*/
|
|
54
|
+
export interface UrlMetadata {
|
|
55
|
+
/**
|
|
56
|
+
* The title of the page, extracted from `<title>` or `<meta property="og:title">` tags.
|
|
57
|
+
*/
|
|
58
|
+
title?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The description of the page, extracted from `<meta name="description">` or
|
|
61
|
+
* `<meta property="og:description">` tags.
|
|
62
|
+
*/
|
|
63
|
+
description?: string;
|
|
64
|
+
/**
|
|
65
|
+
* The logo or icon URL for the page, extracted from `<meta property="og:image">` or
|
|
66
|
+
* `<link rel="icon">` tags.
|
|
67
|
+
*/
|
|
68
|
+
logo?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Additional metadata properties that may be extracted from the page.
|
|
71
|
+
*/
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Valid values for the HTML anchor element's `target` attribute.
|
|
76
|
+
*
|
|
77
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target | MDN - target attribute}
|
|
78
|
+
*/
|
|
79
|
+
export type ATarget = '_self' | '_blank' | '_top' | '_parent';
|
|
80
|
+
/**
|
|
81
|
+
* Options passed to the card DOM renderer function.
|
|
82
|
+
*
|
|
83
|
+
* This interface provides all the necessary information to render a link card,
|
|
84
|
+
* including the URL, display preferences, and styling options.
|
|
85
|
+
*/
|
|
86
|
+
export interface CardDomRenderOptions {
|
|
87
|
+
/**
|
|
88
|
+
* The URL to link to.
|
|
89
|
+
*/
|
|
90
|
+
href: string;
|
|
91
|
+
/**
|
|
92
|
+
* The title text to display on the link (typically from the markdown link text).
|
|
93
|
+
*/
|
|
94
|
+
linkTitle: string;
|
|
95
|
+
/**
|
|
96
|
+
* Where to display the linked URL when clicked.
|
|
97
|
+
*/
|
|
98
|
+
target: ATarget;
|
|
99
|
+
/**
|
|
100
|
+
* CSS class name prefix for the card DOM elements.
|
|
101
|
+
*/
|
|
102
|
+
classPrefix?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Function signature for custom card DOM renderers.
|
|
106
|
+
*
|
|
107
|
+
* This type defines a function that takes URL metadata and rendering options,
|
|
108
|
+
* and returns an HTML string representing the link card.
|
|
109
|
+
*
|
|
110
|
+
* @param data - The metadata extracted from the URL
|
|
111
|
+
* @param options - Rendering and styling options for the card
|
|
112
|
+
* @returns An HTML string representing the rendered link card
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* const customRender: CardDomRender = (data, options) => {
|
|
117
|
+
* return `<div class="${options.classPrefix}">
|
|
118
|
+
* <h3>${data.title}</h3>
|
|
119
|
+
* <p>${data.description}</p>
|
|
120
|
+
* </div>`
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export type CardDomRender = (data: UrlMetadata, options: CardDomRenderOptions) => string;
|