Compare commits

..

No commits in common. "master" and "1.1" have entirely different histories.
master ... 1.1

36 changed files with 601 additions and 3011 deletions

3
.gitignore vendored
View file

@ -1,3 +0,0 @@
node_modules/
tests/pages/
.DS_Store

View file

@ -2,12 +2,10 @@
Save a web page/selection as an eBook (.epub format) - a Chrome/Firefox/Opera Web Extension
<img src="https://github.com/alexadam/save-as-ebook/blob/master/ex11.png?raw=true" width="350">
![alt ex1.png](https://github.com/alexadam/save-as-ebook/blob/master/ex1.png?raw=true)
![alt ex2.png](https://github.com/alexadam/save-as-ebook/blob/master/ex2.png?raw=true)
![alt ex3.png](https://github.com/alexadam/save-as-ebook/blob/master/ex3.png?raw=true)
## How to install it
### From [Chrome Web Store](https://chrome.google.com/webstore/detail/save-as-ebook/haaplkpoiimngbppjihnegfmpejdnffj)
@ -45,91 +43,21 @@ sudo apt-get install calibre
ebook-convert "book.epub" "book.mobi"
```
## Default Keyboard Shortcuts
**NOTE** These shortcuts are not fixed and the browser will assign a different shortcut if the default one is taken
| Shortcut | Description |
| --- | --- |
| Alt + Shift + 1 | Save current page as eBook |
| Alt + Shift + 2 | Save current selection as eBook |
| Alt + Shift + 3 | Add current page as chapter |
| Alt + Shift + 4 | Add current selection as chapter |
## How to change the default Shortcuts
in Chrome:
```
1. Navigate to chrome://extensions/
2. Scroll down
3. Click on Keyboard shortcuts
```
## Added in 1.4.2
- Added MIME type to the generated .epub file
## Added in 1.4.1
- Remove unnecessary permissions
- Detect image type if the URL doesn't have a file extension (jpg, gif, png)
- Reset the Busy indicator on errors
- Remove hidden elements when style is not included
- Replace iframes with divs
## Added in 1.4
- Smaller ebook file size
- Fix for #37 - custom styles not applied
- Fix for #36 - br tag missing from pre blocks
- Fix for #31 - hanging in Busy state
- Other misc bug fixes
## Added in v1.3.4
- Fix for MathML - the rendered expression is too large (Issue #26)
- Add translation in Russian (thanks to @ Emil Khalikov) & Brazilian Portuguese (thanks to @welksonramos)
## Added in v1.3
- Keyboard shortcuts
- Simplified tool bar menu
- Misc bug fixes
## Added in v1.2.2
- fixed & &amp; issue in title; Issue # 10
## Added in v1.2.1
- support for hr/br html tags
## Added in v1.2
- BETA: Support for CSS
- BETA: Create / edit custom Styles
- No errors from EPUB Validator (http://validator.idpf.org/) + this should fix the Google Play upload issue
## Added in v1.1
- Chapter Editor: option to save changes
- Chapter Editor: option to remove all chapters
- persist Chapter Editor changes & chapters after generating an eBook or after a browser restart
## To-Do
- make the Custom Style Editor more user friendly
- support backup / restore for Custom Styles
- DONE fix all 'epubcheck' errors (https://github.com/IDPF/epubcheck)
- fix all 'epubcheck' errors (https://github.com/IDPF/epubcheck)
- clean & optimize code
- create tests
- 'save as image' option (render a selection and save it as image instead of text)
- support other formats (mobi, pdf etc.)
- show confirmations (ui/ux)
- display errors (ui/ux)
- DONE support custom style
- add 'remove from ebook' right click menu action
## Run Tests (Work in progress...)
```
cd tests
yarn install # install puppeteer
node test/index.js # should start a chrome instance with Save as eBook loaded
# it will generate and save the ebook in ./tmp-downloads
....
```
- add settings & options page (ui/ux)
- support custom style
## Credits
- http://ebooks.stackexchange.com/questions/1183/what-is-the-minimum-required-content-for-a-valid-epub
@ -137,5 +65,4 @@ in Chrome:
- https://stuk.github.io/jszip/
- http://johnny.github.io/jquery-sortable/
- https://github.com/eligrey/FileSaver.js/
- https://www.iconfinder.com/icons/753890/book_books_education_library_study_icon#size=128
- Thanks to [pumpk0n](https://github.com/pumpk0n) and [Francois Bocquet](https://github.com/fbocquet) for helping me with the French translation
- https://www.iconfinder.com/icons/1031371/book_empty_library_reading_icon#size=128

BIN
ex1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
ex11.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

BIN
ex3.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

BIN
tests.tar Normal file

Binary file not shown.

View file

@ -1,52 +0,0 @@
const puppeteer = require('puppeteer');
const fs = require('fs')
const CRX_PATH = '../web-extension';
const REFERENCE_EBOOK_PATH = 'reference-ebook'
const TEST_RESULT_EBOOK_PATH = 'test-result-ebook'
const TEST_EBOOK_FILE_NAME = 'test.epub'
const testPaths = [
]
puppeteer.launch({
headless: false,
args: [
`--disable-extensions-except=${CRX_PATH}`,
`--load-extension=${CRX_PATH}`,
'--user-agent=PuppeteerTestingAgent'
]
}).then(async browser => {
prepareTests()
await runLocalFullPageTests(browser)
});
async function runLocalFullPageTests(browser) {
const testedFileName = 'mathjax' //'canvas' //'svg' //'special-chars' // 'p2' //'p1'
const testUrl = 'file://'+__dirname+'/pages/'+testedFileName+'/page/index.html'
const resultDownloadPath = './pages/'+testedFileName+'/' + TEST_RESULT_EBOOK_PATH
const page = await browser.newPage();
await page._client.send('Page.setDownloadBehavior', {behavior: 'allow', downloadPath: resultDownloadPath});
await page.setViewport({ width: 1280, height: 800 })
// await page.goto('https://en.wikipedia.org/wiki/E-book', { waitUntil: 'networkidle0' });
// await page.goto('file://'+__dirname+'/../pages/p1/E-book - Wikipedia.html', { waitUntil: 'networkidle0' });
await page.goto(testUrl, { waitUntil: 'networkidle0' });
}
function prepareTests() {
const testedFileName = 'p1'
const pathToDelete = './pages/'+testedFileName+'/'+TEST_RESULT_EBOOK_PATH+'/'+TEST_EBOOK_FILE_NAME
try {
if (fs.existsSync(pathToDelete)) {
fs.unlinkSync(pathToDelete)
}
} catch(err) {
console.log('Error while deleting file ', err);
}
}

View file

@ -1,9 +0,0 @@
{
"name": "web-extension-test",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"puppeteer": "^1.14.0"
}
}

Binary file not shown.

View file

@ -1,94 +0,0 @@
{
"extName": {
"message": "Save as eBook",
"description": "Extension name"
},
"includeStyle": {
"message": "Include (Custom) Style"
},
"editStyles": {
"message": "Edit Custom Styles ..."
},
"savePage": {
"message": "Save Page"
},
"saveSelection": {
"message": "Save Selection"
},
"pageChapter": {
"message": "Add Page as Chapter"
},
"selectionChapter": {
"message": "Add Selection as Chapter"
},
"editChapters": {
"message": "Edit Chapters ..."
},
"waitMessage": {
"message": "Please Wait ..."
},
"chapterEditorTitle": {
"message": "Chapter Editor"
},
"ebookTitleLabel": {
"message": "eBook Title:"
},
"rawPreview": {
"message": "Raw Preview"
},
"remove": {
"message": "Remove"
},
"removeChapters": {
"message": "Remove Chapters"
},
"removeChaptersConfirm": {
"message": "Do you want to remove all chapters?"
},
"cancel": {
"message": "Cancel"
},
"generateEbook": {
"message": "Generate eBook ..."
},
"saveChanges": {
"message": "Save changes"
},
"emptyBookWarning": {
"message": "Can't generate an empty eBook!"
},
"confirmDeleteStyle": {
"message": "Are you sure you want to delete this style?"
},
"invalidRegex": {
"message": "Invalid regular expression"
},
"styleSaved": {
"message": "Style saved!"
},
"styleEditor": {
"message": "Style Editor"
},
"selectExistingCSS": {
"message": "Select Existing CSS"
},
"orLabel": {
"message": "or"
},
"createNewStyle": {
"message": "Create New Style"
},
"styleNameLabel": {
"message": "Name"
},
"howToWriteRegexLabel": {
"message": "How to write a regular expression pattern"
},
"removeStyle": {
"message": "Remove Style"
},
"saveStyle": {
"message": "Save Style"
}
}

View file

@ -1,99 +0,0 @@
{
"extName": {
"message": "Enregistrer comme eBook",
"description": "Nom de l'extension"
},
"includeStyle": {
"message": "Inclure le style"
},
"styleLabel": {
"message": "Style personnalisé :"
},
"applyStyle": {
"message": "Appliquer"
},
"editStyles": {
"message": "Éditer les styles..."
},
"savePage": {
"message": "Enregistrer la page"
},
"saveSelection": {
"message": "Enregistrer la sélection"
},
"pageChapter": {
"message": "Ajouter la page en tant que chapitre"
},
"selectionChapter": {
"message": "Ajouter la sélection en tant que chapitre"
},
"editChapters": {
"message": "Modifier les chapitres..."
},
"waitMessage": {
"message": "Merci de patienter..."
},
"chapterEditorTitle": {
"message": "Éditeur de chapitre"
},
"ebookTitleLabel": {
"message": "Titre de l'eBook :"
},
"rawPreview": {
"message": "Aperçu brut"
},
"remove": {
"message": "Retirer"
},
"removeChapters": {
"message": "Supprimer les chapitres"
},
"removeChaptersConfirm": {
"message": "Voulez-vous supprimer tous les chapitres ?"
},
"cancel": {
"message": "Annuler"
},
"generateEbook": {
"message": "Générer l'eBook..."
},
"saveChanges": {
"message": "Enregistrer les modifications"
},
"emptyBookWarning": {
"message": "Impossible de générer un eBook vide !"
},
"confirmDeleteStyle": {
"message": "Êtes-vous sûr de vouloir supprimer ce style ?"
},
"invalidRegex": {
"message": "Expression régulière invalide"
},
"styleSaved": {
"message": "Style enregistré !"
},
"styleEditor": {
"message": "Éditeur de style"
},
"selectExistingCSS": {
"message": "Sélectionner un style existant"
},
"orLabel": {
"message": "ou"
},
"createNewStyle": {
"message": "Créer un nouveau style"
},
"styleNameLabel": {
"message": "Nom"
},
"howToWriteRegexLabel": {
"message": "Comment écrire une expression régulière"
},
"removeStyle": {
"message": "Supprimer le style"
},
"saveStyle": {
"message": "Enregistrer le style"
}
}

View file

@ -1,93 +0,0 @@
{
"extName": {
"message": "Salvar como eBook",
"description": "Nome da extensão"
},
"includeStyle": {
"message": "Incuir estilo (personalizado)"
},
"editStyles": {
"message": "Editar estilos personalizados ..."
},
"savePage": {
"message": "Salvar Página"
},
"saveSelection": {
"message": "Salvar Selecão"
},
"pageChapter": {
"message": "Adicionar página como capítulo"
},
"selectionChapter": {
"message": "Adicionar seleção como capítulo"
},
"editChapters": {
"message": "Editar Capítulos ..."
},
"waitMessage": {
"message": "Por favor, aguarde ..."
},
"chapterEditorTitle": {
"message": "Editor de capítulos"
},
"ebookTitleLabel": {
"message": "Título do eBook:"
},
"rawPreview": {
"message": "Pré-visualização"
},
"remove": {
"message": "Remover"
},
"removeChapters": {
"message": "Remover capítulos"
},
"removeChaptersConfirm": {
"message": "Você quer remover todos os capítulos?"
},
"cancel": {
"message": "Cancelar"
},
"generateEbook": {
"message": "Gerar eBook ..."
},
"saveChanges": {
"message": "Salvar alterações"
},
"emptyBookWarning": {
"message": "Não é possível gerar um eBook vazio!"
},
"confirmDeleteStyle": {
"message": "Tem certeza de que deseja excluir este estilo?"
},
"invalidRegex": {
"message": "Expressão regular inválida"
},
"styleSaved": {
"message": "Estilo salvo!"
},
"styleEditor": {
"message": "Editor de estilo"
},
"selectExistingCSS": {
"message": "Selecionar CSS Existente"
},
"orLabel": {
"message": "ou"
},
"createNewStyle": {
"message": "Criar Novo Estilo"
},
"styleNameLabel": {
"message": "Nome"
},
"howToWriteRegexLabel": {
"message": "Como escrever um padrão de expressão regular"
},
"removeStyle": {
"message": "Remover Estilo"
},
"saveStyle": {
"message": "Salvar Estilo"
}
}

View file

@ -1,93 +0,0 @@
{
"extName": {
"message": "Save as eBook",
"description": "Extension name"
},
"includeStyle": {
"message": "Подключить CSS"
},
"editStyles": {
"message": "Редактировать CSS ..."
},
"savePage": {
"message": "Сохранить страницу"
},
"saveSelection": {
"message": "Сохранить выделение"
},
"pageChapter": {
"message": "Добавить страницу как главу"
},
"selectionChapter": {
"message": "Добавить выделение как главу"
},
"editChapters": {
"message": "Редактировать главы ..."
},
"waitMessage": {
"message": "Пожалуйста ожидайте ..."
},
"chapterEditorTitle": {
"message": "Редактор глав"
},
"ebookTitleLabel": {
"message": "Заголовок книги:"
},
"rawPreview": {
"message": "Предпросмотр"
},
"remove": {
"message": "Удалить"
},
"removeChapters": {
"message": "Удалить главы"
},
"removeChaptersConfirm": {
"message": "Вы точно хотите удалить все главы?"
},
"cancel": {
"message": "Отмена"
},
"generateEbook": {
"message": "Создать книгу ..."
},
"saveChanges": {
"message": "Сохранить изменения"
},
"emptyBookWarning": {
"message": "Невозможно создать пустую книгу"
},
"confirmDeleteStyle": {
"message": "Вы точно хотите удалить этот стиль?"
},
"invalidRegex": {
"message": "Ошибка в регулярном выражении"
},
"styleSaved": {
"message": "Стиль сохранен!"
},
"styleEditor": {
"message": "Редактор стилей"
},
"selectExistingCSS": {
"message": "Выбрать существующий стиль"
},
"orLabel": {
"message": "или"
},
"createNewStyle": {
"message": "Создать новый стиль"
},
"styleNameLabel": {
"message": "Название"
},
"howToWriteRegexLabel": {
"message": "Введите регулярное выражение для адреса"
},
"removeStyle": {
"message": "Удалить стиль"
},
"saveStyle": {
"message": "Сохранить стиль"
}
}

View file

@ -1,354 +1,5 @@
///////////////////
///////////////////
///////////////////
///////////////////
/// Only for testing
chrome.runtime.onInstalled.addListener(details => {
if (navigator.userAgent === 'PuppeteerTestingAgent') {
let TEST_TIMER = null
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (TEST_TIMER) {
clearTimeout(TEST_TIMER)
}
TEST_TIMER = setTimeout(()=> {
executeCommand({type: 'save-page'})
}, 2000)
});
}
});
///////////////////
///////////////////
///////////////////
///////////////////
var isBusy = false;
var busyResetTimer = null
var defaultStyles = [
{
title: 'Reddit Comments',
url: 'reddit\\.com\\/r\\/[^\\/]+\\/comments',
style: `.side {
display: none;
}
#header {
display: none;
}
.arrow, .expand, .score, .live-timestamp, .flat-list, .buttons, .morecomments, .footer-parent, .icon {
display: none !important;
}
`
},{
title: 'Wikipedia Article',
url: 'wikipedia\\.org\\/wiki\\/',
style: `#mw-navigation {
display: none;
}
#footer {
display: none;
}
#mw-panel {
display: none;
}
#mw-head {
display: none;
}
`
},{
title: 'YCombinator News Comments',
url: 'news\\.ycombinator\\.com\\/item\\?id=[0-9]+',
style: `#hnmain > tbody > tr:nth-child(1) > td > table {
display: none;
}
* {
background-color: white;
}
.title, .storylink {
text-align: left;
font-weight: bold;
font-size: 20px;
}
.score {
display: none;
}
.age {
display: none;
}
.hnpast {
display: none;
}
.togg {
display: none;
}
.votelinks, .rank {
display: none;
}
.votearrow {
display: none;
}
.yclinks {
display: none;
}
form {
display: none;
}
a.hnuser {
font-weight: bold;
color: black !important;
padding: 3px;
}
.subtext > span, .subtext > a:not(:nth-child(2)) {
display: none;
}
`
},{
title: 'Medium Article',
url: 'medium\\.com',
style: `.metabar {
display: none !important;
}
header.container {
display: none;
}
.js-postShareWidget {
display: none;
}
footer, canvas {
display: none !important;
}
.u-fixed, .u-bottom0 {
display: none;
}
`
},{
title: 'Twitter',
url: 'twitter\\.com\\/.+',
style: `.topbar {
display: none !important;
}
.ProfileCanopy, .ProfileCanopy-inner {
display: none;
}
.ProfileSidebar {
display: none;
}
.ProfileHeading {
display: none !important;
}
.ProfileTweet-actionList {
display: none;
}
`
}
];
chrome.commands.onCommand.addListener((command) => {
executeCommand({type: command})
});
function executeCommand(command) {
if (isBusy) {
chrome.tabs.query({
currentWindow: true,
active: true
}, (tab) => {
chrome.tabs.sendMessage(tab[0].id, {'alert': 'Work in progress! Please wait until the current eBook is generated!'}, (r) => {
console.log(r);
});
})
return;
}
if (command.type === 'save-page') {
dispatch('extract-page', false, []);
} else if (command.type === 'save-selection') {
dispatch('extract-selection', false, []);
} else if (command.type === 'add-page') {
dispatch('extract-page', true, []);
} else if (command.type === 'add-selection') {
dispatch('extract-selection', true, []);
}
isBusy = true
//
busyResetTimer = setTimeout(() => {
resetBusy()
}, 20000)
}
function dispatch(action, justAddToBuffer, appliedStyles) {
if (!justAddToBuffer) {
_execRequest({type: 'remove'});
}
chrome.browserAction.setBadgeBackgroundColor({color:"red"});
chrome.browserAction.setBadgeText({text: "Busy"});
chrome.tabs.query({
currentWindow: true,
active: true
}, (tab) => {
isIncludeStyles((result) =>{
let isIncludeStyle = result.includeStyle
prepareStyles(tab, isIncludeStyle, appliedStyles, (tmpAppliedStyles) => {
applyAction(tab, action, justAddToBuffer, isIncludeStyle, tmpAppliedStyles, () => {
alert('done')
})
})
})
});
}
function isIncludeStyles(callback) {
chrome.storage.local.get('includeStyle', (data) => {
if (!data) {
callback({includeStyle: false});
} else {
callback({includeStyle: data.includeStyle});
}
});
}
function prepareStyles(tab, includeStyle, appliedStyles, callback) {
if (!includeStyle) {
callback(appliedStyles)
return
}
chrome.storage.local.get('styles', (data) => {
let styles = defaultStyles;
if (data && data.styles) {
styles = data.styles;
}
let currentUrl = tab[0].url;
let currentStyle = null;
if (!styles) {
callback(appliedStyles)
return
}
if (styles.length === 0) {
callback(appliedStyles)
return
}
let allMatchingStyles = [];
for (let i = 0; i < styles.length; i++) {
currentUrl = currentUrl.replace(/(http[s]?:\/\/|www\.)/i, '').toLowerCase();
let styleUrl = styles[i].url;
let styleUrlRegex = null;
try {
styleUrlRegex = new RegExp(styleUrl, 'i');
} catch (e) {
}
if (styleUrlRegex && styleUrlRegex.test(currentUrl)) {
allMatchingStyles.push({
index: i,
length: styleUrl.length
});
}
}
if (allMatchingStyles.length === 0) {
callback(appliedStyles)
return
}
allMatchingStyles.sort((a, b) => b.length - a.length);
let selStyle = allMatchingStyles[0];
if (!selStyle) {
callback(appliedStyles)
return
}
currentStyle = styles[selStyle.index];
if (!currentStyle) {
callback(appliedStyles)
return
}
if (!currentStyle.style) {
callback(appliedStyles)
return
}
chrome.tabs.insertCSS(tab[0].id, { code: currentStyle.style }, () => {
appliedStyles.push(currentStyle);
callback(appliedStyles)
});
});
}
function applyAction(tab, action, justAddToBuffer, includeStyle, appliedStyles, callback) {
chrome.tabs.sendMessage(tab[0].id, {
type: action,
includeStyle: includeStyle,
appliedStyles: appliedStyles
}, (response) => {
if (!response) {
resetBusy()
chrome.tabs.sendMessage(tab[0].id, {'alert': 'Save as eBook does not work on this web site!'}, (r) => {});
return;
}
if (response.content.trim() === '') {
resetBusy()
if (justAddToBuffer) {
chrome.tabs.sendMessage(tab[0].id, {'alert': 'Cannot add an empty selection as chapter!'}, (r) => {});
} else {
chrome.tabs.sendMessage(tab[0].id, {'alert': 'Cannot generate the eBook from an empty selection!'}, (r) => {});
}
return;
}
if (!justAddToBuffer) {
chrome.tabs.sendMessage(tab[0].id, {'shortcut': 'build-ebook', response: [response]}, (r) => {});
} else {
chrome.storage.local.get('allPages', (data) => {
if (!data || !data.allPages) {
data.allPages = [];
}
data.allPages.push(response);
chrome.storage.local.set({'allPages': data.allPages});
resetBusy()
chrome.tabs.sendMessage(tab[0].id, {'alert': 'Page or selection added as chapter!'}, (r) => {});
})
}
});
}
function resetBusy() {
isBusy = false
if (busyResetTimer) {
clearTimeout(busyResetTimer)
busyResetTimer = null
}
chrome.browserAction.setBadgeText({text: ""})
let popups = chrome.extension.getViews({type: "popup"});
if (popups && popups.length > 0) {
popups[0].close()
}
}
chrome.runtime.onMessage.addListener(_execRequest);
function _execRequest(request, sender, sendResponse) {
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.type === 'get') {
chrome.storage.local.get('allPages', function (data) {
if (!data || !data.allPages) {
@ -376,54 +27,5 @@ function _execRequest(request, sender, sendResponse) {
if (request.type === 'set title') {
chrome.storage.local.set({'title': request.title});
}
if (request.type === 'get styles') {
chrome.storage.local.get('styles', function (data) {
if (!data || !data.styles) {
sendResponse({styles: defaultStyles});
} else {
sendResponse({styles: data.styles});
}
});
}
if (request.type === 'set styles') {
chrome.storage.local.set({'styles': request.styles});
}
if (request.type === 'get current style') {
chrome.storage.local.get('currentStyle', function (data) {
if (!data || !data.currentStyle) {
sendResponse({currentStyle: 0});
} else {
sendResponse({currentStyle: data.currentStyle});
}
});
}
if (request.type === 'set current style') {
chrome.storage.local.set({'currentStyle': request.currentStyle});
}
if (request.type === 'get include style') {
chrome.storage.local.get('includeStyle', function (data) {
if (!data) {
sendResponse({includeStyle: false});
} else {
sendResponse({includeStyle: data.includeStyle});
}
});
}
if (request.type === 'set include style') {
chrome.storage.local.set({'includeStyle': request.includeStyle});
}
if (request.type === 'is busy?') {
sendResponse({isBusy: isBusy})
}
if (request.type === 'set is busy') {
isBusy = request.isBusy
}
if (request.type === 'save-page' || request.type === 'save-selection' ||
request.type === 'add-page' || request.type === 'add-selection') {
executeCommand({type: request.type})
}
if (request.type === 'done') {
resetBusy()
}
return true;
}
});

View file

@ -18,7 +18,7 @@ ul.chapterEditor-chapters-list li.placeholder {
font-weight: bold;
float: left;
display: inline-block;
font-family: sans-serif;
font-family: "sans-serif";
}
#chapterEditor-ebookTitleHolder {
@ -35,7 +35,7 @@ ul.chapterEditor-chapters-list li.placeholder {
#chapterEditor-ebookTitle {
padding: 5px;
font-size: 15px;
font-family: sans-serif;
font-family: "sans-serif";
width: 85%;
}
ul, ul.chapterEditor-chapters-list {
@ -66,7 +66,7 @@ ul, ul.chapterEditor-chapters-list {
.chapterEditor-chapter-item > input[type="text"] {
padding: 5px;
font-size: 15px;
font-family: sans-serif;
font-family: "sans-serif";
width: 80%;
border: none;
border-bottom: solid 1px #aaa;
@ -79,6 +79,9 @@ ul, ul.chapterEditor-chapters-list {
}
#chapterEditor-modalList {
display: block;
/*width: 100%;*/
/*border-top: solid 1px black;*/
/*border-bottom: solid 1px black;*/
padding: 20px;
padding-top: 0;
}
@ -97,7 +100,7 @@ ul, ul.chapterEditor-chapters-list {
.chapterEditor-text-button {
border: none;
font-size: 15px;
font-family: sans-serif;
font-family: "sans-serif";
padding: 5px;
background-color: rgba(0, 0, 0, 0);
margin: 0 3px;
@ -120,8 +123,6 @@ ul, ul.chapterEditor-chapters-list {
}
.chapterEditor-footer-button {
position: relative;
overflow: hidden;
padding: 18px 20px;
margin: 0;
font-size: 18px;
@ -134,34 +135,10 @@ ul, ul.chapterEditor-chapters-list {
background-color: rgba(0, 0, 0, 1);
cursor: pointer;
}
.chapterEditor-footer-button:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, .5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
@keyframes ripple {
0% { transform: scale( 0, 0); opacity: 1; }
20% { transform: scale(25, 25); opacity: 1; }
100% { transform: scale(40, 40); opacity: 0; }
}
.chapterEditor-footer-button:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
.chapterEditor-footer-button.chapterEditor-generate-button {
.chapterEditor-generate-button {
background-color: yellow;
}
.chapterEditor-footer-button.chapterEditor-generate-button:hover {
background-color: rgba(0, 0, 0, 1);
}
.chapterEditor-footer-button.chapterEditor-cancel-button {
.chapterEditor-cancel-button {
/*background-color: black;*/
}

View file

@ -26,7 +26,7 @@ function showEditor() {
// Header
var title = document.createElement('span');
title.id = "chapterEditor-Title";
title.innerText = chrome.i18n.getMessage('chapterEditorTitle');
title.innerText = "Chapter Editor";
var upperCloseButton = document.createElement('button');
modalHeader.appendChild(title);
upperCloseButton.onclick = closeModal;
@ -41,7 +41,7 @@ function showEditor() {
var ebookTilteLabel = document.createElement('span');
ebookTilteLabel.id = 'chapterEditor-ebookTitleLabel';
ebookTilteLabel.innerText = chrome.i18n.getMessage('ebookTitleLabel');
ebookTilteLabel.innerText = 'eBook Title: ';
titleHolder.appendChild(ebookTilteLabel);
var ebookTilte = document.createElement('input');
@ -81,12 +81,12 @@ function showEditor() {
var buttons = document.createElement('span');
var previewButton = document.createElement('button');
previewButton.innerText = chrome.i18n.getMessage('rawPreview');
previewButton.innerText = 'Raw Preview';
previewButton.className = 'chapterEditor-text-button';
previewButton.onclick = previewListItem(i);
var removeButton = document.createElement('button');
removeButton.innerText = chrome.i18n.getMessage('remove');
removeButton.innerText = 'Remove';
removeButton.className = 'chapterEditor-text-button chapterEditor-text-red';
removeButton.onclick = removeListItem(i);
@ -108,16 +108,16 @@ function showEditor() {
// Footer
var buttons = document.createElement('div');
var closeButton = document.createElement('button');
closeButton.innerText = chrome.i18n.getMessage('cancel');
closeButton.innerText = 'Cancel';
closeButton.className = 'chapterEditor-footer-button chapterEditor-float-left chapterEditor-cancel-button';
closeButton.onclick = closeModal;
buttons.appendChild(closeButton);
var removeButton = document.createElement('button');
removeButton.innerText = chrome.i18n.getMessage('removeChapters');
removeButton.innerText = 'Remove Chapters';
removeButton.className = 'chapterEditor-footer-button hapterEditor-float-left';
removeButton.onclick = function() {
var result = confirm(chrome.i18n.getMessage('removeChaptersConfirm'));
var result = confirm("Do you want to remove all chapters?");
if (result) {
removeEbook();
closeModal();
@ -130,7 +130,7 @@ function showEditor() {
var newChapters = saveChanges();
prepareEbook(newChapters);
};
saveButton.innerText = chrome.i18n.getMessage('generateEbook');
saveButton.innerText = 'Generate eBook ...';
saveButton.className = 'chapterEditor-footer-button chapterEditor-float-right chapterEditor-generate-button';
buttons.appendChild(saveButton);
@ -138,7 +138,7 @@ function showEditor() {
saveChangesButton.onclick = function() {
saveChanges();
};
saveChangesButton.innerText = chrome.i18n.getMessage('saveChanges');
saveChangesButton.innerText = 'Save changes';
saveChangesButton.className = 'chapterEditor-footer-button chapterEditor-float-right';
buttons.appendChild(saveChangesButton);
@ -213,7 +213,7 @@ function showEditor() {
function prepareEbook(newChapters) {
try {
if (newChapters.length === 0) {
alert(chrome.i18n.getMessage('emptyBookWarning'));
alert('Can\'t generate an empty eBook!');
return;
}
buildEbookFromChapters();
@ -236,11 +236,8 @@ function showEditor() {
}
for (var i = 0; i < tmpChaptersList.length; i++) {
var tmpChapterItem = tmpChaptersList[i];
var listIndex = Number(tmpChapterItem.id.replace('li', ''));
var listIndex = Number(tmpChaptersList[i].id.replace('li', ''));
if (allPagesRef[listIndex].removed === false) {
var newChapterTitle = tmpChapterItem.children.namedItem('text'+listIndex).value;
allPagesRef[listIndex].title = newChapterTitle;
newChapters.push(allPagesRef[listIndex]);
}
}

View file

@ -1,190 +0,0 @@
#cssEditor-removeStyle {
display: none;
}
#cssEditor-saveStyle {
display: none;
}
#cssEditor-selectStyle {
padding: 3px;
cursor: pointer;
font-size: 15px;
font-family: sans-serif;
}
#cssEditor-selectStyle > option {
padding: 3px;
font-size: 1em;
cursor: pointer;
font-size: 15px;
font-family: sans-serif;
}
#cssEditor-orLabel {
padding: 3px;
margin: 0 3px;
font-size: 15px;
font-family: sans-serif;
}
#cssEditor-createNewStyle {
padding: 3px;
cursor: pointer;
font-size: 15px;
font-family: sans-serif;
}
.cssEditor-field-label-holder {
margin-top: 10px;
font-size: 15px;
font-family: sans-serif;
display: flex;
justify-content: space-between;
width: 90%;
}
.cssEditor-field-holder {
padding: 3px;
width: 90%;
}
.cssEditor-field-label {
padding: 0 3px;
margin-top: 5px;
}
#cssEditor-styleEditor {
display: flex;
flex-flow: row;
}
.cssEditor-left-panel {
width: 48%;
display: flex;
flex-flow: column;
}
.cssEditor-right-panel {
width: 48%;
display: flex;
flex-flow: column;
}
#cssEditor-styleName {
width: 100%;
min-width: 100%;
padding: 5px;
border: solid rgba(0, 0, 0, 0.25) 1px;
font-size: 15px;
font-family: sans-serif;
}
#cssEditor-matchUrl {
width: 100%;
min-width: 100%;
padding: 5px;
border: solid rgba(0, 0, 0, 0.25) 1px;
font-size: 15px;
font-family: sans-serif;
}
#cssEditor-styleContent {
width: 100%;
height: 150px;
padding: 5px;
border: solid rgba(0, 0, 0, 0.25) 1px;
font-size: 15px;
font-family: sans-serif;
}
#cssEditor-Title {
font-size: 20px;
font-weight: bold;
float: left;
display: inline-block;
font-family: sans-serif;
}
#cssEditor-ebookTitleHolder {
background-color: #eee;
padding: 10px 20px;
margin-bottom: 10px;
}
#cssEditor-modalHeader {
display: block;
overflow: hidden;
padding: 20px;
}
#cssEditor-modalList {
display: block;
padding: 20px;
padding-top: 0;
}
#cssEditor-modalFooter {
display: block;
overflow: hidden;
}
.cssEditor-text-button {
border: none;
font-size: 15px;
font-family: sans-serif;
padding: 5px;
background-color: rgba(0, 0, 0, 0);
margin: 0 3px;
outline: none;
cursor: pointer;
}
.cssEditor-text-button:hover {
background-color: #000;
color: #fff;
}
.cssEditor-text-red {
color: red;
}
.cssEditor-float-left {
float: left;
}
.cssEditor-float-right {
float: right;
}
.cssEditor-footer-button {
padding: 18px 20px;
margin: 0;
font-size: 18px;
border: none;
background-color: rgba(0, 0, 0, 0);
display: inline-block;
}
.cssEditor-footer-button:hover {
color: white;
background-color: rgba(0, 0, 0, 1);
cursor: pointer;
}
.cssEditor-save-button {
background-color: yellow;
}
.cssEditor-cancel-button {
/*background-color: black;*/
}
@media (max-width: 1700px) {
.cssEditor-text-button {
font-size: 12px;
padding: 3px;
margin: 0 2px;
}
.cssEditor-footer-button {
padding: 15px 20px;
margin: 0;
font-size: 15px;
border: none;
background-color: rgba(0, 0, 0, 0);
}
}
@media (max-width: 1100px) {
.cssEditor-chapter-item {
line-height: 30px;
height: 60px;
padding: 4px 0;
font-size: 14px;
}
.cssEditor-chapter-item > input[type="text"] {
width: 90%;
}
}

View file

@ -1,379 +0,0 @@
for (var i=0; i<document.styleSheets.length; i++) {
document.styleSheets.item(i).disabled = true;
}
var tmp = document.getElementById('cssEditor-Modal');
if (tmp) {
tmp.parentNode.removeChild(tmp);
}
var allPagesRef = null;
var allStyles = [];
var currentStyle = null;
var currentStyleIndex = -1;
showEditor();
function showEditor() {
var body = document.getElementsByTagName('body')[0];
var modalContent = document.createElement('div');
modalContent.id = 'cssEditor-modalContent';
var modalHeader = document.createElement('div');
modalHeader.id = 'cssEditor-modalHeader';
var modalList = document.createElement('div');
modalList.id = 'cssEditor-modalList';
var modalFooter = document.createElement('div');
modalFooter.id = 'cssEditor-modalFooter';
////////
// Header
var title = document.createElement('span');
title.id = "cssEditor-Title";
title.innerText = chrome.i18n.getMessage('styleEditor');
var upperCloseButton = document.createElement('button');
modalHeader.appendChild(title);
upperCloseButton.onclick = closeModal;
upperCloseButton.innerText = 'X';
upperCloseButton.className = 'cssEditor-text-button cssEditor-float-right';
modalHeader.appendChild(upperCloseButton);
/////////////////////
// Content List
var titleHolder = document.createElement('div');
titleHolder.id = 'cssEditor-ebookTitleHolder';
var existingStyles = document.createElement('select');
existingStyles.id = "cssEditor-selectStyle";
existingStyles.onchange = function (event) {
if (existingStyles.selectedIndex === 0) {
currentStyle = null;
currentStyleIndex = -1;
hideStyleEditor();
hideRemoveStyle();
hideSaveStyle();
return;
}
currentStyle = allStyles[existingStyles.selectedIndex - 1];
currentStyleIndex = existingStyles.selectedIndex - 1;
editCurrentStyle();
};
var defaultOption = document.createElement('option');
defaultOption.innerText = chrome.i18n.getMessage('selectExistingCSS');
existingStyles.appendChild(defaultOption);
titleHolder.appendChild(existingStyles);
var titleLabel = document.createElement('label');
titleLabel.id = 'cssEditor-orLabel';
titleLabel.innerText = chrome.i18n.getMessage('orLabel');
titleHolder.appendChild(titleLabel);
var createNewStyleButton = document.createElement('button');
createNewStyleButton.id = 'cssEditor-createNewStyle';
createNewStyleButton.innerText = chrome.i18n.getMessage('createNewStyle');
createNewStyleButton.onclick = createNewStyle;
titleHolder.appendChild(createNewStyleButton);
modalList.appendChild(titleHolder);
function createNewStyle() {
currentStyle = null;
currentStyleIndex = -1;
resetFields();
showStyleEditor();
showSaveStyle();
hideRemoveStyle();
createStyleList();
}
//////
var editorHolder = document.createElement('div');
editorHolder.style.display = 'none';
editorHolder.id = 'cssEditor-styleEditor';
var editorHolderLeft = document.createElement('div');
editorHolderLeft.className = 'cssEditor-left-panel';
var editorHolderRight = document.createElement('div');
editorHolderRight.className = 'cssEditor-right-panel';
var nameLabelHolder = document.createElement('div');
nameLabelHolder.className = 'cssEditor-field-label-holder';
var nameLabel = document.createElement('label');
nameLabel.className = 'cssEditor-field-label';
nameLabel.innerText = chrome.i18n.getMessage('styleNameLabel');
nameLabelHolder.appendChild(nameLabel);
editorHolderLeft.appendChild(nameLabelHolder);
var nameInputHolder = document.createElement('div');
nameInputHolder.className = 'cssEditor-field-holder';
var cssNameInput = document.createElement('input');
cssNameInput.id = 'cssEditor-styleName';
cssNameInput.type = 'text';
nameInputHolder.appendChild(cssNameInput);
editorHolderLeft.appendChild(nameInputHolder);
var urlLabelHolder = document.createElement('div');
urlLabelHolder.className = 'cssEditor-field-label-holder';
var urlLabel = document.createElement('label');
urlLabel.className = 'cssEditor-field-label';
urlLabel.innerText = 'URL Regex'; // TODO addd link to regex tutorial
var regexHelp = document.createElement('a');
regexHelp.innerText = chrome.i18n.getMessage('howToWriteRegexLabel');
regexHelp.setAttribute('href', 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions');
urlLabelHolder.appendChild(urlLabel);
urlLabelHolder.appendChild(regexHelp);
editorHolderLeft.appendChild(urlLabelHolder);
var urlInputHolder = document.createElement('div');
urlInputHolder.className = 'cssEditor-field-holder';
var urlInput = document.createElement('input');
urlInput.id = 'cssEditor-matchUrl';
urlInput.type = 'text';
urlInputHolder.appendChild(urlInput);
editorHolderLeft.appendChild(urlInputHolder);
var contentLabelHolder = document.createElement('div');
contentLabelHolder.className = 'cssEditor-field-label-holder';
var contentLabel = document.createElement('label');
contentLabel.className = 'cssEditor-field-label';
contentLabel.innerText = 'CSS';
contentLabelHolder.appendChild(contentLabel);
editorHolderRight.appendChild(contentLabelHolder);
var contentInputHolder = document.createElement('div');
contentInputHolder.className = 'cssEditor-field-holder';
var contentInput = document.createElement('textarea');
contentInput.id = 'cssEditor-styleContent';
contentInput.setAttribute('data', 'language: css');
contentInputHolder.appendChild(contentInput);
editorHolderRight.appendChild(contentInputHolder);
editorHolder.appendChild(editorHolderLeft);
editorHolder.appendChild(editorHolderRight);
modalList.appendChild(editorHolder);
var saveButtonsHolder = document.createElement('div');
var removeCssButton = document.createElement('button');
removeCssButton.id = 'cssEditor-removeStyle';
removeCssButton.innerText = chrome.i18n.getMessage('removeStyle');
removeCssButton.className = 'cssEditor-footer-button cssEditor-float-left cssEditor-cancel-button';
removeCssButton.onclick = removeStyle;
saveButtonsHolder.appendChild(removeCssButton);
var saveCssButton = document.createElement('button');
saveCssButton.id = 'cssEditor-saveStyle';
saveCssButton.innerText = chrome.i18n.getMessage('saveStyle');
saveCssButton.className = 'cssEditor-footer-button cssEditor-float-right cssEditor-save-button';
saveCssButton.onclick = saveStyle;
saveButtonsHolder.appendChild(saveCssButton);
modalFooter.appendChild(saveButtonsHolder);
//////////////////////////
function createStyleList(allStylesTmp) {
if (allStylesTmp && allStylesTmp.length > 0) {
allStyles = allStyles.concat(allStylesTmp);
}
while (existingStyles.hasChildNodes() && existingStyles.childElementCount > 1) {
existingStyles.removeChild(existingStyles.lastChild);
}
for (var i = 0; i < allStyles.length; i++) {
var listItem = document.createElement('option');
listItem.id = 'option_' + i;
listItem.className = 'cssEditor-chapter-item';
listItem.value = 'option_' + i;
listItem.innerText = allStyles[i].title;
if (currentStyle && (allStyles[i].title === currentStyle.title)) {
listItem.selected = 'selected';
}
existingStyles.appendChild(listItem);
}
}
function editCurrentStyle() {
if (!currentStyle) {
return;
}
showStyleEditor();
showRemoveStyle();
showSaveStyle();
document.getElementById('cssEditor-styleName').value = currentStyle.title;
document.getElementById('cssEditor-matchUrl').value = currentStyle.url;
document.getElementById('cssEditor-styleContent').value = currentStyle.style;
}
function resetFields() {
document.getElementById('cssEditor-styleName').value = '';
document.getElementById('cssEditor-matchUrl').value = '';
document.getElementById('cssEditor-styleContent').value = '';
}
function hideStyleEditor() {
document.getElementById('cssEditor-styleEditor').style.display = 'none';
}
function showStyleEditor() {
document.getElementById('cssEditor-styleEditor').style.display = 'flex';
}
function showRemoveStyle() {
document.getElementById('cssEditor-removeStyle').style.display = 'inline-block';
}
function hideRemoveStyle() {
document.getElementById('cssEditor-removeStyle').style.display = 'none';
}
function showSaveStyle() {
document.getElementById('cssEditor-saveStyle').style.display = 'inline-block';
}
function hideSaveStyle() {
document.getElementById('cssEditor-saveStyle').style.display = 'none';
}
function saveStyle() {
var isRegexValid = checkRegex();
if (!isRegexValid) {
alert(chrome.i18n.getMessage('invalidRegex'));
return;
}
var tmpValue = {
title: document.getElementById('cssEditor-styleName').value,
url: document.getElementById('cssEditor-matchUrl').value,
style: document.getElementById('cssEditor-styleContent').value
}
if (currentStyle === null) {
allStyles.push(tmpValue);
currentStyle = tmpValue;
currentStyleIndex = allStyles.length - 1;
} else {
currentStyle = tmpValue;
allStyles[currentStyleIndex] = currentStyle;
}
setStyles(allStyles);
createStyleList();
showRemoveStyle();
alert(chrome.i18n.getMessage('styleSaved'));
}
function checkRegex() {
var regexContent = document.getElementById('cssEditor-matchUrl').value;
var isValid = true;
try {
new RegExp(regexContent);
} catch(e) {
isValid = false;
}
return isValid;
}
function removeStyle() {
if (confirm(chrome.i18n.getMessage('confirmDeleteStyle')) == true) {
allStyles.splice(currentStyleIndex, 1);
setStyles(allStyles);
hideSaveStyle();
hideRemoveStyle();
hideStyleEditor();
createStyleList();
}
}
/////////////////////
var modal = document.createElement('div');
modal.id = 'cssEditor-Modal';
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalList);
modalContent.appendChild(modalFooter);
modal.appendChild(modalContent);
body.appendChild(modal);
modal.style.display = "none";
modal.style.position = 'fixed';
modal.style.zIndex = '1';
modal.style.left = '0';
modal.style.top = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.overflow = 'auto';
modal.style.backgroundColor = 'rgba(210, 210, 210, 1)';
modalContent.style.zIndex = '2';
modalContent.style.backgroundColor = '#fff';
modalContent.style.margin = '5% auto';
modalContent.style.padding = '0';
modalContent.style.width = '70%';
window.onclick = function(event) {
if (event.target == modal) {
closeModal();
}
};
modal.style.display = "block";
document.onkeydown = function(evt) {
evt = evt || window.event;
if (evt.keyCode == 27) {
closeModal();
}
};
function closeModal() {
for (var i=0; i<document.styleSheets.length; i++) {
document.styleSheets.item(i).disabled = false;
}
modal.style.display = "none";
modalContent.parentNode.removeChild(modalContent);
modal.parentNode.removeChild(modal);
}
function saveChanges() {
var newChapters = [];
var newEbookTitle = ebookTilte.value;
if (newEbookTitle.trim() === '') {
newEbookTitle = 'eBook';
}
try {
var tmpChaptersList = document.getElementsByClassName('cssEditor-chapter-item');
if (!tmpChaptersList || !allPagesRef) {
return;
}
for (var i = 0; i < tmpChaptersList.length; i++) {
var listIndex = Number(tmpChaptersList[i].id.replace('li', ''));
if (allPagesRef[listIndex].removed === false) {
newChapters.push(allPagesRef[listIndex]);
}
}
saveEbookTitle(newEbookTitle);
saveEbookPages(newChapters);
return newChapters;
} catch (e) {
console.log('Error:', e);
}
}
/////////////////////
getStyles(createStyleList);
}

View file

@ -1,50 +1,18 @@
// Used to replace <img> src links that don't have a file extension
// If the image src doesn't have a file type:
// 1. Create a dummy link
// 2. Detect image type from the binary data & create new links
// 3. Replace all the dummy links in tmpGlobalContent with the new links
var tmpGlobalContent = null
var allImages = [];
var extractedImages = [];
var maxNrOfElements = 20000;
var allowedTags = [
'address', 'article', 'aside', 'footer', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'hgroup', 'nav', 'section', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure', 'hr', 'li',
'main', 'ol', 'p', 'pre', 'ul', 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data',
'dfn', 'em', 'i', 'img', 'kbd', 'mark', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small', 'span',
'dfn', 'em', 'i', 'img', 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small', 'span',
'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr', 'del', 'ins', 'caption', 'col', 'colgroup',
'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr',
'math', 'maction', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot',
'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'msgroup', 'mlongdiv', 'mscarries',
'mscarry', 'mstack', 'semantics'
// TODO ?
// ,'form', 'button'
// TODO svg support ?
// , 'svg', 'g', 'path', 'line', 'circle', 'text'
'mscarry', 'mstack'
];
// const svgTags = ['svg', 'g', 'path', 'line', 'circle', 'text']
var mathMLTags = [
'math', 'maction', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot',
'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'msgroup', 'mlongdiv', 'mscarries',
'mscarry', 'mstack', 'semantics'
]
var cssClassesToTmpIds = {};
var tmpIdsToNewCss = {};
var tmpIdsToNewCssSTRING = {};
// src: https://idpf.github.io/a11y-guidelines/content/style/reference.html
var supportedCss = [
'background-color',
'border',
'color',
'font',
'line-height',
'list-style',
'padding',
'text-align',
];
//////
function getImageSrc(srcTxt) {
@ -55,25 +23,16 @@ function getImageSrc(srcTxt) {
if (srcTxt === '') {
return '';
}
var isB64Img = isBase64Img(srcTxt);
var fileExtension = getFileExtension(srcTxt);
var newImgFileName = 'img-' + (Math.floor(Math.random()*1000000*Math.random()*100000)) + '.' + fileExtension;
// TODO move
srcTxt = srcTxt.replace(/&amp;/g, '&')
// TODO - convert <imgs> with svg sources to jpeg OR add support for svg
let fileExtension = getFileExtension(srcTxt);
if (fileExtension === '') {
fileExtension = "TODO-EXTRACT"
}
let newImgFileName = 'img-' + generateRandomNumber(true) + '.' + fileExtension;
let isB64Img = isBase64Img(srcTxt);
if (isB64Img) {
extractedImages.push({
filename: newImgFileName, // TODO name
data: getBase64ImgData(srcTxt)
});
} else {
} else {
allImages.push({
originalUrl: getImgDownloadUrl(srcTxt),
filename: newImgFileName, // TODO name
@ -83,163 +42,164 @@ function getImageSrc(srcTxt) {
return '../images/' + newImgFileName;
}
// tested
function formatPreCodeElements($jQueryElement) {
$jQueryElement.find('pre').each(function (i, pre) {
$(pre).replaceWith('<pre>' + pre.innerText + '</pre>');
});
$jQueryElement.find('code').each(function (i, pre) {
$(pre).replaceWith('<code>' + pre.innerText + '</code>');
});
}
function extractMathMl($htmlObject) {
$htmlObject.find('span[id^="MathJax-Element-"]').each(function (i, el) {
$(el).replaceWith('<span>' + el.getAttribute('data-mathml') + '</span>');
});
}
// tested
function extractCanvasToImg($htmlObject) {
$htmlObject.find('canvas').each(function (index, elem) {
try {
let imgUrl = docEl.toDataURL('image/jpeg');
$(elem).replaceWith('<img src="' + imgUrl + '" alt=""></img>');
} catch (e) {
console.log(e)
}
var tmpXP = getXPath(elem);
tmpXP = tmpXP.replace(/^\/div\[1\]/m, '/html[1]/body[1]');
var docEl = lookupElementByXPath(tmpXP);
var jpegUrl = docEl.toDataURL('image/png');
$(elem).replaceWith('<img src="' + jpegUrl + '" alt=""></img>');
});
}
// tested
function extractSvgToImg($htmlObject) {
let serializer = new XMLSerializer();
var serializer = new XMLSerializer();
$htmlObject.find('svg').each(function (index, elem) {
// add width & height because the result image was too big
let bbox = elem.getBoundingClientRect()
let newWidth = bbox.width
let newHeight = bbox.height
let svgXml = serializer.serializeToString(elem);
let imgSrc = 'data:image/svg+xml;base64,' + window.btoa(svgXml);
$(elem).replaceWith('<img src="' + imgSrc + '" width="'+newWidth+'" height="'+newHeight+'">' + '</img>');
var svgXml = serializer.serializeToString(elem);
var imgSrc = 'data:image/svg+xml;base64,' + window.btoa(svgXml);
$(elem).replaceWith('<img src="' + imgSrc + '">' + '</img>');
});
}
// replaces all iframes by divs with the same innerHTML content
function extractIFrames() {
let allIframes = document.getElementsByTagName('iframe')
let changeIFrames = []
let newDivs = []
for (let iFrame of allIframes) {
if (!iFrame.contentDocument || !iFrame.contentDocument.body) {
continue
}
let bodyContent = iFrame.contentDocument.body.innerHTML
let bbox = iFrame.getBoundingClientRect()
let newDiv = document.createElement('div')
newDiv.style.width = bbox.width
newDiv.style.height = bbox.height
newDiv.innerHTML = bodyContent
changeIFrames.push(iFrame)
newDivs.push(newDiv)
}
for (let i = 0; i < newDivs.length; i++) {
let newDiv = newDivs[i]
let iFrame = changeIFrames[i]
let iframeParent = iFrame.parentNode
iframeParent.replaceChild(newDiv, iFrame)
}
}
function preProcess($htmlObject) {
// TODO
// $htmlObject.find('script, style, noscript, iframe').remove();
// $('body').find('script, style, noscript, iframe').remove()
// $('body').find('script, style, noscript, iframe').contents().remove()
// $('body').find('iframe').remove()
// $('body').find('*:empty').not('img').not('br').not('hr').remove();
// formatPreCodeElements($('body'));
extractMathMl($htmlObject);
extractCanvasToImg($htmlObject);
extractSvgToImg($htmlObject);
$htmlObject.find('script, style, noscript, iframe').remove();
$htmlObject.find('*:empty').not('img').remove();
formatPreCodeElements($htmlObject);
}
function parseHTML(rawContentString) {
function force($content, withError) {
try {
var tagOpen = '@@@' + generateRandomTag();
var tagClose = '###' + generateRandomTag();
var startEl = '<object>';
var endEl = '</object>';
if (withError) {
$content = $($content);
preProcess($content);
}
$content.find('img').each(function (index, elem) {
var imgSrc = getImageSrc($(elem).attr('src'));
if (imgSrc === '') {
$(elem).replaceWith('');
} else {
$(elem).replaceWith(startEl + tagOpen + 'img src="' + imgSrc + '"' + tagClose + tagOpen + '/img' + tagClose + endEl);
}
});
$content.find('a').each(function (index, elem) {
var aHref = getHref($(elem).attr('href'));
if (aHref === '') {
$(elem).replaceWith('');
} else {
$(elem).replaceWith(startEl + tagOpen + 'a href="' + aHref + '"' + tagClose + $(elem).html() + tagOpen + '/a' + tagClose + endEl);
}
});
all($content);
function all($startElement) {
var tagName = $startElement.get(0).tagName.toLowerCase();
if (allowedTags.indexOf(tagName) >= 0) {
var children = $startElement.children();
var childrenLen = children.length;
while (childrenLen--) {
all($(children[childrenLen]));
}
$startElement.replaceWith(startEl + tagOpen + tagName + tagClose + $startElement.html() + tagOpen + '/' + tagName + tagClose + endEl);
}
}
var contentString = $content.text();
var tagOpenRegex = new RegExp(tagOpen, 'gi');
var tagCloseRegex = new RegExp(tagClose, 'gi');
contentString = contentString.replace(tagOpenRegex, '<');
contentString = contentString.replace(tagCloseRegex, '>');
contentString = contentString.replace(/&nbsp;/gi, '&#160;');
return contentString;
} catch (e) {
console.log('Error:', e);
return '';
}
}
function sanitize(rawContentString) {
allImages = [];
extractedImages = [];
let results = '';
let lastFragment = '';
let lastTag = '';
var srcTxt = '';
var dirty = null;
try {
HTMLParser(rawContentString, {
var wdirty = $.parseHTML(rawContentString);
$wdirty = $(wdirty);
preProcess($wdirty);
if ($('*').length > maxNrOfElements) {
return force($wdirty, false);
}
dirty = '<div>' + $wdirty.html() + '</div>';
var results = '';
var lastFragment = '';
var lastTag = '';
HTMLParser(dirty, {
start: function(tag, attrs, unary) {
lastTag = tag;
if (allowedTags.indexOf(tag) < 0) {
return;
}
var tattrs = null;
if (tag === 'img') {
let tmpAttrsTxt = '';
let tmpSrc = ''
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'src') {
tmpSrc = getImageSrc(attrs[i].value)
tmpAttrsTxt += ' src="' + tmpSrc + '"';
} else if (attrs[i].name === 'data-class') {
tmpAttrsTxt += ' class="' + attrs[i].value + '"';
} else if (attrs[i].name === 'width') {
// used when converting svg to img - the result image was too big
tmpAttrsTxt += ' width="' + attrs[i].value + '"';
} else if (attrs[i].name === 'height') {
// used when converting svg to img - the result image was too big
tmpAttrsTxt += ' height="' + attrs[i].value + '"';
}
}
if (tmpSrc === '') {
// ignore imgs without source
lastFragment = ''
} else {
lastFragment = tmpAttrsTxt.length === 0 ? '<img></img>' : '<img' + tmpAttrsTxt + ' alt=""></img>';
}
tattrs = attrs.filter(function(attr) {
return attr.name === 'src';
}).map(function(attr) {
return getImageSrc(attr.value);
});
lastFragment = tattrs.length === 0 ? '<img></img>' : '<img src="' + tattrs[0] + '" alt=""></img>';
} else if (tag === 'a') {
let tmpAttrsTxt = '';
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'href') {
tmpAttrsTxt += ' href="' + getHref(attrs[i].value) + '"';
} else if (attrs[i].name === 'data-class') {
tmpAttrsTxt += ' class="' + attrs[i].value + '"';
}
}
lastFragment = tmpAttrsTxt.length === 0 ? '<a>' : '<a' + tmpAttrsTxt + '>';
} else if (tag === 'br' || tag === 'hr') {
let tmpAttrsTxt = '';
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'data-class') {
tmpAttrsTxt += ' class="' + attrs[i].value + '"';
}
}
lastFragment = '<' + tag + tmpAttrsTxt + '></' + tag + '>';
} else if (tag === 'math') {
let tmpAttrsTxt = '';
tmpAttrsTxt += ' xmlns="http://www.w3.org/1998/Math/MathML"';
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'alttext') {
tmpAttrsTxt += ' alttext="' + attrs[i].value + '"';
}
}
lastFragment = '<' + tag + tmpAttrsTxt + '>';
tattrs = attrs.filter(function(attr) {
return attr.name === 'href';
}).map(function(attr) {
return getHref(attr.value);
});
lastFragment = tattrs.length === 0 ? '<a>' : '<a href="' + tattrs[0] + '">';
} else {
let tmpAttrsTxt = '';
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'data-class') {
tmpAttrsTxt += ' class="' + attrs[i].value + '"';
}
}
lastFragment = '<' + tag + tmpAttrsTxt + '>';
lastFragment = '<' + tag + '>';
}
results += lastFragment;
lastFragment = '';
},
end: function(tag) {
if (allowedTags.indexOf(tag) < 0 || tag === 'img' || tag === 'br' || tag === 'hr') {
if (allowedTags.indexOf(tag) < 0 || tag === 'img') {
return;
}
results += "</" + tag + ">";
results += "</" + tag + ">\n";
},
chars: function(text) {
if (lastTag !== '' && allowedTags.indexOf(lastTag) < 0) {
@ -252,42 +212,50 @@ function parseHTML(rawContentString) {
}
});
// TODO - (re)move
results = results.replace(/&nbsp;/gi, '&#160;');
return results;
} catch (e) {
console.log('Error:', e);
return 'Error: ' + e //+" " + force($(rawContentString))
return force(dirty, true);
}
}
function getContent(htmlContent) {
try {
// TODO - move; called multiple times on selection
preProcess($('body'))
let tmp = document.createElement('div');
var tmp = document.createElement('div');
tmp.appendChild(htmlContent.cloneNode(true));
let tmpHtml = '<div>' + tmp.innerHTML + '</div>';
return parseHTML(tmpHtml);
var dirty = '<div>' + tmp.innerHTML + '</div>';
return sanitize(dirty);
} catch (e) {
console.log('Error:', e);
return htmlContent;
return '';
}
}
/////
function getPageUrl(url) {
return url.toLowerCase().replace(/\s+/g,'_').replace(/[^a-z0-9_]/g,'') + Math.floor(Math.random() * 10000) + '.xhtml';
}
function getPageTitle(title) {
if (title.trim().length === 0) {
return 'ebook';
}
return title;
}
function getSelectedNodes() {
// if (document.selection) {
// return document.selection.createRange().parentElement();
// return document.selection.createRange();
// }
let selection = window.getSelection();
let docfrag = [];
for (let i = 0; i < selection.rangeCount; i++) {
var selection = window.getSelection();
var docfrag = [];
for (var i = 0; i < selection.rangeCount; i++) {
docfrag.push(selection.getRangeAt(i).cloneContents());
}
return docfrag;
@ -295,174 +263,66 @@ function getSelectedNodes() {
/////
function extractCss(includeStyle, appliedStyles) {
if (includeStyle) {
$('body').find('*').each((i, pre) => {
let $pre = $(pre);
if (allowedTags.indexOf(pre.tagName.toLowerCase()) < 0) return;
if (mathMLTags.indexOf(pre.tagName.toLowerCase()) > -1) return;
if (!$pre.is(':visible')) {
$pre.replaceWith('');
} else {
if (pre.tagName.toLowerCase() === 'svg') return;
let classNames = pre.getAttribute('class');
if (!classNames) {
classNames = pre.getAttribute('id');
if (!classNames) {
classNames = pre.tagName + '-' + generateRandomNumber();
}
}
let tmpName = cssClassesToTmpIds[classNames];
let tmpNewCss = tmpIdsToNewCss[tmpName];
if (!tmpName) {
// TODO - collision between class names when multiple pages
tmpName = generateRandomTag(2) + i
cssClassesToTmpIds[classNames] = tmpName;
}
if (!tmpNewCss) {
tmpNewCss = {};
for (let cssTagName of supportedCss) {
let cssValue = $pre.css(cssTagName);
if (cssValue && cssValue.length > 0) {
tmpNewCss[cssTagName] = cssValue;
}
}
// Reuse CSS - if the same css code was generated for another element, reuse it's class name
let tcss = JSON.stringify(tmpNewCss)
let found = false
if (Object.keys(tmpIdsToNewCssSTRING).length === 0) {
tmpIdsToNewCssSTRING[tmpName] = tcss;
tmpIdsToNewCss[tmpName] = tmpNewCss;
} else {
for (const key in tmpIdsToNewCssSTRING) {
if (tmpIdsToNewCssSTRING[key] === tcss) {
tmpName = key
found = true
break
}
}
if (!found) {
tmpIdsToNewCssSTRING[tmpName] = tcss;
tmpIdsToNewCss[tmpName] = tmpNewCss;
}
}
}
pre.setAttribute('data-class', tmpName);
}
});
return jsonToCss(tmpIdsToNewCss);
} else {
// remove hidden elements when style is not included
$('body').find('*').each((i, pre) => {
let $pre = $(pre)
if (!$pre.is(':visible')) {
$pre.replaceWith('')
}
})
let mergedCss = '';
if (appliedStyles && appliedStyles.length > 0) {
for (let i = 0; i < appliedStyles.length; i++) {
mergedCss += appliedStyles[i].style;
}
return mergedCss;
}
}
return null
}
/////
function deferredAddZip(url, filename) {
let deferred = $.Deferred();
JSZipUtils.getBinaryContent(url, function(err, data) {
var deferred = $.Deferred();
JSZipUtils.getBinaryContent(url, function(err, data) {
if (err) {
// deferred.reject(err); TODO
console.log('Error:', err);
deferred.resolve();
} else {
// TODO - move to utils.js
if (filename.endsWith("TODO-EXTRACT")) {
let oldFilename = filename
let arr = (new Uint8Array(data)).subarray(0, 4);
let header = "";
for(let i = 0; i < arr.length; i++) {
header += arr[i].toString(16);
}
if (header.startsWith("89504e47")) {
filename = filename.replace("TODO-EXTRACT", "png")
} else if (header.startsWith("47494638")) {
filename = filename.replace("TODO-EXTRACT", "gif")
} else if (header.startsWith("ffd8ff")) {
filename = filename.replace("TODO-EXTRACT", "jpg")
} else {
// ERROR
console.log("Error! Unable to extract the image type!");
deferred.resolve();
}
tmpGlobalContent = tmpGlobalContent.replace(oldFilename, filename)
}
extractedImages.push({
filename: filename,
// TODO - must be JSON serializable
data: base64ArrayBuffer(data)
});
deferred.resolve();
}
});
return deferred;
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
let imgsPromises = [];
let result = {};
let pageSrc = '';
let tmpContent = '';
let styleFile = null;
extractIFrames()
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
var imgsPromises = [];
var result = {};
var pageSrc = '';
var tmpContent = '';
if (request.type === 'extract-page') {
styleFile = extractCss(request.includeStyle, request.appliedStyles)
pageSrc = document.getElementsByTagName('body')[0];
tmpContent = getContent(pageSrc);
} else if (request.type === 'extract-selection') {
styleFile = extractCss(request.includeStyle, request.appliedStyles)
pageSrc = getSelectedNodes();
pageSrc.forEach((page) => {
pageSrc.forEach(function (page) {
tmpContent += getContent(page);
});
} else if (request.type === 'echo') {
sendResponse({
echo: true
});
return;
}
tmpGlobalContent = tmpContent
if (tmpContent.trim() === '') {
sendResponse('');
return;
}
allImages.forEach((tmpImg) => {
allImages.forEach(function (tmpImg) {
imgsPromises.push(deferredAddZip(tmpImg.originalUrl, tmpImg.filename));
});
$.when.apply($, imgsPromises).done(() => {
let tmpTitle = getPageTitle(document.title);
$.when.apply($, imgsPromises).done(function() {
var tmpTitle = getPageTitle(document.title);
result = {
url: getPageUrl(tmpTitle),
title: tmpTitle,
baseUrl: getCurrentUrl(),
styleFileContent: styleFile,
styleFileName: 'style' + generateRandomNumber() + '.css',
images: extractedImages,
content: tmpGlobalContent
content: tmpContent
};
sendResponse(result);
}).fail((e) => {
}).fail(function(e) {
console.log('Error:', e);
sendResponse(null)
});
return true;

188
web-extension/filesaver.js Normal file
View file

@ -0,0 +1,188 @@
/* FileSaver.js
* A saveAs() FileSaver implementation.
* 1.3.2
* 2016-06-16 18:25:19
*
* By Eli Grey, http://eligrey.com
* License: MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/
/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
var saveAs = saveAs || (function(view) {
"use strict";
// IE <10 is explicitly unsupported
if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
return;
}
var
doc = view.document
// only get URL when necessary in case Blob.js hasn't overridden it yet
, get_URL = function() {
return view.URL || view.webkitURL || view;
}
, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
, can_use_save_link = "download" in save_link
, click = function(node) {
var event = new MouseEvent("click");
node.dispatchEvent(event);
}
, is_safari = /constructor/i.test(view.HTMLElement)
, is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent)
, throw_outside = function(ex) {
(view.setImmediate || view.setTimeout)(function() {
throw ex;
}, 0);
}
, force_saveable_type = "application/octet-stream"
// the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to
, arbitrary_revoke_timeout = 1000 * 40 // in ms
, revoke = function(file) {
var revoker = function() {
if (typeof file === "string") { // file is an object URL
get_URL().revokeObjectURL(file);
} else { // file is a File
file.remove();
}
};
setTimeout(revoker, arbitrary_revoke_timeout);
}
, dispatch = function(filesaver, event_types, event) {
event_types = [].concat(event_types);
var i = event_types.length;
while (i--) {
var listener = filesaver["on" + event_types[i]];
if (typeof listener === "function") {
try {
listener.call(filesaver, event || filesaver);
} catch (ex) {
throw_outside(ex);
}
}
}
}
, auto_bom = function(blob) {
// prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type});
}
return blob;
}
, FileSaver = function(blob, name, no_auto_bom) {
if (!no_auto_bom) {
blob = auto_bom(blob);
}
// First try a.download, then web filesystem, then object URLs
var
filesaver = this
, type = blob.type
, force = type === force_saveable_type
, object_url
, dispatch_all = function() {
dispatch(filesaver, "writestart progress write writeend".split(" "));
}
// on any filesys errors revert to saving with object URLs
, fs_error = function() {
if ((is_chrome_ios || (force && is_safari)) && view.FileReader) {
// Safari doesn't allow downloading of blob urls
var reader = new FileReader();
reader.onloadend = function() {
var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;');
var popup = view.open(url, '_blank');
if(!popup) view.location.href = url;
url=undefined; // release reference before dispatching
filesaver.readyState = filesaver.DONE;
dispatch_all();
};
reader.readAsDataURL(blob);
filesaver.readyState = filesaver.INIT;
return;
}
// don't create more object URLs than needed
if (!object_url) {
object_url = get_URL().createObjectURL(blob);
}
if (force) {
view.location.href = object_url;
} else {
var opened = view.open(object_url, "_blank");
if (!opened) {
// Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html
view.location.href = object_url;
}
}
filesaver.readyState = filesaver.DONE;
dispatch_all();
revoke(object_url);
}
;
filesaver.readyState = filesaver.INIT;
if (can_use_save_link) {
object_url = get_URL().createObjectURL(blob);
setTimeout(function() {
save_link.href = object_url;
save_link.download = name;
click(save_link);
dispatch_all();
revoke(object_url);
filesaver.readyState = filesaver.DONE;
});
return;
}
fs_error();
}
, FS_proto = FileSaver.prototype
, saveAs = function(blob, name, no_auto_bom) {
return new FileSaver(blob, name || blob.name || "download", no_auto_bom);
}
;
// IE 10+ (native saveAs)
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
return function(blob, name, no_auto_bom) {
name = name || blob.name || "download";
if (!no_auto_bom) {
blob = auto_bom(blob);
}
return navigator.msSaveOrOpenBlob(blob, name);
};
}
FS_proto.abort = function(){};
FS_proto.readyState = FS_proto.INIT = 0;
FS_proto.WRITING = 1;
FS_proto.DONE = 2;
FS_proto.error =
FS_proto.onwritestart =
FS_proto.onprogress =
FS_proto.onwrite =
FS_proto.onabort =
FS_proto.onerror =
FS_proto.onwriteend =
null;
return saveAs;
}(
typeof self !== "undefined" && self
|| typeof window !== "undefined" && window
|| this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) {
define([], function() {
return saveAs;
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

After

Width:  |  Height:  |  Size: 605 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 877 B

Before After
Before After

View file

@ -1,297 +0,0 @@
/**
* CSS-JSON Converter for JavaScript
* Converts CSS to JSON and back.
* Version 2.1
*
* Released under the MIT license.
*
* Copyright (c) 2013 Aram Kocharyan, http://aramk.com/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
var CSSJSON = new function () {
var base = this;
base.init = function () {
// String functions
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
};
String.prototype.repeat = function (n) {
return new Array(1 + n).join(this);
};
};
base.init();
var selX = /([^\s\;\{\}][^\;\{\}]*)\{/g;
var endX = /\}/g;
var lineX = /([^\;\{\}]*)\;/g;
var commentX = /\/\*[\s\S]*?\*\//g;
var lineAttrX = /([^\:]+):([^\;]*);/;
// This is used, a concatenation of all above. We use alternation to
// capture.
var altX = /(\/\*[\s\S]*?\*\/)|([^\s\;\{\}][^\;\{\}]*(?=\{))|(\})|([^\;\{\}]+\;(?!\s*\*\/))/gmi;
// Capture groups
var capComment = 1;
var capSelector = 2;
var capEnd = 3;
var capAttr = 4;
var isEmpty = function (x) {
return typeof x == 'undefined' || x.length == 0 || x == null;
};
var isCssJson = function (node) {
return !isEmpty(node) ? (node.attributes && node.children) : false;
}
/**
* Input is css string and current pos, returns JSON object
*
* @param cssString
* The CSS string.
* @param args
* An optional argument object. ordered: Whether order of
* comments and other nodes should be kept in the output. This
* will return an object where all the keys are numbers and the
* values are objects containing "name" and "value" keys for each
* node. comments: Whether to capture comments. split: Whether to
* split each comma separated list of selectors.
*/
base.toJSON = function (cssString, args) {
var node = {
children: {},
attributes: {}
};
var match = null;
var count = 0;
if (typeof args == 'undefined') {
var args = {
ordered: false,
comments: false,
stripComments: false,
split: false
};
}
if (args.stripComments) {
args.comments = false;
cssString = cssString.replace(commentX, '');
}
while ((match = altX.exec(cssString)) != null) {
if (!isEmpty(match[capComment]) && args.comments) {
// Comment
var add = match[capComment].trim();
node[count++] = add;
} else if (!isEmpty(match[capSelector])) {
// New node, we recurse
var name = match[capSelector].trim();
// This will return when we encounter a closing brace
var newNode = base.toJSON(cssString, args);
if (args.ordered) {
var obj = {};
obj['name'] = name;
obj['value'] = newNode;
// Since we must use key as index to keep order and not
// name, this will differentiate between a Rule Node and an
// Attribute, since both contain a name and value pair.
obj['type'] = 'rule';
node[count++] = obj;
} else {
if (args.split) {
var bits = name.split(',');
} else {
var bits = [name];
}
for (i in bits) {
var sel = bits[i].trim();
if (sel in node.children) {
for (var att in newNode.attributes) {
node.children[sel].attributes[att] = newNode.attributes[att];
}
} else {
node.children[sel] = newNode;
}
}
}
} else if (!isEmpty(match[capEnd])) {
// Node has finished
return node;
} else if (!isEmpty(match[capAttr])) {
var line = match[capAttr].trim();
var attr = lineAttrX.exec(line);
if (attr) {
// Attribute
var name = attr[1].trim();
var value = attr[2].trim();
if (args.ordered) {
var obj = {};
obj['name'] = name;
obj['value'] = value;
obj['type'] = 'attr';
node[count++] = obj;
} else {
if (name in node.attributes) {
var currVal = node.attributes[name];
if (!(currVal instanceof Array)) {
node.attributes[name] = [currVal];
}
node.attributes[name].push(value);
} else {
node.attributes[name] = value;
}
}
} else {
// Semicolon terminated line
node[count++] = line;
}
}
}
return node;
};
/**
* @param node
* A JSON node.
* @param depth
* The depth of the current node; used for indentation and
* optional.
* @param breaks
* Whether to add line breaks in the output.
*/
base.toCSS = function (node, depth, breaks) {
var cssString = '';
if (typeof depth == 'undefined') {
depth = 0;
}
if (typeof breaks == 'undefined') {
breaks = false;
}
if (node.attributes) {
for (i in node.attributes) {
var att = node.attributes[i];
if (att instanceof Array) {
for (var j = 0; j < att.length; j++) {
cssString += strAttr(i, att[j], depth);
}
} else {
cssString += strAttr(i, att, depth);
}
}
}
if (node.children) {
var first = true;
for (i in node.children) {
if (breaks && !first) {
cssString += '\n';
} else {
first = false;
}
cssString += strNode(i, node.children[i], depth);
}
}
return cssString;
};
/**
* @param data
* You can pass css string or the CSSJS.toJSON return value.
* @param id (Optional)
* To identify and easy removable of the style element
* @param replace (Optional. defaults to TRUE)
* Whether to remove or simply do nothing
* @return HTMLLinkElement
*/
base.toHEAD = function (data, id, replace) {
var head = document.getElementsByTagName('head')[0];
var xnode = document.getElementById(id);
var _xnodeTest = (xnode !== null && xnode instanceof HTMLStyleElement);
if (isEmpty(data) || !(head instanceof HTMLHeadElement)) return;
if (_xnodeTest) {
if (replace === true || isEmpty(replace)) {
xnode.removeAttribute('id');
} else return;
}
if (isCssJson(data)) {
data = base.toCSS(data);
}
var node = document.createElement('style');
node.type = 'text/css';
if (!isEmpty(id)) {
node.id = id;
} else {
node.id = 'cssjson_' + timestamp();
}
if (node.styleSheet) {
node.styleSheet.cssText = data;
} else {
node.appendChild(document.createTextNode(data));
}
head.appendChild(node);
if (isValidStyleNode(node)) {
if (_xnodeTest) {
xnode.parentNode.removeChild(xnode);
}
} else {
node.parentNode.removeChild(node);
if (_xnodeTest) {
xnode.setAttribute('id', id);
node = xnode;
} else return;
}
return node;
};
// Alias
if (typeof window != 'undefined') {
window.createCSS = base.toHEAD;
}
// Helpers
var isValidStyleNode = function (node) {
return (node instanceof HTMLStyleElement) && node.sheet.cssRules.length > 0;
}
var timestamp = function () {
return Date.now() || +new Date();
};
var strAttr = function (name, value, depth) {
return '\t'.repeat(depth) + name + ': ' + value + ';\n';
};
var strNode = function (name, value, depth) {
var cssString = '\t'.repeat(depth) + name + ' {\n';
cssString += base.toCSS(value, depth + 1);
cssString += '\t'.repeat(depth) + '}\n';
return cssString;
};
};

View file

@ -1,171 +0,0 @@
/*
* FileSaver.js
* A saveAs() FileSaver implementation.
*
* By Eli Grey, http://eligrey.com
*
* License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT)
* source : http://purl.eligrey.com/github/FileSaver.js
*/
// The one and only way of getting global scope in all environments
// https://stackoverflow.com/q/3277182/1008999
var _global = typeof window === 'object' && window.window === window
? window : typeof self === 'object' && self.self === self
? self : typeof global === 'object' && global.global === global
? global
: this
function bom (blob, opts) {
if (typeof opts === 'undefined') opts = { autoBom: false }
else if (typeof opts !== 'object') {
console.warn('Deprecated: Expected third argument to be a object')
opts = { autoBom: !opts }
}
// prepend BOM for UTF-8 XML and text/* types (including HTML)
// note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF
if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
return new Blob([String.fromCharCode(0xFEFF), blob], { type: blob.type })
}
return blob
}
function download (url, name, opts) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'blob'
xhr.onload = function () {
saveAs(xhr.response, name, opts)
}
xhr.onerror = function () {
console.error('could not download file')
}
xhr.send()
}
function corsEnabled (url) {
var xhr = new XMLHttpRequest()
// use sync to avoid popup blocker
xhr.open('HEAD', url, false)
try {
xhr.send()
} catch (e) {}
return xhr.status >= 200 && xhr.status <= 299
}
// `a.click()` doesn't work for all browsers (#465)
function click (node) {
try {
node.dispatchEvent(new MouseEvent('click'))
} catch (e) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80,
20, false, false, false, false, 0, null)
node.dispatchEvent(evt)
}
}
// Detect WebView inside a native macOS app by ruling out all browsers
// We just need to check for 'Safari' because all other browsers (besides Firefox) include that too
// https://www.whatismybrowser.com/guides/the-latest-user-agent/macos
var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent)
var saveAs = _global.saveAs || (
// probably in some web worker
(typeof window !== 'object' || window !== _global)
? function saveAs () { /* noop */ }
// Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView
: ('download' in HTMLAnchorElement.prototype && !isMacOSWebView)
? function saveAs (blob, name, opts) {
var URL = _global.URL || _global.webkitURL
var a = document.createElement('a')
name = name || blob.name || 'download'
a.download = name
a.rel = 'noopener' // tabnabbing
// TODO: detect chrome extensions & packaged apps
// a.target = '_blank'
if (typeof blob === 'string') {
// Support regular links
a.href = blob
if (a.origin !== location.origin) {
corsEnabled(a.href)
? download(blob, name, opts)
: click(a, a.target = '_blank')
} else {
click(a)
}
} else {
// Support blobs
a.href = URL.createObjectURL(blob)
setTimeout(function () { URL.revokeObjectURL(a.href) }, 4E4) // 40s
setTimeout(function () { click(a) }, 0)
}
}
// Use msSaveOrOpenBlob as a second approach
: 'msSaveOrOpenBlob' in navigator
? function saveAs (blob, name, opts) {
name = name || blob.name || 'download'
if (typeof blob === 'string') {
if (corsEnabled(blob)) {
download(blob, name, opts)
} else {
var a = document.createElement('a')
a.href = blob
a.target = '_blank'
setTimeout(function () { click(a) })
}
} else {
navigator.msSaveOrOpenBlob(bom(blob, opts), name)
}
}
// Fallback to using FileReader and a popup
: function saveAs (blob, name, opts, popup) {
// Open a popup immediately do go around popup blocker
// Mostly only available on user interaction and the fileReader is async so...
popup = popup || open('', '_blank')
if (popup) {
popup.document.title =
popup.document.body.innerText = 'downloading...'
}
if (typeof blob === 'string') return download(blob, name, opts)
var force = blob.type === 'application/octet-stream'
var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari
var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)
if ((isChromeIOS || (force && isSafari) || isMacOSWebView) && typeof FileReader !== 'undefined') {
// Safari doesn't allow downloading of blob URLs
var reader = new FileReader()
reader.onloadend = function () {
var url = reader.result
url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
if (popup) popup.location.href = url
else location = url
popup = null // reverse-tabnabbing #460
}
reader.readAsDataURL(blob)
} else {
var URL = _global.URL || _global.webkitURL
var url = URL.createObjectURL(blob)
if (popup) popup.location = url
else location.href = url
popup = null // reverse-tabnabbing #460
setTimeout(function () { URL.revokeObjectURL(url) }, 4E4) // 40s
}
}
)
_global.saveAs = saveAs.saveAs = saveAs
if (typeof module !== 'undefined') {
module.exports = saveAs;
}

View file

@ -1,8 +1,7 @@
{
"manifest_version": 2,
"name": "__MSG_extName__",
"version": "1.4.2",
"default_locale": "en",
"name": "Save As eBook",
"version": "1.1",
"author": "Alex Adam",
"homepage_url": "https://github.com/alexadam/save-as-ebook",
"description": "Save a web page or selection as eBook (.epub)",
@ -11,9 +10,7 @@
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["./libs/jquery.js", "./libs/jszip.js", "./libs/jszip-utils.js",
"./libs/pure-parser.js", "./libs/cssjson.js", "./libs/filesaver.js",
"saveEbook.js", "extractHtml.js", "utils.js"]
"js": ["jquery.js", "jszip.js", "jszip-utils.js", "utils.js", "pure-parser.js", "extractHtml.js"]
}],
"background": {
"scripts": ["background.js"]
@ -28,33 +25,10 @@
"activeTab",
"storage",
"unlimitedStorage",
"tabs",
"notifications",
"http://*/*",
"https://*/*",
"<all_urls>"
],
"commands": {
"save-page": {
"suggested_key": {
"default": "Alt+Shift+1"
},
"description": "Save current page as eBook"
},
"save-selection": {
"suggested_key": {
"default": "Alt+Shift+2"
},
"description": "Save current selection as eBook"
},
"add-page": {
"suggested_key": {
"default": "Alt+Shift+3"
},
"description": "Add current page as chapter"
},
"add-selection": {
"suggested_key": {
"default": "Alt+Shift+4"
},
"description": "Add current selection as chapter"
}
}
]
}

View file

@ -2,162 +2,98 @@
<html>
<head>
<meta charset="utf-8">
<style>
html,
body {
padding: 0;
margin: 0;
box-sizing: border-box;
font-family: sans-serif;
}
<meta charset="utf-8">
<style>
html, body {
padding: 0;
margin: 0;
}
.menu-holder {
padding: 2px;
margin: 0;
width: 200px;
}
.menu-holder {
padding: 2px;
margin: 0;
width: 250px;
background-color: white;
}
#menuTitle {
font-size: 15px;
font-weight: bold;
margin-bottom: 10px;
}
#menuTitle {
font-size: 15px;
font-weight: bold;
padding: 5px 0;
font-family: sans-serif;
padding-top: 10px;
padding-left: 5px;
}
button {
font-family: sans-serif;
font-size: 14px;
width: 100%;
padding: 8px 0;
padding-left: 8px;
border: none;
background-color: white;
text-align: left;
}
.optionContainer {
font-family: sans-serif;
font-size: 14px;
padding: 6px 0;
}
button:hover {
background-color: #007fff;
color: white;
cursor: pointer;
}
button {
font-family: sans-serif;
font-size: 14px;
width: 100%;
padding: 6px 0;
padding-left: 8px;
border: none;
background-color: white;
text-align: left;
outline: none;
}
#busy {
background-color: rgba(0, 0, 0, 0.75);
width: 100%;
height: 100%;
display: none;
position: fixed;
top: 0;
margin: 0;
padding: 0;
}
.spinner {
height: 28px;
width: 28px;
animation: rotate 0.8s infinite linear;
border: 8px solid #fff;
border-right-color: transparent;
border-radius: 50%;
left: 40%;
top: 40%;
position: absolute;
}
.wait-message {
font-size: 20px;
color: white;
padding: 5px;
}
button:hover {
background-color: #007fff;
color: white;
cursor: pointer;
}
#busy {
background-color: rgba(0, 0, 0, 0.75);
width: 100%;
height: 100%;
display: none;
position: fixed;
top: 0;
margin: 0;
padding: 0;
}
.spinner {
height: 28px;
width: 28px;
animation: rotate 0.8s infinite linear;
border: 8px solid #fff;
border-right-color: transparent;
border-radius: 50%;
left: 40%;
top: 40%;
position: absolute;
}
.wait-message {
font-size: 20px;
color: white;
padding: 5px;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#includeStyle {
font-family: sans-serif;
font-size: 14px;
padding: 0;
padding-bottom: 8px;
background-color: white;
text-align: left;
}
select {
font-family: sans-serif;
font-size: 14px;
padding: 2px;
outline: none;
}
input[type="checkbox"] {
outline: none;
font-family: sans-serif;
font-size: 14px;
}
#includeStyle {
outline: none;
font-family: sans-serif;
font-size: 14px;
}
.shortcut {
font-size: 10px;
color: gray;
}
</style>
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="menu-holder">
<div id="menuTitle"></div>
<hr/>
<div class="optionContainer">
<input type="checkbox" name="button" id="includeStyleCheck"></input><span id="includeStyle"></span>
<div class="menu-holder">
<div id="menuTitle">Save as eBook:</div>
<button id="savePage" type="button" name="button">Save Page</button>
<button id="saveSelection" type="button" name="button">Save Selection</button>
<hr/>
<button id="pageChapter" type="button" name="button">Add Page as Chapter</button>
<button id="selectionChapter" type="button" name="button">Add Selection as Chapter</button>
<hr/>
<button id="editChapters" type="button" name="button">Edit Chapters ...</button>
</div>
<div id="busy">
<div class="spinner"></div>
<div class="wait-message">Please Wait...</div>
</div>
<button id="editStyles" type="button" name="button"></button>
<hr/>
<button id="savePage" type="button" name="button">
<div id="savePageLabel"></div>
<div id="savePageShortcut" class="shortcut"></div>
</button>
<button id="saveSelection" type="button" name="button">
<div id="saveSelectionLabel"></div>
<div id="saveSelectionShortcut" class="shortcut"></div>
</button>
<hr/>
<button id="pageChapter" type="button" name="button">
<div id="pageChapterLabel"></div>
<div id="pageChapterShortcut" class="shortcut"></div>
</button>
<button id="selectionChapter" type="button" name="button">
<div id="selectionChapterLabel"></div>
<div id="selectionChapterShortcut" class="shortcut"></div>
</button>
<hr/>
<button id="editChapters" type="button" name="button"></button>
</div>
<div id="busy">
<div class="spinner"></div>
<div id="waitMessage" class="wait-message"></div>
</div>
<script src="menu.js" charset="utf-8"></script>
<script src="jquery.js" charset="utf-8"></script>
<script src="filesaver.js" charset="utf-8"></script>
<script src="jszip.js" charset="utf-8"></script>
<script src="jszip-utils.js" charset="utf-8"></script>
<script src="utils.js" charset="utf-8"></script>
<script src="saveEbook.js" charset="utf-8"></script>
<script src="menu.js" charset="utf-8"></script>
</body>
</html>

View file

@ -1,137 +1,6 @@
var allStyles = [];
var currentStyle = null;
var appliedStyles = [];
// create menu labels
document.getElementById('menuTitle').innerHTML = chrome.i18n.getMessage('extName');
document.getElementById('includeStyle').innerHTML = chrome.i18n.getMessage('includeStyle');
document.getElementById('editStyles').innerHTML = chrome.i18n.getMessage('editStyles');
document.getElementById('savePageLabel').innerHTML = chrome.i18n.getMessage('savePage');
document.getElementById('saveSelectionLabel').innerHTML = chrome.i18n.getMessage('saveSelection');
document.getElementById('pageChapterLabel').innerHTML = chrome.i18n.getMessage('pageChapter');
document.getElementById('selectionChapterLabel').innerHTML = chrome.i18n.getMessage('selectionChapter');
document.getElementById('editChapters').innerHTML = chrome.i18n.getMessage('editChapters');
document.getElementById('waitMessage').innerHTML = chrome.i18n.getMessage('waitMessage');
function removeEbook() {
chrome.runtime.sendMessage({
type: "remove"
}, function(response) {});
}
chrome.runtime.sendMessage({
type: "is busy?"
}, function(response) {
if (response.isBusy) {
document.getElementById('busy').style.display = 'block';
} else {
document.getElementById('busy').style.display = 'none';
}
});
chrome.runtime.sendMessage({
type: "get styles"
}, function(response) {
createStyleList(response.styles);
});
function createStyleList(styles) {
allStyles = styles;
chrome.tabs.query({'active': true}, function (tabs) {
let currentUrl = tabs[0].url;
if (!styles || styles.length === 0) {
return;
}
let foundMatchingUrl = false;
// if multiple URL regexes match, select the longest one
let allMatchingStyles = [];
for (let i = 0; i < styles.length; i++) {
let listItem = document.createElement('option');
listItem.id = 'option_' + i;
listItem.className = 'cssEditor-chapter-item';
listItem.value = 'option_' + i;
listItem.innerText = styles[i].title;
currentUrl = currentUrl.replace(/(http[s]?:\/\/|www\.)/i, '').toLowerCase();
let styleUrl = styles[i].url;
let styleUrlRegex = null;
try {
styleUrlRegex = new RegExp(styleUrl, 'i');
} catch (e) {
}
if (styleUrlRegex && styleUrlRegex.test(currentUrl)) {
allMatchingStyles.push({
index: i,
length: styleUrl.length
});
}
}
if (allMatchingStyles.length >= 1) {
allMatchingStyles.sort(function (a, b) {
return b.length - a.length;
});
let selStyle = allMatchingStyles[0];
currentStyle = styles[selStyle.index];
chrome.runtime.sendMessage({
type: "set current style",
currentStyle: currentStyle
}, function(response) {
});
}
});
}
function createIncludeStyle(data) {
let includeStyleCheck = document.getElementById('includeStyleCheck');
includeStyleCheck.checked = data;
}
chrome.runtime.sendMessage({
type: "get include style"
}, function(response) {
createIncludeStyle(response.includeStyle);
});
document.getElementById('includeStyleCheck').onclick = function () {
let includeStyleCheck = document.getElementById('includeStyleCheck');
chrome.runtime.sendMessage({
type: "set include style",
includeStyle: includeStyleCheck.checked
}, function(response) {
});
}
document.getElementById("editStyles").onclick = function() {
if (document.getElementById('cssEditor-Modal')) {
return;
}
chrome.tabs.query({
currentWindow: true,
active: true
}, function(tab) {
chrome.tabs.insertCSS(tab[0].id, {file: '/cssEditor.css'});
chrome.tabs.executeScript(tab[0].id, {
file: '/cssEditor.js'
});
window.close();
});
};
document.getElementById("editChapters").onclick = function() {
if (document.getElementById('chapterEditor-Modal')) {
return;
}
@ -141,8 +10,13 @@ document.getElementById("editChapters").onclick = function() {
active: true
}, function(tab) {
chrome.tabs.executeScript(tab[0].id, {file: './libs/jquery.js'});
chrome.tabs.executeScript(tab[0].id, {file: './libs/jquery-sortable.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/jquery.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/utils.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/filesaver.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/jszip.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/jszip-utils.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/saveEbook.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/jquery-sortable.js'});
chrome.tabs.insertCSS(tab[0].id, {file: '/chapterEditor.css'});
chrome.tabs.executeScript(tab[0].id, {
@ -153,46 +27,77 @@ document.getElementById("editChapters").onclick = function() {
});
};
function dispatch(commandType, justAddToBuffer) {
function dispatch(action, justAddToBuffer) {
document.getElementById('busy').style.display = 'block';
if (!justAddToBuffer) {
removeEbook();
}
chrome.runtime.sendMessage({
type: commandType
chrome.tabs.query({
currentWindow: true,
active: true
}, function(tab) {
chrome.tabs.sendMessage(tab[0].id, {
type: 'echo'
}, function(response) {
if (!response) {
chrome.tabs.executeScript(tab[0].id, {file: '/jquery.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/utils.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/filesaver.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/jszip.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/jszip-utils.js'});
chrome.tabs.executeScript(tab[0].id, {file: '/pure-parser.js'});
chrome.tabs.executeScript(tab[0].id, {
file: 'extractHtml.js'
}, function() {
sendMessage(tab[0].id, action, justAddToBuffer);
});
} else if (response.echo) {
sendMessage(tab[0].id, action, justAddToBuffer);
}
});
});
}
function sendMessage(tabId, action, justAddToBuffer) {
chrome.tabs.sendMessage(tabId, {
type: action
}, function(response) {
//FIXME - hidden before done
document.getElementById('busy').style.display = 'none';
if (response.length === 0) {
if (justAddToBuffer) {
alert('Cannot add an empty selection as chapter!');
} else {
alert('Cannot generate the eBook from an empty selection!');
}
window.close();
}
if (!justAddToBuffer) {
buildEbook([response]);
} else {
getEbookPages(function (allPages) {
allPages.push(response);
saveEbookPages(allPages);
window.close();
});
}
setTimeout(function () {
document.getElementById('busy').style.display = 'none';
}, 500);
});
}
document.getElementById('savePage').onclick = function() {
dispatch('save-page', false);
dispatch('extract-page', false);
};
document.getElementById('saveSelection').onclick = function() {
dispatch('save-selection', false);
dispatch('extract-selection', false);
};
document.getElementById('pageChapter').onclick = function() {
dispatch('add-page', true);
dispatch('extract-page', true);
};
document.getElementById('selectionChapter').onclick = function() {
dispatch('add-selection', true);
dispatch('extract-selection', true);
};
// get all shortcuts and display them in the menuTitle
chrome.commands.getAll((commands) => {
for (let command of commands) {
if (command.name === 'save-page') {
document.getElementById('savePageShortcut').appendChild(document.createTextNode(command.shortcut));
} else if (command.name === 'save-selection') {
document.getElementById('saveSelectionShortcut').appendChild(document.createTextNode(command.shortcut));
} else if (command.name === 'add-page') {
document.getElementById('pageChapterShortcut').appendChild(document.createTextNode(command.shortcut));
} else if (command.name === 'add-selection') {
document.getElementById('selectionChapterShortcut').appendChild(document.createTextNode(command.shortcut));
}
}
})

View file

@ -1,16 +1,6 @@
var cssFileName = 'ebook.css';
var ebookTitle = null;
chrome.runtime.onMessage.addListener((obj, sender, sendResponse) => {
if (obj.shortcut && obj.shortcut === 'build-ebook') {
buildEbook(obj.response);
} else if (obj.alert) {
console.log(obj.alert);
alert(obj.alert);
}
return true;
})
function getImagesIndex(allImages) {
return allImages.reduce(function(prev, elem, index) {
return prev + '\n' + '<item href="images/' + elem.filename + '" id="img' + elem.filename + '" media-type="image/' + getImageType(elem.filename) + '"/>';
@ -33,28 +23,22 @@ function buildEbookFromChapters() {
})
}
// FIXME remove - keep one function
function buildEbook(allPages, fromMenu=false) {
_buildEbook(allPages, fromMenu);
function buildEbook(allPages) {
_buildEbook(allPages);
}
// http://ebooks.stackexchange.com/questions/1183/what-is-the-minimum-required-content-for-a-valid-epub
function _buildEbook(allPages, fromMenu=false) {
function _buildEbook(allPages) {
allPages = allPages.filter(function(page) {
return page !== null;
});
console.log('Prepare Content...');
var ebookFileName = 'eBook.epub';
if (ebookTitle) {
// ~TODO a pre-processing function to apply escapeXMLChars to all page.titles
ebookName = escapeXMLChars(ebookTitle);
ebookFileName = getEbookFileName(removeSpecialChars(ebookTitle)) + '.epub';
ebookName = ebookTitle + '.epub';
} else {
ebookName = escapeXMLChars(allPages[0].title);
ebookFileName = getEbookFileName(removeSpecialChars(allPages[0].title)) + '.epub';
ebookName = allPages[0].title + '.epub';
}
var zip = new JSZip();
@ -84,8 +68,7 @@ function _buildEbook(allPages, fromMenu=false) {
'<h1 class="frontmatter">Table of Contents</h1>' +
'<ol class="contents">' +
allPages.reduce(function(prev, page) {
var tmpPageTitle = escapeXMLChars(page.title);
return prev + '\n' + '<li><a href="pages/' + page.url + '">' + tmpPageTitle + '</a></li>';
return prev + '\n' + '<li><a href="pages/' + page.url + '">' + page.title + '</a></li>';
}, '') +
'</ol>' +
'</nav>' +
@ -105,10 +88,9 @@ function _buildEbook(allPages, fromMenu=false) {
'</docTitle>' +
'<navMap>' +
allPages.reduce(function(prev, page, index) {
var tmpPageTitle = escapeXMLChars(page.title);
return prev + '\n' +
'<navPoint id="ebook' + index + '" playOrder="' + (index + 1) + '">' +
'<navLabel><text>' + tmpPageTitle + '</text></navLabel>' +
'<navLabel><text>' + page.title + '</text></navLabel>' +
'<content src="pages/' + page.url + '" />' +
'</navPoint>';
}, '') +
@ -116,21 +98,16 @@ function _buildEbook(allPages, fromMenu=false) {
'</ncx>'
);
oebps.file(cssFileName, ''); //TODO
var styleFolder = oebps.folder('style');
allPages.forEach(function(page) {
styleFolder.file(page.styleFileName, page.styleFileContent);
});
oebps.file(cssFileName, '');
var pagesFolder = oebps.folder('pages');
allPages.forEach(function(page) {
var tmpPageTitle = escapeXMLChars(page.title);
pagesFolder.file(page.url,
'<?xml version="1.0" encoding="utf-8"?>' +
'<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">' +
'<head>' +
'<title>' + tmpPageTitle+ '</title>' +
'<link href="../style/' + page.styleFileName + '" rel="stylesheet" type="text/css" />' +
'<title>' + page.title + '</title>' +
'<link href="../' + cssFileName + '" rel="stylesheet" type="text/css" />' +
'</head><body>' +
page.content +
'</body></html>'
@ -143,7 +120,7 @@ function _buildEbook(allPages, fromMenu=false) {
'<metadata>' +
'<dc:title id="t1">'+ ebookName + '</dc:title>' +
'<dc:identifier id="db-id">isbn</dc:identifier>' +
'<meta property="dcterms:modified">' + new Date().toISOString().replace(/\.[0-9]+Z/i, 'Z') + '</meta>' +
'<meta property="dcterms:modified">' + new Date().toISOString() + '</meta>' +
'<dc:language>en</dc:language>' +
'</metadata>' +
'<manifest>' +
@ -153,12 +130,10 @@ function _buildEbook(allPages, fromMenu=false) {
allPages.reduce(function(prev, page, index) {
return prev + '\n' + '<item id="ebook' + index + '" href="pages/' + page.url + '" media-type="application/xhtml+xml" />';
}, '') +
allPages.reduce(function(prev, page, index) {
return prev + '\n' + '<item id="style' + index + '" href="style/' + page.styleFileName + '" media-type="text/css" />';
}, '') +
allPages.reduce(function(prev, page, index) {
return prev + '\n' + getImagesIndex(page.images);
}, '') +
// getExternalLinksIndex() +
'</manifest>' +
'<spine toc="ncx">' +
allPages.reduce(function(prev, page, index) {
@ -169,35 +144,37 @@ function _buildEbook(allPages, fromMenu=false) {
);
///////////////
try {
let imgsFolder = oebps.folder("images");
allPages.forEach(function(page) {
for (let i = 0; i < page.images.length; i++) {
let tmpImg = page.images[i]
// TODO - Must be JSON serializable - see the same comment in extractHtml.js
// if (tmpImg.isBinary) {
// imgsFolder.file(tmpImg.filename, tmpImg.data, {binary: true})
// } else {
imgsFolder.file(tmpImg.filename, tmpImg.data, {base64: true})
// }
}
});
} catch (error) {
console.log(error);
}
var imgsFolder = oebps.folder("images");
allPages.forEach(function(page) {
for (var i = 0; i < page.images.length; i++) {
var tmpImg = page.images[i];
imgsFolder.file(tmpImg.filename, tmpImg.data, {
base64: true
});
}
});
var done = false;
zip.generateAsync({
type: "blob",
mimeType: "application/epub+zip"
type: "blob"
})
.then(function(content) {
done = true;
console.log("done !");
saveAs(content, ebookFileName);
chrome.runtime.sendMessage({
type: "done"
}, (response) => {});
saveAs(content, ebookName);
});
setTimeout(function() {
if (done) {
return;
}
zip.generateAsync({
type: "blob"
})
.then(function(content) {
saveAs(content, ebookName);
});
}, 60000);
}

View file

@ -1,140 +0,0 @@
var lastTargetElem = null
var lastElementData = {}
var lastParentElem = null
var lastParentData = {}
let cleanXPaths = []
let bodyElem = document.getElementsByTagName('body')[0]
let bodyInner = bodyElem.innerHTML
bodyElem.innerHTML = ''
// let container = document.createElement('div')
// container.style.display = "flex"
// container.style.flexFlow = "row"
// let menuContainer = document.createElement('div')
// menuContainer.style.width = '300px'
// menuContainer.style.minWidth = '300px'
// menuContainer.style.height = '600px'
// menuContainer.style.minHeight = '600px'
// let page = document.createElement('div')
// page.id= 'super-selector'
// page.style.width = '90%'
// page.style.height = '90%'
// page.innerHTML = bodyInner
// let menu = document.createElement('div')
// menu.id = "slector-main-menu"
// menu.style.width = '300px'
// menu.style.minWidth = '300px'
// menu.style.height = '600px'
// menu.style.minHeight = '600px'
// menu.style.backgroundColor = 'black'
// menu.style.position = 'fixed'
// menu.style.top = '0'
// container.appendChild(menuContainer)
// container.appendChild(page)
// bodyElem.appendChild(container)
// bodyElem.appendChild(menu)
bodyElem.innerHTML = `<div style="width: 100%;">
<div id="super-selector" style="max-width:75%; position: absolute;">${bodyInner}'</div>
<div id="slector-main-menu" style="width:25%; min-width: 300px; height: 100%; right: 0; position: fixed;">MENU</div>
</div>`
document.getElementById('super-selector').addEventListener('mousemove', (e) => {
// document.getElementsByTagName('body')[0].addEventListener('mousemove', (e) => {
e.preventDefault()
if (lastTargetElem) {
lastTargetElem.style.backgroundColor = lastElementData.backgroundColor
}
let targetElem = document.elementFromPoint(e.clientX, e.clientY);
let targetParent = targetElem.parentNode
lastElementData.backgroundColor = targetElem.style.backgroundColor
targetElem.style.backgroundColor = 'rgba(255,0,0,0.25)'
targetElem.dataset.excluded = 'true'
targetElem.onclick = (e) => {
e.preventDefault() // FIXME except for my menu
e.stopPropagation();
let originalClickedElem = e.target
let clickedElem = e.target
if (!clickedElem) {
return
}
// console.log(createXPathFromElement(clickedElem));
///
let tmpElem = document.createElement('div')
tmpElem.innerText = createXPathFromElement(clickedElem);
document.getElementById('slector-main-menu').appendChild(tmpElem)
let clickedElemParent = clickedElem.parentNode;
let foundClickedEleme = false
while (clickedElemParent) {
if (!clickedElemParent || !clickedElemParent.style) {
break
}
if (clickedElemParent.id === 'slector-main-menu') {
// ignore main menu
return
}
if (clickedElemParent.style.opacity === '0.1') {
foundClickedEleme = true
clickedElem = clickedElemParent
break
}
clickedElemParent = clickedElemParent.parentNode;
}
if (!clickedElem.style.opacity || clickedElem.style.opacity > 0.2) {
clickedElem.style.opacity = 0.1
clickedElem.style.border = 'solid 3px black'
}
else {
clickedElem.style.opacity = 1
clickedElem.style.border = 'none'
}
}
lastTargetElem = targetElem
})
// src https://stackoverflow.com/questions/2661818/javascript-get-xpath-of-a-node
function createXPathFromElement(elm) {
var allNodes = document.getElementsByTagName('*');
for (var segs = []; elm && elm.nodeType == 1; elm = elm.parentNode)
{
if (elm.hasAttribute('id')) {
var uniqueIdCount = 0;
for (var n=0;n < allNodes.length;n++) {
if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++;
if (uniqueIdCount > 1) break;
};
if ( uniqueIdCount == 1) {
segs.unshift('id("' + elm.getAttribute('id') + '")');
return segs.join('/');
} else {
segs.unshift(elm.localName.toLowerCase() + '[@id="' + elm.getAttribute('id') + '"]');
}
} else if (elm.hasAttribute('class')) {
segs.unshift(elm.localName.toLowerCase() + '[@class="' + elm.getAttribute('class') + '"]');
} else {
for (i = 1, sib = elm.previousSibling; sib; sib = sib.previousSibling) {
if (sib.localName == elm.localName) i++; };
segs.unshift(elm.localName.toLowerCase() + '[' + i + ']');
};
};
return segs.length ? '/' + segs.join('/') : null;
};

View file

@ -1,51 +1,3 @@
function setIncludeStyle(includeStyle) {
chrome.runtime.sendMessage({
type: "set include style",
includeStyle: includeStyle
}, function(response) {
});
}
function getIncludeStyle(callback) {
chrome.runtime.sendMessage({
type: "get include style"
}, function(response) {
callback(response.includeStyle);
});
}
function setCurrentStyle(currentStyle) {
chrome.runtime.sendMessage({
type: "set current style",
currentStyle: currentStyle
}, function(response) {
});
}
function getCurrentStyle(callback) {
chrome.runtime.sendMessage({
type: "get current style"
}, function(response) {
callback(response.currentStyle);
});
}
function getStyles(callback) {
chrome.runtime.sendMessage({
type: "get styles"
}, function(response) {
callback(response.styles);
});
}
function setStyles(styles) {
chrome.runtime.sendMessage({
type: "set styles",
styles: styles
}, function(response) {
});
}
function getEbookTitle(callback) {
chrome.runtime.sendMessage({
type: "get title"
@ -83,24 +35,9 @@ function removeEbook() {
}, function(response) {});
}
function checkIfBusy(callback) {
chrome.runtime.sendMessage({
type: "is busy?"
}, function(response) {
callback(response);
});
}
function setIsBusy(isBusy) {
chrome.runtime.sendMessage({
type: "set is busy",
isBusy: isBusy
}, function(response) {});
}
/////
function getCurrentUrl() {
let url = window.location.href;
var url = window.location.href;
if (url.indexOf('?') > 0) {
url = window.location.href.split('?')[0];
}
@ -109,7 +46,7 @@ function getCurrentUrl() {
}
function getOriginUrl() {
let originUrl = window.location.origin;
var originUrl = window.location.origin;
if (!originUrl) {
originUrl = window.location.protocol + "//" + window.location.host;
}
@ -118,7 +55,7 @@ function getOriginUrl() {
function getFileExtension(fileName) {
try {
let tmpFileName = '';
var tmpFileName = '';
if (isBase64Img(fileName)) {
tmpFileName = getBase64ImgType(fileName);
@ -134,10 +71,8 @@ function getFileExtension(fileName) {
tmpFileName = 'jpeg';
} else if (tmpFileName === 'svg+xml') {
tmpFileName = 'svg';
}
if (['png', 'gif', 'jpeg', 'svg'].indexOf(tmpFileName.trim()) < 0) {
return ''
} else if (tmpFileName.trim() === '') {
tmpFileName = '';
}
return tmpFileName;
} catch (e) {
@ -147,7 +82,7 @@ function getFileExtension(fileName) {
}
function getImageType(fileName) {
let imageType = getFileExtension(fileName);
var imageType = getFileExtension(fileName);
if (imageType === 'svg') {
imageType = 'svg+xml';
}
@ -162,21 +97,18 @@ function getImgDownloadUrl(imgSrc) {
return getAbsoluteUrl(imgSrc);
}
function getAbsoluteUrl(urlStr) {
function getAbsoluteUrl(urlStr) {
if (!urlStr) {
return '';
}
if (urlStr.length === 0) {
return '';
}
try {
urlStr = decodeHtmlEntity(urlStr);
let currentUrl = getCurrentUrl();
let originUrl = getOriginUrl();
let absoluteUrl = urlStr;
originUrl = removeEndingSlash(originUrl)
currentUrl = removeEndingSlash(currentUrl)
if (urlStr.length === 0) {
return '';
}
urlStr = decodeHtmlEntity(urlStr);
var currentUrl = getCurrentUrl();
var originUrl = getOriginUrl();
var absoluteUrl = urlStr;
if (urlStr.indexOf('//') === 0) {
absoluteUrl = window.location.protocol + urlStr;
@ -187,8 +119,6 @@ function getAbsoluteUrl(urlStr) {
} else if (urlStr.indexOf('http') !== 0) {
absoluteUrl = currentUrl + '/' + urlStr;
}
// TODO is this needed?
// absoluteUrl = escapeXMLChars(absoluteUrl);
return absoluteUrl;
} catch (e) {
console.log('Error:', e);
@ -196,27 +126,20 @@ function getAbsoluteUrl(urlStr) {
}
}
function removeEndingSlash(inputStr) {
if (inputStr.endsWith('/')) {
return inputStr.substring(0, inputStr.length - 1);
}
return inputStr;
}
// https://gist.github.com/jonleighton/958841
function base64ArrayBuffer(arrayBuffer) {
let base64 = '';
let encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var base64 = '';
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let bytes = new Uint8Array(arrayBuffer);
let byteLength = bytes.byteLength;
let byteRemainder = byteLength % 3;
let mainLength = byteLength - byteRemainder;
var bytes = new Uint8Array(arrayBuffer);
var byteLength = bytes.byteLength;
var byteRemainder = byteLength % 3;
var mainLength = byteLength - byteRemainder;
let a, b, c, d;
let chunk;
var a, b, c, d;
var chunk;
for (let i = 0; i < mainLength; i = i + 3) {
for (var i = 0; i < mainLength; i = i + 3) {
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
a = (chunk & 16515072) >> 18;
b = (chunk & 258048) >> 12;
@ -271,13 +194,11 @@ function getBase64ImgData(srcTxt) {
}
function getXPath(elm) {
if (!elm) return ''
let allNodes = document.getElementsByTagName('*');
for (let segs = []; elm && elm.nodeType === 1; elm = elm.parentNode) {
var allNodes = document.getElementsByTagName('*');
for (var segs = []; elm && elm.nodeType === 1; elm = elm.parentNode) {
if (elm.hasAttribute('id')) {
let uniqueIdCount = 0;
for (let n = 0; n < allNodes.length; n++) {
var uniqueIdCount = 0;
for (var n = 0; n < allNodes.length; n++) {
if (allNodes[n].hasAttribute('id') && allNodes[n].id === elm.id) {
uniqueIdCount++;
}
@ -306,71 +227,17 @@ function getXPath(elm) {
}
function lookupElementByXPath(path) {
let evaluator = new XPathEvaluator();
let result = evaluator.evaluate(path, document.documentElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
var evaluator = new XPathEvaluator();
var result = evaluator.evaluate(path, document.documentElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
return result.singleNodeValue;
}
function generateRandomTag(tagLen) {
tagLen = tagLen || 5;
let text = '';
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
for(let i = 0; i < tagLen; i++) {
var text = '';
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
for(var i = 0; i < tagLen; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
function generateRandomNumber(bigNr) {
if (bigNr) {
return Math.floor(Math.random()*1000000*Math.random()*1000000)
}
return Math.floor(Math.random()*1000000)
}
function removeSpecialChars(text) {
// FIXME remove white spaces ?
return text.replace(/\//g, '-')
}
function escapeXMLChars(text) {
return text.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function getEbookFileName(name) {
return name.replace(/&amp;/ig, '&')
.replace(/&gt;/ig, '')
.replace(/&lt;/ig, '')
.replace(/&quot;/ig, '')
.replace(/&apos;/ig, '');
}
function getPageUrl(url) {
return url.toLowerCase().replace(/\s+/g,'_').replace(/[^a-z0-9_]/g,'') + Math.floor(Math.random() * 10000) + '.xhtml';
}
function getPageTitle(title) {
if (title.trim().length === 0) {
return 'ebook';
}
return title;
}
function jsonToCss(jsonObj) {
let keys = Object.keys(jsonObj);
let result = '';
for (let i = 0; i < keys.length; i++) {
let tmpJsonObj = jsonObj[keys[i]];
let tmpKeys = Object.keys(tmpJsonObj);
result += '.' + keys[i] + ' {';
for (let j = 0; j < tmpKeys.length; j++) {
result += tmpKeys[j] + ':' + tmpJsonObj[tmpKeys[j]] + ';';
}
result += '} ';
}
return result;
}