@ryusenkei/maezae 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/dist/delivery.js CHANGED
@@ -116,18 +116,129 @@ function createDeliveryClient(delivery, { drafts = false, at } = {}) {
116
116
  prefetch
117
117
  };
118
118
  }
119
+ // ../model/src/mediaUrl.ts
120
+ /** 配信する形式と拡張子 */
121
+ const EXTENSIONS = {
122
+ "image/jpeg": "jpg",
123
+ "image/png": "png",
124
+ "image/webp": "webp",
125
+ "image/gif": "gif",
126
+ "video/mp4": "mp4",
127
+ "video/webm": "webm"
128
+ };
129
+ /** 拡張子の形式(配信しない拡張子は null) */
130
+ const mediaMimeOf = (ext) => Object.entries(EXTENSIONS).find(([, e]) => e === ext)?.[0] ?? null;
131
+ const IMAGE_PATH = /^\/images\/([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+)-(\d+)x(\d+)\.([a-z0-9]+)$/;
132
+ const VIDEO_PATH = /^\/videos\/([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+)\.([a-z0-9]+)$/;
133
+ /** 配信 URL のパスを解析(形が違えば null) */
134
+ function parseMediaPath(pathname) {
135
+ const image = IMAGE_PATH.exec(pathname);
136
+ if (image) {
137
+ const [, tenant, id, width, height, ext] = image;
138
+ return mediaMimeOf(ext)?.startsWith("image/") ? {
139
+ kind: "image",
140
+ tenant,
141
+ id,
142
+ ext,
143
+ width: Number(width),
144
+ height: Number(height)
145
+ } : null;
146
+ }
147
+ const video = VIDEO_PATH.exec(pathname);
148
+ if (video) {
149
+ const [, tenant, id, ext] = video;
150
+ return mediaMimeOf(ext)?.startsWith("video/") ? {
151
+ kind: "video",
152
+ tenant,
153
+ id,
154
+ ext
155
+ } : null;
156
+ }
157
+ return null;
158
+ }
159
+ /** 幅・高さの上限(画素) */
160
+ const IMAGE_SIZE_MAX = 4096;
161
+ const FITS = [
162
+ "scale-down",
163
+ "contain",
164
+ "cover",
165
+ "crop",
166
+ "pad"
167
+ ];
168
+ const FORMATS = [
169
+ "jpg",
170
+ "png",
171
+ "webp",
172
+ "avif"
173
+ ];
174
+ /** URL に画像の変換のクエリを設定(ほかのクエリは残す) */
175
+ function withImageParams(url, params) {
176
+ const u = new URL(url);
177
+ const { rect, ...rest } = params;
178
+ for (const [k, v] of Object.entries(rest)) if (v !== void 0) u.searchParams.set(k, String(v));
179
+ if (rect) u.searchParams.set("rect", [
180
+ rect.left,
181
+ rect.top,
182
+ rect.width,
183
+ rect.height
184
+ ].map(Math.round).join(","));
185
+ return u.href;
186
+ }
187
+ const intIn = (v, min, max) => {
188
+ if (v === null) return void 0;
189
+ if (!/^\d+$/.test(v)) return null;
190
+ const n = Number(v);
191
+ return n >= min && n <= max ? n : null;
192
+ };
193
+ /** クエリから画像の変換(不正な値があれば null、変換なしは空のオブジェクト) */
194
+ function imageParamsOf(query) {
195
+ const w = intIn(query.get("w"), 1, IMAGE_SIZE_MAX);
196
+ const h = intIn(query.get("h"), 1, IMAGE_SIZE_MAX);
197
+ const q = intIn(query.get("q"), 1, 100);
198
+ if (w === null || h === null || q === null) return null;
199
+ const fit = query.get("fit");
200
+ const fm = query.get("fm");
201
+ const auto = query.get("auto");
202
+ if (fit !== null && !FITS.includes(fit) || fm !== null && !FORMATS.includes(fm) || auto !== null && auto !== "format") return null;
203
+ const rectValue = query.get("rect");
204
+ let rect;
205
+ if (rectValue !== null) {
206
+ const parts = rectValue.split(",");
207
+ if (parts.length !== 4 || !parts.every((p) => /^\d+$/.test(p))) return null;
208
+ const [left, top, width, height] = parts.map(Number);
209
+ if (width < 1 || height < 1) return null;
210
+ rect = {
211
+ left,
212
+ top,
213
+ width,
214
+ height
215
+ };
216
+ }
217
+ return {
218
+ ...w !== void 0 && { w },
219
+ ...h !== void 0 && { h },
220
+ ...q !== void 0 && { q },
221
+ ...fit !== null && { fit },
222
+ ...fm !== null && { fm },
223
+ ...auto !== null && { auto: "format" },
224
+ ...rect && { rect }
225
+ };
226
+ }
119
227
  // ../delivery-client/src/media.ts
228
+ /** 配信 URL の画像の切り抜き(クエリ rect、範囲なしは元の URL) */
229
+ const deliveredImageUrl = (src, rect) => rect ? withImageParams(src, { rect }) : src;
120
230
  /**
121
231
  * 実体から表示の素材への変換(動画は代わりの画像も)
122
- * - base: 実体の URL の前半(URL は base/<ID>)
123
- * - 画像の変換: imageUrl があれば画像(動画の実体は除く)を変換の URL に、切り抜きは切り抜いた画像の URL・寸法・見せたい点
124
- * - imageUrl なし: 元の画像のまま(手元の開発)
232
+ * - URL: 実体の url、なしは base/<ID>(base は実体の URL の前半)
233
+ * - 画像の変換: imageUrl で画像(動画の実体は除く)を変換の URL に、url のある実体の既定は deliveredImageUrl、切り抜きは切り抜いた画像の URL・寸法・見せたい点
234
+ * - url も imageUrl もない: 元の画像のまま(手元の開発)
125
235
  */
126
236
  function mediaOf(base, imageUrl) {
127
237
  const still = (a) => {
128
- const src = `${base}/${a.id}`;
238
+ const src = a.url ?? `${base}/${a.id}`;
239
+ const transform = imageUrl ?? (a.url ? deliveredImageUrl : void 0);
129
240
  const alt = a.alt ?? "";
130
- if (!imageUrl || a.kind !== "image") return {
241
+ if (!transform || a.kind !== "image") return {
131
242
  src,
132
243
  width: a.width,
133
244
  height: a.height,
@@ -135,7 +246,7 @@ function mediaOf(base, imageUrl) {
135
246
  ...a.hotspot ? { hotspot: a.hotspot } : {}
136
247
  };
137
248
  if (!a.crop) return {
138
- src: imageUrl(src),
249
+ src: transform(src),
139
250
  width: a.width,
140
251
  height: a.height,
141
252
  alt,
@@ -143,7 +254,7 @@ function mediaOf(base, imageUrl) {
143
254
  };
144
255
  const rect = cropRect(a.width, a.height, a.crop);
145
256
  return {
146
- src: imageUrl(src, rect),
257
+ src: transform(src, rect),
147
258
  width: rect.width,
148
259
  height: rect.height,
149
260
  alt,
@@ -196,4 +307,4 @@ function createDeliveryClock(timeZone) {
196
307
  })
197
308
  };
198
309
  }
199
- export { DELIVERY_PATH, DeliveryError, createDeliveryClient, createDeliveryClock, fieldOf, limitOf, listParams, mediaOf, optionLabels, pagePath, subPath };
310
+ export { DELIVERY_PATH, DeliveryError, createDeliveryClient, createDeliveryClock, deliveredImageUrl, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, optionLabels, pagePath, parseMediaPath, subPath, withImageParams };
package/dist/model.js CHANGED
@@ -358,7 +358,8 @@ const contentType = z.object({
358
358
  from: key,
359
359
  field: fieldKey
360
360
  })).optional(),
361
- og: z.string().startsWith("/", "OG 画像の雛形は / で始まる").optional()
361
+ og: z.string().startsWith("/", "OG 画像の雛形は / で始まる").optional(),
362
+ idInput: z.boolean().optional()
362
363
  }).refine((t) => t.format !== "object" || !t.core?.length, { message: "オブジェクト形式の型はコアの項目なし" }).refine((t) => new Set([...t.core ?? [], ...t.fields].map((f) => f.key)).size === (t.core?.length ?? 0) + t.fields.length, { message: "項目の key が重複(コアの項目を含む)" }).refine((t) => conditionsValid([...t.core ?? [], ...t.fields]), { message: "項目の条件が同じ階層の選択肢の項目と値を指していない" }).refine((t) => (t.pages ?? []).every((p) => conditionValid(p.when, [...t.core ?? [], ...t.fields])), { message: "ページの条件が型の選択肢の項目と値を指していない" }).superRefine((t, ctx) => {
363
364
  for (const k of reservedKeysOf(t)) ctx.addIssue({
364
365
  code: "custom",
@@ -51,6 +51,7 @@ export declare function createDeliveryClient(delivery: Delivery, { drafts, at }?
51
51
  pages?: readonly import("../../model/src/index.js").PageDef[];
52
52
  counts?: readonly import("../../model/src/index.js").CountDef[];
53
53
  og?: string;
54
+ idInput?: boolean;
54
55
  }>>(path: string, query: ListQuery<T> & Readonly<{
55
56
  limit: number;
56
57
  }>) => Promise<Page<I>>;
@@ -1,5 +1,6 @@
1
1
  export { createDeliveryClient, DELIVERY_PATH, DeliveryError, subPath, type BundleResult, type Delivery, type DeliveryClient, type Prefetched } from './client.js';
2
- export { mediaOf, type ImageUrl, type MediaOf } from './media.js';
2
+ export { deliveredImageUrl, mediaOf, type ImageUrl, type MediaOf } from './media.js';
3
+ export { imageParamsOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from '../../model/src/index.js';
3
4
  export { listParams, type ListFilter, type ListQuery, type Page } from './query.js';
4
5
  export { createDeliveryClock, fieldOf, limitOf, optionLabels, pagePath, type DeliveryClock } from './schema.js';
5
6
  export type { DeliveredAsset, FieldValue, Item, ItemOf } from './values.js';
@@ -1,12 +1,14 @@
1
1
  import { type CropRect, type Media } from '../../model/src/index.js';
2
2
  /** 配信する画像の URL(元の URL と、元画像の画素での切り抜きの範囲から、配信の側で変換する URL) */
3
3
  export type ImageUrl = (src: string, rect?: CropRect) => string;
4
+ /** 配信 URL の画像の切り抜き(クエリ rect、範囲なしは元の URL) */
5
+ export declare const deliveredImageUrl: ImageUrl;
4
6
  /** 実体から表示の素材(見つからない実体はなし) */
5
7
  export type MediaOf = (asset: unknown) => Media | undefined;
6
8
  /**
7
9
  * 実体から表示の素材への変換(動画は代わりの画像も)
8
- * - base: 実体の URL の前半(URL は base/<ID>)
9
- * - 画像の変換: imageUrl があれば画像(動画の実体は除く)を変換の URL に、切り抜きは切り抜いた画像の URL・寸法・見せたい点
10
- * - imageUrl なし: 元の画像のまま(手元の開発)
10
+ * - URL: 実体の url、なしは base/<ID>(base は実体の URL の前半)
11
+ * - 画像の変換: imageUrl で画像(動画の実体は除く)を変換の URL に、url のある実体の既定は deliveredImageUrl、切り抜きは切り抜いた画像の URL・寸法・見せたい点
12
+ * - url も imageUrl もない: 元の画像のまま(手元の開発)
11
13
  */
12
14
  export declare function mediaOf(base: string, imageUrl?: ImageUrl): MediaOf;
@@ -2,10 +2,11 @@ import type { Body } from '../../body/src/index.js';
2
2
  import type { ContentType, Crop, DateString, FieldDef, Hotspot } from '../../model/src/index.js';
3
3
  /** 配信 API の1件の値(項目の key と id・updated_at) */
4
4
  export type Item = Readonly<Record<string, unknown>>;
5
- /** 配信 API が解決した実体(hotspot・crop は元の画像に対する割合) */
5
+ /** 配信 API が解決した実体(url は配信 URL で配信のオリジンがある CMS だけ、hotspot・crop は元の画像に対する割合) */
6
6
  export type DeliveredAsset = Readonly<{
7
7
  id: string;
8
8
  kind: 'image' | 'video';
9
+ url?: string;
9
10
  width: number;
10
11
  height: number;
11
12
  alt: string;
@@ -2,11 +2,12 @@ export { fieldZod, jsonSchemaOf, normalizeValues, zodOf, type ValueOptions } fro
2
2
  export { assetRefSchema, cropSchema, hotspotSchema, mediaSchema, type Asset, type AssetRef, type Crop, type Hotspot, type LocalImage, type Media } from './asset.js';
3
3
  export { coverPosition, coverStart, cropRect, croppedHotspot, objectPosition, stillOf, type CropRect } from './image.js';
4
4
  export { IMAGE_TYPE, VIDEO_TYPE } from './media.js';
5
+ export { IMAGE_SIZE_MAX, imageParamsOf, mediaExtOf, mediaMimeOf, mediaPathOf, parseMediaPath, withImageParams, type ImageParams, type MediaPath } from './mediaUrl.js';
5
6
  export { diffSchema, withFixed, type ChangeKind, type ChangeLevel, type DiffOptions, type SchemaChange } from './changes.js';
6
7
  export { requiredTypeWithTrait, tableTraitsOf, typeWithTrait, type TableTraits } from './capabilities.js';
7
8
  export { appliesTo, checkFields, fieldsFor } from './check.js';
8
9
  export { deliveredFields, emptyValues, pickValues, referencedIds, referencesOf, valueFieldsOf } from './values.js';
9
- export { imageFieldOf, initialValueOf, isEditorial, isEntryType, isListType, isSingleton, optionLabelOf, optionsOf, publishingOf, requestReferencesOf, tableOf, titleFieldOf, toneOf, typeOf, type ContentFormat, type ContentModel, type ContentType, type FieldCondition, type FieldDef, type FieldDerive, type FieldFixedBy, type CountDef, type FieldProblem, type MediaKind, type OptionTone, type Publishing, type ReferenceRules, type SelectOption } from './model.js';
10
+ export { idInputOf, imageFieldOf, initialValueOf, isEditorial, isEntryType, isListType, isSingleton, optionLabelOf, optionsOf, publishingOf, requestReferencesOf, tableOf, titleFieldOf, toneOf, typeOf, type ContentFormat, type ContentModel, type ContentType, type FieldCondition, type FieldDef, type FieldDerive, type FieldFixedBy, type CountDef, type FieldProblem, type MediaKind, type OptionTone, type Publishing, type ReferenceRules, type SelectOption } from './model.js';
10
11
  export { hasPublicPage, imageFrameOf, pagePathOf, publicPathOf, type ImageFrame, type ImageFrameDef, type PageDef, type PageEntry, type PageRule } from './page.js';
11
12
  export { schemaRulesOf, type SchemaRule } from './rules.js';
12
13
  export { parseModel } from './schema.js';
@@ -0,0 +1,41 @@
1
+ import type { CropRect } from './image.js';
2
+ /** 形式の拡張子(配信しない形式は null) */
3
+ export declare const mediaExtOf: (mimeType: string) => string | null;
4
+ /** 拡張子の形式(配信しない拡張子は null) */
5
+ export declare const mediaMimeOf: (ext: string) => string | null;
6
+ /** 配信 URL のパスの要素(寸法は画像だけ) */
7
+ export type MediaPath = Readonly<{
8
+ kind: 'image' | 'video';
9
+ tenant: string;
10
+ id: string;
11
+ ext: string;
12
+ width?: number;
13
+ height?: number;
14
+ }>;
15
+ /** 配信 URL のパス(媒体・ID の形が不正、配信しない形式なら null) */
16
+ export declare function mediaPathOf({ kind, tenant, id, mimeType, width, height }: Readonly<{
17
+ kind: 'image' | 'video';
18
+ tenant: string;
19
+ id: string;
20
+ mimeType: string;
21
+ width: number;
22
+ height: number;
23
+ }>): string | null;
24
+ /** 配信 URL のパスを解析(形が違えば null) */
25
+ export declare function parseMediaPath(pathname: string): MediaPath | null;
26
+ /** 画像の変換のクエリ(w・h は表示の画素、rect は元の画像の画素での切り抜き、auto=format はブラウザに合わせた形式) */
27
+ export type ImageParams = Readonly<{
28
+ w?: number;
29
+ h?: number;
30
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad';
31
+ rect?: CropRect;
32
+ q?: number;
33
+ fm?: 'jpg' | 'png' | 'webp' | 'avif';
34
+ auto?: 'format';
35
+ }>;
36
+ /** 幅・高さの上限(画素) */
37
+ export declare const IMAGE_SIZE_MAX = 4096;
38
+ /** URL に画像の変換のクエリを設定(ほかのクエリは残す) */
39
+ export declare function withImageParams(url: string, params: ImageParams): string;
40
+ /** クエリから画像の変換(不正な値があれば null、変換なしは空のオブジェクト) */
41
+ export declare function imageParamsOf(query: URLSearchParams): ImageParams | null;
@@ -129,6 +129,7 @@ export type Publishing = 'none' | 'draft' | 'review';
129
129
  * コンテンツタイプ
130
130
  * - counts: 被参照の件数(ページの公開中の条件・ページの公開で必須の判定に使用)
131
131
  * - og: サイトの OG 画像のパスの雛形({key} は行の値、管理画面の OG 画像のプレビュー)
132
+ * - idInput: 作成のときに管理画面で ID を入力できる(汎用の型だけ、ページのある型は常に入力可、空欄は自動)
132
133
  */
133
134
  export type ContentType = Readonly<{
134
135
  id: string;
@@ -142,6 +143,7 @@ export type ContentType = Readonly<{
142
143
  pages?: readonly PageDef[];
143
144
  counts?: readonly CountDef[];
144
145
  og?: string;
146
+ idInput?: boolean;
145
147
  }>;
146
148
  /** コンテンツスキーマ(導入先ごとに1つ) */
147
149
  export type ContentModel = Readonly<{
@@ -155,6 +157,8 @@ export type FieldProblem = Readonly<{
155
157
  }>;
156
158
  /** 汎用の表 entries に保存する型か(コアの項目がない型、コアの項目がある型は専用の表) */
157
159
  export declare const isEntryType: (type: ContentType) => boolean;
160
+ /** 作成のときに ID を入力できる型か(汎用の型で、宣言 idInput かページを持つ型) */
161
+ export declare const idInputOf: (type: ContentType) => boolean;
158
162
  /** オブジェクト形式の型か(行は表 entries の id が型の id の1行、一覧・作成の対象外) */
159
163
  export declare const isSingleton: (type: ContentType) => boolean;
160
164
  /** 公開の管理(なしは none) */
@@ -1,2 +1,2 @@
1
- export { createDeliveryClient, createDeliveryClock, DELIVERY_PATH, DeliveryError, fieldOf, limitOf, listParams, mediaOf, optionLabels, pagePath, subPath } from '../../delivery-client/src/index.js';
2
- export type { BundleResult, DeliveredAsset, Delivery, DeliveryClient, DeliveryClock, FieldValue, ImageUrl, Item, ItemOf, ListFilter, ListQuery, MediaOf, Page, Prefetched } from '../../delivery-client/src/index.js';
1
+ export { createDeliveryClient, createDeliveryClock, deliveredImageUrl, DELIVERY_PATH, DeliveryError, fieldOf, imageParamsOf, limitOf, listParams, mediaOf, optionLabels, pagePath, parseMediaPath, subPath, withImageParams } from '../../delivery-client/src/index.js';
2
+ export type { BundleResult, DeliveredAsset, Delivery, DeliveryClient, DeliveryClock, FieldValue, ImageParams, ImageUrl, Item, ItemOf, ListFilter, ListQuery, MediaOf, MediaPath, Page, Prefetched } from '../../delivery-client/src/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/maezae",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "maezae の導入先のサイト向けの SDK(配信 API の取得・本文の型・プレビューの署名)",
5
5
  "license": "UNLICENSED",
6
6
  "author": "RYUSENKEI",