Compare commits

...

117 commits
1.1 ... master

Author SHA1 Message Date
Alex Adam
04f9e5de60
Merge pull request #56 from MichaelPetre/issue-54
Fixed issue-54
2021-11-18 18:24:27 +02:00
Michael PETRE
5365ec3552 Fixed issue-54 2021-11-16 20:11:47 +01:00
Alex Adam
12a4cbb081 Add MIME type to the generated .epub file 2020-12-24 15:52:14 +02:00
Alex Adam
03dd484827 add fixed menu to selector 2020-11-12 18:21:40 +02:00
Alex Adam
bca73403eb update readme 2020-06-26 11:48:49 +03:00
Alex Adam
432db9de04 update manifest version 2020-06-26 11:21:16 +03:00
Alex Adam
6b4268f5b0 check if iframe content exists before replacing it 2020-06-18 16:39:44 +03:00
Alex Adam
ab229d926e move 3rd party libs to /libs folder 2020-06-18 16:04:00 +03:00
Alex Adam
1d40954594 remove hidden elements when style is not included 2020-06-18 13:35:59 +03:00
Alex Adam
4a4dad44af replace iframes with divs 2020-06-18 13:19:36 +03:00
Alex Adam
f3664a493d update manifest.js 2020-06-18 13:15:20 +03:00
Alex Adam
3f162d75dc handle base64 & binary images 2020-06-16 18:19:04 +03:00
Alex Adam
21ccfcf766 handle base64 & binary images 2020-06-16 17:14:27 +03:00
Alex Adam
43dd4aefda update permissions in manifest.json 2020-06-16 15:56:02 +03:00
Alex Adam
9a475b62f5 update permissions in manifest.json 2020-06-16 15:17:33 +03:00
Alex Adam
4bfc906eb9 detect image type from img src links without a file's extension 2020-06-16 14:38:30 +03:00
Alex Adam
37bd9328b8 add timer to force reset the Busy indicator 2020-06-16 14:36:36 +03:00
Alex Adam
791f523e8c add manifest for chrome 2020-03-17 20:27:26 +02:00
Alex Adam
70a826709f update readme 2020-03-17 14:52:17 +02:00
Alex Adam
8a0d4210c6 change manifest version for new release 2020-03-17 14:45:03 +02:00
Alex Adam
66139e71a3 misc fixes 2020-03-16 20:53:14 +02:00
Alex Adam
0eb26ddef4 fix for issue #37 2020-03-09 18:30:47 +02:00
Alex Adam
9a75e6e92a misc changes; removed force() 2020-02-28 14:15:48 +02:00
Alex Adam
b49b78ad88 misc changes - with force() 2020-02-28 14:05:43 +02:00
Alex Adam
411eb0f86a misc selector updates WIP 2020-02-24 19:27:53 +02:00
Alex Adam
7670aff952 misc updates WIP 2020-02-24 18:25:49 +02:00
Alex Adam
b72d7934dd misc updates WIP 2020-02-24 14:00:33 +02:00
Alex Adam
74673b5449 misc updates WIP 2020-02-24 10:50:00 +02:00
Alex Adam
5d1ff43012 misc updates WIP 2020-02-21 15:15:11 +02:00
Alex Adam
c39c62b8ec update tests 2020-02-20 16:02:01 +02:00
Alex Adam
8abb752520 update tests 2020-02-20 15:12:45 +02:00
Alex Adam
278f42d4c7 update tests 2020-02-20 14:48:50 +02:00
Alex Adam
c541ac08d4 update tests 2020-02-20 14:48:04 +02:00
Alex Adam
65c7b94c5c update tests 2020-02-20 13:56:06 +02:00
Alex Adam
8774ed096e update tests 2020-02-20 12:59:59 +02:00
Alex Adam
aad8004790 update tests 2020-02-20 12:20:42 +02:00
Alex Adam
500db33fc7 misc changes WIP 2019-06-03 18:00:23 +03:00
Alex Adam
467962e565 add tests - WIP 2019-04-15 20:28:10 +03:00
Alex Adam
612a62ecac add experimental element selector 2018-11-03 20:42:48 +02:00
Alex Adam
167ddf40ab update manifest.js 2018-11-03 13:17:14 +02:00
Alex Adam
a7258b3c81 misc bug fixes 2018-11-03 13:15:17 +02:00
Alex Adam
5e02b1e745 update readme & manifest 2018-07-19 18:21:52 +03:00
Alex Adam
77d622d460 fix for MathML not rendered correctly; Include style menu option not saved 2018-07-14 17:32:45 +03:00
Alex Adam
8a8db5c1db
Merge pull request #27 from welksonramos/master
add brazilian portuguese language
2018-07-14 14:04:11 +03:00
Welkson Ramos
6632712802 add brazilian portuguese language 2018-07-13 13:22:24 -03:00
Alex Adam
a0bf262ebc
Merge pull request #25 from AxelRUS/feature/ru-lang
Added russian language
2018-05-29 19:07:56 +03:00
Emil' Khalikov
2ef3bf90dd added russian language 2018-05-29 13:08:18 +03:00
Alex Adam
4576d87526 update manifest.js 2018-03-26 17:47:55 +03:00
Alex Adam
9a64e72e89 update french translation 2018-03-24 20:50:59 +02:00
Alex Adam
d3ea23f3bf misc bug fixes 2018-03-24 20:45:41 +02:00
Alex Adam
4d14fd5baf saveAs dialog not triggered when invoked from menu 2018-03-21 20:40:51 +02:00
Alex Adam
9237935f69 misc bug fixes 2018-03-12 19:52:10 +02:00
Alex Adam
bc8dfab9b3 update readme 2018-03-10 14:53:10 +02:00
Alex Adam
7234ccf8b0 update screenshot 2018-03-10 14:35:10 +02:00
Alex Adam
2a99530d18 update manifest version 2018-03-10 14:30:46 +02:00
Alex Adam
74f4263678 bug fix - do not apply styles on svg elements 2018-03-10 14:28:48 +02:00
Alex Adam
917d8da78c misc fixes 2018-03-10 14:21:01 +02:00
Alex Adam
782e96340e fix issue #22 - relative image url's starting with / are not saved 2018-02-27 20:45:57 +02:00
Alex Adam
39b66f2f8c show busy indicator in menu when triggered from shortcut 2018-02-24 21:19:51 +02:00
Alex Adam
50dd1c3150 misc menu fixes 2018-02-23 21:57:36 +02:00
Alex Adam
d2e62949fb misc menu changes WIP 2018-02-22 23:12:35 +02:00
Alex Adam
bad1190a37 misc fixes 2018-02-22 22:31:38 +02:00
Alex Adam
0faa8655d9 misc changes WIP 2018-02-22 00:30:14 +02:00
Alex Adam
5d5e35021e automatically apply styles WIP 2018-01-20 16:53:32 +02:00
Alex Adam
264518de89 display shortcuts in the menu 2018-01-20 15:30:33 +02:00
Alex Adam
062f95f7c9 display shortcuts in the menu 2018-01-20 15:29:53 +02:00
Alex Adam
1db3b504ec show busy indicator badge text 2018-01-09 21:48:22 +02:00
Alex Adam
745a2b7ada add keyboard shortcuts WIP 2018-01-08 20:35:38 +02:00
Alex Adam
268ab1da64 add support for internationalization WIP 2017-11-04 18:43:56 +02:00
Alex Adam
d25e851edc escape XML chars in title 2017-10-26 18:06:31 +03:00
Alex Adam
36c9b68e10 fix & & problem in title for chapters; Issue #10 2017-10-25 21:24:03 +03:00
Alex Adam
bc64fd360c fixed typo in readme 2017-10-25 20:49:29 +03:00
Alex Adam
0d540b707a update readme & manifest version 2017-10-25 20:48:27 +03:00
Alex Adam
eefc97bdfb fix & & problem in title; Issue #10 2017-10-25 20:43:45 +03:00
Alex Adam
1bfaa75c12 update readme & manifiest 2017-10-23 20:39:26 +03:00
Alex Adam
11604a8a56 include hr and br html tags when extracting data 2017-10-11 21:19:24 +03:00
Alex Adam
0671fca24f Merge pull request #12 from gircysm/master
Chapter Editor Update Button
2017-10-11 19:45:13 +03:00
gircysm
2670d79640 ripple effect on button press; preserve "generate" button colour 2017-10-08 19:05:13 -04:00
gircysm
269212f406 chapter update button to save modified chapter titles 2017-10-08 18:52:39 -04:00
Alex Adam
587e520881 update readme 2017-10-05 23:19:49 +03:00
Alex Adam
6c72c713be update readme 2017-10-05 22:59:13 +03:00
Alex Adam
97dade4498 update readme 2017-10-05 22:58:20 +03:00
Alex Adam
18ceddba41 update readme 2017-10-05 22:56:51 +03:00
Alex Adam
add117d5b6 update readme 2017-10-05 22:56:26 +03:00
Alex Adam
76c8b1c22f update readme 2017-10-05 22:55:54 +03:00
Alex Adam
105e64cc11 update readme 2017-10-05 22:54:41 +03:00
Alex Adam
30b99d1291 update readme 2017-10-05 22:53:20 +03:00
Alex Adam
9cf851f9ef update readme 2017-10-05 22:51:08 +03:00
Alex Adam
bec5c544f6 update readme 2017-10-05 22:46:31 +03:00
Alex Adam
772794e3e5 update readme 2017-10-05 22:44:53 +03:00
Alex Adam
19e548ea2f add new example images 2017-10-05 22:43:03 +03:00
Alex Adam
1d6ad7282c add new icons 2017-10-05 22:39:51 +03:00
Alex Adam
a64acc3457 hide custom styles if Include Style is not set 2017-10-05 22:09:44 +03:00
Alex Adam
0a8748220a Apply custom style only if Include Style is set 2017-10-04 20:44:19 +03:00
Alex Adam
22ddc6e918 add more predefined styles 2017-10-04 20:28:49 +03:00
Alex Adam
2a5abbd7d8 add more predefined styles 2017-09-07 20:51:16 +03:00
Alex Adam
5cabb7a458 add more predefined styles 2017-09-06 21:47:33 +03:00
Alex Adam
db77b562b1 review supported css properties 2017-09-06 20:12:25 +03:00
Alex Adam
ec17bd741d add testing label 2017-08-09 21:57:56 +03:00
Alex Adam
2ad80ba324 misc styling chnages 2017-08-09 21:52:37 +03:00
alexadam
6b493b25a7 add style support for force mode 2017-07-22 20:20:38 +03:00
alexadam
f76512d55b misc bug fixes 2017-07-14 22:27:09 +03:00
alexadam
5f01a960f1 misc fixes for Google Play Books upload issue 2017-07-14 22:00:25 +03:00
alexadam
e4f6940ecc check for regex errors & misc ui changes 2017-07-13 22:38:42 +03:00
alexadam
a867a88550 change the Style Editor layout & show confirmations when save/delete style 2017-07-12 22:48:35 +03:00
alexadam
53620b6456 keep track of applied styles and add them if Include Style is not set 2017-07-11 21:52:04 +03:00
alexadam
126ce6f039 style url as regex instead of plain starts with 2017-07-10 22:36:16 +03:00
alexadam
84dfab759f extract custom style WIP 2017-06-13 21:18:47 +03:00
alexadam
3c626ac2bc link include styles to extract css 2017-06-12 22:12:44 +03:00
alexadam
475ea4c158 misc style fixes - WIP 2017-06-12 20:26:11 +03:00
alexadam
1d6905332b add include style option WIP 2017-05-30 22:44:44 +03:00
alexadam
e4180c1808 add more css tags + apply style should work;; WIP 2017-05-20 21:45:47 +03:00
alexadam
20db190360 fix for default style 2017-05-20 21:44:13 +03:00
alexadam
ad1abc5f61 move default styles 2017-05-20 11:47:59 +03:00
alexadam
9e3ed840fd css fixes for img and links 2017-04-10 22:03:40 +03:00
alexadam
cbf7e1816a css editor ui changes 2017-04-08 12:36:17 +03:00
alexadam
7dd34efee5 styles - wip 2017-03-11 14:21:05 +02:00
36 changed files with 3014 additions and 604 deletions

