photosuite 0.3.0 → 0.4.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/README.md +40 -1
- package/README.zh-CN.md +44 -3
- package/dist/photosuite.integration.cjs +2 -2
- package/dist/photosuite.integration.js +154 -135
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,39 @@ photosuite({
|
|
|
130
130
|
})
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
**Per-Page Filtering:**
|
|
134
|
+
|
|
135
|
+
`scope` is a CSS selector that only governs *client-side* behavior (lightbox, captions, grid). The EXIF rehype plugin runs at build time on **every** Markdown file's `<img>` tags and rewrites their DOM into `.photosuite-item` + `.photosuite-exif`. If you have non-article pages (e.g. `about.md`) outside the `scope` container, you'll still see the injected EXIF markup in the HTML.
|
|
136
|
+
|
|
137
|
+
To restrict EXIF injection to specific pages, use either of:
|
|
138
|
+
|
|
139
|
+
**Option 1 Glob patterns in config**:
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
photosuite({
|
|
143
|
+
scope: '#article',
|
|
144
|
+
exif: {
|
|
145
|
+
// Only process matched files; omit to process all
|
|
146
|
+
include: ['src/content/posts/**/*.md'],
|
|
147
|
+
// Skip matched files; takes precedence over include
|
|
148
|
+
exclude: ['src/content/pages/**/*.md'],
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Patterns are matched against paths relative to the project root, normalized with `/` separators. Supported wildcards: `*` (within a segment), `**` (across segments), `?` (single character).
|
|
154
|
+
|
|
155
|
+
**Option 2 Frontmatter opt-out**:
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
---
|
|
159
|
+
title: About
|
|
160
|
+
exif: false # or: photosuite: false
|
|
161
|
+
---
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Pages with `exif: false`, `photosuite: false`, or `photosuite.exif: false` in frontmatter are skipped regardless of `include`/`exclude`.
|
|
165
|
+
|
|
133
166
|
### 3. Image Grid
|
|
134
167
|
|
|
135
168
|
Photosuite supports automatically combining consecutive images into a grid layout. When 2-3 images are placed adjacently in Markdown, they will be automatically combined into a grid, and each image remains independently clickable.
|
|
@@ -254,7 +287,10 @@ photosuite({
|
|
|
254
287
|
'ISO', // ISO
|
|
255
288
|
'DateTimeOriginal' // Date Original
|
|
256
289
|
],
|
|
257
|
-
separator: ' · '
|
|
290
|
+
separator: ' · ', // Separator
|
|
291
|
+
// Per-page filtering (since v0.3.1)
|
|
292
|
+
include: undefined, // string[] of glob patterns; omit to process all pages
|
|
293
|
+
exclude: undefined, // string[] of glob patterns; takes precedence over include
|
|
258
294
|
},
|
|
259
295
|
|
|
260
296
|
// Fancybox native options
|
|
@@ -282,6 +318,9 @@ photosuite({
|
|
|
282
318
|
|
|
283
319
|
## FAQ
|
|
284
320
|
|
|
321
|
+
**Q: My non-article pages (e.g. about page) still show EXIF info even though they're outside the `scope` container.**
|
|
322
|
+
A: `scope` only controls *client-side* behavior. The EXIF rehype plugin runs at build time on every Markdown file. Restrict it via `exif.include` / `exif.exclude` glob patterns, or add `exif: false` to a page's frontmatter. See [EXIF Data Display § Per-Page Filtering](#2-exif-data-display).
|
|
323
|
+
|
|
285
324
|
**Q: Why isn't EXIF data showing?**
|
|
286
325
|
A: Please check the following:
|
|
287
326
|
1. Does the image contain EXIF data? (Some compression tools strip EXIF)
|
package/README.zh-CN.md
CHANGED
|
@@ -131,6 +131,39 @@ photosuite({
|
|
|
131
131
|
})
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
+
**按页面过滤:**
|
|
135
|
+
|
|
136
|
+
`scope` 是一个 CSS 选择器,只控制**客户端**行为(灯箱、标题、拼图)。EXIF rehype 插件在**构建时**对**所有** Markdown 中的 `<img>` 进行处理,将其改写为 `.photosuite-item` + `.photosuite-exif` 的 DOM 结构。如果你有非文章页(如 `about.md`)不在 `scope` 容器内,构建产物的 HTML 中仍然会出现 EXIF 注入的标签。
|
|
137
|
+
|
|
138
|
+
要限定 EXIF 注入只作用于特定页面,可任选其一:
|
|
139
|
+
|
|
140
|
+
**方式一 配置 glob 模式**:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
photosuite({
|
|
144
|
+
scope: '#article',
|
|
145
|
+
exif: {
|
|
146
|
+
// 仅处理匹配的文件;未配置时处理所有 Markdown
|
|
147
|
+
include: ['src/content/posts/**/*.md'],
|
|
148
|
+
// 跳过匹配的文件;优先级高于 include
|
|
149
|
+
exclude: ['src/content/pages/**/*.md'],
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
模式相对于项目根目录解析,分隔符自动归一化为 `/`。支持通配符:`*`(段内任意字符)、`**`(跨段匹配)、`?`(单字符)。
|
|
155
|
+
|
|
156
|
+
**方式二 Frontmatter 单页 opt-out**:
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
---
|
|
160
|
+
title: 关于
|
|
161
|
+
exif: false # 或:photosuite: false
|
|
162
|
+
---
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
页面 frontmatter 含 `exif: false`、`photosuite: false` 或 `photosuite.exif: false` 时直接跳过,优先级高于 `include` / `exclude`。
|
|
166
|
+
|
|
134
167
|
### 2. 图片拼图
|
|
135
168
|
|
|
136
169
|
Photosuite 支持自动将连续的图片组合成拼图布局。当 Markdown 中有 2-3 张图片紧挨着时,它们会自动组合成拼图,且每张图片都独立可点击。
|
|
@@ -259,7 +292,10 @@ photosuite({
|
|
|
259
292
|
'ISO', // 感光度
|
|
260
293
|
'DateTimeOriginal' // 拍摄时间
|
|
261
294
|
],
|
|
262
|
-
separator: ' · '
|
|
295
|
+
separator: ' · ', // 分隔符
|
|
296
|
+
// 按页面过滤(v0.3.1 起支持)
|
|
297
|
+
include: undefined, // string[] glob 模式;未配置时处理所有页面
|
|
298
|
+
exclude: undefined, // string[] glob 模式;优先级高于 include
|
|
263
299
|
},
|
|
264
300
|
|
|
265
301
|
// Fancybox 原生配置传递
|
|
@@ -287,13 +323,16 @@ photosuite({
|
|
|
287
323
|
|
|
288
324
|
## 常见问题
|
|
289
325
|
|
|
290
|
-
**1
|
|
326
|
+
**1.我的非文章页(如关于页)虽然不在 `scope` 容器内,为什么仍然带有 EXIF 信息?**
|
|
327
|
+
A: `scope` 仅控制**客户端**行为。EXIF rehype 插件在**构建时**对所有 Markdown 生效。可使用 `exif.include` / `exif.exclude` glob 模式过滤,或在页面 frontmatter 中添加 `exif: false`。详见 [EXIF 信息展示 § 按页面过滤](#2-exif-信息展示)。
|
|
328
|
+
|
|
329
|
+
**2.为什么 EXIF 信息没有显示?**
|
|
291
330
|
A: 请检查以下几点:
|
|
292
331
|
|
|
293
332
|
1. 图片是否包含 EXIF 信息(某些压缩工具会去除 EXIF)
|
|
294
333
|
2. EXIF 信息至少有曝光三要素(焦距、光圈、快门速度)时,才会显示
|
|
295
334
|
|
|
296
|
-
**
|
|
335
|
+
**3.我想只在某些图片上使用 Photosuite,怎么办?**
|
|
297
336
|
A: 您可以通过 CSS 选择器精确控制范围(多个选择器用逗号分隔)例如,只在类名为 `'#main` 的元素内部生效:
|
|
298
337
|
|
|
299
338
|
```javascript
|
|
@@ -303,6 +342,8 @@ photosuite({
|
|
|
303
342
|
})
|
|
304
343
|
```
|
|
305
344
|
|
|
345
|
+
注意:`scope` 只影响客户端行为;若希望服务端 EXIF 注入也只对特定页面生效,请使用 `exif.include` / `exif.exclude` 或 frontmatter opt-out(见上)。
|
|
346
|
+
|
|
306
347
|
## 贡献者们
|
|
307
348
|
|
|
308
349
|
一行代码,一个插件,对于独立博客而言,微不足道,如同尘埃。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`exiftool-vendored`),l=require(`node:fs`);l=s(l);let u=require(`node:fs/promises`);u=s(u);let d=require(`node:path`);d=s(d);let f=require(`node:http`);f=s(f);let p=require(`node:https`);p=s(p);let m=require(`node:os`);m=s(m);let h=require(`node:url`);var g=(...e)=>{let t=e.filter(e=>typeof e==`string`&&e!==``);if(t.length===0)return``;if(t.length===1)return t[0];let n=t[0],r=t[1],i=`${n.endsWith(`/`)?n.slice(0,-1):n}/${r.startsWith(`/`)?r.slice(1):r}`;return t.length>2?g(i,...t.slice(2)):i},_=e=>!(!e||/^https?:\/\//i.test(e)||e.startsWith(`/`)||e.startsWith(`./`)||e.startsWith(`../`));function v(e={}){let{imageBase:t,imageDir:n=`imageDir`,fileDir:r=!1}=e;return(e,i)=>{let a=(i?.data?.astro?.frontmatter||i?.data?.frontmatter||{})[n]||``,o=(i?.path||i?.history?.[0]||``).split(/[\\/]/).pop()?.split(`.`).shift()||``,s=r?o:a,c=e=>{if(e){if(e.type===`image`){let n=e.url||``;(t||s)&&_(n)&&(e.url=g(t,s,n))}e.children&&e.children.length&&e.children.forEach(c)}};c(e)}}var y=[`Model`,`LensModel`,`FocalLength`,`FNumber`,`ExposureTime`,`ISO`,`DateTimeOriginal`];function b(e){let t=typeof e.exif==`object`&&e.exif!==null?e.exif:{};return{cache:t.cache!==!1,concurrency:typeof t.concurrency==`number`&&t.concurrency>0?t.concurrency:6,timeout:typeof t.timeout==`number`&&t.timeout>0?t.timeout:15e3,headerBytes:typeof t.headerBytes==`number`&&t.headerBytes>=0?t.headerBytes:131072,fields:Array.isArray(t.fields)&&t.fields.length>0?t.fields:y,separator:typeof t.separator==`string`?t.separator:` ·
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`exiftool-vendored`),l=require(`node:fs`);l=s(l);let u=require(`node:fs/promises`);u=s(u);let d=require(`node:path`);d=s(d);let f=require(`node:http`);f=s(f);let p=require(`node:https`);p=s(p);let m=require(`node:os`);m=s(m);let h=require(`node:url`);var g=(...e)=>{let t=e.filter(e=>typeof e==`string`&&e!==``);if(t.length===0)return``;if(t.length===1)return t[0];let n=t[0],r=t[1],i=`${n.endsWith(`/`)?n.slice(0,-1):n}/${r.startsWith(`/`)?r.slice(1):r}`;return t.length>2?g(i,...t.slice(2)):i},_=e=>!(!e||/^https?:\/\//i.test(e)||e.startsWith(`/`)||e.startsWith(`./`)||e.startsWith(`../`));function v(e={}){let{imageBase:t,imageDir:n=`imageDir`,fileDir:r=!1}=e;return(e,i)=>{let a=(i?.data?.astro?.frontmatter||i?.data?.frontmatter||{})[n]||``,o=(i?.path||i?.history?.[0]||``).split(/[\\/]/).pop()?.split(`.`).shift()||``,s=r?o:a,c=e=>{if(e){if(e.type===`image`){let n=e.url||``;(t||s)&&_(n)&&(e.url=g(t,s,n))}e.children&&e.children.length&&e.children.forEach(c)}};c(e)}}var y=[`Model`,`LensModel`,`FocalLength`,`FNumber`,`ExposureTime`,`ISO`,`DateTimeOriginal`];function b(e){let t=e.replace(/\\/g,`/`),n=``;for(let e=0;e<t.length;e++){let r=t[e];r===`*`?t[e+1]===`*`?(n+=`.*`,e++,t[e+1]===`/`&&e++):n+=`[^/]*`:r===`?`?n+=`[^/]`:/[.+^${}()|[\]\\]/.test(r)?n+=`\\`+r:n+=r}return RegExp(`^`+n+`$`)}function x(e){let t=typeof e.exif==`object`&&e.exif!==null?e.exif:{},n=e=>Array.isArray(e)&&e.length>0?e.filter(e=>typeof e==`string`).map(b):null;return{cache:t.cache!==!1,concurrency:typeof t.concurrency==`number`&&t.concurrency>0?t.concurrency:6,timeout:typeof t.timeout==`number`&&t.timeout>0?t.timeout:15e3,headerBytes:typeof t.headerBytes==`number`&&t.headerBytes>=0?t.headerBytes:131072,fields:Array.isArray(t.fields)&&t.fields.length>0?t.fields:y,separator:typeof t.separator==`string`?t.separator:` · `,include:n(t.include),exclude:n(t.exclude)??[]}}var S=class{available;waiters=[];constructor(e){this.available=Math.max(1,e)}async acquire(){this.available>0?this.available--:await new Promise(e=>this.waiters.push(e));let e=!1;return()=>{e||(e=!0,this.release())}}release(){let e=this.waiters.shift();e?e():this.available++}},C=null;function w(e){return C||=new S(e),C}var T=1,E=null,D=!1,O=Promise.resolve();function k(){return d.join(process.cwd(),`node_modules`,`.cache`,`photosuite`,`exif-cache.json`)}function A(){return E||=(async()=>{try{let e=await u.readFile(k(),`utf-8`),t=JSON.parse(e);if(t&&t.version===T&&t.entries&&typeof t.entries==`object`)return t}catch{}return{version:T,entries:{}}})(),E}async function j(){if(!D||!E)return;let e=await E;D=!1,O=O.then(async()=>{let t=k(),n=d.dirname(t);await u.mkdir(n,{recursive:!0});let r=d.join(n,`.exif-cache.${process.pid}.${Date.now()}.tmp`);await u.writeFile(r,JSON.stringify(e),`utf-8`),await u.rename(r,t)}).catch(()=>{}),await O}function M(e){try{let t=new h.URL(e);return t.protocol===`http:`||t.protocol===`https:`}catch{return!1}}function N(e,t,n){return new Promise((r,i)=>{let a=e.startsWith(`https:`)?p:f,o={};t.headerBytes>0&&(o.Range=`bytes=0-${t.headerBytes-1}`);let s=a.get(e,{headers:o},a=>{let o=a.statusCode||0;if(o>=300&&o<400&&a.headers.location){if(a.resume(),n>=5){i(Error(`Too many redirects`));return}N(new h.URL(a.headers.location,e).toString(),t,n+1).then(r,i);return}if(o!==200&&o!==206){a.resume(),i(Error(`HTTP `+o));return}let s=m.tmpdir(),c=`.bin`;try{c=d.extname(new h.URL(e).pathname)||`.bin`}catch{}let f=d.join(s,`exif-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${c}`),p=l.createWriteStream(f);a.pipe(p),p.on(`finish`,()=>{r({path:f,cleanup:async()=>{try{await u.unlink(f)}catch{}}})}),p.on(`error`,e=>{try{l.unlinkSync(f)}catch{}i(e)})});s.setTimeout(t.timeout,()=>{s.destroy(Object.assign(Error(`timeout`),{code:`ETIMEDOUT`}))}),s.on(`error`,i)})}async function P(e,t){try{return await N(e,t,0)}catch(n){let r=n&&n.code;if(r===`ECONNRESET`||r===`ETIMEDOUT`||r===`ECONNREFUSED`)return await N(e,t,0);throw n}}async function F(e){let t=await c.exiftool.read(e);return{SourceFile:t.SourceFile,ExifToolVersion:t.ExifToolVersion,MIMEType:t.MIMEType,FileType:t.FileType,Make:t.Make,Model:t.Model,LensModel:t.LensModel,DateTimeOriginal:t.DateTimeOriginal,CreateDate:t.CreateDate,ModifyDate:t.ModifyDate,ImageWidth:t.ImageWidth,ImageHeight:t.ImageHeight,GPSLatitude:t.GPSLatitude,GPSLongitude:t.GPSLongitude,FNumber:t.FNumber,ExposureTime:t.ExposureTime,ISO:t.ISO,FocalLength:t.FocalLength,warnings:t.warnings||[],errors:t.errors||[]}}function I(e,t){if(t==null)return``;switch(e){case`FNumber`:return`ƒ/${Number(t).toFixed(1)}`;case`ExposureTime`:return typeof t==`number`?t>=1?`${t}s`:`1/${Math.round(1/t)}s`:t.toString();case`ISO`:return`ISO ${t}`;case`FocalLength`:let e=t.toString();return e.endsWith(`mm`)?e:`${e}mm`;case`DateTimeOriginal`:return typeof t==`object`&&t.year?`${t.year}/${t.month}/${t.day}`:t.toString();default:return t.toString()}}async function L(e,t,n){let r=``,i;try{if(M(e)){let t=await w(n.concurrency).acquire();try{let t=await P(e,n);r=t.path,i=t.cleanup}finally{t()}}else{if(d.isAbsolute(e))r=e;else{let n=d.dirname(t.path);r=d.resolve(n,e)}if(!l.existsSync(r)){let e=decodeURIComponent(r);if(l.existsSync(e))r=e;else return null}}if(!r||!l.existsSync(r))return null;let a=await F(r);return a.FNumber&&a.ExposureTime&&a.ISO?a:null}finally{i&&await i()}}function R(e,t,n){let r=n.fields.map(e=>{let n=t[e];return n?I(e,n):null}).filter(Boolean);if(r.length===0)return;let i=r.join(n.separator),a={...e.properties};e.tagName=`div`,e.properties={className:[`photosuite-item`]},e.children=[{type:`element`,tagName:`img`,properties:a,children:[]},{type:`element`,tagName:`div`,properties:{className:[`photosuite-exif`]},children:[{type:`text`,value:i}]}]}async function z(e,t,n){let r=e.properties?.src;if(!r)return;let i=M(r),a=n.cache&&i,o;if(a){let e=await A();Object.prototype.hasOwnProperty.call(e.entries,r)&&(o=e.entries[r])}if(o===void 0){try{o=await L(r,t,n)}catch(e){console.warn(`[photosuite] Failed to get EXIF for ${r}:`,e);return}if(a){let e=await A();e.entries[r]=o,D=!0}}o&&R(e,o,n)}function B(e,t){let n=e?.data?.astro?.frontmatter||e?.data?.frontmatter||{};if(n.exif===!1||n.photosuite===!1||n.photosuite&&typeof n.photosuite==`object`&&n.photosuite.exif===!1)return!1;let r=e?.path||e?.history?.[0]||``;if(!r)return!0;let i=d.relative(process.cwd(),r).replace(/\\/g,`/`);return!(t.exclude.length>0&&t.exclude.some(e=>e.test(i))||t.include&&!t.include.some(e=>e.test(i)))}function V(e={}){let t=x(e);return async(e,n)=>{if(!B(n,t))return;let r=[],i=e=>{e.type===`element`&&e.tagName===`img`&&r.push(z(e,n,t)),e.children&&e.children.forEach(i)};i(e),r.length>0&&await Promise.all(r),t.cache&&await j()}}function H(e){return{name:`photosuite`,hooks:{"astro:config:setup":({injectScript:t,updateConfig:n})=>{t(`page`,`
|
|
2
2
|
import { photosuite } from 'photosuite/client';
|
|
3
3
|
const __opts = ${JSON.stringify(e)};
|
|
4
4
|
const __run = () => photosuite(__opts);
|
|
5
5
|
__run();
|
|
6
6
|
document.addEventListener('astro:page-load', __run);
|
|
7
|
-
`);let r=[],i=[];i.push([v,e]),e.exif!==!1&&r.push([
|
|
7
|
+
`);let r=[],i=[];i.push([v,e]),e.exif!==!1&&r.push([V,e]);let a={markdown:{}};r.length>0&&(a.markdown.rehypePlugins=r),i.length>0&&(a.markdown.remarkPlugins=i),Object.keys(a.markdown).length>0&&n(a)}}}}const U=H;exports.default=H,exports.exiftoolVendored=V,exports.imageUrl=v,exports.photosuite=U;
|
|
@@ -7,25 +7,25 @@ import * as https from "node:https";
|
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import { URL } from "node:url";
|
|
9
9
|
var join = (...n) => {
|
|
10
|
-
let
|
|
11
|
-
if (
|
|
12
|
-
if (
|
|
13
|
-
let
|
|
14
|
-
return
|
|
10
|
+
let x = n.filter((n) => typeof n == "string" && n !== "");
|
|
11
|
+
if (x.length === 0) return "";
|
|
12
|
+
if (x.length === 1) return x[0];
|
|
13
|
+
let S = x[0], C = x[1], w = `${S.endsWith("/") ? S.slice(0, -1) : S}/${C.startsWith("/") ? C.slice(1) : C}`;
|
|
14
|
+
return x.length > 2 ? join(w, ...x.slice(2)) : w;
|
|
15
15
|
}, isShort = (n) => !(!n || /^https?:\/\//i.test(n) || n.startsWith("/") || n.startsWith("./") || n.startsWith("../"));
|
|
16
16
|
function imageUrl(n = {}) {
|
|
17
|
-
let { imageBase:
|
|
18
|
-
return (n,
|
|
19
|
-
let
|
|
17
|
+
let { imageBase: x, imageDir: S = "imageDir", fileDir: C = !1 } = n;
|
|
18
|
+
return (n, w) => {
|
|
19
|
+
let T = (w?.data?.astro?.frontmatter || w?.data?.frontmatter || {})[S] || "", E = (w?.path || w?.history?.[0] || "").split(/[\\/]/).pop()?.split(".").shift() || "", D = C ? E : T, A = (n) => {
|
|
20
20
|
if (n) {
|
|
21
21
|
if (n.type === "image") {
|
|
22
|
-
let
|
|
23
|
-
(
|
|
22
|
+
let S = n.url || "";
|
|
23
|
+
(x || D) && isShort(S) && (n.url = join(x, D, S));
|
|
24
24
|
}
|
|
25
|
-
n.children && n.children.length && n.children.forEach(
|
|
25
|
+
n.children && n.children.length && n.children.forEach(A);
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
-
|
|
28
|
+
A(n);
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
var DEFAULT_FIELDS = [
|
|
@@ -37,15 +37,25 @@ var DEFAULT_FIELDS = [
|
|
|
37
37
|
"ISO",
|
|
38
38
|
"DateTimeOriginal"
|
|
39
39
|
];
|
|
40
|
+
function globToRegExp(n) {
|
|
41
|
+
let x = n.replace(/\\/g, "/"), S = "";
|
|
42
|
+
for (let n = 0; n < x.length; n++) {
|
|
43
|
+
let C = x[n];
|
|
44
|
+
C === "*" ? x[n + 1] === "*" ? (S += ".*", n++, x[n + 1] === "/" && n++) : S += "[^/]*" : C === "?" ? S += "[^/]" : /[.+^${}()|[\]\\]/.test(C) ? S += "\\" + C : S += C;
|
|
45
|
+
}
|
|
46
|
+
return /* @__PURE__ */ RegExp("^" + S + "$");
|
|
47
|
+
}
|
|
40
48
|
function resolveExifOptions(n) {
|
|
41
|
-
let
|
|
49
|
+
let x = typeof n.exif == "object" && n.exif !== null ? n.exif : {}, S = (n) => Array.isArray(n) && n.length > 0 ? n.filter((n) => typeof n == "string").map(globToRegExp) : null;
|
|
42
50
|
return {
|
|
43
|
-
cache:
|
|
44
|
-
concurrency: typeof
|
|
45
|
-
timeout: typeof
|
|
46
|
-
headerBytes: typeof
|
|
47
|
-
fields: Array.isArray(
|
|
48
|
-
separator: typeof
|
|
51
|
+
cache: x.cache !== !1,
|
|
52
|
+
concurrency: typeof x.concurrency == "number" && x.concurrency > 0 ? x.concurrency : 6,
|
|
53
|
+
timeout: typeof x.timeout == "number" && x.timeout > 0 ? x.timeout : 15e3,
|
|
54
|
+
headerBytes: typeof x.headerBytes == "number" && x.headerBytes >= 0 ? x.headerBytes : 131072,
|
|
55
|
+
fields: Array.isArray(x.fields) && x.fields.length > 0 ? x.fields : DEFAULT_FIELDS,
|
|
56
|
+
separator: typeof x.separator == "string" ? x.separator : " · ",
|
|
57
|
+
include: S(x.include),
|
|
58
|
+
exclude: S(x.exclude) ?? []
|
|
49
59
|
};
|
|
50
60
|
}
|
|
51
61
|
var Semaphore = class {
|
|
@@ -76,8 +86,8 @@ function cacheFilePath() {
|
|
|
76
86
|
function loadCache() {
|
|
77
87
|
return cachePromise ||= (async () => {
|
|
78
88
|
try {
|
|
79
|
-
let n = await fsp.readFile(cacheFilePath(), "utf-8"),
|
|
80
|
-
if (
|
|
89
|
+
let n = await fsp.readFile(cacheFilePath(), "utf-8"), x = JSON.parse(n);
|
|
90
|
+
if (x && x.version === CACHE_VERSION && x.entries && typeof x.entries == "object") return x;
|
|
81
91
|
} catch {}
|
|
82
92
|
return {
|
|
83
93
|
version: CACHE_VERSION,
|
|
@@ -89,152 +99,152 @@ async function flushCache() {
|
|
|
89
99
|
if (!cacheDirty || !cachePromise) return;
|
|
90
100
|
let n = await cachePromise;
|
|
91
101
|
cacheDirty = !1, writing = writing.then(async () => {
|
|
92
|
-
let
|
|
93
|
-
await fsp.mkdir(
|
|
94
|
-
let
|
|
95
|
-
await fsp.writeFile(
|
|
102
|
+
let x = cacheFilePath(), w = path.dirname(x);
|
|
103
|
+
await fsp.mkdir(w, { recursive: !0 });
|
|
104
|
+
let T = path.join(w, `.exif-cache.${process.pid}.${Date.now()}.tmp`);
|
|
105
|
+
await fsp.writeFile(T, JSON.stringify(n), "utf-8"), await fsp.rename(T, x);
|
|
96
106
|
}).catch(() => {}), await writing;
|
|
97
107
|
}
|
|
98
108
|
function isHttpUrl(n) {
|
|
99
109
|
try {
|
|
100
|
-
let
|
|
101
|
-
return
|
|
110
|
+
let x = new URL(n);
|
|
111
|
+
return x.protocol === "http:" || x.protocol === "https:";
|
|
102
112
|
} catch {
|
|
103
113
|
return !1;
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
|
-
function downloadAttempt(n,
|
|
107
|
-
return new Promise((
|
|
108
|
-
let
|
|
109
|
-
|
|
110
|
-
let
|
|
111
|
-
let
|
|
112
|
-
if (
|
|
113
|
-
if (
|
|
114
|
-
|
|
116
|
+
function downloadAttempt(n, O, k) {
|
|
117
|
+
return new Promise((A, j) => {
|
|
118
|
+
let M = n.startsWith("https:") ? https : http, N = {};
|
|
119
|
+
O.headerBytes > 0 && (N.Range = `bytes=0-${O.headerBytes - 1}`);
|
|
120
|
+
let P = M.get(n, { headers: N }, (w) => {
|
|
121
|
+
let T = w.statusCode || 0;
|
|
122
|
+
if (T >= 300 && T < 400 && w.headers.location) {
|
|
123
|
+
if (w.resume(), k >= 5) {
|
|
124
|
+
j(/* @__PURE__ */ Error("Too many redirects"));
|
|
115
125
|
return;
|
|
116
126
|
}
|
|
117
|
-
downloadAttempt(new URL(
|
|
127
|
+
downloadAttempt(new URL(w.headers.location, n).toString(), O, k + 1).then(A, j);
|
|
118
128
|
return;
|
|
119
129
|
}
|
|
120
|
-
if (
|
|
121
|
-
|
|
130
|
+
if (T !== 200 && T !== 206) {
|
|
131
|
+
w.resume(), j(/* @__PURE__ */ Error("HTTP " + T));
|
|
122
132
|
return;
|
|
123
133
|
}
|
|
124
|
-
let
|
|
134
|
+
let M = os.tmpdir(), N = ".bin";
|
|
125
135
|
try {
|
|
126
|
-
|
|
136
|
+
N = path.extname(new URL(n).pathname) || ".bin";
|
|
127
137
|
} catch {}
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
path:
|
|
138
|
+
let P = path.join(M, `exif-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${N}`), F = fs.createWriteStream(P);
|
|
139
|
+
w.pipe(F), F.on("finish", () => {
|
|
140
|
+
A({
|
|
141
|
+
path: P,
|
|
132
142
|
cleanup: async () => {
|
|
133
143
|
try {
|
|
134
|
-
await fsp.unlink(
|
|
144
|
+
await fsp.unlink(P);
|
|
135
145
|
} catch {}
|
|
136
146
|
}
|
|
137
147
|
});
|
|
138
|
-
}),
|
|
148
|
+
}), F.on("error", (n) => {
|
|
139
149
|
try {
|
|
140
|
-
fs.unlinkSync(
|
|
150
|
+
fs.unlinkSync(P);
|
|
141
151
|
} catch {}
|
|
142
|
-
|
|
152
|
+
j(n);
|
|
143
153
|
});
|
|
144
154
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}),
|
|
155
|
+
P.setTimeout(O.timeout, () => {
|
|
156
|
+
P.destroy(Object.assign(/* @__PURE__ */ Error("timeout"), { code: "ETIMEDOUT" }));
|
|
157
|
+
}), P.on("error", j);
|
|
148
158
|
});
|
|
149
159
|
}
|
|
150
|
-
async function downloadToTemp(n,
|
|
160
|
+
async function downloadToTemp(n, x) {
|
|
151
161
|
try {
|
|
152
|
-
return await downloadAttempt(n,
|
|
153
|
-
} catch (
|
|
154
|
-
let
|
|
155
|
-
if (
|
|
156
|
-
throw
|
|
162
|
+
return await downloadAttempt(n, x, 0);
|
|
163
|
+
} catch (S) {
|
|
164
|
+
let C = S && S.code;
|
|
165
|
+
if (C === "ECONNRESET" || C === "ETIMEDOUT" || C === "ECONNREFUSED") return await downloadAttempt(n, x, 0);
|
|
166
|
+
throw S;
|
|
157
167
|
}
|
|
158
168
|
}
|
|
159
|
-
async function handleExif(
|
|
160
|
-
let
|
|
169
|
+
async function handleExif(x) {
|
|
170
|
+
let S = await exiftool.read(x);
|
|
161
171
|
return {
|
|
162
|
-
SourceFile:
|
|
163
|
-
ExifToolVersion:
|
|
164
|
-
MIMEType:
|
|
165
|
-
FileType:
|
|
166
|
-
Make:
|
|
167
|
-
Model:
|
|
168
|
-
LensModel:
|
|
169
|
-
DateTimeOriginal:
|
|
170
|
-
CreateDate:
|
|
171
|
-
ModifyDate:
|
|
172
|
-
ImageWidth:
|
|
173
|
-
ImageHeight:
|
|
174
|
-
GPSLatitude:
|
|
175
|
-
GPSLongitude:
|
|
176
|
-
FNumber:
|
|
177
|
-
ExposureTime:
|
|
178
|
-
ISO:
|
|
179
|
-
FocalLength:
|
|
180
|
-
warnings:
|
|
181
|
-
errors:
|
|
172
|
+
SourceFile: S.SourceFile,
|
|
173
|
+
ExifToolVersion: S.ExifToolVersion,
|
|
174
|
+
MIMEType: S.MIMEType,
|
|
175
|
+
FileType: S.FileType,
|
|
176
|
+
Make: S.Make,
|
|
177
|
+
Model: S.Model,
|
|
178
|
+
LensModel: S.LensModel,
|
|
179
|
+
DateTimeOriginal: S.DateTimeOriginal,
|
|
180
|
+
CreateDate: S.CreateDate,
|
|
181
|
+
ModifyDate: S.ModifyDate,
|
|
182
|
+
ImageWidth: S.ImageWidth,
|
|
183
|
+
ImageHeight: S.ImageHeight,
|
|
184
|
+
GPSLatitude: S.GPSLatitude,
|
|
185
|
+
GPSLongitude: S.GPSLongitude,
|
|
186
|
+
FNumber: S.FNumber,
|
|
187
|
+
ExposureTime: S.ExposureTime,
|
|
188
|
+
ISO: S.ISO,
|
|
189
|
+
FocalLength: S.FocalLength,
|
|
190
|
+
warnings: S.warnings || [],
|
|
191
|
+
errors: S.errors || []
|
|
182
192
|
};
|
|
183
193
|
}
|
|
184
|
-
function formatField(n,
|
|
185
|
-
if (
|
|
194
|
+
function formatField(n, x) {
|
|
195
|
+
if (x == null) return "";
|
|
186
196
|
switch (n) {
|
|
187
|
-
case "FNumber": return `ƒ/${Number(
|
|
188
|
-
case "ExposureTime": return typeof
|
|
189
|
-
case "ISO": return `ISO ${
|
|
197
|
+
case "FNumber": return `ƒ/${Number(x).toFixed(1)}`;
|
|
198
|
+
case "ExposureTime": return typeof x == "number" ? x >= 1 ? `${x}s` : `1/${Math.round(1 / x)}s` : x.toString();
|
|
199
|
+
case "ISO": return `ISO ${x}`;
|
|
190
200
|
case "FocalLength":
|
|
191
|
-
let n =
|
|
201
|
+
let n = x.toString();
|
|
192
202
|
return n.endsWith("mm") ? n : `${n}mm`;
|
|
193
|
-
case "DateTimeOriginal": return typeof
|
|
194
|
-
default: return
|
|
203
|
+
case "DateTimeOriginal": return typeof x == "object" && x.year ? `${x.year}/${x.month}/${x.day}` : x.toString();
|
|
204
|
+
default: return x.toString();
|
|
195
205
|
}
|
|
196
206
|
}
|
|
197
|
-
async function extractExif(n,
|
|
198
|
-
let
|
|
207
|
+
async function extractExif(n, S, w) {
|
|
208
|
+
let T = "", E;
|
|
199
209
|
try {
|
|
200
210
|
if (isHttpUrl(n)) {
|
|
201
|
-
let
|
|
211
|
+
let x = await getSemaphore(w.concurrency).acquire();
|
|
202
212
|
try {
|
|
203
|
-
let
|
|
204
|
-
|
|
213
|
+
let x = await downloadToTemp(n, w);
|
|
214
|
+
T = x.path, E = x.cleanup;
|
|
205
215
|
} finally {
|
|
206
|
-
|
|
216
|
+
x();
|
|
207
217
|
}
|
|
208
218
|
} else {
|
|
209
|
-
if (path.isAbsolute(n))
|
|
219
|
+
if (path.isAbsolute(n)) T = n;
|
|
210
220
|
else {
|
|
211
|
-
let
|
|
212
|
-
|
|
221
|
+
let x = path.dirname(S.path);
|
|
222
|
+
T = path.resolve(x, n);
|
|
213
223
|
}
|
|
214
|
-
if (!fs.existsSync(
|
|
215
|
-
let n = decodeURIComponent(
|
|
216
|
-
if (fs.existsSync(n))
|
|
224
|
+
if (!fs.existsSync(T)) {
|
|
225
|
+
let n = decodeURIComponent(T);
|
|
226
|
+
if (fs.existsSync(n)) T = n;
|
|
217
227
|
else return null;
|
|
218
228
|
}
|
|
219
229
|
}
|
|
220
|
-
if (!
|
|
221
|
-
let
|
|
222
|
-
return
|
|
230
|
+
if (!T || !fs.existsSync(T)) return null;
|
|
231
|
+
let D = await handleExif(T);
|
|
232
|
+
return D.FNumber && D.ExposureTime && D.ISO ? D : null;
|
|
223
233
|
} finally {
|
|
224
|
-
|
|
234
|
+
E && await E();
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
|
-
function renderExifNode(n,
|
|
228
|
-
let
|
|
229
|
-
let
|
|
230
|
-
return
|
|
237
|
+
function renderExifNode(n, x, S) {
|
|
238
|
+
let C = S.fields.map((n) => {
|
|
239
|
+
let S = x[n];
|
|
240
|
+
return S ? formatField(n, S) : null;
|
|
231
241
|
}).filter(Boolean);
|
|
232
|
-
if (
|
|
233
|
-
let
|
|
242
|
+
if (C.length === 0) return;
|
|
243
|
+
let w = C.join(S.separator), T = { ...n.properties };
|
|
234
244
|
n.tagName = "div", n.properties = { className: ["photosuite-item"] }, n.children = [{
|
|
235
245
|
type: "element",
|
|
236
246
|
tagName: "img",
|
|
237
|
-
properties:
|
|
247
|
+
properties: T,
|
|
238
248
|
children: []
|
|
239
249
|
}, {
|
|
240
250
|
type: "element",
|
|
@@ -242,56 +252,65 @@ function renderExifNode(n, y, b) {
|
|
|
242
252
|
properties: { className: ["photosuite-exif"] },
|
|
243
253
|
children: [{
|
|
244
254
|
type: "text",
|
|
245
|
-
value:
|
|
255
|
+
value: w
|
|
246
256
|
}]
|
|
247
257
|
}];
|
|
248
258
|
}
|
|
249
|
-
async function processNode(n,
|
|
250
|
-
let
|
|
251
|
-
if (!
|
|
252
|
-
let
|
|
253
|
-
if (
|
|
259
|
+
async function processNode(n, x, S) {
|
|
260
|
+
let C = n.properties?.src;
|
|
261
|
+
if (!C) return;
|
|
262
|
+
let w = isHttpUrl(C), T = S.cache && w, E;
|
|
263
|
+
if (T) {
|
|
254
264
|
let n = await loadCache();
|
|
255
|
-
Object.prototype.hasOwnProperty.call(n.entries,
|
|
265
|
+
Object.prototype.hasOwnProperty.call(n.entries, C) && (E = n.entries[C]);
|
|
256
266
|
}
|
|
257
|
-
if (
|
|
267
|
+
if (E === void 0) {
|
|
258
268
|
try {
|
|
259
|
-
|
|
269
|
+
E = await extractExif(C, x, S);
|
|
260
270
|
} catch (n) {
|
|
261
|
-
console.warn(`[photosuite] Failed to get EXIF for ${
|
|
271
|
+
console.warn(`[photosuite] Failed to get EXIF for ${C}:`, n);
|
|
262
272
|
return;
|
|
263
273
|
}
|
|
264
|
-
if (
|
|
274
|
+
if (T) {
|
|
265
275
|
let n = await loadCache();
|
|
266
|
-
n.entries[
|
|
276
|
+
n.entries[C] = E, cacheDirty = !0;
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
|
-
|
|
279
|
+
E && renderExifNode(n, E, S);
|
|
280
|
+
}
|
|
281
|
+
function shouldProcessFile(n, x) {
|
|
282
|
+
let S = n?.data?.astro?.frontmatter || n?.data?.frontmatter || {};
|
|
283
|
+
if (S.exif === !1 || S.photosuite === !1 || S.photosuite && typeof S.photosuite == "object" && S.photosuite.exif === !1) return !1;
|
|
284
|
+
let w = n?.path || n?.history?.[0] || "";
|
|
285
|
+
if (!w) return !0;
|
|
286
|
+
let T = path.relative(process.cwd(), w).replace(/\\/g, "/");
|
|
287
|
+
return !(x.exclude.length > 0 && x.exclude.some((n) => n.test(T)) || x.include && !x.include.some((n) => n.test(T)));
|
|
270
288
|
}
|
|
271
289
|
function exiftoolVendored(n = {}) {
|
|
272
|
-
let
|
|
273
|
-
return async (n,
|
|
274
|
-
|
|
275
|
-
|
|
290
|
+
let x = resolveExifOptions(n);
|
|
291
|
+
return async (n, S) => {
|
|
292
|
+
if (!shouldProcessFile(S, x)) return;
|
|
293
|
+
let C = [], w = (n) => {
|
|
294
|
+
n.type === "element" && n.tagName === "img" && C.push(processNode(n, S, x)), n.children && n.children.forEach(w);
|
|
276
295
|
};
|
|
277
|
-
|
|
296
|
+
w(n), C.length > 0 && await Promise.all(C), x.cache && await flushCache();
|
|
278
297
|
};
|
|
279
298
|
}
|
|
280
299
|
function astroPhotosuite(n) {
|
|
281
300
|
return {
|
|
282
301
|
name: "photosuite",
|
|
283
|
-
hooks: { "astro:config:setup": ({ injectScript:
|
|
284
|
-
|
|
302
|
+
hooks: { "astro:config:setup": ({ injectScript: x, updateConfig: S }) => {
|
|
303
|
+
x("page", `
|
|
285
304
|
import { photosuite } from 'photosuite/client';
|
|
286
305
|
const __opts = ${JSON.stringify(n)};
|
|
287
306
|
const __run = () => photosuite(__opts);
|
|
288
307
|
__run();
|
|
289
308
|
document.addEventListener('astro:page-load', __run);
|
|
290
309
|
`);
|
|
291
|
-
let
|
|
292
|
-
|
|
293
|
-
let
|
|
294
|
-
|
|
310
|
+
let C = [], w = [];
|
|
311
|
+
w.push([imageUrl, n]), n.exif !== !1 && C.push([exiftoolVendored, n]);
|
|
312
|
+
let T = { markdown: {} };
|
|
313
|
+
C.length > 0 && (T.markdown.rehypePlugins = C), w.length > 0 && (T.markdown.remarkPlugins = w), Object.keys(T.markdown).length > 0 && S(T);
|
|
295
314
|
} }
|
|
296
315
|
};
|
|
297
316
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -145,4 +145,18 @@ export interface PhotosuiteExifOptions {
|
|
|
145
145
|
* 大幅减少传输量。设为 0 则下载完整文件。
|
|
146
146
|
*/
|
|
147
147
|
headerBytes?: number;
|
|
148
|
+
/**
|
|
149
|
+
* 仅对匹配的 Markdown 文件启用 EXIF 注入
|
|
150
|
+
* @example ["src/content/posts/**\/*.md"]
|
|
151
|
+
* @description 相对于项目根目录的 glob 模式数组。未配置时对所有 Markdown 生效。
|
|
152
|
+
* 支持 `*`(匹配单段)、`**`(跨段匹配)、`?`(匹配单字符)。
|
|
153
|
+
*/
|
|
154
|
+
include?: string[];
|
|
155
|
+
/**
|
|
156
|
+
* 跳过匹配的 Markdown 文件,不进行 EXIF 注入
|
|
157
|
+
* @example ["src/content/pages/**\/*.md"]
|
|
158
|
+
* @description 相对于项目根目录的 glob 模式数组。优先级高于 include。
|
|
159
|
+
* 也可在单个页面的 frontmatter 中设置 `exif: false` 或 `photosuite: false` 跳过该页。
|
|
160
|
+
*/
|
|
161
|
+
exclude?: string[];
|
|
148
162
|
}
|