dsh-fonttune 0.2.2 → 0.2.4

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/lib/index.js CHANGED
@@ -71,6 +71,30 @@ const MAX_STACK = 200;
71
71
  */
72
72
  const SAFE_FEATURES = /^[a-zA-Z0-9"' ,]*$/;
73
73
 
74
+ /**
75
+ * Mark a schema editable without reloading, where the running schemastery
76
+ * knows the modifier.
77
+ *
78
+ * DSH 0.1.7-alpha.x derives a settings page from one form per composed entry
79
+ * and only projects the fields under a node marked `.volatile()` — an entry
80
+ * whose schema has no such node is skipped entirely, so the plugin would have
81
+ * no configuration page there. The modifier arrived in schemastery 3.18.x for
82
+ * the alpha line; the 0.1.5-rc.x line ships 3.18.2, where calling it while
83
+ * this module is imported would throw. Feature-detecting it is therefore the
84
+ * whole compatibility shim: the rc line keeps the plain schema it has always
85
+ * used, and the alpha gets the marking it needs.
86
+ *
87
+ * Marking the ROOT object makes every field below it live-editable; schemastery
88
+ * refuses a volatile node nested inside another volatile one, so nothing below
89
+ * may carry the modifier as well.
90
+ *
91
+ * @param {object} schema - the object schema.
92
+ * @returns {object} the schema, marked when the modifier exists.
93
+ */
94
+ function volatileWhenSupported(schema) {
95
+ return typeof schema.volatile === "function" ? schema.volatile() : schema;
96
+ }
97
+
74
98
  /**
75
99
  * The durable settings section.
76
100
  *
@@ -82,7 +106,7 @@ const SAFE_FEATURES = /^[a-zA-Z0-9"' ,]*$/;
82
106
  * value. Installing the plugin therefore changes nothing until the user asks
83
107
  * for something.
84
108
  */
85
- export const Config = z.object({
109
+ export const Config = volatileWhenSupported(z.object({
86
110
  [SANS_FIELD]: z
87
111
  .string()
88
112
  .max(MAX_STACK)
@@ -220,13 +244,46 @@ export const Config = z.object({
220
244
  .max(shared.MAX_PRESET_NAME)
221
245
  .default("")
222
246
  .description("Name of the preset the card currently edits (edits auto-save into it)"),
223
- });
247
+ }));
224
248
 
225
249
  /**
226
250
  * One row of the structured index injection table.
227
251
  * @typedef {{kind: "style", text: string} | {kind: "html", placement: "head", html: string}} StyleRow
228
252
  */
229
253
 
254
+ /**
255
+ * The cosmokit volatile-reference protocol: a parsed config field may be a
256
+ * frozen `{ get(), [write](value) }` reference rather than a plain value.
257
+ *
258
+ * DSH 0.1.7-alpha.x marks the plugin's whole schema `.volatile()` so a settings
259
+ * write can land without remounting the entry, and the loader then hands this
260
+ * plugin a config whose fields ARE those references. Reading them as plain data
261
+ * (which `normalizeConfig` expects) would see objects where strings belong and
262
+ * emit nothing, so the references are unwrapped first. On the 0.1.5-rc.x line
263
+ * no field is a reference and this is a plain deep copy.
264
+ */
265
+ const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
266
+
267
+ /**
268
+ * Replace every volatile reference in a parsed config with its current value.
269
+ *
270
+ * Exported because it is half of this plugin's host-line contract: a caller that
271
+ * wants to read a resolved config — a test, or a later composition — has to go
272
+ * through it, since on the alpha a parsed field is a reference rather than data.
273
+ * @param {unknown} value - a parsed config value, or a reference to one.
274
+ * @returns {unknown} the same shape, references unwrapped.
275
+ */
276
+ export function plainConfigValue(value) {
277
+ if (value === null || typeof value !== "object") return value;
278
+ if (VOLATILE_WRITE in value && typeof value.get === "function") {
279
+ return plainConfigValue(value.get());
280
+ }
281
+ if (Array.isArray(value)) return value.map(plainConfigValue);
282
+ const out = {};
283
+ for (const [key, child] of Object.entries(value)) out[key] = plainConfigValue(child);
284
+ return out;
285
+ }
286
+
230
287
  /**
231
288
  * The style row carrying one configuration.
232
289
  *
@@ -240,7 +297,7 @@ export const Config = z.object({
240
297
  * @returns {StyleRow} the row the web server renders into `<head>`.
241
298
  */
242
299
  function styleRow(resolved) {
243
- const cfg = normalizeConfig(resolved);
300
+ const cfg = normalizeConfig(plainConfigValue(resolved));
244
301
  // 0.2.0 ships the per-theme editor out; light and dark stay one value set
245
302
  // (the dark fields remain in the schema, dormant, for a future release).
246
303
  cfg[PER_THEME_FIELD] = false;
@@ -258,20 +315,47 @@ function styleRow(resolved) {
258
315
 
259
316
  /**
260
317
  * Register the settings section and keep the index in sync with it.
318
+ *
319
+ * TWO HOST DIALECTS. Up to DSH 0.1.5-rc.x the settings service registers a
320
+ * named section with a schema and reads it back by that name
321
+ * (`settings.installSection`), and the section name is what the browser half
322
+ * binds. From 0.1.7-alpha.x the service derives one form per composed entry and
323
+ * addresses it by the PROFILE ENTRY ID — there is no `installSection` at all,
324
+ * and an instance announces its page policy with `settings.configure` instead.
325
+ * Both are pure registrations that the browser half then reads through its own
326
+ * service, so which one ran does not change anything else here.
327
+ *
261
328
  * @param {object} ctx - host cordis context.
262
329
  * @param {unknown} config - the plugin entry's composition config (base layer).
263
330
  */
264
331
  export function apply(ctx, config) {
265
332
  let current = () => config;
266
333
  ctx.inject(["settings"], (settingsCtx) => {
267
- settingsCtx.settings.installSection(ctx, NAMESPACE, Config, config ?? {}, {
268
- setSource: (source) => {
269
- current = source;
270
- },
271
- // The row table is rebuilt on every index render and every worker boot
272
- // payload, so each read is already fresh; nothing to invalidate here.
273
- onChange: () => {},
274
- });
334
+ const settings = settingsCtx.settings;
335
+ if (settings && typeof settings.installSection === "function") {
336
+ settings.installSection(ctx, NAMESPACE, Config, config ?? {}, {
337
+ setSource: (source) => {
338
+ current = source;
339
+ },
340
+ // The row table is rebuilt on every index render and every worker boot
341
+ // payload, so each read is already fresh; nothing to invalidate here.
342
+ onChange: () => {},
343
+ });
344
+ return;
345
+ }
346
+ if (settings && typeof settings.configure === "function") {
347
+ // `auto: true` keeps the host's generated page as the fallback wherever
348
+ // no custom page of ours is reachable.
349
+ settingsCtx.effect(() => settings.configure({ auto: true }, ctx.fiber));
350
+ // This dialect has no registered reader: the entry's live, merged value
351
+ // is the fiber's own resolved config, which is what the settings service
352
+ // shows a form as well. The browser half adopts and rewrites the served
353
+ // element on activation either way, so this only decides the first frame.
354
+ current = () => {
355
+ const live = ctx.fiber && ctx.fiber.config;
356
+ return live === undefined || live === null ? config : live;
357
+ };
358
+ }
275
359
  });
276
360
  ctx.on("webserver/index-inject", (table) => {
277
361
  table.push(styleRow(current()));