-
Notifications
You must be signed in to change notification settings - Fork 2
/
Bookmarks.lhs
848 lines (705 loc) · 26.2 KB
/
Bookmarks.lhs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
Simplified-Bookmarks-Spec
===
## Overview
The contents of this repository define a specification describing the format
of bookmark data shared between clients and server in the Library Simplified
ecosystem. The intention is to declare a common format for bookmarks that
clients on different platforms (Web, iOS, Android) can use to synchronize
reading positions. The specification is described as executable Literate
Haskell and can be executed and inspected directly using ghci.
```
$ ghci -W -Wall -Werror -pgmL markdown-unlit Bookmarks.lhs
```
## Typographic Conventions
Within this document, commands given at the GHCI prompt are prefixed
with `*Bookmarks>` to indicate that the commands are being executed within
the `Bookmarks` module.
The main specification definitions are given in the [Bookmarks](Bookmarks.lhs) module:
```haskell
{-# LANGUAGE Haskell2010, ExplicitForAll #-}
module Bookmarks where
import qualified Data.Map as DM
```
The specification makes references to [RFC 3986 URI](https://tools.ietf.org/html/rfc3986)
values. Within this specification, URIs are treated as opaque strings.
```haskell
type URI = String
```
## Terminology
* User: A human (typically a library patron) using one or more of the
Library Simplified applications.
* Client: An application running on a user's device. This can refer to
native applications such as [SimplyE](https://github.com/NYPL-Simplified/Simplified-iOS),
or the [web-based interface](https://github.com/NYPL-Simplified/circulation-patron-web).
* Bookmark: A stored position within a publication that can be used to
navigate to that position at a later date.
## Web Annotations
The base format for bookmark data is the W3C [Web Annotations](https://www.w3.org/annotation/)
format. The bookmark data described in this specification is expressed in terms
of an _annotation_ with a set of strictly-defined required and optional fields.
## Compatibility
Historically, the Library Simplified applications have not had a consistent
standard with regard to how bookmarks are serialized. Applications MAY
accept bookmarks in older formats, but MUST serialize all new bookmarks
using the format described here. This allows for a degree of migration
compatibility; over time, all bookmarks in circulation will effectively be
converted to the new format.
## Locators
A _Locator_ uniquely identifies a position within a book. There are specific
types of locators tailored to specific reading contexts and book formats, because
each of those contexts typically has a different means to specify locations
within books.
A _Locator_ is one of the following:
* [LocatorLegacyCFI](#locatorlegacycfi)
* [LocatorHrefProgression](#locatorhrefprogression)
* [LocatorPage](#locatorpage)
* [LocatorAudioBookTime](#locatoraudiobooktime)
```haskell
data Locator
= L_CFI LocatorLegacyCFI
| L_HrefProgression LocatorHrefProgression
| L_Page LocatorPage
| L_AudioBookTime LocatorAudioBookTime
deriving (Eq, Ord, Show)
```
### Chapter Progression
A _progression_ value is a real number in the range `[0, 1]` where `0` is the
beginning of a chapter, and `1` is the end of the chapter.
```haskell
data Progression
= Progression Double
deriving (Eq, Ord, Show)
progression :: Double -> Progression
progression x =
if (x >= 0.0 && x <= 1.0)
then Progression x
else error "Progression must be in the range [0,1]"
```
### LocatorLegacyCFI
A `LocatorLegacyCFI` value consists of a set of properties used to express
[content fragment identifiers](http://idpf.org/epub/linking/cfi/epub-cfi.html),
such as those frequently consumed by the [Readium 1](https://readium.org/development/readium-sdk-overview/) reader.
There is very little consistency in the values consumed by Library Simplified
applications between platforms, hence the _legacy_ status of this locator type
and the optional fields. Applications are encouraged to attempt to write a
non-`Nothing` value to at least one of the fields.
The `lcIdRef` property refers to the `id` value of the _spine item_ of the
target [EPUB](http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm). This, in
practice, is the `idRef` value returned by Readium 1.
The `lcContentCFI` property refers to the _content fragment identifier_ used
to point to a specific element within the specified _spine item_.
```haskell
data LocatorLegacyCFI = LocatorLegacyCFI {
lcIdRef :: Maybe String,
lcContentCFI :: Maybe String,
lcChapterProgression :: Maybe Progression
} deriving (Eq, Ord, Show)
```
### LocatorHrefProgression
A `LocatorHrefProgression`
consists of a [URI](https://tools.ietf.org/html/rfc3986) that uniquely
identifies a chapter within a publication, and a _progression_ value.
`LocatorHrefProgression` values are used to describe the positions of books
being consumed in the [Readium 2](https://readium.org/technical/r2-toc/) reader
and are expected to be the preferred form for sharing book locations for the
forseeable future.
```haskell
data LocatorHrefProgression = LocatorHrefProgression {
hpChapterHref :: URI,
hpChapterProgression :: Progression
} deriving (Eq, Ord, Show)
```
### LocatorPage
A `LocatorPage` consists of a single integer value that uniquely identifies
a page within an integer page-based publication such as PDF.
A `Page` number must be non-negative.
```haskell
data Page
= Page Integer
deriving (Eq, Ord, Show)
page :: Integer -> Page
page x =
if (x >= 0)
then Page x
else error "Page must be in non-negative"
data LocatorPage = LocatorPage {
ipPage :: Page
} deriving (Eq, Ord, Show)
```
### LocatorAudioBookTime
A `LocatorAudioBookTime` consists of a _part_ and _chapter_ number, and a time
in milliseconds. This is expected to uniquely identify a position within an
audio book.
`Part` and `Chapter` numbers must be non-negative, as must `TimeMilliseconds` values.
```haskell
data Part
= Part Integer
deriving (Eq, Ord, Show)
data Chapter
= Chapter Integer
deriving (Eq, Ord, Show)
data Title
= Title String
deriving (Eq, Ord, Show)
data AudiobookID
= AudiobookID String
deriving (Eq, Ord, Show)
data Duration
= Duration Integer
deriving (Eq, Ord, Show)
data TimeMilliseconds
= TimeMilliseconds Integer
deriving (Eq, Ord, Show)
part :: Integer -> Part
part x =
if (x >= 0)
then Part x
else error "Part must be in non-negative"
chapter :: Integer -> Chapter
chapter x =
if (x >= 0)
then Chapter x
else error "Chapter must be in non-negative"
duration :: Integer -> Duration
duration x =
if (x >= 0)
then Duration x
else error "Duration must be in non-negative"
time :: Integer -> TimeMilliseconds
time x =
if (x >= 0)
then TimeMilliseconds x
else error "TimeMilliseconds must be in non-negative"
data LocatorAudioBookTime = LocatorAudioBookTime {
abtPart :: Part,
abtChapter :: Chapter,
abtTitle :: Title,
abtAudiobookID :: AudiobookID,
abtDuration :: Duration,
abtTime :: TimeMilliseconds
} deriving (Eq, Ord, Show)
```
#### Interpretation
Audiobook players differ in their support for `part` values. Some manifests will not contain `part` numbers,
whilst other manifests are provided to players that actually require them in order to work at all. Manifests
that represent _Findaway_ audiobooks, for example, include both `findaway:part` and `findaway:sequence` values in
each entry of the manifest's `readingOrder`, and the _Findaway_ player cannot work without access to these
values. Other manifest formats do not include `part` and `chapter` numbers at all, and simply assume that players
will walk through the list of chapters in manifest declaration order. This raises the question of how the
`abtPart` and `abtChapter` fields in `LocatorAudioBookTime` values should be interpreted when loaded into
an arbitrary audiobook player.
For _Findaway_ audiobooks, the `abtPart` and `abtChapter` fields for a serialized locator should be equal to
the `findaway:part` and `findaway:sequence` fields, respectively, of the `readingOrder` manifest element that
was active when the locator was serialized.
For all other audiobooks, the `abtPart` field should be `0`, and the `abtChapter` field should be equal to the
index of the `readingOrder` manifest element that was active when the locator was serialized.
When loading a locator value `L` in a _Findaway_ player, search for a `readingOrder` element that contains
a `findaway:part` and `findaway:sequence` value equal to the `L.abtPart` and `L.abtChapter` fields, respectively.
```pseudocode
LocatorAudioBookTime L;
for (element in readingOrder) {
if (element.part == L.abtPart && element.chapter == L.abtChapter) {
openForReading (element);
return;
}
}
throw ErrorNoSuchChapter();
```
When loading a locator value `L` in any other player, use `readingOrder[L.abtChapter]`.
```pseudocode
LocatorAudioBookTime L;
if (L.abtChapter < readingOrder.size) {
openForReading (readingOrder [L.abtChapter]);
return;
}
throw ErrorNoSuchChapter();
```
### Serialization
Locators _MUST_ be serialized using the following [JSON schema](locatorSchema.json):
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:org.librarysimplified.bookmarks:locator:1.0",
"title": "Simplified Bookmark Locator",
"description": "A bookmark locator",
"type": "object",
"oneOf": [
{
"type": "object",
"properties": {
"@type": {
"description": "The type of locator",
"type": "string",
"pattern": "LocatorHrefProgression"
},
"href": {
"description": "The unique identifier for a chapter (hpChapterHref)",
"type": "string"
},
"progressWithinChapter": {
"description": "The progress within a chapter (hpChapterProgression)",
"type": "number",
"minimum": 0.0,
"maximum": 1.0
}
},
"required": [
"@type",
"href",
"progressWithinChapter"
]
},
{
"type": "object",
"properties": {
"@type": {
"description": "The type of locator",
"type": "string",
"pattern": "LocatorLegacyCFI"
},
"idref": {
"description": "The unique identifier for a chapter (lcIdRef)",
"type": "string"
},
"contentCFI": {
"description": "The content fragment identifier (lcContentCFI)",
"type": "string"
},
"progressWithinChapter": {
"description": "The progress within a chapter (lcChapterProgression)",
"type": "number",
"minimum": 0.0,
"maximum": 1.0
}
},
"required": [
"@type"
]
},
{
"type": "object",
"properties": {
"@type": {
"description": "The type of locator",
"type": "string",
"pattern": "LocatorPage"
},
"page": {
"description": "The integer page number (ipPage)",
"type": "number",
"minimum": 0
}
},
"required": [
"@type",
"page"
]
},
{
"type": "object",
"properties": {
"@type": {
"description": "The type of locator",
"type": "string",
"pattern": "LocatorAudioBookTime"
},
"part": {
"description": "The part number (abtPart)",
"type": "number",
"minimum": 0
},
"chapter": {
"description": "The chapter number (abtChapter)",
"type": "number",
"minimum": 0
},
"title": {
"description": "The title (abtTitle)",
"type": "string"
},
"audiobookID": {
"description": "The audiobook ID (abtAudiobookID)",
"type": "string"
},
"duration": {
"description": "The duration (abtDuration)",
"type": "number",
"minimum": 0
}
"time": {
"description": "The time (abtTime)",
"type": "number",
"minimum": 0
}
},
"required": [
"@type",
"part",
"chapter",
"title",
"audiobookID",
"duration",
"time"
]
}
]
}
```
A [LocatorHrefProgression](#locatorhrefprogression) value MUST be serialized
using the schema with `@type = LocatorHrefProgression`.
A [LocatorLegacyCFI](#locatorlegacycfi) value MUST be serialized using the
schema with `@type = LocatorLegacyCFI`.
A [LocatorPage](#locatorpage) value MUST be serialized using the
schema with `@type = LocatorPage`.
A [LocatorAudioBookTime](#locatoraudiobooktime) value MUST be serialized using the
schema with `@type = LocatorAudioBookTime`.
When encountering a locator without a `@type` property, applications SHOULD
assume that the format is `LocatorLegacyCFI` and parse it accordingly.
#### Examples
An example of a valid, serialized locator is given in [valid-locator-0.json](valid-locator-0.json):
```json
{
"@type": "LocatorHrefProgression",
"href": "/xyz.html",
"progressWithinChapter": 0.666
}
```
An example of a valid, serialized locator is given in [valid-locator-1.json](valid-locator-1.json):
```json
{
"@type": "LocatorLegacyCFI",
"idref": "xyz-html",
"contentCFI": "/4/2/2/2",
"progressWithinChapter": 0.25
}
```
An example of a valid, serialized locator is given in [valid-locator-2.json](valid-locator-2.json):
```json
{
"@type": "LocatorPage",
"page": 23
}
```
An example of a valid, serialized locator is given in [valid-locator-3.json](valid-locator-3.json):
```json
{
"@type": "LocatorAudioBookTime",
"part": 3,
"chapter": 32,
"title": "Chapter title",
"audiobookID": "urn:uuid:b309844e-7d4e-403e-945b-fbc78acd5e03",
"duration": 190000,
"time": 78000
}
```
## Bookmarks
A _Bookmark_ is a Web Annotation with the following data:
* A [body](#bodies) containing optional metadata such as the reader's current
progress through the entire publication.
* A [motivation](#motivations) indicating the type of bookmark.
* A [target](#targets) that uniquely identifies the publication, and includes
a _selector_ that includes a serialized [Locator](#locators).
* An optional _id_ value that uniquely identifies the bookmark. This
is typically assigned by the server, and a server publishing bookmarks
to a client MUST include this value in each bookmark.
```haskell
data Bookmark = Bookmark {
bookmarkId :: Maybe URI,
bookmarkTarget :: BookmarkTarget,
bookmarkMotivation :: Motivation,
bookmarkBody :: BookmarkBody
} deriving (Eq, Show)
```
### Bodies
A _body_ contains metadata that applications _MAY_ use to derive extra data for
display in the application. Currently, bodies are defined as simple maps of
strings to strings with a couple of extra mandatory fields.
```haskell
data BookmarkBody = BookmarkBody {
bodyDeviceId :: String,
bodyTime :: String,
bodyOthers :: DM.Map String String
} deriving (Eq, Show)
```
The `bodyTime` field _MUST_ contain an [RFC 3339](https://tools.ietf.org/html/rfc3339)
timestamp indicating the creation time of the bookmark. The timestamp _MUST_
be in the [UTC](https://en.wikipedia.org/wiki/Coordinated_Universal_Time)
time zone.
The `bodyDeviceId` field denotes the unique identifier of the device that
created the bookmark. This is typically a [UUID](https://tools.ietf.org/html/rfc4122)
value expressed as a [URN](https://tools.ietf.org/html/rfc3986), such as:
```
urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c
```
Clients that do not have access to an identifier in this form _SHOULD_
use a string value of `null`. Note that this does mean serializing the
literal value `null` as a quoted string:
```
{
...
"http://librarysimplified.org/terms/device" = "null",
...
}
```
### Targets
A _target_ uniquely identifies a publication, and uses a [Locator](#locators)
to uniquely identify a position within that publication. The value of the
`targetSource` field is typically taken from metadata included in the publication,
or from the OPDS feed that originally delivered the publication.
```haskell
data BookmarkTarget = BookmarkTarget {
targetLocator :: Locator,
targetSource :: String
} deriving (Eq, Show)
```
### Motivations
A _motivation_ is value that simply indicates whether a bookmark was
created explicitly by the user, or created implicitly by the application
each time the user navigates to a new page. Explicitly created bookmarks
are denoted by the _bookmarking_ motivation, whilst implicitly created bookmarks
are denoted by the _idling_ motivation. In practice, there is exactly one
_idling_ bookmark in the user's set of bookmarks at any given time, and
the reading application effectively replaces the current _idling_ bookmark
each time the user turns a page in a given publication.
```haskell
data Motivation
= Bookmarking
| Idling
deriving (Eq, Ord, Show)
```
### JSON Serialization
Bookmarks _MUST_ be serialized as Web Annotation values according to
the following rules:
* [Body](#bodies) values _MUST_ be serialized as string-typed properties
with string-typed values in the annotation's `body` property, with the
following extra constraints:
* The `bodyDeviceId` field _MUST_ be serialized as string-typed property with
the name `http://librarysimplified.org/terms/device`.
* The `bodyTime` field _MUST_ be serialized as string-typed property with
the name `http://librarysimplified.org/terms/time`.
* [Motivation](#motivations) values _MUST_ be serialized as one of
the two possible string values according to the `motivationJSON` function:
```haskell
motivationJSON :: Motivation -> String
motivationJSON Bookmarking = "http://www.w3.org/ns/oa#bookmarking"
motivationJSON Idling = "http://librarysimplified.org/terms/annotation/idling"
```
* [Target](#targets) values _MUST_ be serialized with:
* A `selector` property containing an object with:
* A `type` property equal to `"oa:FragmentSelector"`.
* A `value` property containing a [Locator](#locators) serialized as a string value.
* A `source` property with a string value that uniquely identifies the publication.
If present, the bookmark's `id` field _MUST_ be serialized as an `id`
property with a string value equal to the `id` field.
The bookmark _SHOULD_ be serialized with a `type` property set to the string
value `"Annotation"`, and a `@context` property set to the string
`"http://www.w3.org/ns/anno.jsonld"`.
An example of a valid bookmark is given in [valid-bookmark-0.json](valid-bookmark-0.json):
```json
{
"@context": "http://www.w3.org/ns/anno.jsonld",
"type": "Annotation",
"id": "urn:uuid:715885bc-23d3-4d7d-bd87-f5e7a042c4ba",
"body": {
"http://librarysimplified.org/terms/time": "2021-03-12T16:32:49Z",
"http://librarysimplified.org/terms/device": "urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c"
},
"motivation": "http://librarysimplified.org/terms/annotation/idling",
"target": {
"selector": {
"type": "oa:FragmentSelector",
"value": "{\n \"@type\": \"LocatorHrefProgression\",\n \"href\": \"/xyz.html\",\n \"progressWithinChapter\": 0.666\n}\n"
},
"source": "urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0"
}
}
```
## Test Cases
This specification includes a number of test cases. Applications MUST include
unit tests that give the results specified below for each test case, and MUST
succeed or fail for the reasons specified. For tests cases that must succeed,
their required interpretation is listed below.
|File|Type|Result|Reason|
|----|----|------|------|
|[invalid-bookmark-0.json](invalid-bookmark-0.json)|bookmark|❌ failure|Missing a body|
|[invalid-bookmark-1.json](invalid-bookmark-1.json)|bookmark|❌ failure|Missing a motivation|
|[invalid-bookmark-2.json](invalid-bookmark-2.json)|bookmark|❌ failure|Missing a target|
|[invalid-bookmark-3.json](invalid-bookmark-3.json)|bookmark|❌ failure|Target selector has an invalid type|
|[invalid-bookmark-4.json](invalid-bookmark-4.json)|bookmark|❌ failure|Target selector has an invalid value|
|[invalid-bookmark-5.json](invalid-bookmark-5.json)|bookmark|❌ failure|Body lacks device ID property|
|[invalid-bookmark-6.json](invalid-bookmark-6.json)|bookmark|❌ failure|Body lacks time property|
|[invalid-bookmark-7.json](invalid-bookmark-7.json)|bookmark|❌ failure|Target selector lacks page|
|[invalid-locator-1.json](invalid-locator-1.json)|locator|❌ failure|Missing href property|
|[invalid-locator-2.json](invalid-locator-2.json)|locator|❌ failure|Missing progressWithinChapter property|
|[invalid-locator-3.json](invalid-locator-3.json)|locator|❌ failure|Chapter progression is negative|
|[invalid-locator-4.json](invalid-locator-4.json)|locator|❌ failure|Chapter progression is greater than 1.0|
|[invalid-locator-5.json](invalid-locator-5.json)|locator|❌ failure|Chapter number is negative|
|[invalid-locator-6.json](invalid-locator-6.json)|locator|❌ failure|Page number is negative|
|[valid-bookmark-0.json](valid-bookmark-0.json)|bookmark|✅ success|Valid bookmark|
|[valid-bookmark-1.json](valid-bookmark-1.json)|bookmark|✅ success|Valid bookmark|
|[valid-bookmark-2.json](valid-bookmark-2.json)|bookmark|✅ success|Valid bookmark|
|[valid-bookmark-3.json](valid-bookmark-3.json)|bookmark|✅ success|Valid bookmark|
|[valid-bookmark-4.json](valid-bookmark-4.json)|bookmark|✅ success|Valid bookmark|
|[valid-bookmark-5.json](valid-bookmark-5.json)|bookmark|✅ success|Valid bookmark|
|[valid-locator-0.json](valid-locator-0.json)|locator|✅ success|Valid locator|
|[valid-locator-1.json](valid-locator-1.json)|locator|✅ success|Valid locator|
|[valid-locator-2.json](valid-locator-2.json)|locator|✅ success|Valid locator|
|[valid-locator-3.json](valid-locator-3.json)|locator|✅ success|Valid locator|
### valid-bookmark-0.json
```haskell
validBookmark0 :: Bookmark
validBookmark0 = Bookmark {
bookmarkId = Just "urn:uuid:715885bc-23d3-4d7d-bd87-f5e7a042c4ba",
bookmarkBody = BookmarkBody {
bodyDeviceId = "urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c",
bodyTime = "2021-03-12T16:32:49Z",
bodyOthers = DM.empty
},
bookmarkMotivation = Idling,
bookmarkTarget = BookmarkTarget {
targetLocator = L_HrefProgression $ LocatorHrefProgression {
hpChapterHref = "/xyz.html",
hpChapterProgression = progression 0.666
},
targetSource = "urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0"
}
}
```
### valid-bookmark-1.json
```haskell
validBookmark1 :: Bookmark
validBookmark1 = Bookmark {
bookmarkId = Nothing,
bookmarkBody = BookmarkBody {
bodyDeviceId = "urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c",
bodyTime = "2021-03-12T16:32:49Z",
bodyOthers = DM.empty
},
bookmarkMotivation = Idling,
bookmarkTarget = BookmarkTarget {
targetLocator = L_HrefProgression $ LocatorHrefProgression {
hpChapterHref = "/xyz.html",
hpChapterProgression = progression 0.666
},
targetSource = "urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0"
}
}
```
### valid-bookmark-2.json
```haskell
validBookmark2 :: Bookmark
validBookmark2 = Bookmark {
bookmarkId = Nothing,
bookmarkBody = BookmarkBody {
bodyDeviceId = "urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c",
bodyTime = "2021-03-12T16:32:49Z",
bodyOthers = DM.empty
},
bookmarkMotivation = Bookmarking,
bookmarkTarget = BookmarkTarget {
targetLocator = L_HrefProgression $ LocatorHrefProgression {
hpChapterHref = "/xyz.html",
hpChapterProgression = progression 0.666
},
targetSource = "urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0"
}
}
```
### valid-bookmark-3.json
```haskell
validBookmark3 :: Bookmark
validBookmark3 = Bookmark {
bookmarkId = Just "urn:uuid:715885bc-23d3-4d7d-bd87-f5e7a042c4ba",
bookmarkBody = BookmarkBody {
bodyDeviceId = "urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c",
bodyTime = "2021-03-12T16:32:49Z",
bodyOthers = DM.empty
},
bookmarkMotivation = Bookmarking,
bookmarkTarget = BookmarkTarget {
targetLocator = L_HrefProgression $ LocatorHrefProgression {
hpChapterHref = "/xyz.html",
hpChapterProgression = progression 0.666
},
targetSource = "urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0"
}
}
```
### valid-bookmark-4.json
```haskell
validBookmark4 :: Bookmark
validBookmark4 = Bookmark {
bookmarkId = Just "urn:uuid:715885bc-23d3-4d7d-bd87-f5e7a042c4ba",
bookmarkBody = BookmarkBody {
bodyChapter = "Chapter title",
bodyDeviceId = "urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c",
bodyTime = "2022-06-27T12:47:49Z"
},
bookmarkMotivation = Bookmarking,
bookmarkTarget = BookmarkTarget {
targetLocator = L_ABT $ LocatorAudioBookTime {
hpTitle = "Chapter title",
hpAudiobookID = "urn:uuid:b309844e-7d4e-403e-945b-fbc78acd5e03",
hpChapter = chapter 32,
hpDuration = duration 190000,
hpTime = time 78000,
hpPart = part 3
},
targetSource = "urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0"
}
}
```
### valid-bookmark-5.json
```haskell
validBookmark5 :: Bookmark
validBookmark5 = Bookmark {
bookmarkId = Just "urn:uuid:715885bc-23d3-4d7d-bd87-f5e7a042c4ba",
bookmarkBody = BookmarkBody {
bodyDeviceId = "urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c",
bodyTime = "2022-08-05T16:32:49Z"
},
bookmarkMotivation = Bookmarking,
bookmarkTarget = BookmarkTarget {
targetLocator = L_P $ LocatorPage {
hpPage = page 2
},
targetSource = "urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0"
}
}
```
### valid-locator-0.json
```haskell
validLocator0 :: Locator
validLocator0 = L_HrefProgression $ LocatorHrefProgression {
hpChapterHref = "/xyz.html",
hpChapterProgression = progression 0.666
}
```
### valid-locator-1.json
```haskell
validLocator1 :: Locator
validLocator1 = L_CFI $ LocatorLegacyCFI {
lcIdRef = Just "xyz-html",
lcContentCFI = Just "/4/2/2/2",
lcChapterProgression = Just $ progression 0.25
}
```
### valid-locator-2.json
```haskell
validLocator2 :: Locator
validLocator2 = L_P $ LocatorPage {
lcPage = Just $ page 2
}
```
### valid-locator-3.json
```haskell
validLocator3 :: Locator
validLocator3 = L_ABT$ LocatorAudioBookTime {
lcPart = Just $ part 3,
lcChapter = Just $ chapter 32,
lcTitle = Just "Chapter title",
lcAudiobookID = Just "urn:uuid:b309844e-7d4e-403e-945b-fbc78acd5e03",
lcDuration = Just $ duration 190000,
lcTime = Just $ time 78000
}
```