Skip to content
This repository was archived by the owner on Aug 29, 2023. It is now read-only.

Commit dcb6695

Browse files
authored
Merge pull request #58 from hmelder/nested_sections
Add nested sections
2 parents 4047d66 + 22a3982 commit dcb6695

File tree

6 files changed

+270
-21
lines changed

6 files changed

+270
-21
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
*.epub
22
epubcheck*
3+
.DS_Store
4+
*.swp

epub.go

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ func (e *FileRetrievalError) Error() string {
6262
return fmt.Sprintf("Error retrieving %q from source: %+v", e.Source, e.Err)
6363
}
6464

65+
// ParentDoesNotExistError is thrown by AddSubSection if the parent with the
66+
// previously defined internal filename does not exist.
67+
type ParentDoesNotExistError struct {
68+
Filename string // Filename that caused the error
69+
}
70+
71+
func (e *ParentDoesNotExistError) Error() string {
72+
return fmt.Sprintf("Parent with the internal filename %s does not exist", e.Filename)
73+
}
74+
6575
// Folder names used for resources inside the EPUB
6676
const (
6777
CSSFolderName = "css"
@@ -137,6 +147,7 @@ type epubCover struct {
137147
type epubSection struct {
138148
filename string
139149
xhtml *xhtml
150+
children *[]epubSection
140151
}
141152

142153
// NewEpub returns a new Epub.
@@ -257,30 +268,86 @@ func (e *Epub) AddVideo(source string, videoFilename string) (string, error) {
257268
func (e *Epub) AddSection(body string, sectionTitle string, internalFilename string, internalCSSPath string) (string, error) {
258269
e.Lock()
259270
defer e.Unlock()
260-
return e.addSection(body, sectionTitle, internalFilename, internalCSSPath)
271+
return e.addSection("", body, sectionTitle, internalFilename, internalCSSPath)
261272
}
262273

263-
func (e *Epub) addSection(body string, sectionTitle string, internalFilename string, internalCSSPath string) (string, error) {
274+
// AddSubSection adds a nested section (chapter, etc) to an existing section.
275+
// The method returns a relative path to the section that can be used from another
276+
// section (for links).
277+
//
278+
// The parent filename must be a valid filename from another section already added.
279+
//
280+
// The body must be valid XHTML that will go between the <body> tags of the
281+
// section XHTML file. The content will not be validated.
282+
//
283+
// The title will be used for the table of contents. The section will be shown
284+
// as a nested entry of the parent section in the table of contents. The
285+
// title is optional; if no title is provided, the section will not be added to
286+
// the table of contents.
287+
//
288+
// The internal filename will be used when storing the section file in the EPUB
289+
// and must be unique among all section files. If the same filename is used more
290+
// than once, FilenameAlreadyUsedError will be returned. The internal filename is
291+
// optional; if no filename is provided, one will be generated.
292+
//
293+
// The internal path to an already-added CSS file (as returned by AddCSS) to be
294+
// used for the section is optional.
295+
func (e *Epub) AddSubSection(parentFilename string, body string, sectionTitle string, internalFilename string, internalCSSPath string) (string, error) {
296+
e.Lock()
297+
defer e.Unlock()
298+
return e.addSection(parentFilename, body, sectionTitle, internalFilename, internalCSSPath)
299+
}
300+
301+
func (e *Epub) addSection(parentFilename string, body string, sectionTitle string, internalFilename string, internalCSSPath string) (string, error) {
302+
parentIndex := -1
303+
264304
// Generate a filename if one isn't provided
265305
if internalFilename == "" {
266306
index := 1
267307
for internalFilename == "" {
268308
internalFilename = fmt.Sprintf(sectionFileFormat, index)
269-
for _, section := range e.sections {
309+
for item, section := range e.sections {
310+
if section.filename == parentFilename {
311+
parentIndex = item
312+
}
270313
if section.filename == internalFilename {
271314
internalFilename, index = "", index+1
272-
break
315+
if parentFilename == "" || parentIndex != -1 {
316+
break
317+
}
318+
}
319+
// Check for nested sections with the same filename to avoid duplicate entries
320+
if section.children != nil {
321+
for _, subsection := range *section.children {
322+
if subsection.filename == internalFilename {
323+
internalFilename, index = "", index+1
324+
}
325+
}
273326
}
274327
}
275328
}
276329
} else {
277-
for _, section := range e.sections {
330+
for item, section := range e.sections {
331+
if section.filename == parentFilename {
332+
parentIndex = item
333+
}
278334
if section.filename == internalFilename {
279335
return "", &FilenameAlreadyUsedError{Filename: internalFilename}
280336
}
337+
if section.children != nil {
338+
for _, subsection := range *section.children {
339+
if subsection.filename == internalFilename {
340+
return "", &FilenameAlreadyUsedError{Filename: internalFilename}
341+
}
342+
}
343+
}
281344
}
282345
}
283346

347+
if parentFilename != "" && parentIndex == -1 {
348+
return "", &ParentDoesNotExistError{Filename: parentFilename}
349+
}
350+
284351
x := newXhtml(body)
285352
x.setTitle(sectionTitle)
286353
x.setXmlnsEpub(xmlnsEpub)
@@ -292,8 +359,18 @@ func (e *Epub) addSection(body string, sectionTitle string, internalFilename str
292359
s := epubSection{
293360
filename: internalFilename,
294361
xhtml: x,
362+
children: nil,
363+
}
364+
365+
if parentIndex != -1 {
366+
if e.sections[parentIndex].children == nil {
367+
var section []epubSection
368+
e.sections[parentIndex].children = &section
369+
}
370+
(*e.sections[parentIndex].children) = append(*e.sections[parentIndex].children, s)
371+
} else {
372+
e.sections = append(e.sections, s)
295373
}
296-
e.sections = append(e.sections, s)
297374

298375
return internalFilename, nil
299376
}
@@ -398,10 +475,10 @@ func (e *Epub) SetCover(internalImagePath string, internalCSSPath string) {
398475
coverBody := fmt.Sprintf(defaultCoverBody, internalImagePath)
399476
// Title won't be used since the cover won't be added to the TOC
400477
// First try to use the default cover filename
401-
coverPath, err := e.addSection(coverBody, "", defaultCoverXhtmlFilename, internalCSSPath)
478+
coverPath, err := e.addSection("", coverBody, "", defaultCoverXhtmlFilename, internalCSSPath)
402479
// If that doesn't work, generate a filename
403480
if _, ok := err.(*FilenameAlreadyUsedError); ok {
404-
coverPath, err = e.addSection(coverBody, "", "", internalCSSPath)
481+
coverPath, err = e.addSection("", coverBody, "", "", internalCSSPath)
405482
if _, ok := err.(*FilenameAlreadyUsedError); ok {
406483
// This shouldn't cause an error since we're not specifying a filename
407484
panic(fmt.Sprintf("Error adding default cover XHTML file: %s", err))

epub_test.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,52 @@ func TestAddSection(t *testing.T) {
397397
cleanup(testEpubFilename, tempDir)
398398
}
399399

400+
func TestAddSubSection(t *testing.T) {
401+
e := NewEpub(testEpubTitle)
402+
testSection1Path, err := e.AddSection(testSectionBody, testSectionTitle, testSectionFilename, "")
403+
if err != nil {
404+
t.Errorf("Error adding section: %s", err)
405+
}
406+
407+
testSection2Path, err := e.AddSubSection(testSection1Path, testSectionBody, testSectionTitle, "", "")
408+
if err != nil {
409+
t.Errorf("Error adding subsection: %s", err)
410+
}
411+
412+
tempDir := writeAndExtractEpub(t, e, testEpubFilename)
413+
414+
contents, err := storage.ReadFile(filesystem, filepath.Join(tempDir, contentFolderName, xhtmlFolderName, testSection1Path))
415+
if err != nil {
416+
t.Errorf("Unexpected error reading section file: %s", err)
417+
}
418+
419+
testSectionContents := fmt.Sprintf(testSectionContentTemplate, testSectionTitle, testSectionBody)
420+
if trimAllSpace(string(contents)) != trimAllSpace(testSectionContents) {
421+
t.Errorf(
422+
"Section file contents don't match\n"+
423+
"Got: %s\n"+
424+
"Expected: %s",
425+
contents,
426+
testSectionContents)
427+
}
428+
429+
contents, err = storage.ReadFile(filesystem, filepath.Join(tempDir, contentFolderName, xhtmlFolderName, testSection2Path))
430+
if err != nil {
431+
t.Errorf("Unexpected error reading section file: %s", err)
432+
}
433+
434+
if trimAllSpace(string(contents)) != trimAllSpace(testSectionContents) {
435+
t.Errorf(
436+
"Section file contents don't match\n"+
437+
"Got: %s\n"+
438+
"Expected: %s",
439+
contents,
440+
testSectionContents)
441+
}
442+
443+
cleanup(testEpubFilename, tempDir)
444+
}
445+
400446
func TestEpubAuthor(t *testing.T) {
401447
e := NewEpub(testEpubTitle)
402448
e.SetAuthor(testEpubAuthor)
@@ -737,11 +783,13 @@ func testEpubValidity(t testing.TB) {
737783
e := NewEpub(testEpubTitle)
738784
testCoverCSSPath, _ := e.AddCSS(testCoverCSSSource, testCoverCSSFilename)
739785
e.AddCSS(testCoverCSSSource, "")
740-
e.AddSection(testSectionBody, testSectionTitle, testSectionFilename, testCoverCSSPath)
786+
testSectionPath, _ := e.AddSection(testSectionBody, testSectionTitle, testSectionFilename, testCoverCSSPath)
787+
e.AddSubSection(testSectionPath, testSectionBody, "Test subsection", "subsection.xhtml", "")
741788

742789
e.AddFont(testFontFromFileSource, "")
743790
// Add CSS referencing the font in order to validate the font MIME type
744791
testFontCSSPath, _ := e.AddCSS(testFontCSSSource, testFontCSSFilename)
792+
745793
e.AddSection(testSectionBody, testSectionTitle, "", testFontCSSPath)
746794

747795
testImagePath, _ := e.AddImage(testImageFromFileSource, testImageFromFileFilename)

example_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,35 @@ func ExampleEpub_AddSection() {
110110
// section0001.xhtml
111111
}
112112

113+
func ExampleEpub_AddSubSection() {
114+
e := epub.NewEpub("My title")
115+
116+
// Add a section. The CSS path is optional
117+
section1Body := ` <h1>Section 1</h1>
118+
<p>This is a paragraph.</p>`
119+
section1Path, err := e.AddSection(section1Body, "Section 1", "firstsection.xhtml", "")
120+
if err != nil {
121+
log.Fatal(err)
122+
}
123+
124+
// Link to the first section
125+
section2Body := fmt.Sprintf(` <h1>Section 2</h1>
126+
<a href="%s">Link to section 1</a>`,
127+
section1Path)
128+
// The title and filename are also optional
129+
section2Path, err := e.AddSubSection(section1Path, section2Body, "", "", "")
130+
if err != nil {
131+
log.Fatal(err)
132+
}
133+
134+
fmt.Println(section1Path)
135+
fmt.Println(section2Path)
136+
137+
// Output:
138+
// firstsection.xhtml
139+
// section0001.xhtml
140+
}
141+
113142
func ExampleEpub_SetCover() {
114143
e := epub.NewEpub("My title")
115144

0 commit comments

Comments
 (0)