@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.
- package/LICENSE +21 -0
- package/README.md +112 -2
- package/dist/bin/content.js +216 -0
- package/dist/bridge.d.mts +82 -0
- package/dist/bridge.d.mts.map +1 -0
- package/dist/bridge.d.ts +82 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +920 -0
- package/dist/bridge.mjs +915 -0
- package/dist/config-c6qktlz6.mjs +8 -0
- package/dist/config-ci2hxmas.js +13 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +300 -0
- package/dist/index.mjs +284 -0
- package/dist/preview-by8d54dt.js +68 -0
- package/dist/preview-kvotryzw.mjs +64 -0
- package/dist/prompt.d.mts +26 -0
- package/dist/prompt.d.mts.map +1 -0
- package/dist/prompt.d.ts +26 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +112 -0
- package/dist/prompt.mjs +108 -0
- package/dist/site.d.mts +34 -0
- package/dist/site.d.mts.map +1 -0
- package/dist/site.d.ts +34 -0
- package/dist/site.d.ts.map +1 -0
- package/dist/site.js +310 -0
- package/dist/site.mjs +302 -0
- package/dist/types-pa96z6po.d.ts +120 -0
- package/dist/types-pa96z6po.d.ts.map +1 -0
- package/package.json +91 -5
|
@@ -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;
|
package/dist/index.d.mts
ADDED
|
@@ -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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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;
|