@heroiclands/package-build 23.4.0 → 23.5.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/CHANGELOG.md +34 -0
- package/README.md +29 -25
- package/assets/images/provenance.yaml +1 -0
- package/bin/package-build.mjs +259 -28
- package/ci/prettier.mjs +73 -0
- package/config.mjs +10 -0
- package/container.mjs +232 -1
- package/content-config.mjs +5 -1
- package/docs/api.md +100 -52
- package/docs/authoring/assets.md +16 -1
- package/docs/commands.md +154 -7
- package/docs/configuration.md +39 -2
- package/docs/content-format.md +30 -6
- package/docs/guides/e2e.md +38 -0
- package/docs/guides/icons.md +5 -0
- package/docs/guides/release-and-deploy.md +36 -1
- package/docs/reference/format-details.md +218 -50
- package/docs/reference/note-types.md +71 -71
- package/engine/asset-index.mjs +63 -22
- package/engine/asset-references.mjs +355 -0
- package/engine/asset-shipping.mjs +363 -0
- package/engine/asset-types.mjs +70 -0
- package/engine/base-compiler.mjs +24 -1
- package/engine/cdn-publish.mjs +618 -0
- package/engine/index.mjs +6 -0
- package/engine/note-vocabulary.mjs +40 -2
- package/engine/pathnames.mjs +21 -10
- package/engine/rank-ladder.mjs +40 -16
- package/engine/scenes.mjs +1 -1
- package/engine/standing-terms.mjs +3 -1
- package/package.json +4 -3
- package/packagebuild-metadata.jsonl +13 -13
- package/release.mjs +69 -7
- package/sohl/actors.mjs +12 -4
- package/sohl/default-item-art.mjs +23 -19
- package/types/config.d.mts +7 -0
- package/types/container.d.mts +137 -1
- package/types/engine/asset-index.d.mts +3 -2
- package/types/engine/asset-references.d.mts +136 -0
- package/types/engine/asset-shipping.d.mts +240 -0
- package/types/engine/asset-types.d.mts +36 -0
- package/types/engine/base-compiler.d.mts +7 -1
- package/types/engine/cdn-publish.d.mts +311 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/pathnames.d.mts +2 -1
- package/types/engine/rank-ladder.d.mts +25 -6
- package/types/release.d.mts +5 -3
- package/types/sohl/default-item-art.d.mts +2 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The media type a file is uploaded with.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} name - A file name or key.
|
|
5
|
+
* @returns {string} Its media type, `application/octet-stream` when unknown.
|
|
6
|
+
*/
|
|
7
|
+
export function contentTypeOf(name: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Why a `--set` value cannot name a set file, or `""` when it can.
|
|
10
|
+
*
|
|
11
|
+
* @param {unknown} file - The value given.
|
|
12
|
+
* @returns {string} The problem, as a clause, or `""`.
|
|
13
|
+
*/
|
|
14
|
+
export function setFileProblem(file: unknown): string;
|
|
15
|
+
/**
|
|
16
|
+
* The store's settings from the environment.
|
|
17
|
+
*
|
|
18
|
+
* @param {Record<string, string|undefined>} env - The environment.
|
|
19
|
+
* @returns {{endpoint: string, bucket: string, accessKeyId: string,
|
|
20
|
+
* secretAccessKey: string}|null} The settings, or `null` when none of the
|
|
21
|
+
* variables is set.
|
|
22
|
+
* @throws {Error} When some are set and others are not, naming the missing
|
|
23
|
+
* ones and never a value.
|
|
24
|
+
*/
|
|
25
|
+
export function cdnCredentials(env: Record<string, string | undefined>): {
|
|
26
|
+
endpoint: string;
|
|
27
|
+
bucket: string;
|
|
28
|
+
accessKeyId: string;
|
|
29
|
+
secretAccessKey: string;
|
|
30
|
+
} | null;
|
|
31
|
+
/**
|
|
32
|
+
* The S3-compatible store the CDN is served from, signed with AWS SigV4.
|
|
33
|
+
*
|
|
34
|
+
* Addressed path-style, `<endpoint>/<bucket>/<key>`, which R2 and every other
|
|
35
|
+
* S3-compatible server accept.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} opts
|
|
38
|
+
* @param {string} opts.endpoint - The S3 endpoint.
|
|
39
|
+
* @param {string} opts.bucket - The bucket.
|
|
40
|
+
* @param {string} opts.accessKeyId - The access key id.
|
|
41
|
+
* @param {string} opts.secretAccessKey - The secret access key.
|
|
42
|
+
* @param {typeof fetch} [opts.fetch] - The fetch to sign requests for.
|
|
43
|
+
* @returns {CdnStore} The store.
|
|
44
|
+
*/
|
|
45
|
+
export function s3Store({ endpoint, bucket, accessKeyId, secretAccessKey, fetch: fetchImpl }: {
|
|
46
|
+
endpoint: string;
|
|
47
|
+
bucket: string;
|
|
48
|
+
accessKeyId: string;
|
|
49
|
+
secretAccessKey: string;
|
|
50
|
+
fetch?: typeof fetch | undefined;
|
|
51
|
+
}): CdnStore;
|
|
52
|
+
/**
|
|
53
|
+
* The CDN read through its public address — what a dry run without
|
|
54
|
+
* credentials compares against. It cannot write.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} host - `site.assets`, the URL the bucket is served at.
|
|
57
|
+
* @param {typeof fetch} [fetchImpl] - The fetch to use.
|
|
58
|
+
* @returns {CdnStore} The read-only store.
|
|
59
|
+
*/
|
|
60
|
+
export function publicStore(host: string, fetchImpl?: typeof fetch): CdnStore;
|
|
61
|
+
/**
|
|
62
|
+
* One picture to publish.
|
|
63
|
+
*
|
|
64
|
+
* @typedef {object} CdnUpload
|
|
65
|
+
* @property {object} record - The asset record.
|
|
66
|
+
* @property {string} address - Its canonical address.
|
|
67
|
+
* @property {string} target - `<icons|images>/<basename>`.
|
|
68
|
+
* @property {string} key - The object key, `<package>/<target>`.
|
|
69
|
+
* @property {Buffer} body - The bytes, transformed as the stage transforms
|
|
70
|
+
* them.
|
|
71
|
+
* @property {string} contentType - The media type it is served with.
|
|
72
|
+
* @property {boolean} shipped - Whether the Foundry package ships it.
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Everything a publish uploads, worked out before anything is sent.
|
|
76
|
+
*
|
|
77
|
+
* @typedef {object} CdnPlan
|
|
78
|
+
* @property {object[]} findings - What stops the publish; nothing is planned
|
|
79
|
+
* when non-empty.
|
|
80
|
+
* @property {object[]} records - Every asset record the package has.
|
|
81
|
+
* @property {CdnUpload[]} uploads - One per picture, in key order.
|
|
82
|
+
* @property {{key: string, body: Buffer, set: object}|null} set - The set file.
|
|
83
|
+
* @property {string} cdnRoot - `<site.assets>/<package>/`.
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* Work out what a publish uploads.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} opts
|
|
89
|
+
* @param {string} opts.assetsBase - The package's asset directory.
|
|
90
|
+
* @param {string} opts.contentPackage - The content package.
|
|
91
|
+
* @param {string} opts.host - `site.assets`.
|
|
92
|
+
* @param {string} opts.setFile - The set file's name.
|
|
93
|
+
* @param {string} opts.setName - The set's display name.
|
|
94
|
+
* @param {(sourcePath: string) => string|null|undefined} [opts.transform] -
|
|
95
|
+
* The repository's per-file transform.
|
|
96
|
+
* @param {readonly string[]} [opts.shippingLicenses] - The licences the
|
|
97
|
+
* package adds to the shipping list, so the set leaves out what the stage
|
|
98
|
+
* leaves out.
|
|
99
|
+
* @returns {CdnPlan} The plan.
|
|
100
|
+
*/
|
|
101
|
+
export function planCdnPublish({ assetsBase, contentPackage, host, setFile, setName, transform, shippingLicenses, }: {
|
|
102
|
+
assetsBase: string;
|
|
103
|
+
contentPackage: string;
|
|
104
|
+
host: string;
|
|
105
|
+
setFile: string;
|
|
106
|
+
setName: string;
|
|
107
|
+
transform?: ((sourcePath: string) => string | null | undefined) | undefined;
|
|
108
|
+
shippingLicenses?: readonly string[] | undefined;
|
|
109
|
+
}): CdnPlan;
|
|
110
|
+
/**
|
|
111
|
+
* Upload a plan.
|
|
112
|
+
*
|
|
113
|
+
* A picture that fails is recorded and the rest carry on, so one refused file
|
|
114
|
+
* costs one file rather than the run. The set file is written only when every
|
|
115
|
+
* picture it lists is on the CDN: a set naming a file the CDN lacks sends a
|
|
116
|
+
* world to fetch nothing.
|
|
117
|
+
*
|
|
118
|
+
* @param {CdnPlan} plan - From {@link planCdnPublish}, with no findings.
|
|
119
|
+
* @param {object} opts
|
|
120
|
+
* @param {CdnStore} opts.store - Where the objects go.
|
|
121
|
+
* @param {boolean} [opts.dryRun] - Compare only; write nothing.
|
|
122
|
+
* @param {(line: string) => void} [opts.log] - One line per object.
|
|
123
|
+
* @returns {Promise<{uploaded: CdnUpload[], skipped: CdnUpload[],
|
|
124
|
+
* failed: {upload: CdnUpload, message: string}[],
|
|
125
|
+
* set: "uploaded"|"skipped"|"withheld", published: Set<string>}>} What was
|
|
126
|
+
* (or, in a dry run, would be) uploaded, skipped and refused; what became of
|
|
127
|
+
* the set file; and every address whose picture is on the CDN at its
|
|
128
|
+
* planned size once the run ends — or would be, in a dry run.
|
|
129
|
+
*/
|
|
130
|
+
export function publishToCdn(plan: CdnPlan, { store, dryRun, log }: {
|
|
131
|
+
store: CdnStore;
|
|
132
|
+
dryRun?: boolean | undefined;
|
|
133
|
+
log?: ((line: string) => void) | undefined;
|
|
134
|
+
}): Promise<{
|
|
135
|
+
uploaded: CdnUpload[];
|
|
136
|
+
skipped: CdnUpload[];
|
|
137
|
+
failed: {
|
|
138
|
+
upload: CdnUpload;
|
|
139
|
+
message: string;
|
|
140
|
+
}[];
|
|
141
|
+
set: "uploaded" | "skipped" | "withheld";
|
|
142
|
+
published: Set<string>;
|
|
143
|
+
}>;
|
|
144
|
+
/**
|
|
145
|
+
* Every place a package's notes name one of its own pictures, and what each
|
|
146
|
+
* names.
|
|
147
|
+
*
|
|
148
|
+
* Reads the three places a note names art: an art slot under `data:`, an
|
|
149
|
+
* embed (`![[address|label]]`) and a body image whose pathname is the
|
|
150
|
+
* package's own. A reference naming another package, or one that does not
|
|
151
|
+
* parse as an address, is another check's business and is not returned.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} opts
|
|
154
|
+
* @param {string} opts.contentBase - The content tree.
|
|
155
|
+
* @param {readonly string[]} opts.skipDirectories - Directory names the walk
|
|
156
|
+
* ignores.
|
|
157
|
+
* @param {string} opts.contentPackage - The content package.
|
|
158
|
+
* @param {readonly object[]} opts.records - The package's asset records.
|
|
159
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
160
|
+
* written: string, record: object|null}>} One per reference, `record` `null`
|
|
161
|
+
* where the package has no such picture. `file` is relative to the working
|
|
162
|
+
* directory.
|
|
163
|
+
*/
|
|
164
|
+
export function noteArtReferences({ contentBase, skipDirectories, contentPackage, records }: {
|
|
165
|
+
contentBase: string;
|
|
166
|
+
skipDirectories: readonly string[];
|
|
167
|
+
contentPackage: string;
|
|
168
|
+
records: readonly object[];
|
|
169
|
+
}): Array<{
|
|
170
|
+
file: string;
|
|
171
|
+
line?: number;
|
|
172
|
+
column?: number;
|
|
173
|
+
written: string;
|
|
174
|
+
record: object | null;
|
|
175
|
+
}>;
|
|
176
|
+
/**
|
|
177
|
+
* The findings a publish reports against the package's notes.
|
|
178
|
+
*
|
|
179
|
+
* Both are warnings:
|
|
180
|
+
*
|
|
181
|
+
* - a note names a picture the package does not have, so no address answers
|
|
182
|
+
* it and nothing is published for it;
|
|
183
|
+
* - a note names a picture the package leaves out, and the CDN does not hold
|
|
184
|
+
* it, so a world adding the set gets no file for that document.
|
|
185
|
+
*
|
|
186
|
+
* @param {ReturnType<typeof noteArtReferences>} references - From
|
|
187
|
+
* {@link noteArtReferences}.
|
|
188
|
+
* @param {object} opts
|
|
189
|
+
* @param {string} opts.contentPackage - The content package.
|
|
190
|
+
* @param {ReadonlySet<string>} opts.published - Every address on the CDN at
|
|
191
|
+
* its planned size.
|
|
192
|
+
* @param {ReadonlySet<object>} opts.excluded - The records the package leaves
|
|
193
|
+
* out.
|
|
194
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
195
|
+
* severity: "warning", message: string}>} In note order.
|
|
196
|
+
*/
|
|
197
|
+
export function noteArtFindings(references: ReturnType<typeof noteArtReferences>, { contentPackage, published, excluded }: {
|
|
198
|
+
contentPackage: string;
|
|
199
|
+
published: ReadonlySet<string>;
|
|
200
|
+
excluded: ReadonlySet<object>;
|
|
201
|
+
}): Array<{
|
|
202
|
+
file: string;
|
|
203
|
+
line?: number;
|
|
204
|
+
column?: number;
|
|
205
|
+
severity: "warning";
|
|
206
|
+
message: string;
|
|
207
|
+
}>;
|
|
208
|
+
/**
|
|
209
|
+
* The environment variables naming the S3-compatible store behind the CDN.
|
|
210
|
+
*
|
|
211
|
+
* For Cloudflare R2, `CDN_ENDPOINT` is the account's S3 endpoint,
|
|
212
|
+
* `https://<account id>.r2.cloudflarestorage.com`, and the key pair is an R2
|
|
213
|
+
* API token's.
|
|
214
|
+
*
|
|
215
|
+
* @type {Readonly<{endpoint: string, bucket: string, accessKeyId: string, secretAccessKey: string}>}
|
|
216
|
+
*/
|
|
217
|
+
export const CDN_ENV: Readonly<{
|
|
218
|
+
endpoint: string;
|
|
219
|
+
bucket: string;
|
|
220
|
+
accessKeyId: string;
|
|
221
|
+
secretAccessKey: string;
|
|
222
|
+
}>;
|
|
223
|
+
/**
|
|
224
|
+
* A store holding the CDN's objects.
|
|
225
|
+
*/
|
|
226
|
+
export type CdnStore = {
|
|
227
|
+
/**
|
|
228
|
+
* - An object's
|
|
229
|
+
* size in bytes, `undefined` when there is none.
|
|
230
|
+
*/
|
|
231
|
+
size: (key: string) => Promise<number | undefined>;
|
|
232
|
+
/**
|
|
233
|
+
* - An object's
|
|
234
|
+
* bytes, `undefined` when there is none.
|
|
235
|
+
*/
|
|
236
|
+
read: (key: string) => Promise<Buffer | undefined>;
|
|
237
|
+
/**
|
|
238
|
+
* -
|
|
239
|
+
* Store an object; absent on a read-only store.
|
|
240
|
+
*/
|
|
241
|
+
write: ((key: string, body: Buffer, contentType: string) => Promise<void>) | undefined;
|
|
242
|
+
/**
|
|
243
|
+
* - What the store is, for the log. Never a
|
|
244
|
+
* secret.
|
|
245
|
+
*/
|
|
246
|
+
describe: string;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* One picture to publish.
|
|
250
|
+
*/
|
|
251
|
+
export type CdnUpload = {
|
|
252
|
+
/**
|
|
253
|
+
* - The asset record.
|
|
254
|
+
*/
|
|
255
|
+
record: object;
|
|
256
|
+
/**
|
|
257
|
+
* - Its canonical address.
|
|
258
|
+
*/
|
|
259
|
+
address: string;
|
|
260
|
+
/**
|
|
261
|
+
* - `<icons|images>/<basename>`.
|
|
262
|
+
*/
|
|
263
|
+
target: string;
|
|
264
|
+
/**
|
|
265
|
+
* - The object key, `<package>/<target>`.
|
|
266
|
+
*/
|
|
267
|
+
key: string;
|
|
268
|
+
/**
|
|
269
|
+
* - The bytes, transformed as the stage transforms
|
|
270
|
+
* them.
|
|
271
|
+
*/
|
|
272
|
+
body: Buffer;
|
|
273
|
+
/**
|
|
274
|
+
* - The media type it is served with.
|
|
275
|
+
*/
|
|
276
|
+
contentType: string;
|
|
277
|
+
/**
|
|
278
|
+
* - Whether the Foundry package ships it.
|
|
279
|
+
*/
|
|
280
|
+
shipped: boolean;
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Everything a publish uploads, worked out before anything is sent.
|
|
284
|
+
*/
|
|
285
|
+
export type CdnPlan = {
|
|
286
|
+
/**
|
|
287
|
+
* - What stops the publish; nothing is planned
|
|
288
|
+
* when non-empty.
|
|
289
|
+
*/
|
|
290
|
+
findings: object[];
|
|
291
|
+
/**
|
|
292
|
+
* - Every asset record the package has.
|
|
293
|
+
*/
|
|
294
|
+
records: object[];
|
|
295
|
+
/**
|
|
296
|
+
* - One per picture, in key order.
|
|
297
|
+
*/
|
|
298
|
+
uploads: CdnUpload[];
|
|
299
|
+
/**
|
|
300
|
+
* - The set file.
|
|
301
|
+
*/
|
|
302
|
+
set: {
|
|
303
|
+
key: string;
|
|
304
|
+
body: Buffer;
|
|
305
|
+
set: object;
|
|
306
|
+
} | null;
|
|
307
|
+
/**
|
|
308
|
+
* - `<site.assets>/<package>/`.
|
|
309
|
+
*/
|
|
310
|
+
cdnRoot: string;
|
|
311
|
+
};
|
package/types/engine/index.d.mts
CHANGED
|
@@ -52,6 +52,8 @@ export * as foundryEntries from "./foundry-entries.mjs";
|
|
|
52
52
|
export * as contentIndex from "./content-index.mjs";
|
|
53
53
|
export * as assetTypes from "./asset-types.mjs";
|
|
54
54
|
export * as assetIndex from "./asset-index.mjs";
|
|
55
|
+
export * as assetShipping from "./asset-shipping.mjs";
|
|
56
|
+
export * as assetReferences from "./asset-references.mjs";
|
|
55
57
|
export * as packages from "./packages.mjs";
|
|
56
58
|
export * as packagebuildIndex from "./packagebuild-index.mjs";
|
|
57
59
|
export * as siteBuild from "./site-build.mjs";
|
|
@@ -148,7 +148,8 @@ export type PathnameForms = {
|
|
|
148
148
|
*/
|
|
149
149
|
package: string | null;
|
|
150
150
|
/**
|
|
151
|
-
* The path inside that package's
|
|
151
|
+
* The path inside that package's `assets/`
|
|
152
|
+
* directory, as its repository arranges it.
|
|
152
153
|
*/
|
|
153
154
|
suffix: string | null;
|
|
154
155
|
/**
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Every affiliation's ladder has its two floor rungs: level 0, those the body
|
|
3
|
+
* excludes, and level 1, its ordinary members.
|
|
4
|
+
*
|
|
5
|
+
* A body that can name its members can name those it shuts out, whether or not
|
|
6
|
+
* it ever does, so both rungs are required of every ladder. A missing or empty
|
|
7
|
+
* ladder is one finding naming both; a ladder lacking either rung is one
|
|
8
|
+
* finding per rung it lacks, at the `ranks` key.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} note - The note being checked.
|
|
11
|
+
* @param {string} note.file - Its path, as a diagnostic spells it.
|
|
12
|
+
* @param {string} [note.raw] - Its source, for the position.
|
|
13
|
+
* @param {object} [note.fm] - Its parsed frontmatter.
|
|
14
|
+
* @returns {Array<{file: string, line?: number, column?: number,
|
|
15
|
+
* severity: string, message: string}>} The findings.
|
|
16
|
+
*/
|
|
17
|
+
export function checkAffiliationRankFloor(note: {
|
|
18
|
+
file: string;
|
|
19
|
+
raw?: string | undefined;
|
|
20
|
+
fm?: object | undefined;
|
|
21
|
+
}): Array<{
|
|
22
|
+
file: string;
|
|
5
23
|
line?: number;
|
|
6
24
|
column?: number;
|
|
7
|
-
|
|
8
|
-
|
|
25
|
+
severity: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
9
28
|
/**
|
|
10
29
|
* Check one affiliation's `data.governance.ranks` against the rung contract.
|
|
11
30
|
*
|
package/types/release.d.mts
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* @param {boolean} [opts.pdf] - Whether to build the books that ship beside the
|
|
23
23
|
* archive. `true` by default; `false` skips the build and reports the skip.
|
|
24
24
|
* @returns {Promise<{zip: string, manifest: string, metadata?: string,
|
|
25
|
-
* schema?: string,
|
|
26
|
-
* bytes: number, version: string}>} The paths
|
|
27
|
-
* found, the archive's size, and the version
|
|
25
|
+
* schema?: string, pictures: string[], pdfs: string[], pdfFindings: object[],
|
|
26
|
+
* pdfSkipped: string|null, bytes: number, version: string}>} The paths
|
|
27
|
+
* written, what the book build found, the archive's size, and the version
|
|
28
|
+
* the manifest declares. `pictures` is each picture archive written.
|
|
28
29
|
* `metadata` is absent when the manifest advertises no content index,
|
|
29
30
|
* `schema` is absent when the stage carries no `schema.json`, and `pdfs` is
|
|
30
31
|
* every book's PDF that was written, in declared order. `pdfSkipped` is
|
|
@@ -45,6 +46,7 @@ export function packRelease({ stageDir, outDir, artifact, metadataDir, pdf, }?:
|
|
|
45
46
|
manifest: string;
|
|
46
47
|
metadata?: string;
|
|
47
48
|
schema?: string;
|
|
49
|
+
pictures: string[];
|
|
48
50
|
pdfs: string[];
|
|
49
51
|
pdfFindings: object[];
|
|
50
52
|
pdfSkipped: string | null;
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @param {string} type - The SoHL Item document subtype — `armorgear`, not the
|
|
12
12
|
* `armor` note type that compiles into one.
|
|
13
|
-
* @returns {string} the default image
|
|
13
|
+
* @returns {string} the default image as an authored pathname,
|
|
14
|
+
* `sohl/assets/icons/<basename>`.
|
|
14
15
|
*/
|
|
15
16
|
export function defaultItemArt(type: string): string;
|
|
16
17
|
export namespace DEFAULT_ITEM_ART {
|