Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
61 changes: 37 additions & 24 deletions src/entities/EPubs/structs/EPubData.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
/* eslint-disable complexity */
import _ from 'lodash';
import { v4 as uuid } from 'uuid';
import CTError from 'utils/use-error';
import { buildMDFromChapters } from '../html-converters';
import EPubChapterData from './EPubChapterData';
import EPubImageData from './EPubImageData';

/**
* The error which occurred when the required information
* The error which occurred when the required information
* for creating an ePub file is invalid
*/
export const EPubDataValidationError =
new CTError('EPubDataValidationError', 'Invalid I-Note data.');
export const EPubDataValidationError = new CTError(
'EPubDataValidationError',
'Invalid I-Note data.',
);

/**
* The class for an ePub data
Expand All @@ -27,7 +28,8 @@ export default class EPubData {
cover: null,
chapters: [],
h3: true,
condition: { 'default': true },
condition: { default: true },
jsonMetadata: {},
};

/**
Expand All @@ -46,10 +48,9 @@ export default class EPubData {
else if (typeof data === 'object') {
this.__data__ = {
...this.__data__,
...data
...data,
};
}
else {
} else {
throw EPubDataValidationError;
}

Expand All @@ -73,6 +74,10 @@ export default class EPubData {
this.h3 = true;
}

if (!this.jsonMetadata) {
this.jsonMetadata = data.jsonMetadata;
}

// set up cover image
if (!this.cover) {
this.cover = new EPubImageData();
Expand All @@ -83,18 +88,21 @@ export default class EPubData {

initFromRawData(rawEPubData) {
this.chapters = [
new EPubChapterData({
title: 'Default Chapter',
items: rawEPubData,
}, true, this.sourceId).toObject()
new EPubChapterData(
{
title: 'Default Chapter',
items: rawEPubData,
},
true,
this.sourceId,
).toObject(),
];
// this.condition = ['default'];
this.condition.default = true;

this.items = rawEPubData.map((item) => (EPubImageData.createWithTimestamp(item, this.sourceId)));
this.items = rawEPubData.map((item) => EPubImageData.createWithTimestamp(item, this.sourceId));

if (!this.cover.src && this.items.length > 0) {
this.cover = { ...this.items[0], descriptions: [], alt: "cover image" };
this.cover = { ...this.items[0], descriptions: [], alt: 'cover image' };
}
}

Expand Down Expand Up @@ -205,6 +213,14 @@ export default class EPubData {
this.__data__.chapters = chapters;
}

set jsonMetadata(jsonMetadata) {
this.__data__.jsonMetadata = jsonMetadata;
}

get jsonMetadata() {
return this.__data__.jsonMetadata;
}

/**
* @returns {EPubChapterData[]}
*/
Expand All @@ -216,7 +232,9 @@ export default class EPubData {
return {
...this.__data__,
cover: this.cover instanceof EPubImageData ? this.cover.toObject() : this.cover,
chapters: this.chapters.map(chapter => (chapter instanceof EPubChapterData ? chapter.toObject() : chapter))
chapters: this.chapters.map((chapter) =>
chapter instanceof EPubChapterData ? chapter.toObject() : chapter,
),
};
}

