diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ecec59f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+tests/pages/
+.DS_Store
\ No newline at end of file
diff --git a/README.md b/README.md
index 85411e2..17fec2e 100644
--- a/README.md
+++ b/README.md
@@ -2,13 +2,17 @@
Save a web page/selection as an eBook (.epub format) - a Chrome/Firefox/Opera Web Extension
-
+

+
+
## How to install it
-### Chrome (tested on v. 52.0.2743.116)
+### From [Chrome Web Store](https://chrome.google.com/webstore/detail/save-as-ebook/haaplkpoiimngbppjihnegfmpejdnffj)
+
+or manually (tested on v. 52.0.2743.116)
```
1. Navigate to chrome://extensions/
@@ -16,7 +20,9 @@ Save a web page/selection as an eBook (.epub format) - a Chrome/Firefox/Opera We
3. Select the extension's directory
```
-### Firefox (tested on v. 50.0a2)
+### From [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/saveasebook/)
+
+or manually (tested on v. 50.0a2)
```
1. Navigate to about:debugging
@@ -39,19 +45,91 @@ 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
- - fix all 'epubcheck' errors (https://github.com/IDPF/epubcheck)
- - * there are some issues with relative URLs
+ - make the Custom Style Editor more user friendly
+ - support backup / restore for Custom Styles
+ - DONE fix all 'epubcheck' errors (https://github.com/IDPF/epubcheck)
- 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)
- - add settings & options page (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
+
+ ....
+ ```
## Credits
- http://ebooks.stackexchange.com/questions/1183/what-is-the-minimum-required-content-for-a-valid-epub
@@ -59,4 +137,5 @@ ebook-convert "book.epub" "book.mobi"
- https://stuk.github.io/jszip/
- http://johnny.github.io/jquery-sortable/
- https://github.com/eligrey/FileSaver.js/
- - https://www.iconfinder.com/icons/1031371/book_empty_library_reading_icon#size=128
+ - 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
diff --git a/ex1.png b/ex1.png
deleted file mode 100644
index f6de59c..0000000
Binary files a/ex1.png and /dev/null differ
diff --git a/ex11.png b/ex11.png
new file mode 100644
index 0000000..3d60c53
Binary files /dev/null and b/ex11.png differ
diff --git a/ex2.png b/ex2.png
index 375eb8f..8967c0d 100644
Binary files a/ex2.png and b/ex2.png differ
diff --git a/ex3.png b/ex3.png
new file mode 100644
index 0000000..88a153e
Binary files /dev/null and b/ex3.png differ
diff --git a/tests/index.js b/tests/index.js
new file mode 100644
index 0000000..6988655
--- /dev/null
+++ b/tests/index.js
@@ -0,0 +1,52 @@
+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
new file mode 100644
index 0000000..f4adacd
--- /dev/null
+++ b/tests/package.json
@@ -0,0 +1,9 @@
+{
+ "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
new file mode 100644
index 0000000..7e5ecce
Binary files /dev/null and b/tests/pages.zip differ
diff --git a/web-extension/_locales/en/messages.json b/web-extension/_locales/en/messages.json
new file mode 100644
index 0000000..951c629
--- /dev/null
+++ b/web-extension/_locales/en/messages.json
@@ -0,0 +1,94 @@
+{
+ "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
new file mode 100644
index 0000000..5366e29
--- /dev/null
+++ b/web-extension/_locales/fr/messages.json
@@ -0,0 +1,99 @@
+{
+ "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
new file mode 100644
index 0000000..9c59f7c
--- /dev/null
+++ b/web-extension/_locales/pt_BR/messages.json
@@ -0,0 +1,93 @@
+{
+ "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
new file mode 100644
index 0000000..7726ba9
--- /dev/null
+++ b/web-extension/_locales/ru/messages.json
@@ -0,0 +1,93 @@
+{
+ "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 60c2fba..a577260 100644
--- a/web-extension/background.js
+++ b/web-extension/background.js
@@ -1,44 +1,429 @@
-var customStorage = null;
-function _getEbookPages() {
- try {
- // var allPages = localStorage.getItem('ebook');
- var allPages = customStorage;
- if (!allPages) {
- allPages = [];
+///////////////////
+///////////////////
+///////////////////
+///////////////////
+/// 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 {
- allPages = JSON.parse(allPages);
+ callback({includeStyle: data.includeStyle});
}
- return allPages;
- } catch (e) {
- alert(e);
- return [];
+ });
+}
+
+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()
}
}
-function _saveEbookPages(pages) {
- try {
- // localStorage.setItem('ebook', JSON.stringify(pages));
- customStorage = JSON.stringify(pages);
- } catch (e) {
- alert(e);
- }
-}
+chrome.runtime.onMessage.addListener(_execRequest);
-function _removeEbook() {
- // localStorage.removeItem('ebook');
- customStorage = null;
-}
-
-chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
+function _execRequest(request, sender, sendResponse) {
if (request.type === 'get') {
- sendResponse({allPages: _getEbookPages()});
+ chrome.storage.local.get('allPages', function (data) {
+ if (!data || !data.allPages) {
+ sendResponse({allPages: []});
+ }
+ sendResponse({allPages: data.allPages});
+ })
}
if (request.type === 'set') {
- _saveEbookPages(request.pages);
+ chrome.storage.local.set({'allPages': request.pages});
}
if (request.type === 'remove') {
- _removeEbook();
+ 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()
}
return true;
-});
+}
diff --git a/web-extension/chapterEditor.css b/web-extension/chapterEditor.css
index 86e9e57..e3d42e2 100644
--- a/web-extension/chapterEditor.css
+++ b/web-extension/chapterEditor.css
@@ -12,24 +12,13 @@ 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 {
@@ -46,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 {
@@ -77,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;
@@ -90,9 +79,6 @@ 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;
}
@@ -111,7 +97,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;
@@ -134,21 +120,48 @@ 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-generate-button {
+.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 {
background-color: yellow;
}
-.chapterEditor-cancel-button {
+.chapterEditor-footer-button.chapterEditor-generate-button:hover {
+ background-color: rgba(0, 0, 0, 1);
+}
+.chapterEditor-footer-button.chapterEditor-cancel-button {
/*background-color: black;*/
}
diff --git a/web-extension/chapterEditor.js b/web-extension/chapterEditor.js
index d91445a..d54e4f3 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 = "Chapter Editor";
+ title.innerText = chrome.i18n.getMessage('chapterEditorTitle');
var upperCloseButton = document.createElement('button');
modalHeader.appendChild(title);
upperCloseButton.onclick = closeModal;
@@ -41,13 +41,15 @@ function showEditor() {
var ebookTilteLabel = document.createElement('span');
ebookTilteLabel.id = 'chapterEditor-ebookTitleLabel';
- ebookTilteLabel.innerText = 'eBook Title: ';
+ ebookTilteLabel.innerText = chrome.i18n.getMessage('ebookTitleLabel');
titleHolder.appendChild(ebookTilteLabel);
var ebookTilte = document.createElement('input');
ebookTilte.id = 'chapterEditor-ebookTitle';
ebookTilte.type = 'text';
- ebookTilte.value = 'eBook';
+ getEbookTitle(function (title) {
+ ebookTilte.value = title;
+ });
titleHolder.appendChild(ebookTilte);
modalList.appendChild(titleHolder);
@@ -79,12 +81,12 @@ function showEditor() {
var buttons = document.createElement('span');
var previewButton = document.createElement('button');
- previewButton.innerText = 'Raw Preview';
+ previewButton.innerText = chrome.i18n.getMessage('rawPreview');
previewButton.className = 'chapterEditor-text-button';
previewButton.onclick = previewListItem(i);
var removeButton = document.createElement('button');
- removeButton.innerText = 'Remove';
+ removeButton.innerText = chrome.i18n.getMessage('remove');
removeButton.className = 'chapterEditor-text-button chapterEditor-text-red';
removeButton.onclick = removeListItem(i);
@@ -106,18 +108,40 @@ function showEditor() {
// Footer
var buttons = document.createElement('div');
var closeButton = document.createElement('button');
- closeButton.innerText = 'Cancel';
+ closeButton.innerText = chrome.i18n.getMessage('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() {
- prepareEbook();
+ var newChapters = saveChanges();
+ prepareEbook(newChapters);
};
- saveButton.innerText = 'Generate eBook ...';
+ saveButton.innerText = chrome.i18n.getMessage('generateEbook');
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);
/////////////////////
@@ -182,11 +206,23 @@ function showEditor() {
function previewListItem(atIndex) {
return function() {
- alert(allPagesRef[atIndex].content.trim().substring(0, 500).replace(/<[^>]+>/gi, '') + ' ...');
+ alert(allPagesRef[atIndex].content.trim().replace(/<[^>]+>/gi, '').replace(/\s+/g, ' ').substring(0, 1000) + ' ...');
};
}
- function prepareEbook() {
+ function prepareEbook(newChapters) {
+ try {
+ if (newChapters.length === 0) {
+ alert(chrome.i18n.getMessage('emptyBookWarning'));
+ return;
+ }
+ buildEbookFromChapters();
+ } catch (e) {
+ console.log('Error:', e);
+ }
+ }
+
+ function saveChanges() {
var newChapters = [];
var newEbookTitle = ebookTilte.value;
if (newEbookTitle.trim() === '') {
@@ -194,35 +230,27 @@ function showEditor() {
}
try {
-
var tmpChaptersList = document.getElementsByClassName('chapterEditor-chapter-item');
if (!tmpChaptersList || !allPagesRef) {
return;
}
for (var i = 0; i < tmpChaptersList.length; i++) {
- var listIndex = Number(tmpChaptersList[i].id.replace('li', ''));
+ var tmpChapterItem = tmpChaptersList[i];
+ var listIndex = Number(tmpChapterItem.id.replace('li', ''));
if (allPagesRef[listIndex].removed === false) {
+ var newChapterTitle = tmpChapterItem.children.namedItem('text'+listIndex).value;
+ allPagesRef[listIndex].title = newChapterTitle;
newChapters.push(allPagesRef[listIndex]);
}
}
- if (newChapters.length === 0) {
- alert('Can\'t generate an empty eBook!');
- return;
- }
-
- newChapters.splice(0, 0, {
- type: 'title',
- title: newEbookTitle
- });
-
+ saveEbookTitle(newEbookTitle);
saveEbookPages(newChapters);
- buildEbookFromChapters();
+ return newChapters;
} catch (e) {
console.log('Error:', e);
}
-
}
/////////////////////
diff --git a/web-extension/cssEditor.css b/web-extension/cssEditor.css
new file mode 100644
index 0000000..88ed70f
--- /dev/null
+++ b/web-extension/cssEditor.css
@@ -0,0 +1,190 @@
+#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
new file mode 100644
index 0000000..ea28993
--- /dev/null
+++ b/web-extension/cssEditor.js
@@ -0,0 +1,379 @@
+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 allImages = [];
-var maxNrOfElements = 10000;
+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',
+];
//////
function getImageSrc(srcTxt) {
if (!srcTxt) {
return '';
}
- allImgSrc[srcTxt] = 'img-' + (Math.floor(Math.random()*1000000)) + '.' + getFileExtension(srcTxt);
- return '../images/' + allImgSrc[srcTxt];
+ 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;
}
-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 extractMathMl($htmlObject) {
+ $htmlObject.find('span[id^="MathJax-Element-"]').each(function (i, el) {
+ $(el).replaceWith('' + el.getAttribute('data-mathml') + '');
+ });
}
-
-// 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 {
- 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);
- }
- });
+// 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)
}
+ });
+}
- contentString = $content.text();
+// 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('
' + '');
+ });
+}
- 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);
+// 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)
}
}
-// https://github.com/blowsie/Pure-JavaScript-HTML5-Parser
-function sanitize(rawContentString) {
- allImgSrc = {};
- var srcTxt = '';
- var dirty = null;
+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 = '';
+
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, {
+ HTMLParser(rawContentString, {
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') {
- tattrs = attrs.filter(function(attr) {
- return attr.name === 'src';
- }).map(function(attr) {
- return getImageSrc(attr.escaped);
- });
- lastFragment = tattrs.length === 0 ? '
' : '
';
+ 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 ? '
' : '
';
+ }
} else if (tag === 'a') {
- tattrs = attrs.filter(function(attr) {
- return attr.name === 'href';
- }).map(function(attr) {
- return getHref(attr.escaped);
- });
- lastFragment = tattrs.length === 0 ? '' : '';
+ 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 + '>' + 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 + '>';
} else {
- lastFragment = '<' + tag + '>';
+ let tmpAttrsTxt = '';
+ for (let i = 0; i < attrs.length; i++) {
+ if (attrs[i].name === 'data-class') {
+ tmpAttrsTxt += ' class="' + attrs[i].value + '"';
+ }
+ }
+ lastFragment = '<' + tag + tmpAttrsTxt + '>';
}
results += lastFragment;
lastFragment = '';
},
end: function(tag) {
- if (allowedTags.indexOf(tag) < 0 || tag === 'img') {
+ if (allowedTags.indexOf(tag) < 0 || tag === 'img' || tag === 'br' || tag === 'hr') {
return;
}
- if (tag === 'ol' || tag === 'ul') {
- inList = false;
- }
- if (tag === 'li' && !inList) {
- tag = 'p';
- }
-
- results += "" + tag + ">\n";
+ results += "" + tag + ">";
},
chars: function(text) {
if (lastTag !== '' && allowedTags.indexOf(lastTag) < 0) {
@@ -232,49 +252,42 @@ function sanitize(rawContentString) {
}
});
- // results = results.replace(/&[a-z]+;/gim, '');
- results = results.replace(/&/gi, '&');
- results = results.replace(/&/gi, '&');
+ // TODO - (re)move
+ results = results.replace(/ /gi, ' ');
return results;
} catch (e) {
console.log('Error:', e);
- return force(dirty);
+ return 'Error: ' + e //+" " + force($(rawContentString))
}
}
function getContent(htmlContent) {
try {
- var tmp = document.createElement('div');
+ // TODO - move; called multiple times on selection
+ preProcess($('body'))
+ let tmp = document.createElement('div');
tmp.appendChild(htmlContent.cloneNode(true));
- var dirty = '' + tmp.innerHTML + '
';
- return sanitize(dirty);
+ let tmpHtml = '' + tmp.innerHTML + '
';
+ return parseHTML(tmpHtml);
} catch (e) {
console.log('Error:', e);
- return '';
+ return htmlContent;
}
}
/////
-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();
// }
- var selection = window.getSelection();
- var docfrag = [];
- for (var i = 0; i < selection.rangeCount; i++) {
+ let selection = window.getSelection();
+ let docfrag = [];
+ for (let i = 0; i < selection.rangeCount; i++) {
docfrag.push(selection.getRangeAt(i).cloneContents());
}
return docfrag;
@@ -282,67 +295,174 @@ function getSelectedNodes() {
/////
-function deferredAddZip(url, filename, zip) {
- var deferred = $.Deferred();
- JSZipUtils.getBinaryContent(url, function(err, data) {
+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) {
if (err) {
// deferred.reject(err); TODO
+ console.log('Error:', err);
deferred.resolve();
} else {
- var tmpImg = {
+ // 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)
- };
- allImages.push(tmpImg);
+ });
+
deferred.resolve();
}
});
return deferred;
}
-chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
- console.log('Extract Html...');
- var imgsPromises = [];
- allImgSrc = {};
- allImages = [];
- var result = {};
- var pageSrc = '';
- var tmpContent = '';
+chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
+ let imgsPromises = [];
+ let result = {};
+ let pageSrc = '';
+ let tmpContent = '';
+ let styleFile = null;
+
+ extractIFrames()
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(function (page) {
+ pageSrc.forEach((page) => {
tmpContent += getContent(page);
});
}
- if (tmpContent.trim() === '') {
- return;
- }
+ tmpGlobalContent = tmpContent
- Object.keys(allImgSrc).forEach(function(imgSrc, index) {
- try {
- var tmpDeffered = deferredAddZip(getImgDownloadUrl(imgSrc), allImgSrc[imgSrc]);
- imgsPromises.push(tmpDeffered);
- } catch (e) {
- console.log('Error:', e);
- }
+ allImages.forEach((tmpImg) => {
+ imgsPromises.push(deferredAddZip(tmpImg.originalUrl, tmpImg.filename));
});
- $.when.apply($, imgsPromises).done(function() {
+ $.when.apply($, imgsPromises).done(() => {
+ let tmpTitle = getPageTitle(document.title);
result = {
- url: getPageUrl(document.title),
- title: getPageTitle(document.title),
+ url: getPageUrl(tmpTitle),
+ title: tmpTitle,
baseUrl: getCurrentUrl(),
- images: allImages,
- content: tmpContent
+ styleFileContent: styleFile,
+ styleFileName: 'style' + generateRandomNumber() + '.css',
+ images: extractedImages,
+ content: tmpGlobalContent
};
sendResponse(result);
- }).fail(function(e) {
+ }).fail((e) => {
console.log('Error:', e);
+ sendResponse(null)
});
return true;
diff --git a/web-extension/filesaver.js b/web-extension/filesaver.js
deleted file mode 100644
index 239db12..0000000
--- a/web-extension/filesaver.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/* 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 3b5d8fd..14ae99c 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 81d30eb..84ee1f9 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 ada2f39..d06befb 100644
Binary files a/web-extension/icons/book48.png and b/web-extension/icons/book48.png differ
diff --git a/web-extension/libs/cssjson.js b/web-extension/libs/cssjson.js
new file mode 100644
index 0000000..01b6332
--- /dev/null
+++ b/web-extension/libs/cssjson.js
@@ -0,0 +1,297 @@
+/**
+ * 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
new file mode 100644
index 0000000..5d204ae
--- /dev/null
+++ b/web-extension/libs/filesaver.js
@@ -0,0 +1,171 @@
+/*
+* 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/jquery-sortable.js b/web-extension/libs/jquery-sortable.js
similarity index 100%
rename from web-extension/jquery-sortable.js
rename to web-extension/libs/jquery-sortable.js
diff --git a/web-extension/jquery.js b/web-extension/libs/jquery.js
similarity index 100%
rename from web-extension/jquery.js
rename to web-extension/libs/jquery.js
diff --git a/web-extension/jszip-utils.js b/web-extension/libs/jszip-utils.js
similarity index 100%
rename from web-extension/jszip-utils.js
rename to web-extension/libs/jszip-utils.js
diff --git a/web-extension/jszip.js b/web-extension/libs/jszip.js
similarity index 100%
rename from web-extension/jszip.js
rename to web-extension/libs/jszip.js
diff --git a/web-extension/pure-parser.js b/web-extension/libs/pure-parser.js
similarity index 100%
rename from web-extension/pure-parser.js
rename to web-extension/libs/pure-parser.js
diff --git a/web-extension/manifest.json b/web-extension/manifest.json
index d0a90e3..0b65008 100644
--- a/web-extension/manifest.json
+++ b/web-extension/manifest.json
@@ -1,39 +1,60 @@
{
-
"manifest_version": 2,
- "name": "Save As eBook",
- "version": "1.0",
-
+ "name": "__MSG_extName__",
+ "version": "1.4.2",
+ "default_locale": "en",
+ "author": "Alex Adam",
+ "homepage_url": "https://github.com/alexadam/save-as-ebook",
"description": "Save a web page or selection as eBook (.epub)",
-
"icons": {
"48": "icons/book48.png"
},
-
"content_scripts": [{
"matches": [""],
- "js": ["jquery.js", "jszip.js", "jszip-utils.js", "utils.js", "pure-parser.js", "extractHtml.js"]
+ "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"]
}],
-
"background": {
"scripts": ["background.js"]
},
-
"browser_action": {
"default_icon": "icons/book32.png",
"default_title": "Save as eBook",
"default_popup": "menu.html"
},
-
"permissions": [
"contextMenus",
"activeTab",
"storage",
- "tabs",
+ "unlimitedStorage",
"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 7973fba..0906d7b 100644
--- a/web-extension/menu.html
+++ b/web-extension/menu.html
@@ -2,57 +2,162 @@
-
-
+ .optionContainer {
+ font-family: sans-serif;
+ font-size: 14px;
+ padding: 6px 0;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
-