Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion bakery-js/src/epub/__snapshots__/page.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`Pages conversions converts a page that exercises a bunch of serializer
<body>


<h2 data-type="document-title">KinematicsInFourDimensions</h2>
<h1 data-type="document-title">KinematicsInFourDimensions</h1>
<m:math xmlns:m="http://www.w3.org/1998/Math/MathML">

</m:math>
Expand Down Expand Up @@ -50,6 +50,8 @@ exports[`Pages parses all the fields with a simple page 1`] = `
PageFile {
"_parsed": undefined,
"_readPath": "/pageLink1",
"ancestorTitle": null,
"ariaSpec": null,
},
],
"resources": [],
Expand Down
32 changes: 29 additions & 3 deletions bakery-js/src/epub/__snapshots__/toc.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ exports[`TocFile and Friends Cover image with TocFile and Friends with an empty
<!-- # sourceMappingURL=thebooktoc.xhtml.map -->"
`;

exports[`TocFile and Friends with a small book captures data-toc-type for units, chapters, and pages 1`] = `
[
{
"children": [
{
"children": [
{
"page": "/foo/iamthepage.xhtml",
"title": "PageTitle",
"tocTargetType": "intro",
"tocType": "page",
"type": "LEAF",
},
],
"title": "ChapterTitle",
"tocType": "chapter",
"type": "INNER",
},
],
"title": "UnitTitle",
"tocType": "unit",
"type": "INNER",
},
]
`;

exports[`TocFile and Friends with a small book generates an NCX file 1`] = `
"<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head>
Expand Down Expand Up @@ -133,13 +159,13 @@ exports[`TocFile and Friends with a small book parses a ToC with one page 1`] =
<body>
<nav epub:type="toc">
<ol>
<li>
<li data-toc-type="unit">
<a href="../foo/iamthepage.xhtml">UnitTitle</a>
<ol>
<li>
<li data-toc-type="chapter">
<a href="../foo/iamthepage.xhtml">ChapterTitle</a>
<ol>
<li>
<li data-toc-type="page" data-toc-target-type="intro">
<a href="../foo/iamthepage.xhtml">PageTitle</a>
</li>
</ol>
Expand Down
218 changes: 218 additions & 0 deletions bakery-js/src/epub/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { factorio } from './singletons'
import { XmlFile } from '../model/file'
import { PageFile } from './page'
import { parseXml } from '../utils'
import { dom } from '../minidom'
Comment thread
TylerZeroMaster marked this conversation as resolved.
Outdated

jest.mock('fs')

Expand Down Expand Up @@ -151,4 +152,221 @@ describe('Pages', () => {
expect(p.newPath).toBe('/dir1/dir2/newname')
})
})

describe('heading levels', () => {
const somePos = {
source: { fileName: 'somefile.cnxml', content: null },
lineNumber: 1,
columnNumber: 1,
}

it('promotes the page title to h1 and demotes sibling headings to match', async () => {
const page = `
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
<body>
<div data-type="page">
<h2 data-type="document-title">${titleText}</h2>
<h3 data-type="title">SectionOne</h3>
<h3 data-type="title">SectionTwo</h3>
</div>
</body>
</html>`
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(page))
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')
expect(output).toContain(
`<h1 data-type="document-title">${titleText}</h1>`
)
// Both siblings should land at the same level, not one nested
// under the other, even though only the first one gets rewritten
// by the time the second is visited.
expect(output).toMatch(/<h2[^>]*>SectionOne<\/h2>/)
expect(output).toMatch(/<h2[^>]*>SectionTwo<\/h2>/)
})

it('clamps a heading that jumps more than one level deeper', async () => {
const page = `
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
<body>
<div data-type="page">
<h2 data-type="document-title">${titleText}</h2>
<h5 data-type="title">TooDeep</h5>
</div>
</body>
</html>`
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(page))
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')
expect(output).toContain(
`<h1 data-type="document-title">${titleText}</h1>`
)
expect(output).toMatch(/<h2[^>]*>TooDeep<\/h2>/)
})

it('does not touch a heading that only descends by exactly one level', async () => {
const page = `
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
<body>
<div data-type="page">
<h1 data-type="document-title">${titleText}</h1>
<h2 data-type="title">SectionOne</h2>
</div>
</body>
</html>`
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(page))
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')
expect(output).toContain(
`<h1 data-type="document-title">${titleText}</h1>`
)
expect(output).toMatch(/<h2[^>]*>SectionOne<\/h2>/)
})

it("inserts a chapter/unit's ancestorTitle as a leading h1 into the page div, ahead of any pre-existing heading", async () => {
const chapterTitle = 'Observing the Sky: The Birth of Astronomy'
// "Chapter Outline" is a nav widget that (in real content) shows up
// before the page's own title in document order.
const page = `
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
<body>
<div data-type="page">
<h2 class="os-title">Chapter Outline</h2>
<h2 data-type="document-title">${titleText}</h2>
</div>
</body>
</html>`
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(page))
p.ancestorTitle = { title: chapterTitle, pos: somePos }
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')

expect(output).toContain(
`<h1 data-type="document-title">${chapterTitle}</h1>`
)
// Only one h1 on the page - the inserted ancestor title, not the
// page's own title and not the outline widget.
expect(output.match(/<h1[ >]/g)?.length).toBe(1)

// The inserted h1 must land inside div[data-type="page"], as its
// first child - not as a sibling of that div under <body>.
expect(output).toMatch(
new RegExp(
`<div data-type="page">\\s*<h1 data-type="document-title">${chapterTitle}</h1>`
)
)

// Search from <body> onward - <head><title> also contains titleText,
// and it always precedes the body regardless of heading order.
const bodyIndex = output.indexOf('<body')
const h1Index = output.indexOf('<h1', bodyIndex)
const outlineIndex = output.indexOf('Chapter Outline', bodyIndex)
const titleIndex = output.indexOf(titleText, bodyIndex)
expect(h1Index).toBeGreaterThanOrEqual(0)
expect(h1Index).toBeLessThan(outlineIndex)
expect(h1Index).toBeLessThan(titleIndex)

// Both the outline widget and the page's own title become h2
// siblings of each other, under the real ancestor h1 - neither one
// is falsely nested under the other.
expect(output).toMatch(/<h2[^>]*>Chapter Outline<\/h2>/)
expect(output).toMatch(new RegExp(`<h2[^>]*>${titleText}</h2>`))
})

it('does not insert anything when ancestorTitle is unset', async () => {
const page = `
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
<body>
<div data-type="page">
<h1 data-type="document-title">${titleText}</h1>
</div>
</body>
</html>`
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(page))
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')
expect(output.match(/<h1[ >]/g)?.length).toBe(1)
expect(output).toContain(
`<h1 data-type="document-title">${titleText}</h1>`
)
})
})

describe('structural roles', () => {
const pageWithOwnDiv = `
<html xmlns="http://www.w3.org/1999/xhtml">
<head/>
<body>
<div data-type="page">
<h1 data-type="document-title">${titleText}</h1>
</div>
</body>
</html>`

function pageDivTag(output: string) {
return output.match(/<div data-type="page"[^>]*>/)?.[0] ?? ''
}
function bodyTag(output: string) {
return output.match(/<body[^>]*>/)?.[0] ?? ''
}

it('adds role, epub:type, and aria-label to the page div - not the body - when ariaSpec is set', async () => {
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(pageWithOwnDiv))
p.ariaSpec = {
role: 'doc-chapter',
label: 'Observing the Sky: The Birth of Astronomy',
}
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')

expect(pageDivTag(output)).toContain('role="doc-chapter"')
expect(pageDivTag(output)).toContain('epub:type="chapter"')
expect(pageDivTag(output)).toContain(
'aria-label="Observing the Sky: The Birth of Astronomy"'
)
// Not on <body> - it does nothing there.
expect(bodyTag(output)).not.toContain('role=')
})

it("falls back to the page's own title as the aria-label when ariaSpec.label is null", async () => {
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(pageWithOwnDiv))
p.ariaSpec = { role: 'doc-preface', label: null }
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')

expect(pageDivTag(output)).toContain('epub:type="preface"')
expect(pageDivTag(output)).toContain(`aria-label="${titleText}"`)
})

it('does not add role, epub:type, or aria-label when ariaSpec is unset', async () => {
const p = new PageFile('somepath')
p.readXml = (_) => Promise.resolve(parseXml(pageWithOwnDiv))
await p.parse(factorio)
await p.write()
const output = readFileSync(p.newPath, 'utf8')

expect(pageDivTag(output)).not.toContain('role=')
expect(pageDivTag(output)).not.toContain('epub:type=')
expect(pageDivTag(output)).not.toContain('aria-label=')
expect(bodyTag(output)).not.toContain('role=')
})
})
})
Loading
Loading