Expand Down Expand Up @@ -245,25 +263,20 @@ export default class EPubData {
removeChapter(index) {
let chapters = this.chapters;
let chapter = chapters[index];
this.chapters = [
...chapters.slice(0, index),
...chapters.slice(index + 1)
];
this.chapters = [...chapters.slice(0, index), ...chapters.slice(index + 1)];

return chapter;
}


static create(rawEPubData, data) {
const newData = new EPubData({
...data
...data,
});
newData.initFromRawData(rawEPubData);

return newData;
}


// static copyChapterStructure(rawEPubData, chapters) {
// let lastIdx = 0;
// return _.map(chapters, (chapter) => {
Expand All @@ -276,4 +289,4 @@ export default class EPubData {
// })
// });
// }
}
}
96 changes: 67 additions & 29 deletions src/screens/EPub/controllers/file-builders/EPubFileBuilder.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash';
import AdmZip from 'adm-zip';
import PlaylistTypes from 'entities/Playlists/PlaylistTypes';
import { dedent } from 'dentist';
import { KATEX_MIN_CSS, PRISM_CSS } from './file-templates/styles';
import { glossaryToHTMLString } from './GlossaryCreator';
Expand Down Expand Up @@ -83,19 +84,18 @@ class EPubFileBuilder {

// IMAGES — add one <item> per embedded image
if (Array.isArray(this.imageItems) && this.imageItems.length) {
const temp = this.imageItems.map(img =>
`<item id="${img.id}" href="${img.href}" media-type="${img.mediaType}" />`
).join('\n\t\t');
const temp = this.imageItems
.map((img) => `<item id="${img.id}" href="${img.href}" media-type="${img.mediaType}" />`)
.join('\n\t\t');
contentItems += `\n\t\t${temp}`;
}

// content itemrefs
let contentItemsRefs = _.map(chapters, (ch) => `<itemref idref="${ch.id}"/>`
).join('\n\t\t');
let contentItemsRefs = _.map(chapters, (ch) => `<itemref idref="${ch.id}"/>`).join('\n\t\t');

if (this.glossary && !_.isEmpty(this.glossary) && !this.data.chapterGlossary) {
contentItems += `<item id="glossary" href="glossary.xhtml" media-type="application/xhtml+xml" />`;
contentItemsRefs += `<itemref idref="glossary"/>`
contentItemsRefs += `<itemref idref="glossary"/>`;
}

return OEBPS_CONTENT_OPF({
Expand All @@ -110,16 +110,20 @@ class EPubFileBuilder {
}

buildTocXHTML(chapters) {
let navContents = "";
let navContents = '';
_.forEach(chapters, (ch, index) => {
navContents += `
<dt class="table-of-content">
<a href="${ch.id}.xhtml">${index + 1} - ${ch.title} </a>
</dt>
`}
)
`;
});

const toc_xhtml = OEBPS_TOC_XHTML({ title: this.data.title, language: this.language, navContents });
const toc_xhtml = OEBPS_TOC_XHTML({
title: this.data.title,
language: this.language,
navContents,
});
this.zip.addFile('OEBPS/toc.xhtml', toc_xhtml);
}

Expand All @@ -131,16 +135,20 @@ class EPubFileBuilder {
<dt class="table-of-content">
<a href="${this.data.chapters[chIdx].id}.xhtml"><img src="${img.src}"/></a>
</dt>
`
}).join("\n")
}).join("\n");

const toc_xhtml = OEBPS_TOC_XHTML({ title: this.data.title, language: this.language, navContents });
`;
}).join('\n');
}).join('\n');

const toc_xhtml = OEBPS_TOC_XHTML({
title: this.data.title,
language: this.language,
navContents,
});

this.zip.addFile('OEBPS/toc.xhtml', toc_xhtml);
}
buildTocNCX(chapters) {
let navPoints = "";
let navPoints = '';
_.forEach(chapters, (ch, index) => {
navPoints += `
<navPoint id="${ch.id}" playOrder="${index}" class="chapter">
Expand All @@ -163,26 +171,38 @@ class EPubFileBuilder {
this.buildTocNCX(chapters);
}
convertGlossary(glossary) {
return OEBPS_CONTENT_XHTML({ title: "Glossary", content: glossaryToHTMLString(glossary), language: this.language });
return OEBPS_CONTENT_XHTML({
title: 'Glossary',
content: glossaryToHTMLString(glossary),
language: this.language,
});
}

