@obelism/content 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ const DEFAULT_URL = 'https://content.obelism.studio';
2
+ const DELIVERY_PATH = '/api/delivery/v1';
3
+ const DEFAULT_TIMEOUT = 10_000;
4
+ // Origin only, without a trailing slash: what API paths are appended to and
5
+ // what the bridge checks messages against.
6
+ const normalizeUrl = (url)=>new URL(url).origin;
7
+
8
+ export { DEFAULT_URL as D, DEFAULT_TIMEOUT as a, DELIVERY_PATH as b, normalizeUrl as n };
@@ -0,0 +1,13 @@
1
+ const DEFAULT_URL = 'https://content.obelism.studio';
2
+ const DELIVERY_PATH = '/api/delivery/v1';
3
+ const MANAGEMENT_PATH = '/api/management/v1';
4
+ const DEFAULT_TIMEOUT = 10_000;
5
+ // Origin only, without a trailing slash: what API paths are appended to and
6
+ // what the bridge checks messages against.
7
+ const normalizeUrl = (url)=>new URL(url).origin;
8
+
9
+ exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
10
+ exports.DEFAULT_URL = DEFAULT_URL;
11
+ exports.DELIVERY_PATH = DELIVERY_PATH;
12
+ exports.MANAGEMENT_PATH = MANAGEMENT_PATH;
13
+ exports.normalizeUrl = normalizeUrl;
@@ -0,0 +1,93 @@
1
+ import { R as RichTextFormat, E as Entry, a as EntryList, M as ModelFile, A as Asset, b as EntryStub, c as RichTextNode } from './types-pa96z6po.js';
2
+ export { B as Block, C as ContentTypeDefinition, d as ContentTypeKind, e as Environment, F as FieldDefinition, f as FieldType, g as RichTextDocument, h as RichTextMark } from './types-pa96z6po.js';
3
+
4
+ type ContentClientOptions = {
5
+ token: string;
6
+ url?: string | undefined;
7
+ locale?: string | undefined;
8
+ depth?: number | undefined;
9
+ richText?: RichTextFormat | undefined;
10
+ timeout?: number | undefined;
11
+ init?: RequestInit | undefined;
12
+ fetch?: typeof fetch | undefined;
13
+ };
14
+ type RequestOptions = {
15
+ locale?: string | undefined;
16
+ depth?: number | undefined;
17
+ richText?: RichTextFormat | undefined;
18
+ init?: RequestInit | undefined;
19
+ };
20
+ type EntriesQuery = RequestOptions & {
21
+ contentType?: string | undefined;
22
+ parentPath?: string | undefined;
23
+ limit?: number | undefined;
24
+ skip?: number | undefined;
25
+ };
26
+ declare class ContentClient {
27
+ #private;
28
+ readonly url: string;
29
+ constructor({ token, url, locale, depth, richText, timeout, init, fetch: fetchImpl, }: ContentClientOptions);
30
+ getPage: <TEntry extends Entry = Entry>(path: string, options?: RequestOptions) => Promise<TEntry | null>;
31
+ getEntry: <TEntry extends Entry = Entry>(id: string, options?: RequestOptions) => Promise<TEntry | null>;
32
+ getEntries: <TEntry extends Entry = Entry>({ contentType, parentPath, limit, skip, ...options }?: EntriesQuery) => Promise<EntryList<TEntry>>;
33
+ getAllEntries<TEntry extends Entry = Entry>(query?: Omit<EntriesQuery, 'skip'>): AsyncGenerator<TEntry, void, undefined>;
34
+ getContentModel: (options?: Pick<RequestOptions, "init">) => Promise<ModelFile>;
35
+ }
36
+
37
+ declare const DEFAULT_URL = "https://content.obelism.studio";
38
+
39
+ declare const UID_ATTRIBUTE = "data-cms-uid";
40
+ declare const ENTRY_ATTRIBUTE = "data-cms-entry";
41
+ type Editable = {
42
+ 'data-cms-uid'?: string;
43
+ 'data-cms-entry'?: string;
44
+ };
45
+ declare const editable: (target: {
46
+ _uid?: string;
47
+ } | {
48
+ id: string;
49
+ fields: unknown;
50
+ } | null | undefined) => Editable;
51
+
52
+ declare class ContentApiError extends Error {
53
+ readonly status: number;
54
+ readonly url: string;
55
+ readonly body: unknown;
56
+ constructor(status: number, url: string, body: unknown);
57
+ }
58
+
59
+ type ImageTransform = {
60
+ w?: number | undefined;
61
+ h?: number | undefined;
62
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad' | undefined;
63
+ format?: 'auto' | 'avif' | 'webp' | 'jpeg' | 'png' | undefined;
64
+ q?: number | undefined;
65
+ };
66
+ declare const imageUrl: (asset: Pick<Asset, "url"> | string, transform?: ImageTransform) => string;
67
+ declare const imageSrcSet: (asset: Pick<Asset, "url"> | string, widths: number[], transform?: Omit<ImageTransform, "w">) => string;
68
+
69
+ declare const PREVIEW_PARAMS: {
70
+ readonly preview: "cms_preview";
71
+ readonly entry: "cms_entry";
72
+ readonly environment: "cms_environment";
73
+ };
74
+ type PreviewParams = {
75
+ entryId: string | null;
76
+ environment: string | null;
77
+ };
78
+ type SearchParamsInput = URL | URLSearchParams | string | Record<string, string | string[] | undefined>;
79
+ declare const readPreviewParams: (input: SearchParamsInput) => PreviewParams | null;
80
+
81
+ declare const SAFE_HREF: RegExp;
82
+ declare const escapeHtml: (value: string) => string;
83
+ type RichTextHtmlOptions = {
84
+ renderEntry?: (entry: Entry | EntryStub, node: RichTextNode) => string;
85
+ renderAsset?: (asset: Asset, node: RichTextNode) => string;
86
+ };
87
+ declare const richTextToHtml: (doc: RichTextNode | null | undefined, { renderEntry, renderAsset }?: RichTextHtmlOptions) => string;
88
+ declare const headingLevel: (node: RichTextNode) => 1 | 2 | 3 | 4 | 5 | 6;
89
+ declare const richTextToPlainText: (doc: RichTextNode | null | undefined) => string;
90
+
91
+ export { Asset, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, ModelFile, PREVIEW_PARAMS, RichTextFormat, RichTextNode, SAFE_HREF, UID_ATTRIBUTE, editable, escapeHtml, headingLevel, imageSrcSet, imageUrl, readPreviewParams, richTextToHtml, richTextToPlainText };
92
+ export type { ContentClientOptions, EntriesQuery, ImageTransform, PreviewParams, RequestOptions, RichTextHtmlOptions };
93
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/preview.ts","../src/richText.ts"],"mappings":";;;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA;AACA,eAAe,cAAc;AAC7B;AACA,WAAW,WAAW;AACtB,mBAAmB,KAAK;AACxB;AACA,KAAK,cAAc;AACnB;AACA;AACA,eAAe,cAAc;AAC7B,WAAW,WAAW;AACtB;AACA,KAAK,YAAY,GAAG,cAAc;AAClC;AACA;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B;AACA;AACA,2FAA2F,oBAAoB;AAC/G,6BAA6B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO;AAC9F,8BAA8B,KAAK,GAAG,KAAK,wBAAwB,cAAc,KAAK,OAAO;AAC7F,gCAAgC,KAAK,GAAG,KAAK,yDAAyD,YAAY,KAAK,OAAO,CAAC,SAAS;AACxI,iCAAiC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,YAAY,cAAc;AACnG,gCAAgC,IAAI,CAAC,cAAc,cAAc,OAAO,CAAC,SAAS;AAClF;;AChCA,cAAc,WAAW;;ACAzB,cAAc,aAAa;AAC3B,cAAc,eAAe;AAC7B,KAAK,QAAQ;AACb;AACA;AACA;AACA,cAAc,QAAQ;AACtB;AACA;AACA;AACA;AACA,yBAAyB,QAAQ;;ACXjC,cAAc,eAAe,SAAS,KAAK;AAC3C;AACA;AACA;AACA;AACA;;ACJA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,QAAQ,UAAU,IAAI,CAAC,KAAK,+BAA+B,cAAc;AACvF,cAAc,WAAW,UAAU,IAAI,CAAC,KAAK,iDAAiD,IAAI,CAAC,cAAc;;ACTjH,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA,KAAK,iBAAiB,GAAG,GAAG,GAAG,eAAe,YAAY,MAAM;AAChE,cAAc,iBAAiB,UAAU,iBAAiB,KAAK,aAAa;;ACT5E,cAAc,SAAS,EAAE,MAAM;AAC/B,cAAc,UAAU;AACxB,KAAK,mBAAmB;AACxB,0BAA0B,KAAK,GAAG,SAAS,QAAQ,YAAY;AAC/D,0BAA0B,KAAK,QAAQ,YAAY;AACnD;AACA,cAAc,cAAc,QAAQ,YAAY,oDAAoD,mBAAmB;AACvH,cAAc,YAAY,SAAS,YAAY;AAC/C,cAAc,mBAAmB,QAAQ,YAAY;;","names":[]}
@@ -0,0 +1,93 @@
1
+ import { R as RichTextFormat, E as Entry, a as EntryList, M as ModelFile, A as Asset, b as EntryStub, c as RichTextNode } from './types-pa96z6po.js';
2
+ export { B as Block, C as ContentTypeDefinition, d as ContentTypeKind, e as Environment, F as FieldDefinition, f as FieldType, g as RichTextDocument, h as RichTextMark } from './types-pa96z6po.js';
3
+
4
+ type ContentClientOptions = {
5
+ token: string;
6
+ url?: string | undefined;
7
+ locale?: string | undefined;
8
+ depth?: number | undefined;
9
+ richText?: RichTextFormat | undefined;
10
+ timeout?: number | undefined;
11
+ init?: RequestInit | undefined;
12
+ fetch?: typeof fetch | undefined;
13
+ };
14
+ type RequestOptions = {
15
+ locale?: string | undefined;
16
+ depth?: number | undefined;
17
+ richText?: RichTextFormat | undefined;
18
+ init?: RequestInit | undefined;
19
+ };
20
+ type EntriesQuery = RequestOptions & {
21
+ contentType?: string | undefined;
22
+ parentPath?: string | undefined;
23
+ limit?: number | undefined;
24
+ skip?: number | undefined;
25
+ };
26
+ declare class ContentClient {
27
+ #private;
28
+ readonly url: string;
29
+ constructor({ token, url, locale, depth, richText, timeout, init, fetch: fetchImpl, }: ContentClientOptions);
30
+ getPage: <TEntry extends Entry = Entry>(path: string, options?: RequestOptions) => Promise<TEntry | null>;
31
+ getEntry: <TEntry extends Entry = Entry>(id: string, options?: RequestOptions) => Promise<TEntry | null>;
32
+ getEntries: <TEntry extends Entry = Entry>({ contentType, parentPath, limit, skip, ...options }?: EntriesQuery) => Promise<EntryList<TEntry>>;
33
+ getAllEntries<TEntry extends Entry = Entry>(query?: Omit<EntriesQuery, 'skip'>): AsyncGenerator<TEntry, void, undefined>;
34
+ getContentModel: (options?: Pick<RequestOptions, "init">) => Promise<ModelFile>;
35
+ }
36
+
37
+ declare const DEFAULT_URL = "https://content.obelism.studio";
38
+
39
+ declare const UID_ATTRIBUTE = "data-cms-uid";
40
+ declare const ENTRY_ATTRIBUTE = "data-cms-entry";
41
+ type Editable = {
42
+ 'data-cms-uid'?: string;
43
+ 'data-cms-entry'?: string;
44
+ };
45
+ declare const editable: (target: {
46
+ _uid?: string;
47
+ } | {
48
+ id: string;
49
+ fields: unknown;
50
+ } | null | undefined) => Editable;
51
+
52
+ declare class ContentApiError extends Error {
53
+ readonly status: number;
54
+ readonly url: string;
55
+ readonly body: unknown;
56
+ constructor(status: number, url: string, body: unknown);
57
+ }
58
+
59
+ type ImageTransform = {
60
+ w?: number | undefined;
61
+ h?: number | undefined;
62
+ fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad' | undefined;
63
+ format?: 'auto' | 'avif' | 'webp' | 'jpeg' | 'png' | undefined;
64
+ q?: number | undefined;
65
+ };
66
+ declare const imageUrl: (asset: Pick<Asset, "url"> | string, transform?: ImageTransform) => string;
67
+ declare const imageSrcSet: (asset: Pick<Asset, "url"> | string, widths: number[], transform?: Omit<ImageTransform, "w">) => string;
68
+
69
+ declare const PREVIEW_PARAMS: {
70
+ readonly preview: "cms_preview";
71
+ readonly entry: "cms_entry";
72
+ readonly environment: "cms_environment";
73
+ };
74
+ type PreviewParams = {
75
+ entryId: string | null;
76
+ environment: string | null;
77
+ };
78
+ type SearchParamsInput = URL | URLSearchParams | string | Record<string, string | string[] | undefined>;
79
+ declare const readPreviewParams: (input: SearchParamsInput) => PreviewParams | null;
80
+
81
+ declare const SAFE_HREF: RegExp;
82
+ declare const escapeHtml: (value: string) => string;
83
+ type RichTextHtmlOptions = {
84
+ renderEntry?: (entry: Entry | EntryStub, node: RichTextNode) => string;
85
+ renderAsset?: (asset: Asset, node: RichTextNode) => string;
86
+ };
87
+ declare const richTextToHtml: (doc: RichTextNode | null | undefined, { renderEntry, renderAsset }?: RichTextHtmlOptions) => string;
88
+ declare const headingLevel: (node: RichTextNode) => 1 | 2 | 3 | 4 | 5 | 6;
89
+ declare const richTextToPlainText: (doc: RichTextNode | null | undefined) => string;
90
+
91
+ export { Asset, ContentApiError, ContentClient, DEFAULT_URL, ENTRY_ATTRIBUTE, Entry, EntryList, EntryStub, ModelFile, PREVIEW_PARAMS, RichTextFormat, RichTextNode, SAFE_HREF, UID_ATTRIBUTE, editable, escapeHtml, headingLevel, imageSrcSet, imageUrl, readPreviewParams, richTextToHtml, richTextToPlainText };
92
+ export type { ContentClientOptions, EntriesQuery, ImageTransform, PreviewParams, RequestOptions, RichTextHtmlOptions };
93
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sources":["../src/client.ts","../src/config.ts","../src/editable.ts","../src/errors.ts","../src/images.ts","../src/preview.ts","../src/richText.ts"],"mappings":";;;AACA,KAAK,oBAAoB;AACzB;AACA;AACA;AACA;AACA,eAAe,cAAc;AAC7B;AACA,WAAW,WAAW;AACtB,mBAAmB,KAAK;AACxB;AACA,KAAK,cAAc;AACnB;AACA;AACA,eAAe,cAAc;AAC7B,WAAW,WAAW;AACtB;AACA,KAAK,YAAY,GAAG,cAAc;AAClC;AACA;AACA;AACA;AACA;AACA,cAAc,aAAa;AAC3B;AACA;AACA,2FAA2F,oBAAoB;AAC/G,6BAA6B,KAAK,GAAG,KAAK,0BAA0B,cAAc,KAAK,OAAO;AAC9F,8BAA8B,KAAK,GAAG,KAAK,wBAAwB,cAAc,KAAK,OAAO;AAC7F,gCAAgC,KAAK,GAAG,KAAK,yDAAyD,YAAY,KAAK,OAAO,CAAC,SAAS;AACxI,iCAAiC,KAAK,GAAG,KAAK,UAAU,IAAI,CAAC,YAAY,YAAY,cAAc;AACnG,gCAAgC,IAAI,CAAC,cAAc,cAAc,OAAO,CAAC,SAAS;AAClF;;AChCA,cAAc,WAAW;;ACAzB,cAAc,aAAa;AAC3B,cAAc,eAAe;AAC7B,KAAK,QAAQ;AACb;AACA;AACA;AACA,cAAc,QAAQ;AACtB;AACA;AACA;AACA;AACA,yBAAyB,QAAQ;;ACXjC,cAAc,eAAe,SAAS,KAAK;AAC3C;AACA;AACA;AACA;AACA;;ACJA,KAAK,cAAc;AACnB;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,QAAQ,UAAU,IAAI,CAAC,KAAK,+BAA+B,cAAc;AACvF,cAAc,WAAW,UAAU,IAAI,CAAC,KAAK,iDAAiD,IAAI,CAAC,cAAc;;ACTjH,cAAc,cAAc;AAC5B;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA,KAAK,iBAAiB,GAAG,GAAG,GAAG,eAAe,YAAY,MAAM;AAChE,cAAc,iBAAiB,UAAU,iBAAiB,KAAK,aAAa;;ACT5E,cAAc,SAAS,EAAE,MAAM;AAC/B,cAAc,UAAU;AACxB,KAAK,mBAAmB;AACxB,0BAA0B,KAAK,GAAG,SAAS,QAAQ,YAAY;AAC/D,0BAA0B,KAAK,QAAQ,YAAY;AACnD;AACA,cAAc,cAAc,QAAQ,YAAY,oDAAoD,mBAAmB;AACvH,cAAc,YAAY,SAAS,YAAY;AAC/C,cAAc,mBAAmB,QAAQ,YAAY;;","names":[]}
package/dist/index.js ADDED
@@ -0,0 +1,300 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var config = require('./config-ci2hxmas.js');
4
+ var preview = require('./preview-by8d54dt.js');
5
+
6
+ // A response from the API that wasn't a success. `message` is the API's own
7
+ // `error` text when it sent one.
8
+ class ContentApiError extends Error {
9
+ constructor(status, url, body){
10
+ const apiMessage = typeof body === 'object' && body !== null && typeof body.error === 'string' ? body.error : null;
11
+ super(apiMessage ?? `Obelism Content answered ${status} for ${url}`);
12
+ this.name = 'ContentApiError';
13
+ this.status = status;
14
+ this.url = url;
15
+ this.body = body;
16
+ }
17
+ }
18
+
19
+ const trimSlashes = (path)=>path.replace(/^\/+|\/+$/g, '');
20
+ // Reads one space environment's content through the delivery API. The token
21
+ // decides the space, the environment and whether drafts are visible.
22
+ //
23
+ // const content = new ContentClient({ token: process.env.CONTENT_TOKEN })
24
+ // const page = await content.getPage('blog/hello', { locale: 'nl' })
25
+ class ContentClient {
26
+ #token;
27
+ #defaults;
28
+ #timeout;
29
+ #init;
30
+ #fetch;
31
+ constructor({ token, url, locale, depth, richText, timeout, init, fetch: fetchImpl }){
32
+ // The page at a path (`blog/hello`), or null when there is none. A site's
33
+ // catch-all route asks for this.
34
+ this.getPage = async (path, options = {})=>{
35
+ const trimmed = trimSlashes(path);
36
+ if (!trimmed) throw new Error('getPage needs a path');
37
+ const result = await this.#request('/pages', {
38
+ path: trimmed
39
+ }, options, {
40
+ notFound: true
41
+ });
42
+ return result?.entry ?? null;
43
+ };
44
+ // An entry by id, or null when there is none (or it isn't published, for a
45
+ // delivery token).
46
+ this.getEntry = async (id, options = {})=>{
47
+ const result = await this.#request(`/entries/${encodeURIComponent(id)}`, {}, options, {
48
+ notFound: true
49
+ });
50
+ return result?.entry ?? null;
51
+ };
52
+ // One page of entries, ordered as in the space's tree.
53
+ this.getEntries = ({ contentType, parentPath, limit, skip, ...options } = {})=>this.#request('/entries', {
54
+ contentType,
55
+ parentPath: parentPath === undefined ? undefined : trimSlashes(parentPath),
56
+ limit,
57
+ skip
58
+ }, options);
59
+ // The environment's content model, e.g. to generate types in a build.
60
+ this.getContentModel = (options = {})=>this.#request('/content-model', {}, options, {
61
+ defaults: false
62
+ });
63
+ this.url = config.normalizeUrl(url || config.DEFAULT_URL);
64
+ this.#token = token;
65
+ this.#defaults = {
66
+ locale,
67
+ depth,
68
+ richText
69
+ };
70
+ this.#timeout = timeout ?? config.DEFAULT_TIMEOUT;
71
+ this.#init = init;
72
+ this.#fetch = fetchImpl ?? ((...args)=>fetch(...args));
73
+ }
74
+ // Every entry matching the query, fetching pages of `limit` (default 100)
75
+ // as it goes.
76
+ //
77
+ // for await (const post of content.getAllEntries({ contentType: 'post' }))
78
+ async *getAllEntries(query = {}) {
79
+ const limit = query.limit ?? 100;
80
+ let skip = 0;
81
+ while(true){
82
+ const page = await this.getEntries({
83
+ ...query,
84
+ limit,
85
+ skip
86
+ });
87
+ yield* page.items;
88
+ skip += page.items.length;
89
+ if (page.items.length === 0 || skip >= page.total) return;
90
+ }
91
+ }
92
+ // null for a 404 when `notFound` is set; throws ContentApiError otherwise.
93
+ async #request(path, query, { init, ...options }, { notFound = false, defaults = true } = {}) {
94
+ // Checked here rather than in the constructor, so a module creating a
95
+ // client still loads (and a build still runs) without the variable.
96
+ if (!this.#token) throw new Error('ContentClient needs a token');
97
+ const { locale, depth, richText } = {
98
+ ...defaults && this.#defaults,
99
+ ...withoutUndefined(options)
100
+ };
101
+ const params = new URLSearchParams();
102
+ const values = {
103
+ ...query,
104
+ locale,
105
+ depth,
106
+ richtext: richText
107
+ };
108
+ for (const [key, value] of Object.entries(values)){
109
+ if (value !== undefined) params.set(key, String(value));
110
+ }
111
+ const search = params.size > 0 ? `?${params}` : '';
112
+ const url = `${this.url}${config.DELIVERY_PATH}${path}${search}`;
113
+ const merged = {
114
+ ...this.#init,
115
+ ...init
116
+ };
117
+ const timeout = AbortSignal.timeout(this.#timeout);
118
+ const response = await this.#fetch(url, {
119
+ ...merged,
120
+ headers: {
121
+ ...headersToObject(merged.headers),
122
+ Accept: 'application/json',
123
+ Authorization: `Bearer ${this.#token}`
124
+ },
125
+ signal: merged.signal ? AbortSignal.any([
126
+ merged.signal,
127
+ timeout
128
+ ]) : timeout
129
+ });
130
+ if (notFound && response.status === 404) return null;
131
+ const text = await response.text();
132
+ let body = text;
133
+ try {
134
+ body = JSON.parse(text);
135
+ } catch {
136
+ // Not JSON: keep the text for the error.
137
+ }
138
+ if (!response.ok) throw new ContentApiError(response.status, url, body);
139
+ return body;
140
+ }
141
+ }
142
+ const withoutUndefined = (value)=>Object.fromEntries(Object.entries(value).filter(([, entry])=>entry !== undefined));
143
+ const headersToObject = (headers)=>headers ? Object.fromEntries(new Headers(headers)) : {};
144
+
145
+ const KEYS = [
146
+ 'w',
147
+ 'h',
148
+ 'fit',
149
+ 'format',
150
+ 'q'
151
+ ];
152
+ // An asset's URL with a resize / re-encode query. PNG, JPEG, GIF, WebP and
153
+ // AVIF are transformed; other files ignore the query.
154
+ //
155
+ // imageUrl(asset, { w: 800 })
156
+ const imageUrl = (asset, transform = {})=>{
157
+ const url = new URL(typeof asset === 'string' ? asset : asset.url);
158
+ for (const key of KEYS){
159
+ const value = transform[key];
160
+ if (value !== undefined) url.searchParams.set(key, String(value));
161
+ }
162
+ return url.toString();
163
+ };
164
+ // A `srcset` with one candidate per width.
165
+ //
166
+ // <img src={imageUrl(asset, { w: 960 })}
167
+ // srcSet={imageSrcSet(asset, [480, 960, 1440])}
168
+ // sizes="(min-width: 60rem) 50vw, 100vw" />
169
+ const imageSrcSet = (asset, widths, transform = {})=>widths.map((w)=>`${imageUrl(asset, {
170
+ ...transform,
171
+ w
172
+ })} ${w}w`).join(', ');
173
+
174
+ // Links are only rendered for these; anything else (`javascript:` …) keeps its
175
+ // text without the link. The same rule the CMS validates with.
176
+ const SAFE_HREF = /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i;
177
+ const escapeHtml = (value)=>value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
178
+ const MARK_TAGS = {
179
+ bold: 'strong',
180
+ italic: 'em',
181
+ underline: 'u',
182
+ strike: 's',
183
+ code: 'code'
184
+ };
185
+ const defaultRenderAsset = (asset)=>{
186
+ const url = escapeHtml(asset.url);
187
+ if (asset.mimeType.startsWith('image/')) {
188
+ const alt = typeof asset.alt === 'string' ? asset.alt : '';
189
+ const size = asset.width && asset.height ? ` width="${asset.width}" height="${asset.height}"` : '';
190
+ return `<figure><img src="${url}" alt="${escapeHtml(alt)}"${size}></figure>`;
191
+ }
192
+ return `<p><a href="${url}">${escapeHtml(asset.filename)}</a></p>`;
193
+ };
194
+ // Rich text JSON (what the delivery API answers by default) to plain, safe
195
+ // HTML — the same HTML the API renders with `richText: 'html'`, except that
196
+ // embeds are up to you.
197
+ const richTextToHtml = (doc, { renderEntry, renderAsset = defaultRenderAsset } = {})=>{
198
+ if (!doc) return '';
199
+ const marks = (node, html)=>(node.marks ?? []).reduceRight((inner, mark)=>{
200
+ if (mark.type === 'link') {
201
+ const href = String(mark.attrs?.href ?? '');
202
+ if (!SAFE_HREF.test(href)) return inner;
203
+ const target = mark.attrs?.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
204
+ const title = mark.attrs?.title ? ` title="${escapeHtml(String(mark.attrs.title))}"` : '';
205
+ return `<a href="${escapeHtml(href)}"${target}${title}>${inner}</a>`;
206
+ }
207
+ const tag = MARK_TAGS[mark.type];
208
+ return tag ? `<${tag}>${inner}</${tag}>` : inner;
209
+ }, html);
210
+ const render = (node)=>{
211
+ const children = ()=>(node.content ?? []).map(render).join('');
212
+ switch(node.type){
213
+ case 'doc':
214
+ return children();
215
+ case 'text':
216
+ return marks(node, escapeHtml(node.text ?? ''));
217
+ case 'hardBreak':
218
+ return '<br>';
219
+ case 'paragraph':
220
+ return `<p>${children()}</p>`;
221
+ case 'heading':
222
+ {
223
+ const tag = `h${headingLevel(node)}`;
224
+ return `<${tag}>${children()}</${tag}>`;
225
+ }
226
+ case 'blockquote':
227
+ return `<blockquote>${children()}</blockquote>`;
228
+ case 'bulletList':
229
+ return `<ul>${children()}</ul>`;
230
+ case 'orderedList':
231
+ {
232
+ const start = Number(node.attrs?.start ?? 1);
233
+ return `<ol${start !== 1 ? ` start="${start}"` : ''}>${children()}</ol>`;
234
+ }
235
+ case 'listItem':
236
+ return `<li>${children()}</li>`;
237
+ case 'codeBlock':
238
+ {
239
+ const language = node.attrs?.language;
240
+ const className = language ? ` class="language-${escapeHtml(String(language))}"` : '';
241
+ return `<pre><code${className}>${children()}</code></pre>`;
242
+ }
243
+ case 'horizontalRule':
244
+ return '<hr>';
245
+ case 'embeddedEntry':
246
+ {
247
+ const entry = node.attrs?.entry;
248
+ return entry && renderEntry ? renderEntry(entry, node) : '';
249
+ }
250
+ case 'embeddedAsset':
251
+ {
252
+ const asset = node.attrs?.asset;
253
+ return asset ? renderAsset(asset, node) : '';
254
+ }
255
+ default:
256
+ return '';
257
+ }
258
+ };
259
+ return render(doc);
260
+ };
261
+ const headingLevel = (node)=>{
262
+ const level = Number(node.attrs?.level);
263
+ return level >= 1 && level <= 6 ? level : 2;
264
+ };
265
+ // The text of a document without any markup, e.g. for a meta description.
266
+ const richTextToPlainText = (doc)=>{
267
+ if (!doc) return '';
268
+ const BLOCKS = new Set([
269
+ 'paragraph',
270
+ 'heading',
271
+ 'blockquote',
272
+ 'listItem',
273
+ 'codeBlock'
274
+ ]);
275
+ const parts = [];
276
+ const walk = (node)=>{
277
+ if (node.type === 'text') parts.push(node.text ?? '');
278
+ else if (node.type === 'hardBreak') parts.push('\n');
279
+ node.content?.forEach(walk);
280
+ if (BLOCKS.has(node.type)) parts.push('\n');
281
+ };
282
+ walk(doc);
283
+ return parts.join('').replace(/\n+/g, '\n').trim();
284
+ };
285
+
286
+ exports.DEFAULT_URL = config.DEFAULT_URL;
287
+ exports.ENTRY_ATTRIBUTE = preview.ENTRY_ATTRIBUTE;
288
+ exports.PREVIEW_PARAMS = preview.PREVIEW_PARAMS;
289
+ exports.UID_ATTRIBUTE = preview.UID_ATTRIBUTE;
290
+ exports.editable = preview.editable;
291
+ exports.readPreviewParams = preview.readPreviewParams;
292
+ exports.ContentApiError = ContentApiError;
293
+ exports.ContentClient = ContentClient;
294
+ exports.SAFE_HREF = SAFE_HREF;
295
+ exports.escapeHtml = escapeHtml;
296
+ exports.headingLevel = headingLevel;
297
+ exports.imageSrcSet = imageSrcSet;
298
+ exports.imageUrl = imageUrl;
299
+ exports.richTextToHtml = richTextToHtml;
300
+ exports.richTextToPlainText = richTextToPlainText;