diff --git a/.gitignore b/.gitignore deleted file mode 100644 index ecec59f..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules/ -tests/pages/ -.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 17fec2e..85411e2 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,13 @@ Save a web page/selection as an eBook (.epub format) - a Chrome/Firefox/Opera Web Extension - +![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) - -or manually (tested on v. 52.0.2743.116) +### Chrome (tested on v. 52.0.2743.116) ``` 1. Navigate to chrome://extensions/ @@ -20,9 +16,7 @@ or manually (tested on v. 52.0.2743.116) 3. Select the extension's directory ``` -### From [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/saveasebook/) - -or manually (tested on v. 50.0a2) +### Firefox (tested on v. 50.0a2) ``` 1. Navigate to about:debugging @@ -45,91 +39,19 @@ 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 & & 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) + - * there are some issues with relative URLs - clean & optimize code - create tests + - convert svg to png + - base64 imgs + - 'save as image' option (render a selection and save it as image instead of text) - support other formats (mobi, pdf etc.) + - show progress indication (ui/ux) - 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) ## Credits - http://ebooks.stackexchange.com/questions/1183/what-is-the-minimum-required-content-for-a-valid-epub @@ -137,5 +59,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 diff --git a/ex1.png b/ex1.png new file mode 100644 index 0000000..f6de59c Binary files /dev/null and b/ex1.png differ diff --git a/ex11.png b/ex11.png deleted file mode 100644 index 3d60c53..0000000 Binary files a/ex11.png and /dev/null differ diff --git a/ex2.png b/ex2.png index 8967c0d..375eb8f 100644 Binary files a/ex2.png and b/ex2.png differ diff --git a/ex3.png b/ex3.png deleted file mode 100644 index 88a153e..0000000 Binary files a/ex3.png and /dev/null differ diff --git a/tests/index.js b/tests/index.js deleted file mode 100644 index 6988655..0000000 --- a/tests/index.js +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/tests/package.json b/tests/package.json deleted file mode 100644 index f4adacd..0000000 --- a/tests/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "web-extension-test", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "dependencies": { - "puppeteer": "^1.14.0" - } -} diff --git a/tests/pages.zip b/tests/pages.zip deleted file mode 100644 index 7e5ecce..0000000 Binary files a/tests/pages.zip and /dev/null differ diff --git a/web-extension/_locales/en/messages.json b/web-extension/_locales/en/messages.json deleted file mode 100644 index 951c629..0000000 --- a/web-extension/_locales/en/messages.json +++ /dev/null @@ -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" - } - -} diff --git a/web-extension/_locales/fr/messages.json b/web-extension/_locales/fr/messages.json deleted file mode 100644 index 5366e29..0000000 --- a/web-extension/_locales/fr/messages.json +++ /dev/null @@ -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" - } -} diff --git a/web-extension/_locales/pt_BR/messages.json b/web-extension/_locales/pt_BR/messages.json deleted file mode 100644 index 9c59f7c..0000000 --- a/web-extension/_locales/pt_BR/messages.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/web-extension/_locales/ru/messages.json b/web-extension/_locales/ru/messages.json deleted file mode 100644 index 7726ba9..0000000 --- a/web-extension/_locales/ru/messages.json +++ /dev/null @@ -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": "Сохранить стиль" - } -} \ No newline at end of file diff --git a/web-extension/background.js b/web-extension/background.js index a577260..60c2fba 100644 --- a/web-extension/background.js +++ b/web-extension/background.js @@ -1,429 +1,44 @@ +var customStorage = null; -/////////////////// -/////////////////// -/////////////////// -/////////////////// -/// 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}); +function _getEbookPages() { + try { + // var allPages = localStorage.getItem('ebook'); + var allPages = customStorage; + if (!allPages) { + allPages = []; } else { - callback({includeStyle: data.includeStyle}); + allPages = JSON.parse(allPages); } - }); -} - -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() + return allPages; + } catch (e) { + alert(e); + return []; } } -chrome.runtime.onMessage.addListener(_execRequest); +function _saveEbookPages(pages) { + try { + // localStorage.setItem('ebook', JSON.stringify(pages)); + customStorage = JSON.stringify(pages); + } catch (e) { + alert(e); + } +} -function _execRequest(request, sender, sendResponse) { +function _removeEbook() { + // localStorage.removeItem('ebook'); + customStorage = null; +} + +chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.type === 'get') { - chrome.storage.local.get('allPages', function (data) { - if (!data || !data.allPages) { - sendResponse({allPages: []}); - } - sendResponse({allPages: data.allPages}); - }) + sendResponse({allPages: _getEbookPages()}); } if (request.type === 'set') { - chrome.storage.local.set({'allPages': request.pages}); + _saveEbookPages(request.pages); } if (request.type === 'remove') { - chrome.storage.local.remove('allPages'); - chrome.storage.local.remove('title'); - } - if (request.type === 'get title') { - chrome.storage.local.get('title', function (data) { - if (!data || !data.title || data.title.trim().length === 0) { - sendResponse({title: 'eBook'}); - } else { - sendResponse({title: data.title}); - } - }) - } - 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() + _removeEbook(); } return true; -} +}); diff --git a/web-extension/chapterEditor.css b/web-extension/chapterEditor.css index e3d42e2..86e9e57 100644 --- a/web-extension/chapterEditor.css +++ b/web-extension/chapterEditor.css @@ -12,13 +12,24 @@ ul.chapterEditor-chapters-list li.placeholder { padding: 0; border: solid 10px #f5f5f5; } +/*ul.chapterEditor-chapters-list li.placeholder:before { + position: absolute; + content: ""; + margin-top: -5px; + left: -5px; + top: -4px; + border: 5px solid black; + border-left-color: red; + border-right: none; +}*/ + #chapterEditor-Title { font-size: 20px; font-weight: bold; float: left; display: inline-block; - font-family: sans-serif; + font-family: "sans-serif"; } #chapterEditor-ebookTitleHolder { @@ -35,7 +46,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 +77,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 +90,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 +111,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,48 +134,21 @@ ul, ul.chapterEditor-chapters-list { } .chapterEditor-footer-button { - position: relative; - overflow: hidden; padding: 18px 20px; margin: 0; font-size: 18px; border: none; background-color: rgba(0, 0, 0, 0); - display: inline-block; } .chapterEditor-footer-button:hover { color: white; 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;*/ } diff --git a/web-extension/chapterEditor.js b/web-extension/chapterEditor.js index d54e4f3..d91445a 100644 --- a/web-extension/chapterEditor.js +++ b/web-extension/chapterEditor.js @@ -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,15 +41,13 @@ 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'); ebookTilte.id = 'chapterEditor-ebookTitle'; ebookTilte.type = 'text'; - getEbookTitle(function (title) { - ebookTilte.value = title; - }); + ebookTilte.value = 'eBook'; titleHolder.appendChild(ebookTilte); modalList.appendChild(titleHolder); @@ -81,12 +79,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,40 +106,18 @@ 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.className = 'chapterEditor-footer-button hapterEditor-float-left'; - removeButton.onclick = function() { - var result = confirm(chrome.i18n.getMessage('removeChaptersConfirm')); - if (result) { - removeEbook(); - closeModal(); - } - }; - buttons.appendChild(removeButton); - var saveButton = document.createElement('button'); saveButton.onclick = function() { - var newChapters = saveChanges(); - prepareEbook(newChapters); + prepareEbook(); }; - saveButton.innerText = chrome.i18n.getMessage('generateEbook'); + saveButton.innerText = 'Generate eBook ...'; saveButton.className = 'chapterEditor-footer-button chapterEditor-float-right chapterEditor-generate-button'; buttons.appendChild(saveButton); - - var saveChangesButton = document.createElement('button'); - saveChangesButton.onclick = function() { - saveChanges(); - }; - saveChangesButton.innerText = chrome.i18n.getMessage('saveChanges'); - saveChangesButton.className = 'chapterEditor-footer-button chapterEditor-float-right'; - buttons.appendChild(saveChangesButton); - modalFooter.appendChild(buttons); ///////////////////// @@ -206,23 +182,11 @@ function showEditor() { function previewListItem(atIndex) { return function() { - alert(allPagesRef[atIndex].content.trim().replace(/<[^>]+>/gi, '').replace(/\s+/g, ' ').substring(0, 1000) + ' ...'); + alert(allPagesRef[atIndex].content.trim().substring(0, 500).replace(/<[^>]+>/gi, '') + ' ...'); }; } - function prepareEbook(newChapters) { - try { - if (newChapters.length === 0) { - alert(chrome.i18n.getMessage('emptyBookWarning')); - return; - } - buildEbookFromChapters(); - } catch (e) { - console.log('Error:', e); - } - } - - function saveChanges() { + function prepareEbook() { var newChapters = []; var newEbookTitle = ebookTilte.value; if (newEbookTitle.trim() === '') { @@ -230,27 +194,35 @@ function showEditor() { } try { + var tmpChaptersList = document.getElementsByClassName('chapterEditor-chapter-item'); if (!tmpChaptersList || !allPagesRef) { return; } 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]); } } - saveEbookTitle(newEbookTitle); + if (newChapters.length === 0) { + alert('Can\'t generate an empty eBook!'); + return; + } + + newChapters.splice(0, 0, { + type: 'title', + title: newEbookTitle + }); + saveEbookPages(newChapters); - return newChapters; + buildEbookFromChapters(); } catch (e) { console.log('Error:', e); } + } ///////////////////// diff --git a/web-extension/cssEditor.css b/web-extension/cssEditor.css deleted file mode 100644 index 88ed70f..0000000 --- a/web-extension/cssEditor.css +++ /dev/null @@ -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%; - } -} diff --git a/web-extension/cssEditor.js b/web-extension/cssEditor.js deleted file mode 100644 index ea28993..0000000 --- a/web-extension/cssEditor.js +++ /dev/null @@ -1,379 +0,0 @@ -for (var i=0; i 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 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 allImgSrc = {}; var allImages = []; -var extractedImages = []; -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', - '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' -]; -// 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', -]; +var maxNrOfElements = 10000; ////// function getImageSrc(srcTxt) { if (!srcTxt) { return ''; } - srcTxt = srcTxt.trim(); - if (srcTxt === '') { - return ''; - } - - // TODO move - srcTxt = srcTxt.replace(/&/g, '&') - - // TODO - convert 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 { - allImages.push({ - originalUrl: getImgDownloadUrl(srcTxt), - filename: newImgFileName, // TODO name - }); - } - - return '../images/' + newImgFileName; + allImgSrc[srcTxt] = 'img-' + (Math.floor(Math.random()*1000000)) + '.' + getFileExtension(srcTxt); + return '../images/' + allImgSrc[srcTxt]; } -// tested -function extractMathMl($htmlObject) { - $htmlObject.find('span[id^="MathJax-Element-"]').each(function (i, el) { - $(el).replaceWith('' + el.getAttribute('data-mathml') + ''); - }); +function generateRandomTag() { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + for(var i = 0; i < 5; i++) + text += possible.charAt(Math.floor(Math.random() * possible.length)); + + return text; } -// tested -function extractCanvasToImg($htmlObject) { - $htmlObject.find('canvas').each(function (index, elem) { - try { - let imgUrl = docEl.toDataURL('image/jpeg'); - $(elem).replaceWith(''); - } catch (e) { - console.log(e) - } - }); -} -// tested -function extractSvgToImg($htmlObject) { - let 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('' + ''); - }); -} - -// 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); -} - -function parseHTML(rawContentString) { - allImages = []; - extractedImages = []; - let results = ''; - let lastFragment = ''; - let lastTag = ''; +// function force3(dirty) { +// var tagOpen = '@@@';// + generateRandomTag(); +// var tagClose = '###';// + generateRandomTag(); +// var removeElements = ['script', 'style', 'svg', 'canvas', 'noscript']; +// var inlineElements = ['h1', 'h2', 'h3', 'sup', 'b', 'i', 'em', 'code', 'pre', 'p']; +// var replaceElements = [['li', 'p'], ['tr', 'p']]; +// +// // var bodyClone = document.getElementsByTagName('body')[0].cloneNode(true); +// +// var bodyClone = document.createElement('div'); +// bodyClone.innerHTML = dirty; +// +// +// ///// +// +// var imgs = bodyClone.getElementsByTagName('img'); +// for (var i = 0; i < imgs.length; i++) { +// var newImg = document.createElement('span'); +// newImg.innerHTML = tagOpen + 'img src="' + getImageSrc(imgs[i].getAttribute('src')) + '"' + tagClose + tagOpen + '/img' + tagClose; +// imgs[i].parentNode.replaceChild(newImg, imgs[i]); +// } +// +// var links = bodyClone.getElementsByTagName('a'); +// for (i = 0; i < links.length; i++) { +// var newLink = document.createElement('span'); +// newLink.innerHTML = tagOpen + 'a href="' + getHref(links[i].getAttribute('href')) + '"' + tagClose + links[i].innerHTML + tagOpen + '/a' + tagClose; +// links[i].parentNode.replaceChild(newLink, links[i]); +// } +// +// for (i = 0; i < inlineElements.length; i++) { +// var tagName = inlineElements[i]; +// var miscElements = bodyClone.getElementsByTagName(tagName); +// for (var j = 0; j < miscElements.length; j++) { +// var elemToBeReplaced = miscElements[j]; +// var newElement = document.createElement('span'); +// newElement.innerHTML = tagOpen + tagName + tagClose + elemToBeReplaced.innerHTML + tagOpen + '/' + tagName + tagClose; +// elemToBeReplaced.parentNode.replaceChild(newElement, elemToBeReplaced); +// } +// } +// +// for (i = 0; i < replaceElements.length; i++) { +// var crtTagPair = replaceElements[i]; +// var searchForTag = crtTagPair[0]; +// var replaceWithTag = crtTagPair[1]; +// var miscElements = bodyClone.getElementsByTagName(searchForTag); +// for (var j = 0; j < miscElements.length; j++) { +// var elemToBeReplaced = miscElements[j]; +// var newElement = document.createElement('span'); +// newElement.innerHTML = tagOpen + replaceWithTag + tagClose + elemToBeReplaced.innerHTML + tagOpen + '/' + replaceWithTag + tagClose; +// elemToBeReplaced.parentNode.replaceChild(newElement, elemToBeReplaced); +// } +// } +// +// var contentString = bodyClone.innerText; +// +// var tagOpenRegex = new RegExp(tagOpen, 'gi'); +// var tagCloseRegex = new RegExp(tagClose, 'gi'); +// contentString = contentString.replace(tagOpenRegex, '<'); +// contentString = contentString.replace(tagCloseRegex, '>'); +// contentString = contentString.replace(/&/gi, '&'); +// contentString = contentString.replace(/&/gi, '&'); +// +// return contentString; +// +// } +function force(contentString) { try { - HTMLParser(rawContentString, { + var tagOpen = '@@@' + generateRandomTag(); + var tagClose = '###' + generateRandomTag(); + var inlineElements = ['h1', 'h2', 'h3', 'sup', 'b', 'i', 'em', 'code', 'pre', 'p']; + var replaceElements = [['li', 'p'], ['tr', 'p']]; + + var $content = $(contentString); + + $content.find('img').each(function (index, elem) { + $(elem).replaceWith('' + tagOpen + 'img src="' + getImageSrc($(elem).attr('src')) + '"' + tagClose + tagOpen + '/img' + tagClose + ''); + }); + + $content.find('a').each(function (index, elem) { + $(elem).replaceWith('' + tagOpen + 'a href="' + getHref($(elem).attr('href')) + '"' + tagClose + $(elem).html() + tagOpen + '/a' + tagClose + ''); + }); + + if ($('*').length < maxNrOfElements) { + replaceElements.forEach(function (replacePair) { + var searchFor = replacePair[0]; + var tagName = replacePair[1]; + var tmpElems = $content.find(searchFor); + while (tmpElems.length > 0) { + $tmpElem = $(tmpElems[0]); + $tmpElem.replaceWith('' + tagOpen + tagName + tagClose + $tmpElem.html() + tagOpen + '/' + tagName + tagClose + ''); + tmpElems = $content.find(searchFor); + } + }); + + inlineElements.forEach(function (tagName) { + var tmpElems = $content.find(tagName); + while (tmpElems.length > 0) { + $tmpElem = $(tmpElems[0]); + $tmpElem.replaceWith('' + tagOpen + tagName + tagClose + $tmpElem.html() + tagOpen + '/' + tagName + tagClose + ''); + tmpElems = $content.find(tagName); + } + }); + } + + 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(/&/gi, '&'); + contentString = contentString.replace(/&/gi, '&'); + + return contentString; + } catch (e) { + console.log('Error:', e); + } +} + +// https://github.com/blowsie/Pure-JavaScript-HTML5-Parser +function sanitize(rawContentString) { + allImgSrc = {}; + var srcTxt = ''; + var dirty = null; + try { + // dirty = getHtmlAsString(rawContent); + wdirty = $.parseHTML(rawContentString); + $wdirty = $(wdirty); + $wdirty.find('script, style, svg, canvas, noscript').remove(); + $wdirty.find('*:empty').not('img').remove(); + + dirty = '
' + $wdirty.html() + '
'; + + //////////////// + if ($('*').length > maxNrOfElements) { + return force(dirty); + } + + var results = ''; + var lastFragment = ''; + var lastTag = ''; + var inList = false; + var allowedTags = ['div', 'p', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'blockquote', + 'img', 'a', 'ol', 'ul', 'li', 'b', 'i', 'sup', 'strong', 'strike', + 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'pre', 'em' + ]; + var allowedTextTags = ['h4', 'h5', 'h6', 'span']; + + HTMLParser(dirty, { start: function(tag, attrs, unary) { lastTag = tag; if (allowedTags.indexOf(tag) < 0) { return; } + if (tag === 'ol' || tag === 'ul') { + inList = true; + } + if (tag === 'li' && !inList) { + tag = 'p'; + } + + 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 ? '' : ''; - } + tattrs = attrs.filter(function(attr) { + return attr.name === 'src'; + }).map(function(attr) { + return getImageSrc(attr.escaped); + }); + lastFragment = tattrs.length === 0 ? '' : ''; } 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 ? '' : ''; - } 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 + '>'; - } 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.escaped); + }); + lastFragment = tattrs.length === 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 += ""; + if (tag === 'ol' || tag === 'ul') { + inList = false; + } + if (tag === 'li' && !inList) { + tag = 'p'; + } + + results += "\n"; }, chars: function(text) { if (lastTag !== '' && allowedTags.indexOf(lastTag) < 0) { @@ -252,42 +232,49 @@ function parseHTML(rawContentString) { } }); - // TODO - (re)move - results = results.replace(/ /gi, ' '); + // results = results.replace(/&[a-z]+;/gim, ''); + results = results.replace(/&/gi, '&'); + results = results.replace(/&/gi, '&'); return results; } catch (e) { console.log('Error:', e); - return 'Error: ' + e //+" " + force($(rawContentString)) + return force(dirty); } } 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 = '
' + tmp.innerHTML + '
'; - return parseHTML(tmpHtml); + var dirty = '
' + tmp.innerHTML + '
'; + 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) { //TODO + 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 +282,67 @@ 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) { +function deferredAddZip(url, filename, zip) { + 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({ + var tmpImg = { filename: filename, - // TODO - must be JSON serializable data: base64ArrayBuffer(data) - }); - + }; + allImages.push(tmpImg); 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) { + console.log('Extract Html...'); + var imgsPromises = []; + allImgSrc = {}; + allImages = []; + 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); }); } - tmpGlobalContent = tmpContent + if (tmpContent.trim() === '') { + return; + } - allImages.forEach((tmpImg) => { - imgsPromises.push(deferredAddZip(tmpImg.originalUrl, tmpImg.filename)); + Object.keys(allImgSrc).forEach(function(imgSrc, index) { + try { + var tmpDeffered = deferredAddZip(getImgDownloadUrl(imgSrc), allImgSrc[imgSrc]); + imgsPromises.push(tmpDeffered); + } catch (e) { + console.log('Error:', e); + } }); - $.when.apply($, imgsPromises).done(() => { - let tmpTitle = getPageTitle(document.title); + $.when.apply($, imgsPromises).done(function() { result = { - url: getPageUrl(tmpTitle), - title: tmpTitle, + url: getPageUrl(document.title), + title: getPageTitle(document.title), baseUrl: getCurrentUrl(), - styleFileContent: styleFile, - styleFileName: 'style' + generateRandomNumber() + '.css', - images: extractedImages, - content: tmpGlobalContent + images: allImages, + content: tmpContent }; sendResponse(result); - }).fail((e) => { + }).fail(function(e) { console.log('Error:', e); - sendResponse(null) }); return true; diff --git a/web-extension/filesaver.js b/web-extension/filesaver.js new file mode 100644 index 0000000..239db12 --- /dev/null +++ b/web-extension/filesaver.js @@ -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; + }); +} diff --git a/web-extension/icons/book128.png b/web-extension/icons/book128.png index 14ae99c..3b5d8fd 100644 Binary files a/web-extension/icons/book128.png and b/web-extension/icons/book128.png differ diff --git a/web-extension/icons/book32.png b/web-extension/icons/book32.png index 84ee1f9..81d30eb 100644 Binary files a/web-extension/icons/book32.png and b/web-extension/icons/book32.png differ diff --git a/web-extension/icons/book48.png b/web-extension/icons/book48.png index d06befb..ada2f39 100644 Binary files a/web-extension/icons/book48.png and b/web-extension/icons/book48.png differ diff --git a/web-extension/libs/jquery-sortable.js b/web-extension/jquery-sortable.js similarity index 100% rename from web-extension/libs/jquery-sortable.js rename to web-extension/jquery-sortable.js diff --git a/web-extension/libs/jquery.js b/web-extension/jquery.js similarity index 100% rename from web-extension/libs/jquery.js rename to web-extension/jquery.js diff --git a/web-extension/libs/jszip-utils.js b/web-extension/jszip-utils.js similarity index 100% rename from web-extension/libs/jszip-utils.js rename to web-extension/jszip-utils.js diff --git a/web-extension/libs/jszip.js b/web-extension/jszip.js similarity index 100% rename from web-extension/libs/jszip.js rename to web-extension/jszip.js diff --git a/web-extension/libs/cssjson.js b/web-extension/libs/cssjson.js deleted file mode 100644 index 01b6332..0000000 --- a/web-extension/libs/cssjson.js +++ /dev/null @@ -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; - }; - -}; diff --git a/web-extension/libs/filesaver.js b/web-extension/libs/filesaver.js deleted file mode 100644 index 5d204ae..0000000 --- a/web-extension/libs/filesaver.js +++ /dev/null @@ -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; -} diff --git a/web-extension/manifest.json b/web-extension/manifest.json index 0b65008..d0a90e3 100644 --- a/web-extension/manifest.json +++ b/web-extension/manifest.json @@ -1,60 +1,39 @@ { + "manifest_version": 2, - "name": "__MSG_extName__", - "version": "1.4.2", - "default_locale": "en", - "author": "Alex Adam", - "homepage_url": "https://github.com/alexadam/save-as-ebook", + "name": "Save As eBook", + "version": "1.0", + "description": "Save a web page or selection as eBook (.epub)", + "icons": { "48": "icons/book48.png" }, + "content_scripts": [{ "matches": [""], - "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"] }, + "browser_action": { "default_icon": "icons/book32.png", "default_title": "Save as eBook", "default_popup": "menu.html" }, + "permissions": [ "contextMenus", "activeTab", "storage", - "unlimitedStorage", + "tabs", "notifications", + "http://*/*", + "https://*/*", "" - ], - "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" - } - } + ] + } diff --git a/web-extension/menu.html b/web-extension/menu.html index 0906d7b..7973fba 100644 --- a/web-extension/menu.html +++ b/web-extension/menu.html @@ -2,162 +2,57 @@ - - + button:hover { + background-color: #007fff; + color: white; + } + -