convertChapter(idx, chapter, chapterGlossary) {
let text = HTMLFileBuilder.convertChapter(idx, chapter, chapterGlossary, this.data.includeRawLatex, this.videoLinks);

let text = HTMLFileBuilder.convertChapter(
idx,
chapter,
chapterGlossary,
this.data.includeRawLatex,
this.videoLinks,
(this.data.sourceType === PlaylistTypes.BoxID) ? this.data.jsonMetadata.shared_link.url : this.data.sourceId,
this.data.sourceType,
);

// --- Normalize HTML via DOM, strip risky attributes, then XHTML-tidy ---
try {
// normalize curly quotes that can break attrs
text = text
.replace(/[\u201C\u201D]/g, '"') // curly double quotes -> "
.replace(/[\u201C\u201D]/g, '"') // curly double quotes -> "
.replace(/[\u2018\u2019]/g, "'"); // curly single quotes -> '

// Use DOM to normalize attributes & spacing
const wrapper = document.createElement('div');
wrapper.innerHTML = text;

// Strip ALL data-* attributes (optional in EPUB, often malformed)
wrapper.querySelectorAll('*').forEach(el => {
[...el.attributes].forEach(attr => {
wrapper.querySelectorAll('*').forEach((el) => {
[...el.attributes].forEach((attr) => {
if (/^data-/.test(attr.name)) {
el.removeAttribute(attr.name);
}
Expand All @@ -198,7 +218,21 @@ class EPubFileBuilder {
// Final XHTML safety passes:
// - self-close <img> tags
text = text.replace(/<img([^>]*?)>/g, '<img$1 />');
const VOID = ['br','hr','meta','link','input','source','track','area','base','col','embed','param','wbr'];
const VOID = [
'br',
'hr',
'meta',
'link',
'input',
'source',
'track',
'area',
'base',
'col',
'embed',
'param',
'wbr',
];
const voidRe = new RegExp(`<(${VOID.join('|')})([^/>]*?)>`, 'gi');
text = text.replace(voidRe, '<$1$2 />');
const closeVoidRe = new RegExp(`</(?:${VOID.join('|')})\\s*>`, 'gi');
Expand All @@ -211,7 +245,7 @@ class EPubFileBuilder {
</div>
`);

return OEBPS_CONTENT_XHTML({ title: chapter.title, content, language: this.language })
return OEBPS_CONTENT_XHTML({ title: chapter.title, content, language: this.language });
}

prepareAndEmbedImages() {
Expand All @@ -220,8 +254,8 @@ class EPubFileBuilder {

const rewriteImageObject = (imgObj) => {
if (!imgObj) return;
let buffer = null
let mime = null
let buffer = null;
let mime = null;
let ext = 'jpg';

if (typeof imgObj.src === 'string' && imgObj.src.startsWith('data:')) {
Expand Down Expand Up @@ -256,12 +290,12 @@ class EPubFileBuilder {
rewriteImageObject.call(this, c);
}
if (Array.isArray(c.latex)) {
c.latex.forEach(limg => rewriteImageObject.call(this, limg));
c.latex.forEach((limg) => rewriteImageObject.call(this, limg));
}
}
};

(this.data.chapters || []).forEach(ch => {
(this.data.chapters || []).forEach((ch) => {
(ch.contents || []).forEach(scanChapterContent);
});

Expand All @@ -270,7 +304,11 @@ class EPubFileBuilder {

convertEPub() {
_.forEach(this.data.chapters, (ch, idx) => {
const contentXHTML = this.convertChapter(idx, ch, this.data.chapterGlossary ? this.data.chapterGlossary[idx] : false);
const contentXHTML = this.convertChapter(
idx,
ch,
this.data.chapterGlossary ? this.data.chapterGlossary[idx] : false,
);
this.zip.addFile(`OEBPS/${ch.id}.xhtml`, Buffer.from(contentXHTML));
});
if (this.glossary && !_.isEmpty(this.glossary) && !this.data.chapterGlossary) {
Expand Down
Loading