@monoflake/sdk 0.0.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 +22 -0
- package/dist/artifacts/src/anchors.d.ts +34 -0
- package/dist/artifacts/src/anchors.js +64 -0
- package/dist/artifacts/src/api.d.ts +14 -0
- package/dist/artifacts/src/api.js +20 -0
- package/dist/artifacts/src/batch.d.ts +105 -0
- package/dist/artifacts/src/batch.js +81 -0
- package/dist/artifacts/src/engagement.d.ts +61 -0
- package/dist/artifacts/src/engagement.js +67 -0
- package/dist/artifacts/src/feed.d.ts +42 -0
- package/dist/artifacts/src/feed.js +89 -0
- package/dist/artifacts/src/index.d.ts +4224 -0
- package/dist/artifacts/src/index.js +219 -0
- package/dist/artifacts/src/picture.d.ts +116 -0
- package/dist/artifacts/src/picture.js +161 -0
- package/dist/artifacts/src/resource.d.ts +1391 -0
- package/dist/artifacts/src/resource.js +477 -0
- package/dist/artifacts/src/schema.d.ts +5 -0
- package/dist/artifacts/src/schema.js +18 -0
- package/dist/artifacts/src/types.d.ts +396 -0
- package/dist/artifacts/src/types.js +0 -0
- package/dist/cache/src/index.d.ts +67 -0
- package/dist/cache/src/index.js +58 -0
- package/dist/imgsrc/src/index.d.ts +16 -0
- package/dist/imgsrc/src/index.js +89 -0
- package/dist/limits/src/bucket.d.ts +28 -0
- package/dist/limits/src/bucket.js +23 -0
- package/dist/limits/src/index.d.ts +29 -0
- package/dist/limits/src/index.js +62 -0
- package/dist/limits/src/key.d.ts +37 -0
- package/dist/limits/src/key.js +64 -0
- package/dist/robots/src/index.d.ts +98 -0
- package/dist/robots/src/index.js +196 -0
- package/dist/security/src/agents.d.ts +10 -0
- package/dist/security/src/agents.js +95 -0
- package/dist/security/src/index.d.ts +12 -0
- package/dist/security/src/index.js +37 -0
- package/dist/src/index.d.ts +218 -0
- package/dist/src/index.js +219 -0
- package/dist/store/src/index.d.ts +92 -0
- package/dist/store/src/index.js +264 -0
- package/dist/symlink/src/index.d.ts +24 -0
- package/dist/symlink/src/index.js +89 -0
- package/package.json +85 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { HASH_PATTERN, byLocale, hash } from "./schema.js";
|
|
2
|
+
import { unwrap, unwrapAs } from "./api.js";
|
|
3
|
+
import { blockFeedHtml, escapeHtml, feedHtml } from "./feed.js";
|
|
4
|
+
import { CancelAnswerSchema, LikeAnswerSchema, LikedAnswerSchema, NewsletterAnswerSchema, ReadAnswerSchema, ReadsAnswerSchema, StatsAnswerSchema } from "./engagement.js";
|
|
5
|
+
import { ArticlesRequestSchema, BatchRequestSchema, RESOURCES_PER_QUESTION, ReadsRequestSchema, ResourcesRequestSchema, resourceQuestions } from "./batch.js";
|
|
6
|
+
import { ArticleLayerSchema, CANONICAL_PATTERN, ClipLayerSchema, DocumentLayerSchema, FrameLayerSchema, IconLayerSchema, ImageLayerSchema, ImageVariantSchema, LAYERS, MarkLayerSchema, MediaLayerSchema, NoticeLayerSchema, PhotoLayerSchema, RESOURCE_PATTERN, RESOURCE_VERSION, ResourceSchema, SCALABLE_MIMES, ScreenshotLayerSchema, TONES, VideoLayerSchema, expandCanonical, isLayerName, isResourceId, parseResource, parseType, requireSegment } from "./resource.js";
|
|
7
|
+
import { ICON_EXTENSION, VARIANT_EXTENSION, aspect, best, enough, height, namedResources, objectUrl, pictured, resolution, rungs, scalable, toned, width } from "./picture.js";
|
|
8
|
+
import * as v from "valibot";
|
|
9
|
+
import { LOCALE_CODES } from "@canmi/me/locales";
|
|
10
|
+
//#region artifacts/src/index.ts
|
|
11
|
+
/**
|
|
12
|
+
* The shape every published object declares.
|
|
13
|
+
*
|
|
14
|
+
* Bumped when a producer and a consumer can no longer read each other. They deploy separately
|
|
15
|
+
* now, so this is the only thing that tells a Worker it is holding bytes it does not understand.
|
|
16
|
+
*/
|
|
17
|
+
const ARTIFACT_VERSION = 2;
|
|
18
|
+
const ARTIFACT_TYPES = [
|
|
19
|
+
"content",
|
|
20
|
+
"page",
|
|
21
|
+
"markdown"
|
|
22
|
+
];
|
|
23
|
+
/** What each corpus artifact is spelled with, in the address and in the bucket alike. */
|
|
24
|
+
const ARTIFACT_EXTENSIONS = {
|
|
25
|
+
content: "json",
|
|
26
|
+
page: "json",
|
|
27
|
+
markdown: "md"
|
|
28
|
+
};
|
|
29
|
+
const EXTENSION = ARTIFACT_EXTENSIONS;
|
|
30
|
+
/** The one object in the bucket whose name outlives its bytes. */
|
|
31
|
+
const ROOT_KEY = "state/index.json";
|
|
32
|
+
/**
|
|
33
|
+
* The address a published object is served at.
|
|
34
|
+
*
|
|
35
|
+
* `object/{hash}.{ext}` -- not where it is stored, and no longer naming what kind of thing it is
|
|
36
|
+
* either. The type survives as the one thing that decides the extension: a caller asks for a
|
|
37
|
+
* `content` object and the table below says that is spelled `json`.
|
|
38
|
+
*/
|
|
39
|
+
function artifactAddress(type, hash) {
|
|
40
|
+
return `object/${hash}.${EXTENSION[type]}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Where an object lives: its content id, fanned out, and nothing else.
|
|
44
|
+
*
|
|
45
|
+
* **The bucket's layout is not the CDN's URL.** A URL says `/object/{cid}.{ext}`; the bucket stores
|
|
46
|
+
* `{ab}/{cd}/{cid}.{ext}`, because the id already identifies it and a type directory would be a
|
|
47
|
+
* second place to write the same fact. The fan-out is for listing, and the extension is kept so a
|
|
48
|
+
* bucket downloaded whole is still files that open. See web's spec/architecture/data.md, "The
|
|
49
|
+
* bucket stores content ids, and so does the address".
|
|
50
|
+
*/
|
|
51
|
+
function storageKey(cid, extension) {
|
|
52
|
+
return `${cid.slice(0, 2)}/${cid.slice(2, 4)}/${cid}.${extension}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Where a resource's record lives, which is in the other bucket entirely.
|
|
56
|
+
*
|
|
57
|
+
* **Keyed by the rid and never by a cid.** A key ending in a hash reads as content-addressed,
|
|
58
|
+
* and the cache policy read the shape and granted a year -- to a record rewritten whenever its
|
|
59
|
+
* asset is re-derived. That is the confusion spec/architecture/resource.md exists to end. See
|
|
60
|
+
* also web's spec/architecture/data.md, "One bucket holds records and the other holds bytes".
|
|
61
|
+
*/
|
|
62
|
+
function recordKey(resource) {
|
|
63
|
+
return `meta/${resource}.json`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The classifier the cache policy is derived from, rather than a table of key prefixes.
|
|
67
|
+
*
|
|
68
|
+
* A key that parses is content-addressed and may be held forever; one that does not is not, and
|
|
69
|
+
* gets the short life. That is the whole rule, so a new type costs no cache decision.
|
|
70
|
+
*/
|
|
71
|
+
function parseArtifactKey(key) {
|
|
72
|
+
const match = /^([a-z]+)\/([0-9a-f]+)\.([a-z0-9]+)$/.exec(key);
|
|
73
|
+
if (!match) return void 0;
|
|
74
|
+
const [, type, hash, ext] = match;
|
|
75
|
+
if (!type || !hash || !ext) return void 0;
|
|
76
|
+
if (!HASH_PATTERN.test(hash)) return void 0;
|
|
77
|
+
if (!ARTIFACT_TYPES.includes(type)) return void 0;
|
|
78
|
+
if (EXTENSION[type] !== ext) return void 0;
|
|
79
|
+
return {
|
|
80
|
+
type,
|
|
81
|
+
hash,
|
|
82
|
+
ext
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* What an article says it is: the copy a page renders in its own right.
|
|
87
|
+
*
|
|
88
|
+
* `short` is the pair a phone card shows where the row clips, and it is a pair rather than two
|
|
89
|
+
* keys because it is one decision -- see web's spec/i18n/prose.md.
|
|
90
|
+
*/
|
|
91
|
+
const ViewMetaSchema = v.object({
|
|
92
|
+
title: v.string(),
|
|
93
|
+
subtitle: v.string(),
|
|
94
|
+
description: v.string(),
|
|
95
|
+
short: v.object({
|
|
96
|
+
title: v.string(),
|
|
97
|
+
subtitle: v.string()
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* One locale's view, grouped by what each group answers rather than laid out flat.
|
|
102
|
+
*
|
|
103
|
+
* Flat, this was thirteen keys where `title` sat beside `content` and `words` beside
|
|
104
|
+
* `language_tag`, and a reader had to know the whole list to find anything. Each group below
|
|
105
|
+
* answers one question: which objects carry it, which language it is, what it says, when it was
|
|
106
|
+
* written, how big it is, and what a listing shows of it.
|
|
107
|
+
*/
|
|
108
|
+
const RootViewSchema = v.object({
|
|
109
|
+
objects: v.object({
|
|
110
|
+
content: hash,
|
|
111
|
+
card: v.optional(hash)
|
|
112
|
+
}),
|
|
113
|
+
locale: v.object({
|
|
114
|
+
language_tag: v.string(),
|
|
115
|
+
canonical: v.string(),
|
|
116
|
+
/** False when this locale is showing the source article as a safe fallback. */
|
|
117
|
+
translated: v.boolean()
|
|
118
|
+
}),
|
|
119
|
+
meta: ViewMetaSchema,
|
|
120
|
+
/**
|
|
121
|
+
* When the file came into being, when the article went public, and when it last changed.
|
|
122
|
+
* `published` is the author's to edit and the one a reader is shown.
|
|
123
|
+
*
|
|
124
|
+
* **A `v.object` drops a key it does not declare rather than refusing it**, so a producer
|
|
125
|
+
* that writes a fourth date without adding it here hands every consumer `undefined` and
|
|
126
|
+
* nothing reports it. `index.test.ts` pins this key set for that reason.
|
|
127
|
+
*/
|
|
128
|
+
dates: v.object({
|
|
129
|
+
created: v.string(),
|
|
130
|
+
published: v.string(),
|
|
131
|
+
lastmod: v.string()
|
|
132
|
+
}),
|
|
133
|
+
metrics: v.object({ words: v.number() }),
|
|
134
|
+
preview: v.object({ paragraphs: v.array(v.string()) })
|
|
135
|
+
});
|
|
136
|
+
/**
|
|
137
|
+
* The codes an alternate may carry, which are narrower than the locales.
|
|
138
|
+
*
|
|
139
|
+
* `mw` is the source and is never an alternate of itself; `x-default` is the bare URL. Parsed as
|
|
140
|
+
* the picklist rather than as a string so the root validates into `Alternate` -- the type every
|
|
141
|
+
* consumer of these already assumes, and which a looser parse let the root quietly contradict.
|
|
142
|
+
*/
|
|
143
|
+
const alternateCode = v.picklist([...LOCALE_CODES.filter((code) => code !== "mw"), "x-default"]);
|
|
144
|
+
const RootArticleSchema = v.object({
|
|
145
|
+
/** The identity: unique across the corpus, and what every question asks with. */
|
|
146
|
+
slug: v.string(),
|
|
147
|
+
/** The address: where it currently lives, which is the only half that can change. */
|
|
148
|
+
path: v.string(),
|
|
149
|
+
url: v.string(),
|
|
150
|
+
markdown: hash,
|
|
151
|
+
alternates: v.array(v.object({
|
|
152
|
+
code: alternateCode,
|
|
153
|
+
language_tag: v.string(),
|
|
154
|
+
href: v.string()
|
|
155
|
+
})),
|
|
156
|
+
canonical_urls: v.array(v.string()),
|
|
157
|
+
views: byLocale(RootViewSchema)
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* A fixed name, and the object it currently means.
|
|
161
|
+
*
|
|
162
|
+
* The site's own marks -- its icons, its BIMI mark -- are published like anything else, addressed
|
|
163
|
+
* by their content and cached for a year. What a reader or a mail client asks for is the name, so
|
|
164
|
+
* something has to turn one into the other, and this is what it reads. See
|
|
165
|
+
* spec/architecture/delivery.md, "A name is resolved, never stored".
|
|
166
|
+
*/
|
|
167
|
+
const RootAssetSchema = v.object({
|
|
168
|
+
cid: hash,
|
|
169
|
+
extension: v.string()
|
|
170
|
+
});
|
|
171
|
+
const RootSchema = v.object({
|
|
172
|
+
version: v.literal(2),
|
|
173
|
+
generated: v.string(),
|
|
174
|
+
/** Fixed names the alias layer resolves, keyed by the name as it is asked for. */
|
|
175
|
+
assets: v.record(v.string(), RootAssetSchema),
|
|
176
|
+
articles: v.array(RootArticleSchema),
|
|
177
|
+
pages: v.record(v.string(), v.object({
|
|
178
|
+
markdown: hash,
|
|
179
|
+
views: byLocale(v.object({
|
|
180
|
+
content: hash,
|
|
181
|
+
card: v.optional(hash)
|
|
182
|
+
}))
|
|
183
|
+
}))
|
|
184
|
+
});
|
|
185
|
+
/**
|
|
186
|
+
* What a consumer checks before trusting an object's body.
|
|
187
|
+
*
|
|
188
|
+
* Version, slug and locale only -- the slug being the identity, never the path, because an object
|
|
189
|
+
* outlives the directory it was published from. The body is not revalidated at an edge: the
|
|
190
|
+
* producer is trusted and what this catches is version skew. See spec/architecture/artifacts.md,
|
|
191
|
+
* "Validation is heavy where it is free and light where it is not".
|
|
192
|
+
*/
|
|
193
|
+
const EnvelopeSchema = v.object({
|
|
194
|
+
version: v.literal(2),
|
|
195
|
+
slug: v.string(),
|
|
196
|
+
locale: v.picklist(LOCALE_CODES)
|
|
197
|
+
});
|
|
198
|
+
/**
|
|
199
|
+
* The same check for a page, which has no locale to check.
|
|
200
|
+
*
|
|
201
|
+
* A page is compiled once and filed under every locale -- see the builder, and web's
|
|
202
|
+
* spec/i18n/copy.md for why identity copy is not translated. Giving its envelope a locale made nine
|
|
203
|
+
* objects that differed in one field, which is de-duplication defeated by a field that meant
|
|
204
|
+
* nothing.
|
|
205
|
+
*/
|
|
206
|
+
const PageEnvelopeSchema = v.object({
|
|
207
|
+
version: v.literal(2),
|
|
208
|
+
slug: v.string()
|
|
209
|
+
});
|
|
210
|
+
function readPageEnvelope(value, slug) {
|
|
211
|
+
const envelope = v.parse(PageEnvelopeSchema, value);
|
|
212
|
+
if (envelope.slug !== slug) throw new Error(`page artifact is ${envelope.slug}, asked for ${slug}`);
|
|
213
|
+
}
|
|
214
|
+
function readEnvelope(value, slug, locale) {
|
|
215
|
+
const envelope = v.parse(EnvelopeSchema, value);
|
|
216
|
+
if (envelope.slug !== slug || envelope.locale !== locale) throw new Error(`artifact is ${envelope.slug}/${envelope.locale}, asked for ${slug}/${locale}`);
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
export { ARTIFACT_EXTENSIONS, ARTIFACT_TYPES, ARTIFACT_VERSION, ArticleLayerSchema, ArticlesRequestSchema, BatchRequestSchema, CANONICAL_PATTERN, CancelAnswerSchema, ClipLayerSchema, DocumentLayerSchema, EnvelopeSchema, FrameLayerSchema, HASH_PATTERN, ICON_EXTENSION, IconLayerSchema, ImageLayerSchema, ImageVariantSchema, LAYERS, LikeAnswerSchema, LikedAnswerSchema, MarkLayerSchema, MediaLayerSchema, NewsletterAnswerSchema, NoticeLayerSchema, PageEnvelopeSchema, PhotoLayerSchema, RESOURCES_PER_QUESTION, RESOURCE_PATTERN, RESOURCE_VERSION, ROOT_KEY, ReadAnswerSchema, ReadsAnswerSchema, ReadsRequestSchema, ResourceSchema, ResourcesRequestSchema, RootArticleSchema, RootAssetSchema, RootSchema, RootViewSchema, SCALABLE_MIMES, ScreenshotLayerSchema, StatsAnswerSchema, TONES, VARIANT_EXTENSION, VideoLayerSchema, ViewMetaSchema, artifactAddress, aspect, best, blockFeedHtml, enough, escapeHtml, expandCanonical, feedHtml, height, isLayerName, isResourceId, namedResources, objectUrl, parseArtifactKey, parseResource, parseType, pictured, readEnvelope, readPageEnvelope, recordKey, requireSegment, resolution, resourceQuestions, rungs, scalable, storageKey, toned, unwrap, unwrapAs, width };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Block } from "./types.js";
|
|
2
|
+
import { IconLayer, ImageLayer, ImageVariant, ParsedResource, Tone } from "./resource.js";
|
|
3
|
+
//#region artifacts/src/picture.d.ts
|
|
4
|
+
/** Step one: the intrinsic box, which every picture has. No branch and no decision. */
|
|
5
|
+
export declare function width(image: ImageLayer): number;
|
|
6
|
+
export declare function height(image: ImageLayer): number;
|
|
7
|
+
export declare function aspect(image: ImageLayer): string;
|
|
8
|
+
/** Step three: actual pixels, and `null` for a vector, which has none to report. */
|
|
9
|
+
export declare function resolution(image: ImageLayer): {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
} | null;
|
|
13
|
+
/**
|
|
14
|
+
* Whether this picture serves any size, which is the one place that knows which mimes scale.
|
|
15
|
+
*
|
|
16
|
+
* Asked of the variants because that is where a concrete mime is a fact; an absent `resolution`
|
|
17
|
+
* says the same thing and covers a layer whose variants have not been derived yet.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scalable(image: ImageLayer): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Step two, and the point of the other three: whether this can serve a target long edge.
|
|
22
|
+
*
|
|
23
|
+
* **The branch lives here.** A caller drawing a thumbnail never gets further than this line and
|
|
24
|
+
* never learns that formats exist, which is what keeps `scalable` from being a condition repeated
|
|
25
|
+
* in every caller. See spec/architecture/resource.md, "The image layer answers in four steps".
|
|
26
|
+
*/
|
|
27
|
+
export declare function enough(image: ImageLayer, want: number): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Step four: the file to serve for a target long edge.
|
|
30
|
+
*
|
|
31
|
+
* The smallest rung that covers the target, because anything larger is weight a reader pays for and
|
|
32
|
+
* nobody sees; the largest when none of them does, since upscaling is never done and the top rung
|
|
33
|
+
* is the best answer that exists. See web's spec/architecture/media.md, "Variants stop where the
|
|
34
|
+
* layout does".
|
|
35
|
+
*/
|
|
36
|
+
export declare function best(image: ImageLayer, want: number): ImageVariant | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* What an icon's file is spelled with, keyed by what is in it.
|
|
39
|
+
*
|
|
40
|
+
* Its own table rather than the variant one, for the reason `for_icon` is its own on the other
|
|
41
|
+
* side: these bytes were encoded by somebody else's server and arrive as SVG or ICO, neither of
|
|
42
|
+
* which a ladder ever produces -- and the ladder's table answers `avif` for anything it does not
|
|
43
|
+
* recognise, which would be an address to a file nobody wrote. The twin of `icon_mime` in
|
|
44
|
+
* services/apps/local/src/extension.rs, held to it by a test here.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ICON_EXTENSION: Record<string, string>;
|
|
47
|
+
/**
|
|
48
|
+
* Which of an icon's files answers for a tone, and `undefined` when none does.
|
|
49
|
+
*
|
|
50
|
+
* A named tone is that tone or nothing: a caller handed the other one cannot tell it happened,
|
|
51
|
+
* and would draw a light mark on a dark surface believing it had the right one. With none named
|
|
52
|
+
* either will do, light first, an untinted mark being drawn for light backgrounds. The one place
|
|
53
|
+
* that rule is written on this side, having been the alias layer's until an icon became a
|
|
54
|
+
* resource.
|
|
55
|
+
*/
|
|
56
|
+
export declare function toned(icon: IconLayer, want?: Tone): ImageVariant | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* What a published variant's file is called, keyed by what it holds.
|
|
59
|
+
*
|
|
60
|
+
* Beside `ICON_EXTENSION` rather than folded into it, for the reason that table gives: a ladder
|
|
61
|
+
* produces these four and never an SVG or an ICO, and answering `avif` for a mime it does not
|
|
62
|
+
* recognise would be a guess an icon cannot afford. Held to `for_variant` in
|
|
63
|
+
* services/apps/local/src/extension.rs by a test, the two being one fact in two languages.
|
|
64
|
+
*/
|
|
65
|
+
export declare const VARIANT_EXTENSION: Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Where one published file is fetched from: the content id and the extension that says how to
|
|
68
|
+
* read it, under `/object` like every other byte the CDN holds.
|
|
69
|
+
*
|
|
70
|
+
* Here rather than in each caller because the resolution that used to happen at build time now
|
|
71
|
+
* happens in three places -- a build writing the markdown target's address, a Worker rendering a
|
|
72
|
+
* page, a browser rendering the same page again -- and a URL spelled three ways is three chances
|
|
73
|
+
* to spell it wrong. The CDN is passed rather than picked: which one answers depends on the mode.
|
|
74
|
+
*/
|
|
75
|
+
export declare function objectUrl(cdnUrl: string, cid: string, extension: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* The variants a `srcset` can name, smallest first.
|
|
78
|
+
*
|
|
79
|
+
* Only the ones with pixels, because a `w` descriptor is a pixel count and a vector has none to
|
|
80
|
+
* state -- it is the `src`, and one file that serves every width needs no candidates beside it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function rungs(image: ImageLayer): (ImageVariant & {
|
|
83
|
+
width: number;
|
|
84
|
+
})[];
|
|
85
|
+
/** Everything the markup needs about one picture, from the record and the CDN alone. */
|
|
86
|
+
export type Picture = {
|
|
87
|
+
src: string;
|
|
88
|
+
srcset: string;
|
|
89
|
+
/** The intrinsic box, which is what reserves the space before anything is fetched. */
|
|
90
|
+
width: number;
|
|
91
|
+
height: number;
|
|
92
|
+
ratio: string;
|
|
93
|
+
/** The colour block painted under it while it arrives. Absent for a record holding no hash. */
|
|
94
|
+
placeholder?: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Which of a resource's files a picture is drawn from, and the refusal when it is not one.
|
|
98
|
+
*
|
|
99
|
+
* The second per-kind selector, beside `toned`, and one thing separates them: **a mark that does
|
|
100
|
+
* not resolve is absent and a picture that does not is an error.** A card's foot loses an
|
|
101
|
+
* ornament and still says where the link goes; an article loses what the paragraph is about, and
|
|
102
|
+
* a blank there is how a missing image becomes one nobody reports. So this throws, where the
|
|
103
|
+
* build refuses only what its committed manifest does not know -- spec/architecture/resource.md.
|
|
104
|
+
*/
|
|
105
|
+
export declare function pictured(rid: string, record: ParsedResource | undefined, cdnUrl: string): Picture;
|
|
106
|
+
/**
|
|
107
|
+
* Every rid the blocks of one view name, each asked for once.
|
|
108
|
+
*
|
|
109
|
+
* A block declares its resources under a key that says so, and this reads that key and nothing
|
|
110
|
+
* else. What it replaces was a switch over block types, kept in the page that renders them,
|
|
111
|
+
* which grew an arm for every block that came to name one. The shape is what fixes that: with
|
|
112
|
+
* the roles on the block there is no list here to grow. Deduplicated, because two pictures of
|
|
113
|
+
* one subject are one question -- spec/architecture/resource.md, "One question per page".
|
|
114
|
+
*/
|
|
115
|
+
export declare function namedResources(blocks: readonly Block[]): string[];
|
|
116
|
+
//#endregion
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { SCALABLE_MIMES, requireSegment } from "./resource.js";
|
|
2
|
+
//#region artifacts/src/picture.ts
|
|
3
|
+
/**
|
|
4
|
+
* Choosing what to draw out of an image or icon layer: its size, the variant that covers a width,
|
|
5
|
+
* the tone that suits a ground, and the picture a page renders for a rid. See
|
|
6
|
+
* spec/architecture/resource.md.
|
|
7
|
+
*/
|
|
8
|
+
/** Step one: the intrinsic box, which every picture has. No branch and no decision. */
|
|
9
|
+
function width(image) {
|
|
10
|
+
return image.dimension.width;
|
|
11
|
+
}
|
|
12
|
+
function height(image) {
|
|
13
|
+
return image.dimension.height;
|
|
14
|
+
}
|
|
15
|
+
function aspect(image) {
|
|
16
|
+
return image.dimension.aspect;
|
|
17
|
+
}
|
|
18
|
+
/** Step three: actual pixels, and `null` for a vector, which has none to report. */
|
|
19
|
+
function resolution(image) {
|
|
20
|
+
return image.resolution ?? null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether this picture serves any size, which is the one place that knows which mimes scale.
|
|
24
|
+
*
|
|
25
|
+
* Asked of the variants because that is where a concrete mime is a fact; an absent `resolution`
|
|
26
|
+
* says the same thing and covers a layer whose variants have not been derived yet.
|
|
27
|
+
*/
|
|
28
|
+
function scalable(image) {
|
|
29
|
+
return image.variants.some((file) => SCALABLE_MIMES.has(file.mime)) || !image.resolution;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Step two, and the point of the other three: whether this can serve a target long edge.
|
|
33
|
+
*
|
|
34
|
+
* **The branch lives here.** A caller drawing a thumbnail never gets further than this line and
|
|
35
|
+
* never learns that formats exist, which is what keeps `scalable` from being a condition repeated
|
|
36
|
+
* in every caller. See spec/architecture/resource.md, "The image layer answers in four steps".
|
|
37
|
+
*/
|
|
38
|
+
function enough(image, want) {
|
|
39
|
+
if (scalable(image)) return true;
|
|
40
|
+
const pixels = resolution(image);
|
|
41
|
+
return pixels !== null && Math.max(pixels.width, pixels.height) >= want;
|
|
42
|
+
}
|
|
43
|
+
function longEdge(file) {
|
|
44
|
+
return file.resolution ? Math.max(file.resolution.width, file.resolution.height) : Infinity;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Step four: the file to serve for a target long edge.
|
|
48
|
+
*
|
|
49
|
+
* The smallest rung that covers the target, because anything larger is weight a reader pays for and
|
|
50
|
+
* nobody sees; the largest when none of them does, since upscaling is never done and the top rung
|
|
51
|
+
* is the best answer that exists. See web's spec/architecture/media.md, "Variants stop where the
|
|
52
|
+
* layout does".
|
|
53
|
+
*/
|
|
54
|
+
function best(image, want) {
|
|
55
|
+
const vector = image.variants.find((file) => SCALABLE_MIMES.has(file.mime));
|
|
56
|
+
if (vector) return vector;
|
|
57
|
+
const rungs = [...image.variants].sort((a, b) => longEdge(a) - longEdge(b));
|
|
58
|
+
return rungs.find((file) => longEdge(file) >= want) ?? rungs[rungs.length - 1];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What an icon's file is spelled with, keyed by what is in it.
|
|
62
|
+
*
|
|
63
|
+
* Its own table rather than the variant one, for the reason `for_icon` is its own on the other
|
|
64
|
+
* side: these bytes were encoded by somebody else's server and arrive as SVG or ICO, neither of
|
|
65
|
+
* which a ladder ever produces -- and the ladder's table answers `avif` for anything it does not
|
|
66
|
+
* recognise, which would be an address to a file nobody wrote. The twin of `icon_mime` in
|
|
67
|
+
* services/apps/local/src/extension.rs, held to it by a test here.
|
|
68
|
+
*/
|
|
69
|
+
const ICON_EXTENSION = {
|
|
70
|
+
"image/svg+xml": "svg",
|
|
71
|
+
"image/png": "png",
|
|
72
|
+
"image/jpeg": "jpeg",
|
|
73
|
+
"image/x-icon": "ico"
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Which of an icon's files answers for a tone, and `undefined` when none does.
|
|
77
|
+
*
|
|
78
|
+
* A named tone is that tone or nothing: a caller handed the other one cannot tell it happened,
|
|
79
|
+
* and would draw a light mark on a dark surface believing it had the right one. With none named
|
|
80
|
+
* either will do, light first, an untinted mark being drawn for light backgrounds. The one place
|
|
81
|
+
* that rule is written on this side, having been the alias layer's until an icon became a
|
|
82
|
+
* resource.
|
|
83
|
+
*/
|
|
84
|
+
function toned(icon, want) {
|
|
85
|
+
return want ? icon.tones[want] : icon.tones.light ?? icon.tones.dark;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What a published variant's file is called, keyed by what it holds.
|
|
89
|
+
*
|
|
90
|
+
* Beside `ICON_EXTENSION` rather than folded into it, for the reason that table gives: a ladder
|
|
91
|
+
* produces these four and never an SVG or an ICO, and answering `avif` for a mime it does not
|
|
92
|
+
* recognise would be a guess an icon cannot afford. Held to `for_variant` in
|
|
93
|
+
* services/apps/local/src/extension.rs by a test, the two being one fact in two languages.
|
|
94
|
+
*/
|
|
95
|
+
const VARIANT_EXTENSION = {
|
|
96
|
+
"image/avif": "avif",
|
|
97
|
+
"image/webp": "webp",
|
|
98
|
+
"image/png": "png",
|
|
99
|
+
"image/jpeg": "jpeg"
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Where one published file is fetched from: the content id and the extension that says how to
|
|
103
|
+
* read it, under `/object` like every other byte the CDN holds.
|
|
104
|
+
*
|
|
105
|
+
* Here rather than in each caller because the resolution that used to happen at build time now
|
|
106
|
+
* happens in three places -- a build writing the markdown target's address, a Worker rendering a
|
|
107
|
+
* page, a browser rendering the same page again -- and a URL spelled three ways is three chances
|
|
108
|
+
* to spell it wrong. The CDN is passed rather than picked: which one answers depends on the mode.
|
|
109
|
+
*/
|
|
110
|
+
function objectUrl(cdnUrl, cid, extension) {
|
|
111
|
+
return `${cdnUrl}/object/${cid}.${extension}`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The variants a `srcset` can name, smallest first.
|
|
115
|
+
*
|
|
116
|
+
* Only the ones with pixels, because a `w` descriptor is a pixel count and a vector has none to
|
|
117
|
+
* state -- it is the `src`, and one file that serves every width needs no candidates beside it.
|
|
118
|
+
*/
|
|
119
|
+
function rungs(image) {
|
|
120
|
+
return image.variants.flatMap((file) => file.resolution ? [{
|
|
121
|
+
...file,
|
|
122
|
+
width: file.resolution.width
|
|
123
|
+
}] : []).toSorted((a, b) => a.width - b.width);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Which of a resource's files a picture is drawn from, and the refusal when it is not one.
|
|
127
|
+
*
|
|
128
|
+
* The second per-kind selector, beside `toned`, and one thing separates them: **a mark that does
|
|
129
|
+
* not resolve is absent and a picture that does not is an error.** A card's foot loses an
|
|
130
|
+
* ornament and still says where the link goes; an article loses what the paragraph is about, and
|
|
131
|
+
* a blank there is how a missing image becomes one nobody reports. So this throws, where the
|
|
132
|
+
* build refuses only what its committed manifest does not know -- spec/architecture/resource.md.
|
|
133
|
+
*/
|
|
134
|
+
function pictured(rid, record, cdnUrl) {
|
|
135
|
+
if (!record) throw new Error(`no record for resource ${rid}, which an article draws`);
|
|
136
|
+
const image = requireSegment(record, "image");
|
|
137
|
+
const file = best(image, width(image));
|
|
138
|
+
if (!file) throw new Error(`resource ${rid} publishes no file to draw`);
|
|
139
|
+
return {
|
|
140
|
+
src: objectUrl(cdnUrl, file.content, VARIANT_EXTENSION[file.mime] ?? "avif"),
|
|
141
|
+
srcset: rungs(image).map((rung) => `${objectUrl(cdnUrl, rung.content, VARIANT_EXTENSION[rung.mime] ?? "avif")} ${rung.width}w`).join(", "),
|
|
142
|
+
width: width(image),
|
|
143
|
+
height: height(image),
|
|
144
|
+
ratio: aspect(image),
|
|
145
|
+
placeholder: image.placeholder
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Every rid the blocks of one view name, each asked for once.
|
|
150
|
+
*
|
|
151
|
+
* A block declares its resources under a key that says so, and this reads that key and nothing
|
|
152
|
+
* else. What it replaces was a switch over block types, kept in the page that renders them,
|
|
153
|
+
* which grew an arm for every block that came to name one. The shape is what fixes that: with
|
|
154
|
+
* the roles on the block there is no list here to grow. Deduplicated, because two pictures of
|
|
155
|
+
* one subject are one question -- spec/architecture/resource.md, "One question per page".
|
|
156
|
+
*/
|
|
157
|
+
function namedResources(blocks) {
|
|
158
|
+
return [...new Set(blocks.flatMap((block) => "resources" in block ? Object.values(block.resources ?? {}) : []))];
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { ICON_EXTENSION, VARIANT_EXTENSION, aspect, best, enough, height, namedResources, objectUrl, pictured, resolution, rungs, scalable, toned, width };
|