Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7026cb8

Browse files
committedFeb 1, 2025
add TDMRep support for WebPub and EPUB
1 parent f46a800 commit 7026cb8

File tree

10 files changed

+222
-4
lines changed

10 files changed

+222
-4
lines changed
 

‎pkg/manifest/metadata.go

+16
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Metadata struct {
3030
LocalizedSubtitle *LocalizedString `json:"subtitle,omitempty"`
3131
LocalizedSortAs *LocalizedString `json:"sortAs,omitempty"`
3232
Accessibility *A11y `json:"accessibility,omitempty"`
33+
TDM *TDM `json:"tdm,omitempty"`
3334
Modified *time.Time `json:"modified,omitempty"`
3435
Published *time.Time `json:"published,omitempty"`
3536
Languages Strings `json:"language,omitempty" validate:"BCP47"` // TODO validator
@@ -166,6 +167,7 @@ func MetadataFromJSON(rawJson map[string]interface{}) (*Metadata, error) {
166167
return nil, errors.Wrap(err, "failed parsing 'title'")
167168
}
168169

170+
// Accessibility
169171
var a11y *A11y
170172
if a11yJSON, ok := rawJson["accessibility"].(map[string]interface{}); ok {
171173
a11y, err = A11yFromJSON(a11yJSON)
@@ -174,11 +176,21 @@ func MetadataFromJSON(rawJson map[string]interface{}) (*Metadata, error) {
174176
}
175177
}
176178

179+
// TDMRep (Text & Data Mining Reservation Protocol)
180+
var tdm *TDM
181+
if tdmJSON, ok := rawJson["tdm"].(map[string]interface{}); ok {
182+
tdm, err = TDMFromJSON(tdmJSON)
183+
if err != nil {
184+
return nil, errors.Wrap(err, "failed parsing 'tdm'")
185+
}
186+
}
187+
177188
metadata := &Metadata{
178189
Identifier: parseOptString(rawJson["identifier"]),
179190
Type: parseOptString(rawJson["@type"]),
180191
LocalizedTitle: *title,
181192
Accessibility: a11y,
193+
TDM: tdm,
182194
Modified: parseOptTime(rawJson["modified"]),
183195
Published: parseOptTime(rawJson["published"]),
184196
ReadingProgression: ReadingProgression(parseOptString(rawJson["readingProgression"])),
@@ -411,6 +423,7 @@ func MetadataFromJSON(rawJson map[string]interface{}) (*Metadata, error) {
411423
"sortAs",
412424
"subject",
413425
"subtitle",
426+
"tdm",
414427
"title",
415428
"translator",
416429
} {
@@ -470,6 +483,9 @@ func (m Metadata) MarshalJSON() ([]byte, error) {
470483
if m.Accessibility != nil {
471484
j["accessibility"] = *m.Accessibility
472485
}
486+
if m.TDM != nil {
487+
j["tdm"] = *m.TDM
488+
}
473489
if m.Modified != nil {
474490
j["modified"] = *m.Modified
475491
}

‎pkg/manifest/metadata_test.go

+14-4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func TestMetadataUnmarshalFullJSON(t *testing.T) {
4040
"title": {"en": "Title", "fr": "Titre"},
4141
"subtitle": {"en": "Subtitle", "fr": "Sous-titre"},
4242
"accessibility": {"conformsTo": "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a"},
43+
"tdm": {"policy": "https://provider.com/policies/policy.json", "reservation": "all"},
4344
"modified": "2001-01-01T12:36:27.000Z",
4445
"published": "2001-01-02T12:36:27.000Z",
4546
"language": ["en", "fr"],
@@ -80,8 +81,12 @@ func TestMetadataUnmarshalFullJSON(t *testing.T) {
8081
"en": "Title",
8182
"fr": "Titre",
8283
}),
83-
LocalizedSubtitle: &lst,
84-
Accessibility: &a11y,
84+
LocalizedSubtitle: &lst,
85+
Accessibility: &a11y,
86+
TDM: &TDM{
87+
Policy: "https://provider.com/policies/policy.json",
88+
Reservation: TDMReservationAll,
89+
},
8590
Modified: &modified,
8691
Published: &published,
8792
Languages: []string{"en", "fr"},
@@ -205,8 +210,12 @@ func TestMetadataFullJSON(t *testing.T) {
205210
"en": "Title",
206211
"fr": "Titre",
207212
}),
208-
LocalizedSubtitle: &lst,
209-
Accessibility: &a11y,
213+
LocalizedSubtitle: &lst,
214+
Accessibility: &a11y,
215+
TDM: &TDM{
216+
Policy: "https://provider.com/policies/policy.json",
217+
Reservation: TDMReservationAll,
218+
},
210219
Modified: &modified,
211220
Published: &published,
212221
Languages: []string{"en", "fr"},
@@ -255,6 +264,7 @@ func TestMetadataFullJSON(t *testing.T) {
255264
"title": {"en": "Title", "fr": "Titre"},
256265
"subtitle": {"en": "Subtitle", "fr": "Sous-titre"},
257266
"accessibility": {"conformsTo": ["http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa"]},
267+
"tdm": {"policy": "https://provider.com/policies/policy.json", "reservation": "all"},
258268
"modified": "2001-01-01T12:36:27.123Z",
259269
"published": "2001-01-02T12:36:27Z",
260270
"language": ["en", "fr"],

‎pkg/manifest/tdm.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package manifest
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/pkg/errors"
7+
)
8+
9+
// TDMRep (Text & Data Mining Reservation Protocol)
10+
//
11+
// https://www.w3.org/community/reports/tdmrep/CG-FINAL-tdmrep-20240510/
12+
type TDM struct {
13+
Policy string `json:"policy,omitempty"`
14+
Reservation TDMReservation `json:"reservation,omitempty"`
15+
}
16+
17+
func (t *TDM) IsEmpty() bool {
18+
return t.Policy == "" && t.Reservation == ""
19+
}
20+
21+
type TDMReservation string
22+
23+
const (
24+
TDMReservationAll TDMReservation = "all"
25+
TDMReservationNone TDMReservation = "none"
26+
)
27+
28+
func (t TDMReservation) String() string {
29+
return string(t)
30+
}
31+
32+
func TDMFromJSON(rawJSON map[string]interface{}) (*TDM, error) {
33+
if rawJSON == nil {
34+
return nil, nil
35+
}
36+
37+
t := &TDM{}
38+
39+
if policy, ok := rawJSON["policy"].(string); ok {
40+
t.Policy = policy
41+
}
42+
43+
if reservation, ok := rawJSON["reservation"].(string); ok {
44+
t.Reservation = TDMReservation(reservation)
45+
}
46+
47+
if t.IsEmpty() {
48+
return nil, nil
49+
}
50+
51+
return t, nil
52+
}
53+
54+
func (t *TDM) UnmarshalJSON(data []byte) error {
55+
var d interface{}
56+
err := json.Unmarshal(data, &d)
57+
if err != nil {
58+
return err
59+
}
60+
61+
mp, ok := d.(map[string]interface{})
62+
if !ok {
63+
return errors.New("tdm object not a map with string keys")
64+
}
65+
66+
ft, err := TDMFromJSON(mp)
67+
if err != nil {
68+
return err
69+
}
70+
*t = *ft
71+
return nil
72+
}

‎pkg/manifest/tdm_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package manifest
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestTDMFromJSON(t *testing.T) {
11+
rawJSON := map[string]interface{}{
12+
"policy": "https://provider.com/policies/policy.json",
13+
"reservation": "all",
14+
}
15+
tdm, err := TDMFromJSON(rawJSON)
16+
assert.NoError(t, err)
17+
18+
assert.Equal(t, "https://provider.com/policies/policy.json", tdm.Policy)
19+
assert.Equal(t, TDMReservationAll, tdm.Reservation)
20+
21+
rawJSON = map[string]interface{}{
22+
"reservation": "none",
23+
}
24+
tdm, err = TDMFromJSON(rawJSON)
25+
assert.NoError(t, err)
26+
27+
assert.Equal(t, "", tdm.Policy)
28+
assert.Equal(t, TDMReservationNone, tdm.Reservation)
29+
}
30+
31+
func TestTDMMarshalJSON(t *testing.T) {
32+
tdm := TDM{
33+
Policy: "https://provider.com/policies/policy.json",
34+
Reservation: TDMReservationAll,
35+
}
36+
rawJSON, err := json.Marshal(tdm)
37+
assert.NoError(t, err)
38+
39+
expectedJSON := `{"policy":"https://provider.com/policies/policy.json","reservation":"all"}`
40+
assert.JSONEq(t, expectedJSON, string(rawJSON))
41+
42+
tdm = TDM{
43+
Reservation: TDMReservationNone,
44+
}
45+
rawJSON, err = json.Marshal(tdm)
46+
assert.NoError(t, err)
47+
48+
expectedJSON = `{"reservation":"none"}`
49+
assert.JSONEq(t, expectedJSON, string(rawJSON))
50+
}

‎pkg/parser/epub/consts.go

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const (
3232
VocabularyONIX = "http://www.editeur.org/ONIX/book/codelists/current.html#"
3333
VocabularySchema = "http://schema.org/"
3434
VocabularyXSD = "http://www.w3.org/2001/XMLSchema#"
35+
VocabularyTDM = "http://www.w3.org/ns/tdmrep#"
3536

3637
VocabularyMSV = "http://www.idpf.org/epub/vocab/structure/magazine/#"
3738
VocabularyPRISM = "http://www.prismstandard.org/specifications/3.0/PRISM_CV_Spec_3.0.htm#"

‎pkg/parser/epub/metadata.go

+20
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ func (m PubMetadataAdapter) Metadata() manifest.Metadata {
481481
LocalizedSortAs: m.LocalizedSortAs(),
482482
LocalizedSubtitle: m.LocalizedSubtitle(),
483483
Accessibility: m.Accessibility(),
484+
TDM: m.TDM(),
484485
Duration: m.Duration(),
485486
Subjects: m.Subjects(),
486487
Description: m.Description(),
@@ -654,6 +655,25 @@ func (m PubMetadataAdapter) Accessibility() *manifest.A11y {
654655
return &a11y
655656
}
656657

658+
// https://www.w3.org/community/reports/tdmrep/CG-FINAL-tdmrep-20240510/#sec-epub3
659+
func (m PubMetadataAdapter) TDM() *manifest.TDM {
660+
tdm := &manifest.TDM{}
661+
tdm.Policy = m.FirstValue(VocabularyTDM + "policy")
662+
663+
reservation := m.FirstValue(VocabularyTDM + "reservation")
664+
switch reservation {
665+
case "1":
666+
tdm.Reservation = manifest.TDMReservationAll
667+
case "0":
668+
tdm.Reservation = manifest.TDMReservationNone
669+
}
670+
671+
if tdm.IsEmpty() {
672+
return nil
673+
}
674+
return tdm
675+
}
676+
657677
func (m PubMetadataAdapter) a11yConformsTo() []manifest.A11yProfile {
658678
profiles := []manifest.A11yProfile{}
659679

‎pkg/parser/epub/metadata_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,15 @@ func TestMetadataEPUB2Accessibility(t *testing.T) {
328328
assert.Nil(t, m.OtherMetadata["accessibility"])
329329
}
330330

331+
func TestMetadataEPUB2TDM(t *testing.T) {
332+
m, err := loadMetadata("tdm-epub2")
333+
assert.NoError(t, err)
334+
assert.Equal(t, &manifest.TDM{
335+
Policy: "https://provider.com/policies/policy.json",
336+
Reservation: manifest.TDMReservationAll,
337+
}, m.TDM)
338+
}
339+
331340
func TestMetadataEPUB3Accessibility(t *testing.T) {
332341
m, err := loadMetadata("accessibility-epub3")
333342
assert.NoError(t, err)
@@ -351,6 +360,15 @@ func TestMetadataEPUB3Accessibility(t *testing.T) {
351360
assert.Nil(t, m.OtherMetadata["accessibility"])
352361
}
353362

363+
func TestMetadataEPUB3TDM(t *testing.T) {
364+
m, err := loadMetadata("tdm-epub3")
365+
assert.NoError(t, err)
366+
assert.Equal(t, &manifest.TDM{
367+
Policy: "https://provider.com/policies/policy.json",
368+
Reservation: manifest.TDMReservationAll,
369+
}, m.TDM)
370+
}
371+
354372
func TestMetadataTitleFileAs(t *testing.T) {
355373
m2, err := loadMetadata("titles-epub2")
356374
assert.NoError(t, err)

‎pkg/parser/epub/property_data_type.go

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var PackageReservedPrefixes = map[string]string{
1515
"onix": VocabularyONIX,
1616
"schema": VocabularySchema,
1717
"xsd": VocabularyXSD,
18+
"tdm": VocabularyTDM,
1819
}
1920

2021
var ContentReservedPrefixes = map[string]string{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0"?>
2+
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="pub-id" version="2.0" xml:lang="en">
3+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
4+
<dc:title>Alice's Adventures in Wonderland</dc:title>
5+
6+
<meta name="tdm:reservation" content="1" />
7+
<meta name="tdm:policy" content="https://provider.com/policies/policy.json" />
8+
</metadata>
9+
<manifest>
10+
<item id="titlepage" href="titlepage.xhtml"/>
11+
</manifest>
12+
<spine>
13+
<itemref idref="titlepage"/>
14+
</spine>
15+
</package>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0"?>
2+
<package xmlns="http://www.idpf.org/2007/opf" prefix="tdm: http://www.w3.org/ns/tdmrep#" unique-identifier="pub-id" version="3.0" xml:lang="en">
3+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/">
4+
<dc:title>Alice's Adventures in Wonderland</dc:title>
5+
6+
<meta property="tdm:reservation">1</meta>
7+
<meta property="tdm:policy">https://provider.com/policies/policy.json</meta>
8+
</metadata>
9+
<manifest>
10+
<item id="titlepage" href="titlepage.xhtml"/>
11+
</manifest>
12+
<spine>
13+
<itemref idref="titlepage"/>
14+
</spine>
15+
</package>

0 commit comments

Comments
 (0)
Please sign in to comment.