3
.gitignore vendored Normal file
View file

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

View file

@ -2,10 +2,12 @@
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)
<img src="https://github.com/alexadam/save-as-ebook/blob/master/ex11.png?raw=true" width="350">
![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)
@ -43,21 +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 & &amp; issue in title; Issue # 10
## Added in v1.2.1
- support for hr/br html tags
## Added in v1.2
- BETA: Support for CSS
- BETA: Create / edit custom Styles
- No errors from EPUB Validator (http://validator.idpf.org/) + this should fix the Google Play upload issue
## Added in v1.1
- Chapter Editor: option to save changes
- Chapter Editor: option to remove all chapters
- persist Chapter Editor changes & chapters after generating an eBook or after a browser restart
## To-Do
- fix all 'epubcheck' errors (https://github.com/IDPF/epubcheck)
- 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
- 'save as image' option (render a selection and save it as image instead of text)
- support other formats (mobi, pdf etc.)
- show confirmations (ui/ux)
- display errors (ui/ux)
- add settings & options page (ui/ux)
- support custom style
- 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
@ -65,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

BIN
ex1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

BIN
ex11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
ex3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
tests.tar

Binary file not shown.

52
tests/index.js Normal file
View file

@ -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);
}
}

9
tests/package.json Normal file
View file

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

