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 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: ' · ' // 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.为什么 EXIF 信息没有显示?**
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
- **2.我想只在某些图片上使用 Photosuite,怎么办?**
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:` · `}}var x=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++}},S=null;function C(e){return S||=new x(e),S}var w=1,T=null,E=!1,D=Promise.resolve();function O(){return d.join(process.cwd(),`node_modules`,`.cache`,`photosuite`,`exif-cache.json`)}function k(){return T||=(async()=>{try{let e=await u.readFile(O(),`utf-8`),t=JSON.parse(e);if(t&&t.version===w&&t.entries&&typeof t.entries==`object`)return t}catch{}return{version:w,entries:{}}})(),T}async function A(){if(!E||!T)return;let e=await T;E=!1,D=D.then(async()=>{let t=O(),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 D}function j(e){try{let t=new h.URL(e);return t.protocol===`http:`||t.protocol===`https:`}catch{return!1}}function M(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}M(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 N(e,t){try{return await M(e,t,0)}catch(n){let r=n&&n.code;if(r===`ECONNRESET`||r===`ETIMEDOUT`||r===`ECONNREFUSED`)return await M(e,t,0);throw n}}async function P(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 F(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 I(e,t,n){let r=``,i;try{if(j(e)){let t=await C(n.concurrency).acquire();try{let t=await N(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 P(r);return a.FNumber&&a.ExposureTime&&a.ISO?a:null}finally{i&&await i()}}function L(e,t,n){let r=n.fields.map(e=>{let n=t[e];return n?F(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 R(e,t,n){let r=e.properties?.src;if(!r)return;let i=j(r),a=n.cache&&i,o;if(a){let e=await k();Object.prototype.hasOwnProperty.call(e.entries,r)&&(o=e.entries[r])}if(o===void 0){try{o=await I(r,t,n)}catch(e){console.warn(`[photosuite] Failed to get EXIF for ${r}:`,e);return}if(a){let e=await k();e.entries[r]=o,E=!0}}o&&L(e,o,n)}function z(e={}){let t=b(e);return async(e,n)=>{let r=[],i=e=>{e.type===`element`&&e.tagName===`img`&&r.push(R(e,n,t)),e.children&&e.children.forEach(i)};i(e),r.length>0&&await Promise.all(r),t.cache&&await A()}}function B(e){return{name:`photosuite`,hooks:{"astro:config:setup":({injectScript:t,updateConfig:n})=>{t(`page`,`
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([z,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 V=B;exports.default=B,exports.exiftoolVendored=z,exports.imageUrl=v,exports.photosuite=V;
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 y = n.filter((n) => typeof n == "string" && n !== "");
11
- if (y.length === 0) return "";
12
- if (y.length === 1) return y[0];
13
- let b = y[0], x = y[1], S = `${b.endsWith("/") ? b.slice(0, -1) : b}/${x.startsWith("/") ? x.slice(1) : x}`;
14
- return y.length > 2 ? join(S, ...y.slice(2)) : S;
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: y, imageDir: b = "imageDir", fileDir: x = !1 } = n;
18
- return (n, S) => {
19
- let C = (S?.data?.astro?.frontmatter || S?.data?.frontmatter || {})[b] || "", w = (S?.path || S?.history?.[0] || "").split(/[\\/]/).pop()?.split(".").shift() || "", T = x ? w : C, O = (n) => {
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 b = n.url || "";
23
- (y || T) && isShort(b) && (n.url = join(y, T, b));
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(O);
25
+ n.children && n.children.length && n.children.forEach(A);
26
26
  }
27
27
  };
28
- O(n);
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 y = typeof n.exif == "object" && n.exif !== null ? n.exif : {};
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: y.cache !== !1,
44
- concurrency: typeof y.concurrency == "number" && y.concurrency > 0 ? y.concurrency : 6,
45
- timeout: typeof y.timeout == "number" && y.timeout > 0 ? y.timeout : 15e3,
46
- headerBytes: typeof y.headerBytes == "number" && y.headerBytes >= 0 ? y.headerBytes : 131072,
47
- fields: Array.isArray(y.fields) && y.fields.length > 0 ? y.fields : DEFAULT_FIELDS,
48
- separator: typeof y.separator == "string" ? y.separator : " · "
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"), y = JSON.parse(n);
80
- if (y && y.version === CACHE_VERSION && y.entries && typeof y.entries == "object") return y;
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 y = cacheFilePath(), S = path.dirname(y);
93
- await fsp.mkdir(S, { recursive: !0 });
94
- let C = path.join(S, `.exif-cache.${process.pid}.${Date.now()}.tmp`);
95
- await fsp.writeFile(C, JSON.stringify(n), "utf-8"), await fsp.rename(C, y);
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 y = new URL(n);
101
- return y.protocol === "http:" || y.protocol === "https:";
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, E, D) {
107
- return new Promise((O, k) => {
108
- let A = n.startsWith("https:") ? https : http, j = {};
109
- E.headerBytes > 0 && (j.Range = `bytes=0-${E.headerBytes - 1}`);
110
- let M = A.get(n, { headers: j }, (S) => {
111
- let C = S.statusCode || 0;
112
- if (C >= 300 && C < 400 && S.headers.location) {
113
- if (S.resume(), D >= 5) {
114
- k(/* @__PURE__ */ Error("Too many redirects"));
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(S.headers.location, n).toString(), E, D + 1).then(O, k);
127
+ downloadAttempt(new URL(w.headers.location, n).toString(), O, k + 1).then(A, j);
118
128
  return;
119
129
  }
120
- if (C !== 200 && C !== 206) {
121
- S.resume(), k(/* @__PURE__ */ Error("HTTP " + C));
130
+ if (T !== 200 && T !== 206) {
131
+ w.resume(), j(/* @__PURE__ */ Error("HTTP " + T));
122
132
  return;
123
133
  }
124
- let A = os.tmpdir(), j = ".bin";
134
+ let M = os.tmpdir(), N = ".bin";
125
135
  try {
126
- j = path.extname(new URL(n).pathname) || ".bin";
136
+ N = path.extname(new URL(n).pathname) || ".bin";
127
137
  } catch {}
128
- let M = path.join(A, `exif-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${j}`), N = fs.createWriteStream(M);
129
- S.pipe(N), N.on("finish", () => {
130
- O({
131
- path: M,
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(M);
144
+ await fsp.unlink(P);
135
145
  } catch {}
136
146
  }
137
147
  });
138
- }), N.on("error", (n) => {
148
+ }), F.on("error", (n) => {
139
149
  try {
140
- fs.unlinkSync(M);
150
+ fs.unlinkSync(P);
141
151
  } catch {}
142
- k(n);
152
+ j(n);
143
153
  });
144
154
  });
145
- M.setTimeout(E.timeout, () => {
146
- M.destroy(Object.assign(/* @__PURE__ */ Error("timeout"), { code: "ETIMEDOUT" }));
147
- }), M.on("error", k);
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, y) {
160
+ async function downloadToTemp(n, x) {
151
161
  try {
152
- return await downloadAttempt(n, y, 0);
153
- } catch (b) {
154
- let x = b && b.code;
155
- if (x === "ECONNRESET" || x === "ETIMEDOUT" || x === "ECONNREFUSED") return await downloadAttempt(n, y, 0);
156
- throw b;
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(y) {
160
- let b = await exiftool.read(y);
169
+ async function handleExif(x) {
170
+ let S = await exiftool.read(x);
161
171
  return {
162
- SourceFile: b.SourceFile,
163
- ExifToolVersion: b.ExifToolVersion,
164
- MIMEType: b.MIMEType,
165
- FileType: b.FileType,
166
- Make: b.Make,
167
- Model: b.Model,
168
- LensModel: b.LensModel,
169
- DateTimeOriginal: b.DateTimeOriginal,
170
- CreateDate: b.CreateDate,
171
- ModifyDate: b.ModifyDate,
172
- ImageWidth: b.ImageWidth,
173
- ImageHeight: b.ImageHeight,
174
- GPSLatitude: b.GPSLatitude,
175
- GPSLongitude: b.GPSLongitude,
176
- FNumber: b.FNumber,
177
- ExposureTime: b.ExposureTime,
178
- ISO: b.ISO,
179
- FocalLength: b.FocalLength,
180
- warnings: b.warnings || [],
181
- errors: b.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, y) {
185
- if (y == null) return "";
194
+ function formatField(n, x) {
195
+ if (x == null) return "";
186
196
  switch (n) {
187
- case "FNumber": return `ƒ/${Number(y).toFixed(1)}`;
188
- case "ExposureTime": return typeof y == "number" ? y >= 1 ? `${y}s` : `1/${Math.round(1 / y)}s` : y.toString();
189
- case "ISO": return `ISO ${y}`;
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 = y.toString();
201
+ let n = x.toString();
192
202
  return n.endsWith("mm") ? n : `${n}mm`;
193
- case "DateTimeOriginal": return typeof y == "object" && y.year ? `${y.year}/${y.month}/${y.day}` : y.toString();
194
- default: return y.toString();
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, b, S) {
198
- let C = "", w;
207
+ async function extractExif(n, S, w) {
208
+ let T = "", E;
199
209
  try {
200
210
  if (isHttpUrl(n)) {
201
- let y = await getSemaphore(S.concurrency).acquire();
211
+ let x = await getSemaphore(w.concurrency).acquire();
202
212
  try {
203
- let y = await downloadToTemp(n, S);
204
- C = y.path, w = y.cleanup;
213
+ let x = await downloadToTemp(n, w);
214
+ T = x.path, E = x.cleanup;
205
215
  } finally {
206
- y();
216
+ x();
207
217
  }
208
218
  } else {
209
- if (path.isAbsolute(n)) C = n;
219
+ if (path.isAbsolute(n)) T = n;
210
220
  else {
211
- let y = path.dirname(b.path);
212
- C = path.resolve(y, n);
221
+ let x = path.dirname(S.path);
222
+ T = path.resolve(x, n);
213
223
  }
214
- if (!fs.existsSync(C)) {
215
- let n = decodeURIComponent(C);
216
- if (fs.existsSync(n)) C = 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 (!C || !fs.existsSync(C)) return null;
221
- let T = await handleExif(C);
222
- return T.FNumber && T.ExposureTime && T.ISO ? T : null;
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
- w && await w();
234
+ E && await E();
225
235
  }
226
236
  }
227
- function renderExifNode(n, y, b) {
228
- let x = b.fields.map((n) => {
229
- let b = y[n];
230
- return b ? formatField(n, b) : null;
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 (x.length === 0) return;
233
- let S = x.join(b.separator), C = { ...n.properties };
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: C,
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: S
255
+ value: w
246
256
  }]
247
257
  }];
248
258
  }
249
- async function processNode(n, y, b) {
250
- let x = n.properties?.src;
251
- if (!x) return;
252
- let S = isHttpUrl(x), C = b.cache && S, w;
253
- if (C) {
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, x) && (w = n.entries[x]);
265
+ Object.prototype.hasOwnProperty.call(n.entries, C) && (E = n.entries[C]);
256
266
  }
257
- if (w === void 0) {
267
+ if (E === void 0) {
258
268
  try {
259
- w = await extractExif(x, y, b);
269
+ E = await extractExif(C, x, S);
260
270
  } catch (n) {
261
- console.warn(`[photosuite] Failed to get EXIF for ${x}:`, n);
271
+ console.warn(`[photosuite] Failed to get EXIF for ${C}:`, n);
262
272
  return;
263
273
  }
264
- if (C) {
274
+ if (T) {
265
275
  let n = await loadCache();
266
- n.entries[x] = w, cacheDirty = !0;
276
+ n.entries[C] = E, cacheDirty = !0;
267
277
  }
268
278
  }
269
- w && renderExifNode(n, w, b);
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 y = resolveExifOptions(n);
273
- return async (n, b) => {
274
- let x = [], S = (n) => {
275
- n.type === "element" && n.tagName === "img" && x.push(processNode(n, b, y)), n.children && n.children.forEach(S);
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
- S(n), x.length > 0 && await Promise.all(x), y.cache && await flushCache();
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: y, updateConfig: b }) => {
284
- y("page", `
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 x = [], S = [];
292
- S.push([imageUrl, n]), n.exif !== !1 && x.push([exiftoolVendored, n]);
293
- let C = { markdown: {} };
294
- x.length > 0 && (C.markdown.rehypePlugins = x), S.length > 0 && (C.markdown.remarkPlugins = S), Object.keys(C.markdown).length > 0 && b(C);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "photosuite",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "一款零配置的 Astro 图片增强集成,支持灯箱、自动拼图、EXIF 信息及路径解析。",
5
5
  "keywords": [
6
6
  "astro-integration",