@payload-onpage/core 0.2.1 → 0.3.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/dist/autosave.d.ts +2 -2
- package/dist/autosave.d.ts.map +1 -1
- package/dist/autosave.js +2 -1
- package/dist/autosave.js.map +1 -1
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -1
- package/dist/docref.d.ts +18 -3
- package/dist/docref.d.ts.map +1 -1
- package/dist/docref.js +13 -4
- package/dist/docref.js.map +1 -1
- package/dist/encode.d.ts +16 -4
- package/dist/encode.d.ts.map +1 -1
- package/dist/encode.js +42 -62
- package/dist/encode.js.map +1 -1
- package/dist/fields.d.ts +39 -8
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +111 -97
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -12
- package/dist/index.js.map +1 -1
- package/dist/path.d.ts +9 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +27 -8
- package/dist/path.js.map +1 -1
- package/dist/payload.d.ts +5 -3
- package/dist/payload.d.ts.map +1 -1
- package/dist/payload.js +34 -23
- package/dist/payload.js.map +1 -1
- package/dist/ports.d.ts +43 -28
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +11 -1
- package/dist/ports.js.map +1 -1
- package/dist/schema.d.ts +71 -26
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -6
- package/dist/schema.js.map +1 -1
- package/dist/seal.d.ts +17 -0
- package/dist/seal.d.ts.map +1 -1
- package/dist/seal.js +13 -0
- package/dist/seal.js.map +1 -1
- package/dist/session.d.ts +34 -18
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +20 -13
- package/dist/session.js.map +1 -1
- package/dist/stega.d.ts +6 -2
- package/dist/stega.d.ts.map +1 -1
- package/dist/stega.js +18 -13
- package/dist/stega.js.map +1 -1
- package/dist/util.d.ts +9 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +11 -0
- package/dist/util.js.map +1 -0
- package/package.json +1 -1
- package/src/autosave.ts +4 -3
- package/src/constants.ts +16 -0
- package/src/docref.ts +26 -6
- package/src/encode.ts +47 -55
- package/src/fields.ts +166 -98
- package/src/index.ts +125 -12
- package/src/path.ts +32 -10
- package/src/payload.ts +54 -26
- package/src/ports.ts +50 -22
- package/src/schema.ts +100 -17
- package/src/seal.ts +31 -0
- package/src/session.ts +67 -47
- package/src/stega.ts +25 -17
- package/src/util.ts +12 -0
package/src/session.ts
CHANGED
|
@@ -8,18 +8,20 @@
|
|
|
8
8
|
* touched. Publish, discard and reload run one at a time.
|
|
9
9
|
*/
|
|
10
10
|
import { AutosaveQueue, type SaveState } from './autosave.ts'
|
|
11
|
-
import { refKey, type DocRef } from './docref.ts'
|
|
11
|
+
import { refKey, type DocRef, type RefKey } from './docref.ts'
|
|
12
12
|
import { buildPatch, changedKeys } from './fields.ts'
|
|
13
13
|
import { setAt } from './path.ts'
|
|
14
14
|
import type { DataSource, Doc } from './ports.ts'
|
|
15
15
|
import type { DocumentSchema } from './schema.ts'
|
|
16
16
|
import { stegaCleanDeep } from './stega.ts'
|
|
17
|
+
import { errorText } from './util.ts'
|
|
17
18
|
|
|
18
19
|
export interface DocumentState {
|
|
19
|
-
key:
|
|
20
|
+
key: RefKey
|
|
20
21
|
ref: DocRef
|
|
21
22
|
status: 'loading' | 'ready' | 'error'
|
|
22
|
-
error
|
|
23
|
+
/** Why loading failed (status "error"). */
|
|
24
|
+
error?: string | undefined
|
|
23
25
|
schema: DocumentSchema | null
|
|
24
26
|
doc: Doc | null
|
|
25
27
|
save: SaveState
|
|
@@ -27,8 +29,19 @@ export interface DocumentState {
|
|
|
27
29
|
edited: boolean
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
/** A document that can be edited: loaded, with its schema and data. */
|
|
33
|
+
export type ReadyDocumentState = DocumentState & {
|
|
34
|
+
status: 'ready'
|
|
35
|
+
schema: DocumentSchema
|
|
36
|
+
doc: Doc
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Narrows a document's state to one that can be edited (instead of checking `schema && doc` by hand). */
|
|
40
|
+
export const isReady = (state: DocumentState | undefined): state is ReadyDocumentState =>
|
|
41
|
+
state?.status === 'ready' && !!state.schema && !!state.doc
|
|
42
|
+
|
|
30
43
|
export interface SessionSnapshot {
|
|
31
|
-
docs: Record<
|
|
44
|
+
docs: Record<RefKey, DocumentState>
|
|
32
45
|
canUndo: boolean
|
|
33
46
|
canRedo: boolean
|
|
34
47
|
/** A publish, discard or reload is running. */
|
|
@@ -36,44 +49,47 @@ export interface SessionSnapshot {
|
|
|
36
49
|
}
|
|
37
50
|
|
|
38
51
|
interface HistoryEntry {
|
|
39
|
-
key:
|
|
52
|
+
key: RefKey
|
|
40
53
|
before: Doc
|
|
41
54
|
after: Doc
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
export interface EditorSessionOptions {
|
|
45
58
|
dataSource: DataSource
|
|
46
|
-
locale?: string
|
|
59
|
+
locale?: string | undefined
|
|
47
60
|
/** Autosave debounce, ms. Default: the collection's autosave interval, else 800. */
|
|
48
|
-
autosaveDelay?: number
|
|
61
|
+
autosaveDelay?: number | undefined
|
|
49
62
|
/** Called after a document's edits were saved; `seq` increments per save. Renderers refresh here. */
|
|
50
|
-
onSaved?: (ref: DocRef, seq: number) => void
|
|
63
|
+
onSaved?: ((ref: DocRef, seq: number) => void) | undefined
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
export interface PublishResult {
|
|
54
67
|
ok: boolean
|
|
55
|
-
message?: string
|
|
68
|
+
message?: string | undefined
|
|
56
69
|
published: DocRef[]
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
/** Schemas are per collection or global, not per document. */
|
|
73
|
+
type SchemaKey = `${DocRef['kind']}:${string}`
|
|
74
|
+
const schemaKey = (ref: DocRef): SchemaKey => `${ref.kind}:${ref.slug}`
|
|
75
|
+
|
|
59
76
|
const HISTORY_LIMIT = 200
|
|
60
77
|
const IDLE: SaveState = { status: 'idle', seq: 0 }
|
|
61
|
-
const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e))
|
|
62
78
|
|
|
63
79
|
export class EditorSession {
|
|
64
80
|
readonly dataSource: DataSource
|
|
65
|
-
readonly locale
|
|
66
|
-
private docs = new Map<
|
|
67
|
-
private queues = new Map<
|
|
68
|
-
private updatedAt = new Map<
|
|
81
|
+
readonly locale: string | undefined
|
|
82
|
+
private docs = new Map<RefKey, DocumentState>()
|
|
83
|
+
private queues = new Map<RefKey, AutosaveQueue>()
|
|
84
|
+
private updatedAt = new Map<RefKey, string | undefined>()
|
|
69
85
|
/** Last value queued for saving, per document (diff base for the next save). */
|
|
70
|
-
private saved = new Map<
|
|
86
|
+
private saved = new Map<RefKey, Doc>()
|
|
71
87
|
/** Value before the current run of transient edits, per document. */
|
|
72
|
-
private anchors = new Map<
|
|
88
|
+
private anchors = new Map<RefKey, Doc>()
|
|
73
89
|
/** Documents whose value came from the page (client mode); a late server load must not replace it. */
|
|
74
|
-
private fromPage = new Set<
|
|
75
|
-
private loading = new Map<
|
|
76
|
-
private schemaCache = new Map<
|
|
90
|
+
private fromPage = new Set<RefKey>()
|
|
91
|
+
private loading = new Map<RefKey, Promise<void>>()
|
|
92
|
+
private schemaCache = new Map<SchemaKey, Promise<DocumentSchema>>()
|
|
77
93
|
private past: HistoryEntry[] = []
|
|
78
94
|
private future: HistoryEntry[] = []
|
|
79
95
|
private listeners = new Set<() => void>()
|
|
@@ -82,7 +98,7 @@ export class EditorSession {
|
|
|
82
98
|
private snapshot: SessionSnapshot = { docs: {}, canUndo: false, canRedo: false, busy: false }
|
|
83
99
|
private disposed = false
|
|
84
100
|
private readonly autosaveDelay: number | undefined
|
|
85
|
-
private readonly onSaved
|
|
101
|
+
private readonly onSaved: ((ref: DocRef, seq: number) => void) | undefined
|
|
86
102
|
|
|
87
103
|
constructor(opts: EditorSessionOptions) {
|
|
88
104
|
this.dataSource = opts.dataSource
|
|
@@ -110,14 +126,20 @@ export class EditorSession {
|
|
|
110
126
|
for (const fn of this.listeners) fn()
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
|
|
129
|
+
/** After a publish: no unpublished edits, and the local copy says so. */
|
|
130
|
+
private markPublished(key: RefKey) {
|
|
131
|
+
const doc = this.docs.get(key)?.doc
|
|
132
|
+
this.patchState(key, { edited: false, doc: doc ? { ...doc, _status: 'published' } : null })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private patchState(key: RefKey, patch: Partial<DocumentState>) {
|
|
114
136
|
const cur = this.docs.get(key)
|
|
115
137
|
if (!cur) return
|
|
116
138
|
this.docs.set(key, { ...cur, ...patch })
|
|
117
139
|
this.emit()
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
get(key:
|
|
142
|
+
get(key: RefKey): DocumentState | undefined {
|
|
121
143
|
return this.docs.get(key)
|
|
122
144
|
}
|
|
123
145
|
|
|
@@ -139,7 +161,7 @@ export class EditorSession {
|
|
|
139
161
|
|
|
140
162
|
// ── loading ─────────────────────────────────────────────────────────────
|
|
141
163
|
private schemaFor(ref: DocRef): Promise<DocumentSchema> {
|
|
142
|
-
const k =
|
|
164
|
+
const k = schemaKey(ref)
|
|
143
165
|
let p = this.schemaCache.get(k)
|
|
144
166
|
if (!p) {
|
|
145
167
|
p = this.dataSource.loadSchema(ref, { locale: this.locale })
|
|
@@ -202,7 +224,7 @@ export class EditorSession {
|
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
/** Try loading a document again after an error. */
|
|
205
|
-
retry(key:
|
|
227
|
+
retry(key: RefKey): Promise<void> {
|
|
206
228
|
const state = this.docs.get(key)
|
|
207
229
|
if (!state) return Promise.resolve()
|
|
208
230
|
if (state.status === 'error') return this.open(state.ref)
|
|
@@ -210,7 +232,7 @@ export class EditorSession {
|
|
|
210
232
|
return Promise.resolve()
|
|
211
233
|
}
|
|
212
234
|
|
|
213
|
-
private adopt(key:
|
|
235
|
+
private adopt(key: RefKey, raw: Doc) {
|
|
214
236
|
// A page rendered with auto-mapping markers may hand us its marked copy; edit clean values.
|
|
215
237
|
const doc = stegaCleanDeep(raw)
|
|
216
238
|
this.saved.set(key, doc)
|
|
@@ -219,10 +241,11 @@ export class EditorSession {
|
|
|
219
241
|
if (cur) this.docs.set(key, { ...cur, doc })
|
|
220
242
|
}
|
|
221
243
|
|
|
222
|
-
private queueFor(key:
|
|
244
|
+
private queueFor(key: RefKey): AutosaveQueue {
|
|
223
245
|
let q = this.queues.get(key)
|
|
224
246
|
if (q) return q
|
|
225
|
-
const state = this.docs.get(key)
|
|
247
|
+
const state = this.docs.get(key)
|
|
248
|
+
if (!state) throw new Error(`payload-onpage: ${key} is not open in this session`)
|
|
226
249
|
q = new AutosaveQueue({
|
|
227
250
|
delay: this.autosaveDelay ?? state.schema?.autosaveInterval ?? 800,
|
|
228
251
|
onState: (save) => {
|
|
@@ -243,7 +266,7 @@ export class EditorSession {
|
|
|
243
266
|
}
|
|
244
267
|
|
|
245
268
|
/** Schedule the editable top-level fields that changed since the last save. */
|
|
246
|
-
private queueChanges(key:
|
|
269
|
+
private queueChanges(key: RefKey, next: Doc) {
|
|
247
270
|
const schema = this.docs.get(key)?.schema
|
|
248
271
|
if (!schema) return
|
|
249
272
|
const keys = changedKeys(this.saved.get(key) ?? {}, next, schema.editable)
|
|
@@ -254,7 +277,7 @@ export class EditorSession {
|
|
|
254
277
|
|
|
255
278
|
// ── editing ─────────────────────────────────────────────────────────────
|
|
256
279
|
/** Change a document. `transient` updates the view only (live typing): no history, no save. */
|
|
257
|
-
apply(key:
|
|
280
|
+
apply(key: RefKey, fn: (doc: Doc) => Doc, opts: { transient?: boolean } = {}) {
|
|
258
281
|
const state = this.docs.get(key)
|
|
259
282
|
if (!state?.doc) return
|
|
260
283
|
const before = state.doc
|
|
@@ -276,12 +299,12 @@ export class EditorSession {
|
|
|
276
299
|
this.emit()
|
|
277
300
|
}
|
|
278
301
|
|
|
279
|
-
setValue(key:
|
|
302
|
+
setValue(key: RefKey, path: string, value: unknown, opts: { transient?: boolean } = {}) {
|
|
280
303
|
this.apply(key, (d) => setAt(d, path, value), opts)
|
|
281
304
|
}
|
|
282
305
|
|
|
283
306
|
/** Drop transient changes (Escape while typing). */
|
|
284
|
-
cancelTransient(key:
|
|
307
|
+
cancelTransient(key: RefKey) {
|
|
285
308
|
const anchor = this.anchors.get(key)
|
|
286
309
|
const state = this.docs.get(key)
|
|
287
310
|
if (!anchor || !state) return
|
|
@@ -290,7 +313,7 @@ export class EditorSession {
|
|
|
290
313
|
this.emit()
|
|
291
314
|
}
|
|
292
315
|
|
|
293
|
-
private restore(key:
|
|
316
|
+
private restore(key: RefKey, doc: Doc) {
|
|
294
317
|
const state = this.docs.get(key)
|
|
295
318
|
if (!state) return
|
|
296
319
|
this.anchors.delete(key)
|
|
@@ -328,14 +351,14 @@ export class EditorSession {
|
|
|
328
351
|
return state.edited || state.doc?._status === 'draft'
|
|
329
352
|
}
|
|
330
353
|
|
|
331
|
-
private selected(keys?:
|
|
354
|
+
private selected(keys?: RefKey[]): DocumentState[] {
|
|
332
355
|
return [...this.docs.values()].filter(
|
|
333
356
|
(s) => (!keys || keys.includes(s.key)) && s.doc && s.schema && this.unpublished(s),
|
|
334
357
|
)
|
|
335
358
|
}
|
|
336
359
|
|
|
337
360
|
/** Publish every document with unpublished changes (edited here, or saved as a draft before). */
|
|
338
|
-
publish(opts: { keys?:
|
|
361
|
+
publish(opts: { keys?: RefKey[] } = {}): Promise<PublishResult> {
|
|
339
362
|
return this.serial(async () => {
|
|
340
363
|
await this.flush()
|
|
341
364
|
const published: DocRef[] = []
|
|
@@ -372,11 +395,7 @@ export class EditorSession {
|
|
|
372
395
|
published,
|
|
373
396
|
}
|
|
374
397
|
if (res.updatedAt) this.updatedAt.set(key, res.updatedAt)
|
|
375
|
-
|
|
376
|
-
this.patchState(key, {
|
|
377
|
-
edited: false,
|
|
378
|
-
doc: now.doc ? { ...now.doc, _status: 'published' } : now.doc,
|
|
379
|
-
})
|
|
398
|
+
this.markPublished(key)
|
|
380
399
|
published.push(state.ref)
|
|
381
400
|
}
|
|
382
401
|
return { ok: true, published }
|
|
@@ -384,7 +403,12 @@ export class EditorSession {
|
|
|
384
403
|
}
|
|
385
404
|
|
|
386
405
|
/** Reset drafts with unpublished changes to their published versions (also clears a conflict). */
|
|
387
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Reset the drafts to their published values. Done as an edit, so it lands on
|
|
408
|
+
* the undo stack: undo right after a discard brings the changes back (and
|
|
409
|
+
* autosave saves them again).
|
|
410
|
+
*/
|
|
411
|
+
discard(opts: { keys?: RefKey[] } = {}): Promise<void> {
|
|
388
412
|
return this.serial(async () => {
|
|
389
413
|
await this.flush()
|
|
390
414
|
for (const { key, ref } of this.selected(opts.keys)) {
|
|
@@ -393,7 +417,7 @@ export class EditorSession {
|
|
|
393
417
|
this.dataSource.loadDocument(ref, { draft: true, locale: this.locale, depth: 2 }),
|
|
394
418
|
])
|
|
395
419
|
const state = this.docs.get(key)
|
|
396
|
-
if (!state
|
|
420
|
+
if (!isReady(state)) continue
|
|
397
421
|
// Continue from the server's latest draft (so the stale-write guard accepts the reset).
|
|
398
422
|
this.adopt(key, latest)
|
|
399
423
|
this.queues.get(key)?.reset()
|
|
@@ -401,17 +425,13 @@ export class EditorSession {
|
|
|
401
425
|
for (const k of state.schema.editable) next[k] = published[k]
|
|
402
426
|
this.apply(key, () => next)
|
|
403
427
|
await this.queues.get(key)?.flush()
|
|
404
|
-
|
|
405
|
-
this.patchState(key, {
|
|
406
|
-
edited: false,
|
|
407
|
-
doc: now.doc ? { ...now.doc, _status: 'published' } : now.doc,
|
|
408
|
-
})
|
|
428
|
+
this.markPublished(key)
|
|
409
429
|
}
|
|
410
430
|
})
|
|
411
431
|
}
|
|
412
432
|
|
|
413
433
|
/** After a conflict: load the latest draft of a document and continue from it (local edits are dropped). */
|
|
414
|
-
reload(key:
|
|
434
|
+
reload(key: RefKey): Promise<void> {
|
|
415
435
|
return this.serial(async () => {
|
|
416
436
|
const state = this.docs.get(key)
|
|
417
437
|
if (!state) return
|
package/src/stega.ts
CHANGED
|
@@ -14,32 +14,33 @@
|
|
|
14
14
|
* All of these render with zero width and are ignored by `trim()`-insensitive
|
|
15
15
|
* layout; they are stripped before anything is written back to Payload.
|
|
16
16
|
*/
|
|
17
|
+
import { isRefKey, type RefKey } from './docref.ts'
|
|
17
18
|
|
|
18
|
-
export
|
|
19
|
+
export const MARKER_KINDS = ['text', 'textarea', 'richtext', 'media'] as const
|
|
20
|
+
export type MarkerKind = (typeof MARKER_KINDS)[number]
|
|
19
21
|
|
|
20
22
|
export interface Marker {
|
|
21
|
-
ref:
|
|
23
|
+
ref: RefKey
|
|
22
24
|
path: string
|
|
23
25
|
kind: MarkerKind
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
/** The four invisible code points that carry base-4 digits, in digit order. */
|
|
29
|
+
const DIGITS = '\u200b\u200c\u200d\u2060'
|
|
27
30
|
const START = '\u2063\u2062'
|
|
28
31
|
const END = '\u2064'
|
|
29
|
-
const KIND_CODE
|
|
32
|
+
const KIND_CODE = {
|
|
30
33
|
text: 't',
|
|
31
34
|
textarea: 'a',
|
|
32
35
|
richtext: 'r',
|
|
33
36
|
media: 'm',
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
m: 'media',
|
|
40
|
-
}
|
|
37
|
+
} as const satisfies Record<MarkerKind, string>
|
|
38
|
+
/** Derived from KIND_CODE, so the two can't disagree. */
|
|
39
|
+
const CODE_KIND = new Map<string, MarkerKind>(
|
|
40
|
+
(Object.entries(KIND_CODE) as Array<[MarkerKind, string]>).map(([kind, code]) => [code, kind]),
|
|
41
|
+
)
|
|
41
42
|
|
|
42
|
-
const DIGIT_VALUE = new Map<string, number>(DIGITS.map((c, i) => [c, i]))
|
|
43
|
+
const DIGIT_VALUE = new Map<string, number>([...DIGITS].map((c, i) => [c, i]))
|
|
43
44
|
// The codec's characters are single invisible code points (not combining sequences).
|
|
44
45
|
// eslint-disable-next-line no-misleading-character-class
|
|
45
46
|
const MARKER_RE = /\u2063\u2062([\u200b\u200c\u200d\u2060]*)\u2064/g
|
|
@@ -53,7 +54,11 @@ export function encodeMarker(marker: Marker): string {
|
|
|
53
54
|
const bytes = encoder.encode(`${marker.ref}|${marker.path}|${KIND_CODE[marker.kind]}`)
|
|
54
55
|
let body = ''
|
|
55
56
|
for (const b of bytes)
|
|
56
|
-
body +=
|
|
57
|
+
body +=
|
|
58
|
+
DIGITS.charAt((b >> 6) & 3) +
|
|
59
|
+
DIGITS.charAt((b >> 4) & 3) +
|
|
60
|
+
DIGITS.charAt((b >> 2) & 3) +
|
|
61
|
+
DIGITS.charAt(b & 3)
|
|
57
62
|
return START + body + END
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -63,7 +68,7 @@ function decodeBody(body: string): Marker | null {
|
|
|
63
68
|
for (let i = 0; i < bytes.length; i++) {
|
|
64
69
|
let b = 0
|
|
65
70
|
for (let j = 0; j < 4; j++) {
|
|
66
|
-
const v = DIGIT_VALUE.get(body
|
|
71
|
+
const v = DIGIT_VALUE.get(body.charAt(i * 4 + j))
|
|
67
72
|
if (v === undefined) return null
|
|
68
73
|
b = (b << 2) | v
|
|
69
74
|
}
|
|
@@ -76,11 +81,14 @@ function decodeBody(body: string): Marker | null {
|
|
|
76
81
|
return null
|
|
77
82
|
}
|
|
78
83
|
const [ref, path, code, ...extra] = text.split('|')
|
|
79
|
-
const kind = CODE_KIND
|
|
80
|
-
if (!ref || !path || !kind || extra.length) return null
|
|
84
|
+
const kind = code === undefined ? undefined : CODE_KIND.get(code)
|
|
85
|
+
if (!ref || !isRefKey(ref) || !path || !kind || extra.length) return null
|
|
81
86
|
return { ref, path, kind }
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
/** Does this string carry a marker? (A cheap test before decoding, e.g. while scanning a page.) */
|
|
90
|
+
export const hasStegaMarker = (value: string): boolean => value.includes(START)
|
|
91
|
+
|
|
84
92
|
/** Append a marker to a string (an already-marked string is re-marked, not doubled). */
|
|
85
93
|
export function stegaEncode(value: string, marker: Marker): string {
|
|
86
94
|
return stegaClean(value) + encodeMarker(marker)
|
|
@@ -91,7 +99,7 @@ export function stegaDecode(value: string): Marker[] {
|
|
|
91
99
|
const out: Marker[] = []
|
|
92
100
|
if (!value) return out
|
|
93
101
|
for (const m of value.matchAll(MARKER_RE)) {
|
|
94
|
-
const marker = decodeBody(m[1])
|
|
102
|
+
const marker = decodeBody(m[1] ?? '')
|
|
95
103
|
if (marker) out.push(marker)
|
|
96
104
|
}
|
|
97
105
|
return out
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Small type-level and error helpers shared by every package. */
|
|
2
|
+
|
|
3
|
+
/** A thrown value's message (anything can be thrown, not only `Error`s). */
|
|
4
|
+
export const errorText = (e: unknown): string => (e instanceof Error ? e.message : String(e))
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Exhaustiveness check: call in the `default` of a switch over a union. Adding a
|
|
8
|
+
* member to the union then fails to compile until every switch handles it.
|
|
9
|
+
*/
|
|
10
|
+
export function assertNever(value: never, what = 'value'): never {
|
|
11
|
+
throw new Error(`Unexpected ${what}: ${JSON.stringify(value)}`)
|
|
12
|
+
}
|