BIN
tests/pages.zip Normal file

Binary file not shown.

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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": "Сохранить стиль"
}
}

View file

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

View file

@ -18,7 +18,7 @@ ul.chapterEditor-chapters-list li.placeholder {
font-weight: bold;
float: left;
display: inline-block;
font-family: "sans-serif";
font-family: sans-serif;
}
#chapterEditor-ebookTitleHolder {
@ -35,7 +35,7 @@ ul.chapterEditor-chapters-list li.placeholder {
#chapterEditor-ebookTitle {
padding: 5px;
font-size: 15px;
font-family: "sans-serif";
font-family: sans-serif;
width: 85%;
}
ul, ul.chapterEditor-chapters-list {
@ -66,7 +66,7 @@ ul, ul.chapterEditor-chapters-list {
.chapterEditor-chapter-item > input[type="text"] {
padding: 5px;
font-size: 15px;
font-family: "sans-serif";
font-family: sans-serif;
width: 80%;
border: none;
border-bottom: solid 1px #aaa;
@ -79,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;
}
@ -100,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;
@ -123,6 +120,8 @@ ul, ul.chapterEditor-chapters-list {
}
.chapterEditor-footer-button {
position: relative;
overflow: hidden;
padding: 18px 20px;
margin: 0;
font-size: 18px;
@ -135,10 +134,34 @@ ul, ul.chapterEditor-chapters-list {
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;*/
}

View file

@ -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,7 +41,7 @@ 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');
@ -81,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);
@ -108,16 +108,16 @@ 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 = 'Remove Chapters';
removeButton.innerText = chrome.i18n.getMessage('removeChapters');
removeButton.className = 'chapterEditor-footer-button hapterEditor-float-left';
removeButton.onclick = function() {
var result = confirm("Do you want to remove all chapters?");
var result = confirm(chrome.i18n.getMessage('removeChaptersConfirm'));
if (result) {
removeEbook();
closeModal();
@ -130,7 +130,7 @@ function showEditor() {
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);
@ -138,7 +138,7 @@ function showEditor() {
saveChangesButton.onclick = function() {
saveChanges();
};
saveChangesButton.innerText = 'Save changes';
saveChangesButton.innerText = chrome.i18n.getMessage('saveChanges');
saveChangesButton.className = 'chapterEditor-footer-button chapterEditor-float-right';
buttons.appendChild(saveChangesButton);
@ -213,7 +213,7 @@ function showEditor() {
function prepareEbook(newChapters) {
try {
if (newChapters.length === 0) {
alert('Can\'t generate an empty eBook!');
alert(chrome.i18n.getMessage('emptyBookWarning'));
return;
}
buildEbookFromChapters();
@ -236,8 +236,11 @@ function showEditor() {
}
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]);
}
}

190
web-extension/cssEditor.css Normal file
View file

@ -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%;
}
}

379
web-extension/cssEditor.js Normal file
View file

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

View file

@ -1,18 +1,50 @@
// Used to replace <img> src links that don't have a file extension
// If the image src doesn't have a file type:
// 1. Create a dummy link
// 2. Detect image type from the binary data & create new links
// 3. Replace all the dummy links in tmpGlobalContent with the new links
var tmpGlobalContent = null
var allImages = [];
var extractedImages = [];
var maxNrOfElements = 20000;
var allowedTags = [
'address', 'article', 'aside', 'footer', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'hgroup', 'nav', 'section', 'dd', 'div', 'dl', 'dt', 'figcaption', 'figure', 'hr', 'li',
'main', 'ol', 'p', 'pre', 'ul', 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data',
'dfn', 'em', 'i', 'img', 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small', 'span',
'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'
];
'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) {
@ -23,16 +55,25 @@ function getImageSrc(srcTxt) {
if (srcTxt === '') {
return '';
}
var isB64Img = isBase64Img(srcTxt);
var fileExtension = getFileExtension(srcTxt);
var newImgFileName = 'img-' + (Math.floor(Math.random()*1000000*Math.random()*100000)) + '.' + fileExtension;
// TODO move
srcTxt = srcTxt.replace(/&amp;/g, '&')
// TODO - convert <imgs> with svg sources to jpeg OR add support for svg
let fileExtension = getFileExtension(srcTxt);
if (fileExtension === '') {
fileExtension = "TODO-EXTRACT"
}
let newImgFileName = 'img-' + generateRandomNumber(true) + '.' + fileExtension;
let isB64Img = isBase64Img(srcTxt);
if (isB64Img) {
extractedImages.push({
filename: newImgFileName, // TODO name
data: getBase64ImgData(srcTxt)
});
} else {
} else {
allImages.push({
originalUrl: getImgDownloadUrl(srcTxt),
filename: newImgFileName, // TODO name
@ -42,164 +83,163 @@ function getImageSrc(srcTxt) {
return '../images/' + newImgFileName;
}
function formatPreCodeElements($jQueryElement) {
$jQueryElement.find('pre').each(function (i, pre) {
$(pre).replaceWith('<pre>' + pre.innerText + '</pre>');
});
$jQueryElement.find('code').each(function (i, pre) {
$(pre).replaceWith('<code>' + pre.innerText + '</code>');
});
}
// tested
function extractMathMl($htmlObject) {
$htmlObject.find('span[id^="MathJax-Element-"]').each(function (i, el) {
$(el).replaceWith('<span>' + el.getAttribute('data-mathml') + '</span>');
});
}
// tested
function extractCanvasToImg($htmlObject) {
$htmlObject.find('canvas').each(function (index, elem) {
var tmpXP = getXPath(elem);
tmpXP = tmpXP.replace(/^\/div\[1\]/m, '/html[1]/body[1]');
var docEl = lookupElementByXPath(tmpXP);
var jpegUrl = docEl.toDataURL('image/png');
$(elem).replaceWith('<img src="' + jpegUrl + '" alt=""></img>');
try {
let imgUrl = docEl.toDataURL('image/jpeg');
$(elem).replaceWith('<img src="' + imgUrl + '" alt=""></img>');
} catch (e) {
console.log(e)
}
});
}
// tested
function extractSvgToImg($htmlObject) {
var serializer = new XMLSerializer();
let serializer = new XMLSerializer();
$htmlObject.find('svg').each(function (index, elem) {
var svgXml = serializer.serializeToString(elem);
var imgSrc = 'data:image/svg+xml;base64,' + window.btoa(svgXml);
$(elem).replaceWith('<img src="' + imgSrc + '">' + '</img>');
// add width & height because the result image was too big
let bbox = elem.getBoundingClientRect()
let newWidth = bbox.width
let newHeight = bbox.height
let svgXml = serializer.serializeToString(elem);
let imgSrc = 'data:image/svg+xml;base64,' + window.btoa(svgXml);
$(elem).replaceWith('<img src="' + imgSrc + '" width="'+newWidth+'" height="'+newHeight+'">' + '</img>');
});
}
function preProcess($htmlObject) {
extractMathMl($htmlObject);
extractCanvasToImg($htmlObject);
extractSvgToImg($htmlObject);
$htmlObject.find('script, style, noscript, iframe').remove();
$htmlObject.find('*:empty').not('img').remove();
formatPreCodeElements($htmlObject);
}
function force($content, withError) {
try {
var tagOpen = '@@@' + generateRandomTag();
var tagClose = '###' + generateRandomTag();
var startEl = '<object>';
var endEl = '</object>';
if (withError) {
$content = $($content);
preProcess($content);
// 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
}
$content.find('img').each(function (index, elem) {
var imgSrc = getImageSrc($(elem).attr('src'));
if (imgSrc === '') {
$(elem).replaceWith('');
} else {
$(elem).replaceWith(startEl + tagOpen + 'img src="' + imgSrc + '"' + tagClose + tagOpen + '/img' + tagClose + endEl);
}
});
$content.find('a').each(function (index, elem) {
var aHref = getHref($(elem).attr('href'));
if (aHref === '') {
$(elem).replaceWith('');
} else {
$(elem).replaceWith(startEl + tagOpen + 'a href="' + aHref + '"' + tagClose + $(elem).html() + tagOpen + '/a' + tagClose + endEl);
}
});
all($content);
function all($startElement) {
var tagName = $startElement.get(0).tagName.toLowerCase();
if (allowedTags.indexOf(tagName) >= 0) {
var children = $startElement.children();
var childrenLen = children.length;
while (childrenLen--) {
all($(children[childrenLen]));
}
$startElement.replaceWith(startEl + tagOpen + tagName + tagClose + $startElement.html() + tagOpen + '/' + tagName + tagClose + endEl);
}
}
var contentString = $content.text();
var tagOpenRegex = new RegExp(tagOpen, 'gi');
var tagCloseRegex = new RegExp(tagClose, 'gi');
contentString = contentString.replace(tagOpenRegex, '<');
contentString = contentString.replace(tagCloseRegex, '>');
contentString = contentString.replace(/&nbsp;/gi, '&#160;');
return contentString;
} catch (e) {
console.log('Error:', e);
return '';
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 sanitize(rawContentString) {
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 = [];
var srcTxt = '';
var dirty = null;
let results = '';
let lastFragment = '';
let lastTag = '';
try {
var wdirty = $.parseHTML(rawContentString);
$wdirty = $(wdirty);
preProcess($wdirty);
if ($('*').length > maxNrOfElements) {
return force($wdirty, false);
}
dirty = '<div>' + $wdirty.html() + '</div>';
var results = '';
var lastFragment = '';
var lastTag = '';
HTMLParser(dirty, {
HTMLParser(rawContentString, {
start: function(tag, attrs, unary) {
lastTag = tag;
if (allowedTags.indexOf(tag) < 0) {
return;
}
var tattrs = null;
if (tag === 'img') {
tattrs = attrs.filter(function(attr) {
return attr.name === 'src';
}).map(function(attr) {
return getImageSrc(attr.value);
});
lastFragment = tattrs.length === 0 ? '<img></img>' : '<img src="' + tattrs[0] + '" alt=""></img>';
let tmpAttrsTxt = '';
let tmpSrc = ''
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'src') {
tmpSrc = getImageSrc(attrs[i].value)
tmpAttrsTxt += ' src="' + tmpSrc + '"';
} else if (attrs[i].name === 'data-class') {
tmpAttrsTxt += ' class="' + attrs[i].value + '"';
} else if (attrs[i].name === 'width') {
// used when converting svg to img - the result image was too big
tmpAttrsTxt += ' width="' + attrs[i].value + '"';
} else if (attrs[i].name === 'height') {
// used when converting svg to img - the result image was too big
tmpAttrsTxt += ' height="' + attrs[i].value + '"';
}
}
if (tmpSrc === '') {
// ignore imgs without source
lastFragment = ''
} else {
lastFragment = tmpAttrsTxt.length === 0 ? '<img></img>' : '<img' + tmpAttrsTxt + ' alt=""></img>';
}
} else if (tag === 'a') {
tattrs = attrs.filter(function(attr) {
return attr.name === 'href';
}).map(function(attr) {
return getHref(attr.value);
});
lastFragment = tattrs.length === 0 ? '<a>' : '<a href="' + tattrs[0] + '">';
let tmpAttrsTxt = '';
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'href') {
tmpAttrsTxt += ' href="' + getHref(attrs[i].value) + '"';
} else if (attrs[i].name === 'data-class') {
tmpAttrsTxt += ' class="' + attrs[i].value + '"';
}
}
lastFragment = tmpAttrsTxt.length === 0 ? '<a>' : '<a' + tmpAttrsTxt + '>';
} else if (tag === 'br' || tag === 'hr') {
let tmpAttrsTxt = '';
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'data-class') {
tmpAttrsTxt += ' class="' + attrs[i].value + '"';
}
}
lastFragment = '<' + tag + tmpAttrsTxt + '></' + tag + '>';
} else if (tag === 'math') {
let tmpAttrsTxt = '';
tmpAttrsTxt += ' xmlns="http://www.w3.org/1998/Math/MathML"';
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === 'alttext') {
tmpAttrsTxt += ' alttext="' + attrs[i].value + '"';
}
}
lastFragment = '<' + tag + tmpAttrsTxt + '>';
} 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;
}
results += "</" + tag + ">\n";
results += "</" + tag + ">";
},
chars: function(text) {
if (lastTag !== '' && allowedTags.indexOf(lastTag) < 0) {
@ -212,50 +252,42 @@ function sanitize(rawContentString) {
}
});
// TODO - (re)move
results = results.replace(/&nbsp;/gi, '&#160;');
return results;
} catch (e) {
console.log('Error:', e);
return force(dirty, true);
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 = '<div>' + tmp.innerHTML + '</div>';
return sanitize(dirty);
let tmpHtml = '<div>' + tmp.innerHTML + '</div>';
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) {
if (title.trim().length === 0) {
return 'ebook';
}
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;
@ -263,66 +295,174 @@ 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) {
var deferred = $.Deferred();
JSZipUtils.getBinaryContent(url, function(err, data) {
let deferred = $.Deferred();
JSZipUtils.getBinaryContent(url, function(err, data) {
if (err) {
// deferred.reject(err); TODO
console.log('Error:', err);
deferred.resolve();
} else {
// TODO - move to utils.js
if (filename.endsWith("TODO-EXTRACT")) {
let oldFilename = filename
let arr = (new Uint8Array(data)).subarray(0, 4);
let header = "";
for(let i = 0; i < arr.length; i++) {
header += arr[i].toString(16);
}
if (header.startsWith("89504e47")) {
filename = filename.replace("TODO-EXTRACT", "png")
} else if (header.startsWith("47494638")) {
filename = filename.replace("TODO-EXTRACT", "gif")
} else if (header.startsWith("ffd8ff")) {
filename = filename.replace("TODO-EXTRACT", "jpg")
} else {
// ERROR
console.log("Error! Unable to extract the image type!");
deferred.resolve();
}
tmpGlobalContent = tmpGlobalContent.replace(oldFilename, filename)
}
extractedImages.push({
filename: filename,
// TODO - must be JSON serializable
data: base64ArrayBuffer(data)
});
deferred.resolve();
}
});
return deferred;
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
var imgsPromises = [];
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);
});
} else if (request.type === 'echo') {
sendResponse({
echo: true
});
return;
}
if (tmpContent.trim() === '') {
sendResponse('');
return;
}
tmpGlobalContent = tmpContent
allImages.forEach(function (tmpImg) {
allImages.forEach((tmpImg) => {
imgsPromises.push(deferredAddZip(tmpImg.originalUrl, tmpImg.filename));
});
$.when.apply($, imgsPromises).done(function() {
var tmpTitle = getPageTitle(document.title);
$.when.apply($, imgsPromises).done(() => {
let tmpTitle = getPageTitle(document.title);
result = {
url: getPageUrl(tmpTitle),
title: tmpTitle,
baseUrl: getCurrentUrl(),
styleFileContent: styleFile,
styleFileName: 'style' + generateRandomNumber() + '.css',
images: extractedImages,
content: tmpContent
content: tmpGlobalContent
};
sendResponse(result);
}).fail(function(e) {
}).fail((e) => {
console.log('Error:', e);
sendResponse(null)
});
return true;

View file

@ -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;
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 656 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 868 B

Before After
Before After

View file

@ -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;
};
};

View file

@ -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;
}

View file

@ -1,7 +1,8 @@
{
"manifest_version": 2,
"name": "Save As eBook",
"version": "1.1",
"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)",
@ -10,7 +11,9 @@
},
"content_scripts": [{
"matches": ["<all_urls>"],
"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"]
@ -25,10 +28,33 @@
"activeTab",
"storage",
"unlimitedStorage",
"tabs",
"notifications",
"http://*/*",
"https://*/*",
"<all_urls>"
]
],
"commands": {
"save-page": {
"suggested_key": {
"default": "Alt+Shift+1"
},
"description": "Save current page as eBook"
},
"save-selection": {
"suggested_key": {
"default": "Alt+Shift+2"
},
"description": "Save current selection as eBook"
},
"add-page": {
"suggested_key": {
"default": "Alt+Shift+3"
},
"description": "Add current page as chapter"
},
"add-selection": {
"suggested_key": {
"default": "Alt+Shift+4"
},
"description": "Add current selection as chapter"
}
}
}

View file

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

View file

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

View file

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

140
web-extension/selector.js Normal file
View file

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

View file

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