├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md └── workflows │ └── tags.yml ├── .gitignore ├── .nojekyll ├── build ├── chrome │ ├── chrome-obsidian-clipper-0.1.0.crx │ ├── chrome-obsidian-clipper-0.1.0.zip │ ├── chrome-obsidian-clipper-0.1.1.crx │ ├── chrome-obsidian-clipper-0.1.1.zip │ ├── chrome-obsidian-clipper-0.1.2.crx │ ├── chrome-obsidian-clipper-0.1.2.zip │ ├── chrome-obsidian-clipper-0.1.3.crx │ ├── chrome-obsidian-clipper-0.1.3.zip │ ├── chrome-obsidian-clipper-0.1.4.crx │ ├── chrome-obsidian-clipper-0.1.4.zip │ ├── chrome-obsidian-clipper-0.2.0.crx │ ├── chrome-obsidian-clipper-0.2.0.zip │ ├── chrome-obsidian-clipper-0.2.1.crx │ ├── chrome-obsidian-clipper-0.2.1.zip │ ├── chrome-obsidian-clipper-0.2.2.crx │ ├── chrome-obsidian-clipper-0.2.2.zip │ ├── chrome-obsidian-clipper-0.2.3.crx │ ├── chrome-obsidian-clipper-0.2.3.zip │ ├── chrome-obsidian-clipper-0.2.4.crx │ ├── chrome-obsidian-clipper-0.2.4.zip │ ├── chrome-obsidian-clipper-0.2.5.crx │ ├── chrome-obsidian-clipper-0.2.5.zip │ ├── chrome-obsidian-clipper-0.2.6.crx │ ├── chrome-obsidian-clipper-0.2.6.zip │ ├── chrome-obsidian-clipper-0.3.0.crx │ ├── chrome-obsidian-clipper-0.3.0.zip │ ├── chrome-obsidian-clipper-0.3.2.crx │ ├── chrome-obsidian-clipper-0.3.2.zip │ ├── chrome-obsidian-clipper-0.3.3.crx │ ├── chrome-obsidian-clipper-0.3.3.zip │ ├── chrome-obsidian-clipper-0.3.4.crx │ ├── chrome-obsidian-clipper-0.3.4.zip │ ├── chrome-obsidian-clipper-0.3.5.crx │ ├── chrome-obsidian-clipper-0.3.5.zip │ ├── chrome-obsidian-clipper-0.3.6.crx │ ├── chrome-obsidian-clipper-0.3.6.zip │ ├── chrome-obsidian-clipper-0.4.0.crx │ ├── chrome-obsidian-clipper-0.4.0.zip │ ├── chrome-obsidian-clipper-0.4.1.crx │ ├── chrome-obsidian-clipper-0.4.1.zip │ ├── chrome-obsidian-clipper-0.5.0.crx │ └── chrome-obsidian-clipper-0.5.0.zip └── firefox │ ├── firefox-obsidian-clipper-0.1.0.zip │ ├── firefox-obsidian-clipper-0.1.1.zip │ ├── firefox-obsidian-clipper-0.1.2.zip │ ├── firefox-obsidian-clipper-0.1.3.zip │ ├── firefox-obsidian-clipper-0.1.4.zip │ ├── firefox-obsidian-clipper-0.2.0.zip │ ├── firefox-obsidian-clipper-0.2.1.zip │ ├── firefox-obsidian-clipper-0.2.2.zip │ ├── firefox-obsidian-clipper-0.2.3.zip │ ├── firefox-obsidian-clipper-0.2.4.zip │ ├── firefox-obsidian-clipper-0.2.5.zip │ ├── firefox-obsidian-clipper-0.3.0.zip │ ├── firefox-obsidian-clipper-0.3.2.zip │ ├── firefox-obsidian-clipper-0.3.3.zip │ ├── firefox-obsidian-clipper-0.3.4.zip │ ├── firefox-obsidian-clipper-0.3.5.zip │ ├── firefox-obsidian-clipper-0.3.6.zip │ ├── firefox-obsidian-clipper-0.4.0.zip │ ├── firefox-obsidian-clipper-0.4.1.zip │ └── firefox-obsidian-clipper-0.5.0.zip ├── changelog.md ├── docs ├── clip-to-new.html ├── clip.html ├── demo.gif ├── index.html └── style.css ├── readme.md ├── release.md ├── release.sh ├── src ├── background.js ├── icons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-48x48.png │ ├── favicon.ico │ └── site.webmanifest ├── index.html ├── lib │ ├── clip.js │ ├── jquery.js │ ├── moment.js │ ├── rangy.js │ ├── turndown.js │ └── webbrowser-polyfill.js ├── manifest.json ├── options.css ├── options.html ├── options.js └── run.js └── web-ext-artifacts └── obsidian_clipper-0.5.0.xpi /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: jplattel -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: jplattel 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | -------------------------------------------------------------------------------- /.github/workflows/tags.yml: -------------------------------------------------------------------------------- 1 | name: Tags 2 | on: 3 | create: 4 | tags: 5 | # tags that are major minor or patch 6 | - '?\d+\.\d+\.\d+' 7 | workflow_dispatch: 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Get the version 14 | id: get_version 15 | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3) 16 | - id: compressExtension 17 | uses: cardinalby/webext-buildtools-pack-extension-dir-action@v1 18 | with: 19 | extensionDir: "src" 20 | zipFilePath: "build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip" 21 | # job to copy previous zip into build/firefox directory 22 | - id: firefox 23 | run: | 24 | mkdir -p build/firefox 25 | cp build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip build/firefox/ 26 | - id: packExtension 27 | uses: cardinalby/webext-buildtools-chrome-crx-action@v2 28 | with: 29 | # zip file made at the packExtensionDir step 30 | zipFilePath: "build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip" 31 | crxFilePath: "build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.crx" 32 | privateKey: ${{ secrets.CHROME_CRX_PRIVATE_KEY }} 33 | # The following is optional if you need update.xml file 34 | # updateXmlPath: 'build/update.xml' 35 | # updateXmlCodebaseUrl: 'http://...' 36 | # commit the new version 37 | - name: Commit the new version 38 | id: commit_new_version 39 | run: | 40 | git config --local user.email "action@github.com" 41 | git config --local user.name "GitHub Action" 42 | git add build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.crx 43 | git add build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip 44 | git add build/firefox/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip 45 | git commit -m "Release chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}" 46 | - name: Create Release 47 | id: create_release 48 | uses: actions/create-release@v1 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 51 | with: 52 | tag_name: ${{ github.ref }} 53 | release_name: Release ${{ github.ref }} 54 | draft: true 55 | prerelease: false 56 | - name: Upload Chrome extension file 57 | id: upload-chrome-crx 58 | uses: actions/upload-release-asset@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | upload_url: ${{ steps.create_release.outputs.upload_url }} 63 | asset_path: build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.crx 64 | asset_name: chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.crx 65 | asset_content_type: application/x-chrome-extension 66 | - name: Upload Chrome zip file 67 | id: upload-chrome-zip 68 | uses: actions/upload-release-asset@v1 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | with: 72 | upload_url: ${{ steps.create_release.outputs.upload_url }} 73 | asset_path: build/chrome/chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip 74 | asset_name: chrome-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip 75 | asset_content_type: application/zip 76 | - name: Upload Firefox zip file 77 | id: upload-firefox-zip 78 | uses: actions/upload-release-asset@v1 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | with: 82 | upload_url: ${{ steps.create_release.outputs.upload_url }} 83 | asset_path: build/firefox/firefox-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip 84 | asset_name: firefox-obsidian-clipper-${{ steps.get_version.outputs.VERSION }}.zip 85 | asset_content_type: application/zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | key.pem -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/.nojekyll -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.0.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.0.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.0.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.1.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.1.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.1.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.2.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.2.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.2.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.3.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.3.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.3.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.4.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.4.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.1.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.1.4.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.0.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.0.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.0.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.1.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.1.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.1.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.2.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.2.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.2.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.3.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.3.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.3.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.4.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.4.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.4.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.5.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.5.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.5.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.6.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.6.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.2.6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.2.6.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.0.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.0.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.0.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.2.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.2.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.2.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.3.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.3.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.3.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.4.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.4.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.4.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.5.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.5.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.5.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.6.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.6.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.3.6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.3.6.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.4.0.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.4.0.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.4.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.4.0.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.4.1.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.4.1.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.4.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.4.1.zip -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.5.0.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.5.0.crx -------------------------------------------------------------------------------- /build/chrome/chrome-obsidian-clipper-0.5.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/chrome/chrome-obsidian-clipper-0.5.0.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.1.0.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.1.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.1.1.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.1.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.1.2.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.1.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.1.3.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.1.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.1.4.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.2.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.2.0.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.2.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.2.1.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.2.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.2.2.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.2.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.2.3.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.2.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.2.4.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.2.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.2.5.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.3.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.3.0.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.3.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.3.2.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.3.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.3.3.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.3.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.3.4.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.3.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.3.5.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.3.6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.3.6.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.4.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.4.0.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.4.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.4.1.zip -------------------------------------------------------------------------------- /build/firefox/firefox-obsidian-clipper-0.5.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/build/firefox/firefox-obsidian-clipper-0.5.0.zip -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.0 4 | 5 | - Quite a rewrite, reworked the clipping/test functionality so it makes use of the same function. 6 | - The settings page now opens when a user installs the plugin. 7 | - Settings are saved before running a test clip (the tab that's used also need to be close manually, to have time for the confirmation dialog about protocol use). 8 | - If you decide to clip not to clip to a new note, it'll try and open the note with the same name and appending with the content of the clip. 9 | 10 | ## 0.4.0 11 | 12 | - Fixed permissions from the Chrome Web Store 13 | - Moving towards manifest.json V3 14 | 15 | ## 0.3.6 16 | 17 | - Fixed [not clipping to folders anymore](https://github.com/jplattel/obsidian-clipper/issues/40) 18 | - Added the link to the [youtube video by Antone](https://www.youtube.com/watch?v=PZnytCMbR-A) to the readme 19 | 20 | ## 0.3.5 21 | 22 | - Fixed [datetime issue (#32)](https://github.com/jplattel/obsidian-clipper/issues/32). 23 | - Fixed [better documentation for new note in folder (#33)](https://github.com/jplattel/obsidian-clipper/issues/33) 24 | 25 | ## 0.3.4 26 | 27 | - You can now use the `{og:image}` placeholder to add an image to the note (This closes [#23](https://github.com/jplattel/obsidian-clipper/issues/23)). 28 | 29 | ## 0.3.3 30 | 31 | - Release through github actions now works and adds the zip and crx files 32 | 33 | ## 0.3.2 34 | 35 | - Hopefully configured now! 36 | 37 | ## 0.3.1 38 | 39 | - Try to automate releases, not working due to a misconfigured github workflow 40 | 41 | ## 0.3.0 42 | 43 | - Absolute URLs for links and images! 44 | - Fixed zettlekasten ID missing zeroes due to date formatting 45 | 46 | ## 0.2.6 47 | 48 | - Added more template things (thanks to [@Mearman](https://github.com/Mearman)) 49 | 50 | ## 0.1.3 51 | 52 | - Added a {zettel} placeholder for a zettelkasten id. 53 | - Allow a user to test the configuration to see if Obsidian opens 54 | - Added a small icon linking back to my personal page 55 | 56 | ## 0.1.2 57 | 58 | - Added a ko-fi image and some additional information 59 | 60 | ## 0.1.1 61 | 62 | - Updated libraries used and pointing to exact versions of a library for review 63 | 64 | ## 0.1.0 65 | 66 | - Initial release for Firefox & Chrome. 67 | 68 | -------------------------------------------------------------------------------- /docs/clip-to-new.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/clip.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/docs/demo.gif -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Obsidian Clipper 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Obsidian Clipper BETA

12 | 13 |
14 | 15 |

An unoffical Obsidian clipper for Chrome

16 | 17 |

18 | This is an unofficial clipper for Obsidian that allows you to easily clip a selection to a note in 19 | Obsidian. Made by Joost Plattel. 20 | This plugin is available as open-source on Github. 21 |

22 | 23 | 26 | 27 |

Screencast

28 | 29 | 30 | 31 |

Features

32 | 33 | 40 | 41 |

FAQ

42 | 43 |
44 |
45 | Why do I need to approve https://jplattel.github.io/obsidian-clipper/clip.html 46 | to open Obsidian? 47 |
48 |
49 | This is the redirect page that opens Obsidian with the right vault & note. You will only need to 50 | approve this once, further clips will use the same page and thus not require additional authorization. 51 |
52 |
How safe is this extension to use?
53 |
54 | I'm not storing or receiving any details about the clippings you do. In case of doubt, find someone 55 | technical that can review the code and figure out if it's safe! 56 |
57 |
Why can't I install this via the normal Chrome plugin store?
58 |
59 | This plugin uses a loophole to open the obsidian vault via a redirect to a page who does the redirect based on parameters. 60 | While this functionality is standard, the Chrome store surely wouldn't allow it. 61 |
62 |
Is there a firefox extension available as well!
63 |
64 | Yup! But it needs some work to install, have a look at the readme. 65 |
66 |
Who made this and why?!
67 |
68 | Well, I'm glad you asked! That would be me, Joost. 69 | As for why? I'd like to tinker around! Curious about more? Feel free to sign up to my monthly newsletter! 70 |
71 |
Want to support further development?
72 |
73 | Feel free to support me through Ko-Fi. 74 |
75 |
76 |
77 | 78 |

by jplattel

79 | 80 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: #202020; 3 | box-sizing: border-box; 4 | font-family: Arial, Helvetica, sans-serif; 5 | } 6 | 7 | main{ 8 | margin: 0 auto; 9 | display: block; 10 | width: 900px; 11 | color: white; 12 | font-size: 20px; 13 | text-align: center; 14 | } 15 | 16 | p.description{ 17 | text-align: left; 18 | color: #999; 19 | line-height: 140%; 20 | } 21 | a { 22 | color: #7f6df2; 23 | text-decoration: none; 24 | } 25 | a:hover { 26 | color: #8875ff; 27 | text-decoration: underline; 28 | } 29 | 30 | .form-group label{ 31 | margin-top: 20px; 32 | text-align: left; 33 | float: left; 34 | margin-bottom: 10px; 35 | } 36 | 37 | .features li{ 38 | text-align: left; 39 | } 40 | 41 | input{ 42 | width:100%; 43 | padding: 15px; 44 | box-sizing: border-box; 45 | border: 0px; 46 | font-size: 18px; 47 | margin-bottom: 20px; 48 | border-radius: 3px; 49 | } 50 | 51 | #screencast{ 52 | border: 10px solid #333; 53 | border-radius: 10px; 54 | box-sizing: border-box; 55 | } 56 | 57 | h1 { 58 | font-size: 70px; 59 | line-height: 86px; 60 | margin-bottom: 24px; 61 | color: #f8f8f8; 62 | font-family: 'Playfair', sans-serif; 63 | font-weight: 800; 64 | } 65 | 66 | button{ 67 | margin-top: 20px; 68 | font-size: 22px; 69 | padding: 15px; 70 | width: 100%; 71 | border: 0px; 72 | border-radius: 3px; 73 | } 74 | 75 | button:hover{ 76 | background-color: #483699; 77 | color: white; 78 | cursor: pointer; 79 | } 80 | 81 | #status{ 82 | padding-top: 20px; 83 | } 84 | 85 | #faq{ 86 | float: left; 87 | text-align: left; 88 | } 89 | 90 | #faq dt{ 91 | 92 | } 93 | #faq dd{ 94 | margin-left: 0px; 95 | margin-top: 10px; 96 | color: #999; 97 | margin-bottom: 30px; 98 | } 99 | 100 | dt code{ 101 | background-color: #111; 102 | padding: 3px; 103 | color: rgb(237, 237, 102); 104 | border-radius: 4px; 105 | } 106 | 107 | .by-jplattel { 108 | font-family:"Helvetica Neue",sans-serif; 109 | right:0; 110 | bottom:0; 111 | position:fixed; 112 | z-index:100; 113 | border-top-left-radius: 5px; 114 | padding: 6px; 115 | border-top:1px solid #efefef; 116 | border-left:1px solid #efefef; 117 | background:#fff; 118 | color:#6f6f6f; 119 | text-decoration:none; 120 | } 121 | .by-jplattel:hover { 122 | background:#fff; 123 | color:#7f6df2; 124 | } 125 | .by-jplattel img { 126 | border-radius:100%; 127 | width:22px; 128 | vertical-align:middle; 129 | } 130 | .by-jplattel p { 131 | margin:0; 132 | vertical-align:middle; 133 | display:inline; 134 | margin-left:7px; 135 | font-weight:500; 136 | font-size:14px; 137 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > 🚨 This repository is now archived due to the official [Obsidian Web Clipper](https://obsidian.md/clipper) being available. 2 | 3 | # Obsidian Chrome Clipper 4 | 5 | ## Screencast 6 | 7 | ![Screencast](docs/demo.gif) 8 | 9 | This is an unofficial Chrome/Firefox Extension to quickly clip a selection on a webpage to Obsidian. 10 | 11 | ## Installing (Chrome) 12 | 13 | ### From the Chrome Webstore 14 | 15 | https://chrome.google.com/webstore/detail/obsidian-clipper/mphkdfmipddgfobjhphabphmpdckgfhb 16 | 17 | ### Manually 18 | 19 | 1. Download/clone this repository 20 | 2. Navigate to the [Chrome Extension](chrome://extensions) and enabled developer mode (top right of your window) 21 | 3. Unzip the extension at the `build/chrome` folder. Or straight from the source with the `src` folder. 22 | 3. Load unpacked extension and navigate to the folder you just unzipped or `src` of this repository you just downloaded or cloned. 23 | 4. Chrome will now build the extension and you can use the extension menu to pin in to the user interface. 24 | 5. You're now ready to configure the extension, see the steps below in Usage & Settings: 25 | 26 | ## Installing (Firefox) 27 | 28 | 1. Download/clone this repository 29 | 2. Allow unsigned extensions, see [https://www.thewindowsclub.com/allow-unsigned-extensions-installed-firefox](https://www.thewindowsclub.com/allow-unsigned-extensions-installed-firefox). 30 | 3. Navigate to the [Firefox Addons](about:addons) 31 | 4. Add the zipfile from `build/firefox` through the cog menu. 32 | 5. You're now ready to configure the extension, see the steps below in Usage & Settings: 33 | 34 | This extenion is only tested on Chrome/Firefox on OS X. I've heard people got it working on Unix with the flatpak Obsidian app. 35 | 36 | ## Usage & Settings 37 | 38 | [Antone Heyward made an awesome video on how to use the clipper!](https://www.youtube.com/watch?v=PZnytCMbR-A) be sure to have a look! 39 | 40 | 1. Right-click on the extension icon in the menu, and click on options. 41 | 2. A webpage should open where you can configure the options for this extension 42 | 3. You can configure the following: 43 | - `vault`: Allows you to specify which vault to open 44 | - `note`: The name of the note you want to append to 45 | 4. You can specify the clipping template using placeholders like `{clip}`, `{date}` and more like `{month}` or `{year}`. 46 | 5. Decide if you want a markdown clip (HTML is converted to markdown and added to your clipboard) or plain text. 47 | 6. You cen test if Obsidian opens with the right note with the 'Test Configuration' button. 48 | 49 | Once configured, you're now good to go, using it only takes two steps: 50 | 51 | 1. Make a selection on a page and click the icon of the extension _(or use a shortcut key!)_. 52 | 2. Obsidian will try to create or append to the specified note within the vault. 53 | 54 | ## Troubleshooting 55 | 56 | - I click the clipper icon and nothing is being clipped 57 | - _You need a selection for the clipper to work, it doesn't clip entire pages_ 58 | - A tab opens and closes shortly therafter and Obsidian doens't open 59 | - Double check you [configuration](chrome-extension://ljdpoilhdidlcanedjhionbakimbdfjk/options.html) and test it with the button at the bottom of the page. 60 | - _Manually navigate to `obsidian://` first to see if it's a permission issue_ 61 | - _Then try opening: `https://jplattel.github.io/obsidian-clipper/clip.html`_ 62 | - _Try specifying the vault name: `https://jplattel.github.io/obsidian-clipper/clip.html?vault=`_ 63 | - _If nothing works, please make a note at issue [#14](https://github.com/jplattel/obsidian-clipper/issues/14)_ 64 | 65 | 66 | ## Building further upon this extension 67 | 68 | Since Chrome allows you to set a custom shortcut to activate an extenion it should be pretty easy to chain it together with Keyboard Meastro or any other automation technology to both clip & paste the results. 69 | 70 | ## Roadmap 71 | 72 | - ~~Support Firefox~~ 73 | - ~~Allow a user to create a clipping template~~ 74 | - ~~Markdown clipping with [Turndown](https://github.com/domchristie/turndown)~~ 75 | - ~~Make a option that let's you prepend a Zettelkasten id to the clipping itself? (through the template perhaps?)~~ 76 | - ~~Date formatting with Moment~~ 77 | - ~~Once the url-scheme of Obsidian allows the creation of a new note, clip to a new note.~~ 78 | - In the long term future, maybe even offer the possiblity to search through your notes and append it? 79 | - If you have any ideas, please create an issue with the `feature` label on it, thanks! 😁 80 | 81 | ## Technical explanation 82 | 83 | This clipper is made possible with a work-around, since Chrome Extensions are forbidden to open custom url-schemes directly. The way around this issue is a custom html page that is hosted on Github-pages and also included in the repository: `docs/clip.html`. This little file contains javascript that pulls the data like vault & note out of the url params. With this data, it reconstructs the obsidian url and opens the right note! 84 | 85 | ## Support 86 | 87 | Want to support me? You can do so via Ko-Fi: 88 | 89 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R62KRKX) -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Follow the steps below to deploy a new release 4 | 5 | 1. Increase version number in manifest.json 6 | 2. Update the changelog in `changelog.md` 7 | 3. Test in Chrome & Firefox 8 | 4. Run `release.sh` 9 | 5. Approve the draft release in [Github](https://github.com/jplattel/obsidian-clipper/releases) 10 | 6. Add update to [Chrome developers console](https://chrome.google.com/webstore/devconsole/) 11 | 7. Add update to [Firefox Addons console](https://addons.mozilla.org/en-US/developers/addon/obsidian-clipper/versions) 12 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the version 4 | VERSION=$(jq -r .version src/manifest.json) 5 | echo "Version: ${VERSION}" 6 | 7 | echo "Building for Chrome" 8 | 9 | # Zip for deployment 10 | cd src 11 | zip -r ../build/chrome/chrome-obsidian-clipper-${VERSION}.zip . 12 | cd ../ 13 | 14 | # Build packed extension 15 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --pack-extension=./src --pack-extension-key=./key.pem 16 | mv src.crx build/chrome/chrome-obsidian-clipper-${VERSION}.crx 17 | 18 | echo "Building for Firefox" 19 | 20 | # Web-ext builds for firefox 21 | cd src 22 | web-ext build # Build for firefox 23 | mv web-ext-artifacts/obsidian_clipper-${VERSION}.zip ../build/firefox/firefox-obsidian-clipper-${VERSION}.zip 24 | rm -rf web-ext-artifacts # Remove the build folder 25 | cd ../ 26 | 27 | # Push to github 28 | git add build 29 | git commit -m "Releasing ${VERSION}" 30 | git tag ${VERSION} 31 | git push origin main 32 | git push --tags 33 | 34 | open https://github.com/jplattel/obsidian-clipper/releases/new -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // This runs in the background.. waiting for the icon to be clicked.. 2 | // It then loads the libraries required and runs the clip.js script. 3 | // clip.js copies the content and adds it to your clipboard 4 | // Then this script creates a new tab with a redirect that opens the 5 | // Obsidian vault with the specified note. 6 | // Load files necessary for clipping 7 | chrome.action.onClicked.addListener(async function (tab) { 8 | chrome.scripting.executeScript({ 9 | target: {tabId: tab.id}, 10 | files: [ 11 | "lib/webbrowser-polyfill.js", 12 | "lib/jquery.js", 13 | "lib/rangy.js", 14 | "lib/moment.js", 15 | "lib/turndown.js", 16 | ] 17 | }, () => { 18 | chrome.scripting.executeScript({ 19 | target: {tabId: tab.id}, 20 | files: ['run.js'] 21 | }) 22 | }) 23 | }); 24 | 25 | chrome.runtime.onMessage.addListener(async function listener(result) { 26 | const clipAsNewNote = result.clipAsNewNote 27 | const vault = result.vault 28 | const noteName = result.noteName 29 | const note = encodeURIComponent(result.note) 30 | 31 | // const baseURL = 'http://localhost:8080'; // Used for testing... 32 | const baseURL = 'https://jplattel.github.io/obsidian-clipper' 33 | 34 | let redirectUrl; 35 | // Redirect to page (which opens obsidian). 36 | if (clipAsNewNote) { 37 | redirectUrl = `${baseURL}/clip-to-new.html?vault=${encodeURIComponent(vault)}¬e=${encodeURIComponent(noteName)}&content=${encodeURIComponent(note)}` 38 | } else { 39 | redirectUrl = `${baseURL}/clip.html?vault=${encodeURIComponent(vault)}¬e=${encodeURIComponent(noteName)}&content=${encodeURIComponent(note)}` 40 | } 41 | 42 | // Open a new tab for clipping through the protocol, since we cannot go from the extension to this.. 43 | if (result.testing) { 44 | chrome.tabs.create({ url: redirectUrl , active: true},function(obsidianTab){ 45 | // Since we're testing, we are not closing the tag... 46 | }); 47 | } else { 48 | chrome.tabs.create({ url: redirectUrl , active: true},function(obsidianTab){ 49 | setTimeout(function() { chrome.tabs.remove(obsidianTab.id) }, 500); 50 | }); 51 | } 52 | }); 53 | 54 | // On install open the options page: 55 | chrome.runtime.onInstalled.addListener(function (object) { 56 | if (object.reason === chrome.runtime.OnInstalledReason.INSTALL) { 57 | chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }, function (tab) {}); 58 | } 59 | }); -------------------------------------------------------------------------------- /src/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/favicon-128x128.png -------------------------------------------------------------------------------- /src/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/icons/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/favicon-48x48.png -------------------------------------------------------------------------------- /src/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/src/icons/favicon.ico -------------------------------------------------------------------------------- /src/icons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/clip.js: -------------------------------------------------------------------------------- 1 | 2 | export const createTest = async () => create(true); 3 | export const create = async (testing=false) => { 4 | console.log("starting clipper...") 5 | let title = document.title.replace(/\//g, '') 6 | let url = window.location.href 7 | let defaultNoteFormat = `> {clip} 8 | 9 | // Clipped from [{title}]({url}) at {date}.` 10 | 11 | let defaultClippingOptions = { 12 | obsidianVaultName: 'Obsidian', 13 | selectAsMarkdown: false, 14 | obsidianNoteFormat: defaultNoteFormat, 15 | obsidianNoteName: "Chrome Clippings", 16 | clipAsNewNote: true, 17 | dateFormat: "YYYY-MM-DD", 18 | datetimeFormat: "YYYY-MM-DD HH:mm:ss", 19 | timeFormat: "HH:mm:ss", 20 | } 21 | 22 | async function getFromStorage(key) { 23 | return new Promise((resolve, reject) => { 24 | chrome.storage.sync.get(key, resolve); 25 | }) 26 | } 27 | 28 | let clippingOptions = await getFromStorage(defaultClippingOptions) 29 | 30 | let note = clippingOptions.obsidianNoteFormat 31 | 32 | let date = moment().format(clippingOptions.dateFormat) 33 | let datetime = moment().format(clippingOptions.datetimeFormat) 34 | let time = moment().format(clippingOptions.timeFormat) 35 | let day = moment().format("DD") 36 | let month = moment().format("MM") 37 | let year = moment().format("YYYY") 38 | let zettel = moment().format("YYYYMMDDHHmmss") 39 | 40 | let selection = ''; 41 | let link = ''; 42 | let fullLink = ''; 43 | 44 | // If we're testing.. 45 | if (testing) { 46 | selection = "This is a test clipping from the Obsidian Clipper" 47 | } else if (clippingOptions.selectAsMarkdown) { 48 | // Get the HTML selected 49 | let sel = rangy.getSelection().toHtml(); 50 | 51 | // Turndown to markdown 52 | let turndown = new TurndownService() 53 | 54 | // This rule constructs url to be absolute URLs for links & images 55 | let turndownWithAbsoluteURLs = turndown.addRule('baseUrl', { 56 | filter: ['a', 'img'], 57 | replacement: function (content, el, options) { 58 | if (el.nodeName === 'IMG') { 59 | link = el.getAttributeNode('src').value; 60 | fullLink = new URL(link, url) 61 | return `![${content}](${fullLink.href})` 62 | } else if (el.nodeName === 'A') { 63 | link = el.getAttributeNode('href').value; 64 | fullLink = new URL(link, url) 65 | return `[${content}](${fullLink.href})` 66 | } 67 | } 68 | }) 69 | 70 | selection = turndownWithAbsoluteURLs.turndown(sel) 71 | // Otherwise plaintext 72 | } else { 73 | selection = window.getSelection() 74 | } 75 | 76 | // Replace the placeholders: (with regex so multiples are replaced as well..) 77 | note = note.replace(/{clip}/g, selection) 78 | note = note.replace(/{date}/g, date) 79 | note = note.replace(/{datetime}/g, datetime) 80 | note = note.replace(/{time}/g, time) 81 | note = note.replace(/{day}/g, day) 82 | note = note.replace(/{month}/g, month) 83 | note = note.replace(/{year}/g, year) 84 | note = note.replace(/{url}/g, url) 85 | note = note.replace(/{title}/g, title) 86 | note = note.replace(/{zettel}/g, zettel) 87 | 88 | // Clip the og:image if it exists 89 | if (document.querySelector('meta[property="og:image"]')) { 90 | let image = document.querySelector('meta[property="og:image"]').content 91 | note = note.replace(/{og:image}/g, `![](${image})`) // image only works in the content of the note 92 | } else { 93 | note = note.replace(/{og:image}/g, "") 94 | } 95 | 96 | // replace the placeholder in the title, taking into account invalid note names and removing special 97 | // chars like \/:#^\[\]|? that result in no note being created... 98 | let noteName = clippingOptions.obsidianNoteName 99 | noteName = noteName.replace(/{date}/g, date.replace(/[*'\/":#^\[\]|?<>]/g, '')) 100 | noteName = noteName.replace(/{day}/g, day.replace(/[*'\/":#^\[\]|?<>]/g, '')) 101 | noteName = noteName.replace(/{month}/g, month.replace(/[*'\/":#^\[\]|?<>]/g, '')) 102 | noteName = noteName.replace(/{year}/g, year.replace(/[*'\/":#^\[\]|?<>]/g, '')) 103 | noteName = noteName.replace(/{url}/g, url.replace(/[*'\/":#^\[\]|?<>]/g, '')) 104 | noteName = noteName.replace(/{title}/g, title.replace(/[*'\/":#^\[\]|?<>]/g, '')) 105 | noteName = noteName.replace(/{zettel}/g, zettel.replace(/[*'\/":#^\[\]|?<>]/g, '')) 106 | noteName = noteName.replace(/{datetime}/g, datetime.replace(/[*'\/":#^\[\]|?<>]/g, '')) 107 | noteName = noteName.replace(/{time}/g, time.replace(/[*'\/":#^\[\]|?<>]/g, '')) 108 | 109 | // Send a clipping messsage 110 | let data = { 111 | 'testing': testing, 112 | 'noteName': noteName, 113 | 'note': note, 114 | 'vault': clippingOptions.obsidianVaultName, 115 | 'new': clippingOptions.clipAsNewNote 116 | } 117 | console.log("sending data...", data) 118 | chrome.runtime.sendMessage(data) 119 | } 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/lib/moment.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function f(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function m(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function l(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(m(e,t))return;return 1}function r(e){return void 0===e}function h(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function a(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function d(e,t){for(var n=[],s=0;s>>0,s=0;sFe(e)?(r=e+1,a-Fe(e)):(r=e,a);return{year:r,dayOfYear:o}}function Ae(e,t,n){var s,i,r=Ge(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+je(i=e.year()-1,t,n):a>je(e.year(),t,n)?(s=a-je(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function je(e,t,n){var s=Ge(e,t,n),i=Ge(e+1,t,n);return(Fe(e)-s+i)/7}C("w",["ww",2],"wo","week"),C("W",["WW",2],"Wo","isoWeek"),L("week","w"),L("isoWeek","W"),A("week",5),A("isoWeek",5),ce("w",te),ce("ww",te,Q),ce("W",te),ce("WW",te,Q),ge(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=Z(e)});function Ie(e,t){return e.slice(t,7).concat(e.slice(0,t))}C("d",0,"do","day"),C("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),C("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),C("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),C("e",0,0,"weekday"),C("E",0,0,"isoWeekday"),L("day","d"),L("weekday","e"),L("isoWeekday","E"),A("day",11),A("weekday",11),A("isoWeekday",11),ce("d",te),ce("e",te),ce("E",te),ce("dd",function(e,t){return t.weekdaysMinRegex(e)}),ce("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ce("dddd",function(e,t){return t.weekdaysRegex(e)}),ge(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:y(n).invalidWeekday=e}),ge(["d","e","E"],function(e,t,n,s){t[s]=Z(e)});var Ze="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),$e="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),qe=de,Be=de,Je=de;function Qe(){function e(e,t){return t.length-e.length}for(var t,n,s,i,r=[],a=[],o=[],u=[],l=0;l<7;l++)t=_([2e3,1]).day(l),n=me(this.weekdaysMin(t,"")),s=me(this.weekdaysShort(t,"")),i=me(this.weekdays(t,"")),r.push(n),a.push(s),o.push(i),u.push(n),u.push(s),u.push(i);r.sort(e),a.sort(e),o.sort(e),u.sort(e),this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function Xe(){return this.hours()%12||12}function Ke(e,t){C(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function et(e,t){return t._meridiemParse}C("H",["HH",2],0,"hour"),C("h",["hh",2],0,Xe),C("k",["kk",2],0,function(){return this.hours()||24}),C("hmm",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)}),C("hmmss",0,0,function(){return""+Xe.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),C("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),C("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ke("a",!0),Ke("A",!1),L("hour","h"),A("hour",13),ce("a",et),ce("A",et),ce("H",te),ce("h",te),ce("k",te),ce("HH",te,Q),ce("hh",te,Q),ce("kk",te,Q),ce("hmm",ne),ce("hmmss",se),ce("Hmm",ne),ce("Hmmss",se),ye(["H","HH"],Me),ye(["k","kk"],function(e,t,n){var s=Z(e);t[Me]=24===s?0:s}),ye(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ye(["h","hh"],function(e,t,n){t[Me]=Z(e),y(n).bigHour=!0}),ye("hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s)),y(n).bigHour=!0}),ye("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i)),y(n).bigHour=!0}),ye("Hmm",function(e,t,n){var s=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s))}),ye("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[Me]=Z(e.substr(0,s)),t[De]=Z(e.substr(s,2)),t[Se]=Z(e.substr(i))});var tt=z("Hours",!0);var nt,st={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Te,monthsShort:Ne,week:{dow:0,doy:6},weekdays:Ze,weekdaysMin:$e,weekdaysShort:ze,meridiemParse:/[ap]\.?m?\.?/i},it={},rt={};function at(e){return e?e.toLowerCase().replace("_","-"):e}function ot(e){for(var t,n,s,i,r=0;r=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s=t-1)break;t--}r++}return nt}function ut(t){var e;if(void 0===it[t]&&"undefined"!=typeof module&&module&&module.exports)try{e=nt._abbr,require("./locale/"+t),lt(e)}catch(e){it[t]=null}return it[t]}function lt(e,t){var n;return e&&((n=r(t)?dt(e):ht(e,t))?nt=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),nt._abbr}function ht(e,t){if(null===t)return delete it[e],null;var n,s=st;if(t.abbr=e,null!=it[e])Y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=it[e]._config;else if(null!=t.parentLocale)if(null!=it[t.parentLocale])s=it[t.parentLocale]._config;else{if(null==(n=ut(t.parentLocale)))return rt[t.parentLocale]||(rt[t.parentLocale]=[]),rt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return it[e]=new x(b(s,t)),rt[e]&&rt[e].forEach(function(e){ht(e.name,e.config)}),lt(e),it[e]}function dt(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return nt;if(!o(e)){if(t=ut(e))return t;e=[e]}return ot(e)}function ct(e){var t,n=e._a;return n&&-2===y(e).overflow&&(t=n[ve]<0||11xe(n[pe],n[ve])?ke:n[Me]<0||24je(n,r,a)?y(e)._overflowWeeks=!0:null!=u?y(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[pe]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=St(e._a[pe],s[pe]),(e._dayOfYear>Fe(r)||0===e._dayOfYear)&&(y(e)._overflowDayOfYear=!0),n=Ve(r,0,e._dayOfYear),e._a[ve]=n.getUTCMonth(),e._a[ke]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=u[t]=s[t];for(;t<7;t++)e._a[t]=u[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[Me]&&0===e._a[De]&&0===e._a[Se]&&0===e._a[Ye]&&(e._nextDay=!0,e._a[Me]=0),e._d=(e._useUTC?Ve:function(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}).apply(null,u),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[Me]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(y(e).weekdayMismatch=!0)}}function Ot(e){if(e._f!==f.ISO_8601)if(e._f!==f.RFC_2822){e._a=[],y(e).empty=!0;for(var t,n,s,i,r,a,o,u=""+e._i,l=u.length,h=0,d=H(e._f,e._locale).match(N)||[],c=0;cn.valueOf():n.valueOf()"}),pn.toJSON=function(){return this.isValid()?this.toISOString():null},pn.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},pn.unix=function(){return Math.floor(this.valueOf()/1e3)},pn.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},pn.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},pn.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;nthis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},pn.isLocal=function(){return!!this.isValid()&&!this._isUTC},pn.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},pn.isUtc=At,pn.isUTC=At,pn.zoneAbbr=function(){return this._isUTC?"UTC":""},pn.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},pn.dates=n("dates accessor is deprecated. Use date instead.",fn),pn.months=n("months accessor is deprecated. Use month instead",Ue),pn.years=n("years accessor is deprecated. Use year instead",Le),pn.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),pn.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!r(this._isDSTShifted))return this._isDSTShifted;var e,t={};return v(t,this),(t=bt(t))._a?(e=(t._isUTC?_:Tt)(t._a),this._isDSTShifted=this.isValid()&&0= 0 60 | } 61 | 62 | function has (node, tagNames) { 63 | return ( 64 | node.getElementsByTagName && 65 | tagNames.some(function (tagName) { 66 | return node.getElementsByTagName(tagName).length 67 | }) 68 | ) 69 | } 70 | 71 | var rules = {}; 72 | 73 | rules.paragraph = { 74 | filter: 'p', 75 | 76 | replacement: function (content) { 77 | return '\n\n' + content + '\n\n' 78 | } 79 | }; 80 | 81 | rules.lineBreak = { 82 | filter: 'br', 83 | 84 | replacement: function (content, node, options) { 85 | return options.br + '\n' 86 | } 87 | }; 88 | 89 | rules.heading = { 90 | filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], 91 | 92 | replacement: function (content, node, options) { 93 | var hLevel = Number(node.nodeName.charAt(1)); 94 | 95 | if (options.headingStyle === 'setext' && hLevel < 3) { 96 | var underline = repeat((hLevel === 1 ? '=' : '-'), content.length); 97 | return ( 98 | '\n\n' + content + '\n' + underline + '\n\n' 99 | ) 100 | } else { 101 | return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n' 102 | } 103 | } 104 | }; 105 | 106 | rules.blockquote = { 107 | filter: 'blockquote', 108 | 109 | replacement: function (content) { 110 | content = content.replace(/^\n+|\n+$/g, ''); 111 | content = content.replace(/^/gm, '> '); 112 | return '\n\n' + content + '\n\n' 113 | } 114 | }; 115 | 116 | rules.list = { 117 | filter: ['ul', 'ol'], 118 | 119 | replacement: function (content, node) { 120 | var parent = node.parentNode; 121 | if (parent.nodeName === 'LI' && parent.lastElementChild === node) { 122 | return '\n' + content 123 | } else { 124 | return '\n\n' + content + '\n\n' 125 | } 126 | } 127 | }; 128 | 129 | rules.listItem = { 130 | filter: 'li', 131 | 132 | replacement: function (content, node, options) { 133 | content = content 134 | .replace(/^\n+/, '') // remove leading newlines 135 | .replace(/\n+$/, '\n') // replace trailing newlines with just a single one 136 | .replace(/\n/gm, '\n '); // indent 137 | var prefix = options.bulletListMarker + ' '; 138 | var parent = node.parentNode; 139 | if (parent.nodeName === 'OL') { 140 | var start = parent.getAttribute('start'); 141 | var index = Array.prototype.indexOf.call(parent.children, node); 142 | prefix = (start ? Number(start) + index : index + 1) + '. '; 143 | } 144 | return ( 145 | prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') 146 | ) 147 | } 148 | }; 149 | 150 | rules.indentedCodeBlock = { 151 | filter: function (node, options) { 152 | return ( 153 | options.codeBlockStyle === 'indented' && 154 | node.nodeName === 'PRE' && 155 | node.firstChild && 156 | node.firstChild.nodeName === 'CODE' 157 | ) 158 | }, 159 | 160 | replacement: function (content, node, options) { 161 | return ( 162 | '\n\n ' + 163 | node.firstChild.textContent.replace(/\n/g, '\n ') + 164 | '\n\n' 165 | ) 166 | } 167 | }; 168 | 169 | rules.fencedCodeBlock = { 170 | filter: function (node, options) { 171 | return ( 172 | options.codeBlockStyle === 'fenced' && 173 | node.nodeName === 'PRE' && 174 | node.firstChild && 175 | node.firstChild.nodeName === 'CODE' 176 | ) 177 | }, 178 | 179 | replacement: function (content, node, options) { 180 | var className = node.firstChild.getAttribute('class') || ''; 181 | var language = (className.match(/language-(\S+)/) || [null, ''])[1]; 182 | var code = node.firstChild.textContent; 183 | 184 | var fenceChar = options.fence.charAt(0); 185 | var fenceSize = 3; 186 | var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm'); 187 | 188 | var match; 189 | while ((match = fenceInCodeRegex.exec(code))) { 190 | if (match[0].length >= fenceSize) { 191 | fenceSize = match[0].length + 1; 192 | } 193 | } 194 | 195 | var fence = repeat(fenceChar, fenceSize); 196 | 197 | return ( 198 | '\n\n' + fence + language + '\n' + 199 | code.replace(/\n$/, '') + 200 | '\n' + fence + '\n\n' 201 | ) 202 | } 203 | }; 204 | 205 | rules.horizontalRule = { 206 | filter: 'hr', 207 | 208 | replacement: function (content, node, options) { 209 | return '\n\n' + options.hr + '\n\n' 210 | } 211 | }; 212 | 213 | rules.inlineLink = { 214 | filter: function (node, options) { 215 | return ( 216 | options.linkStyle === 'inlined' && 217 | node.nodeName === 'A' && 218 | node.getAttribute('href') 219 | ) 220 | }, 221 | 222 | replacement: function (content, node) { 223 | var href = node.getAttribute('href'); 224 | var title = cleanAttribute(node.getAttribute('title')); 225 | if (title) title = ' "' + title + '"'; 226 | return '[' + content + '](' + href + title + ')' 227 | } 228 | }; 229 | 230 | rules.referenceLink = { 231 | filter: function (node, options) { 232 | return ( 233 | options.linkStyle === 'referenced' && 234 | node.nodeName === 'A' && 235 | node.getAttribute('href') 236 | ) 237 | }, 238 | 239 | replacement: function (content, node, options) { 240 | var href = node.getAttribute('href'); 241 | var title = cleanAttribute(node.getAttribute('title')); 242 | if (title) title = ' "' + title + '"'; 243 | var replacement; 244 | var reference; 245 | 246 | switch (options.linkReferenceStyle) { 247 | case 'collapsed': 248 | replacement = '[' + content + '][]'; 249 | reference = '[' + content + ']: ' + href + title; 250 | break 251 | case 'shortcut': 252 | replacement = '[' + content + ']'; 253 | reference = '[' + content + ']: ' + href + title; 254 | break 255 | default: 256 | var id = this.references.length + 1; 257 | replacement = '[' + content + '][' + id + ']'; 258 | reference = '[' + id + ']: ' + href + title; 259 | } 260 | 261 | this.references.push(reference); 262 | return replacement 263 | }, 264 | 265 | references: [], 266 | 267 | append: function (options) { 268 | var references = ''; 269 | if (this.references.length) { 270 | references = '\n\n' + this.references.join('\n') + '\n\n'; 271 | this.references = []; // Reset references 272 | } 273 | return references 274 | } 275 | }; 276 | 277 | rules.emphasis = { 278 | filter: ['em', 'i'], 279 | 280 | replacement: function (content, node, options) { 281 | if (!content.trim()) return '' 282 | return options.emDelimiter + content + options.emDelimiter 283 | } 284 | }; 285 | 286 | rules.strong = { 287 | filter: ['strong', 'b'], 288 | 289 | replacement: function (content, node, options) { 290 | if (!content.trim()) return '' 291 | return options.strongDelimiter + content + options.strongDelimiter 292 | } 293 | }; 294 | 295 | rules.code = { 296 | filter: function (node) { 297 | var hasSiblings = node.previousSibling || node.nextSibling; 298 | var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings; 299 | 300 | return node.nodeName === 'CODE' && !isCodeBlock 301 | }, 302 | 303 | replacement: function (content) { 304 | if (!content.trim()) return '' 305 | 306 | var delimiter = '`'; 307 | var leadingSpace = ''; 308 | var trailingSpace = ''; 309 | var matches = content.match(/`+/gm); 310 | if (matches) { 311 | if (/^`/.test(content)) leadingSpace = ' '; 312 | if (/`$/.test(content)) trailingSpace = ' '; 313 | while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`'; 314 | } 315 | 316 | return delimiter + leadingSpace + content + trailingSpace + delimiter 317 | } 318 | }; 319 | 320 | rules.image = { 321 | filter: 'img', 322 | 323 | replacement: function (content, node) { 324 | var alt = cleanAttribute(node.getAttribute('alt')); 325 | var src = node.getAttribute('src') || ''; 326 | var title = cleanAttribute(node.getAttribute('title')); 327 | var titlePart = title ? ' "' + title + '"' : ''; 328 | return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '' 329 | } 330 | }; 331 | 332 | function cleanAttribute (attribute) { 333 | return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '' 334 | } 335 | 336 | /** 337 | * Manages a collection of rules used to convert HTML to Markdown 338 | */ 339 | 340 | function Rules (options) { 341 | this.options = options; 342 | this._keep = []; 343 | this._remove = []; 344 | 345 | this.blankRule = { 346 | replacement: options.blankReplacement 347 | }; 348 | 349 | this.keepReplacement = options.keepReplacement; 350 | 351 | this.defaultRule = { 352 | replacement: options.defaultReplacement 353 | }; 354 | 355 | this.array = []; 356 | for (var key in options.rules) this.array.push(options.rules[key]); 357 | } 358 | 359 | Rules.prototype = { 360 | add: function (key, rule) { 361 | this.array.unshift(rule); 362 | }, 363 | 364 | keep: function (filter) { 365 | this._keep.unshift({ 366 | filter: filter, 367 | replacement: this.keepReplacement 368 | }); 369 | }, 370 | 371 | remove: function (filter) { 372 | this._remove.unshift({ 373 | filter: filter, 374 | replacement: function () { 375 | return '' 376 | } 377 | }); 378 | }, 379 | 380 | forNode: function (node) { 381 | if (node.isBlank) return this.blankRule 382 | var rule; 383 | 384 | if ((rule = findRule(this.array, node, this.options))) return rule 385 | if ((rule = findRule(this._keep, node, this.options))) return rule 386 | if ((rule = findRule(this._remove, node, this.options))) return rule 387 | 388 | return this.defaultRule 389 | }, 390 | 391 | forEach: function (fn) { 392 | for (var i = 0; i < this.array.length; i++) fn(this.array[i], i); 393 | } 394 | }; 395 | 396 | function findRule (rules, node, options) { 397 | for (var i = 0; i < rules.length; i++) { 398 | var rule = rules[i]; 399 | if (filterValue(rule, node, options)) return rule 400 | } 401 | return void 0 402 | } 403 | 404 | function filterValue (rule, node, options) { 405 | var filter = rule.filter; 406 | if (typeof filter === 'string') { 407 | if (filter === node.nodeName.toLowerCase()) return true 408 | } else if (Array.isArray(filter)) { 409 | if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true 410 | } else if (typeof filter === 'function') { 411 | if (filter.call(rule, node, options)) return true 412 | } else { 413 | throw new TypeError('`filter` needs to be a string, array, or function') 414 | } 415 | } 416 | 417 | /** 418 | * The collapseWhitespace function is adapted from collapse-whitespace 419 | * by Luc Thevenard. 420 | * 421 | * The MIT License (MIT) 422 | * 423 | * Copyright (c) 2014 Luc Thevenard 424 | * 425 | * Permission is hereby granted, free of charge, to any person obtaining a copy 426 | * of this software and associated documentation files (the "Software"), to deal 427 | * in the Software without restriction, including without limitation the rights 428 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 429 | * copies of the Software, and to permit persons to whom the Software is 430 | * furnished to do so, subject to the following conditions: 431 | * 432 | * The above copyright notice and this permission notice shall be included in 433 | * all copies or substantial portions of the Software. 434 | * 435 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 436 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 437 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 438 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 439 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 440 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 441 | * THE SOFTWARE. 442 | */ 443 | 444 | /** 445 | * collapseWhitespace(options) removes extraneous whitespace from an the given element. 446 | * 447 | * @param {Object} options 448 | */ 449 | function collapseWhitespace (options) { 450 | var element = options.element; 451 | var isBlock = options.isBlock; 452 | var isVoid = options.isVoid; 453 | var isPre = options.isPre || function (node) { 454 | return node.nodeName === 'PRE' 455 | }; 456 | 457 | if (!element.firstChild || isPre(element)) return 458 | 459 | var prevText = null; 460 | var prevVoid = false; 461 | 462 | var prev = null; 463 | var node = next(prev, element, isPre); 464 | 465 | while (node !== element) { 466 | if (node.nodeType === 3 || node.nodeType === 4) { // Node.TEXT_NODE or Node.CDATA_SECTION_NODE 467 | var text = node.data.replace(/[ \r\n\t]+/g, ' '); 468 | 469 | if ((!prevText || / $/.test(prevText.data)) && 470 | !prevVoid && text[0] === ' ') { 471 | text = text.substr(1); 472 | } 473 | 474 | // `text` might be empty at this point. 475 | if (!text) { 476 | node = remove(node); 477 | continue 478 | } 479 | 480 | node.data = text; 481 | 482 | prevText = node; 483 | } else if (node.nodeType === 1) { // Node.ELEMENT_NODE 484 | if (isBlock(node) || node.nodeName === 'BR') { 485 | if (prevText) { 486 | prevText.data = prevText.data.replace(/ $/, ''); 487 | } 488 | 489 | prevText = null; 490 | prevVoid = false; 491 | } else if (isVoid(node)) { 492 | // Avoid trimming space around non-block, non-BR void elements. 493 | prevText = null; 494 | prevVoid = true; 495 | } 496 | } else { 497 | node = remove(node); 498 | continue 499 | } 500 | 501 | var nextNode = next(prev, node, isPre); 502 | prev = node; 503 | node = nextNode; 504 | } 505 | 506 | if (prevText) { 507 | prevText.data = prevText.data.replace(/ $/, ''); 508 | if (!prevText.data) { 509 | remove(prevText); 510 | } 511 | } 512 | } 513 | 514 | /** 515 | * remove(node) removes the given node from the DOM and returns the 516 | * next node in the sequence. 517 | * 518 | * @param {Node} node 519 | * @return {Node} node 520 | */ 521 | function remove (node) { 522 | var next = node.nextSibling || node.parentNode; 523 | 524 | node.parentNode.removeChild(node); 525 | 526 | return next 527 | } 528 | 529 | /** 530 | * next(prev, current, isPre) returns the next node in the sequence, given the 531 | * current and previous nodes. 532 | * 533 | * @param {Node} prev 534 | * @param {Node} current 535 | * @param {Function} isPre 536 | * @return {Node} 537 | */ 538 | function next (prev, current, isPre) { 539 | if ((prev && prev.parentNode === current) || isPre(current)) { 540 | return current.nextSibling || current.parentNode 541 | } 542 | 543 | return current.firstChild || current.nextSibling || current.parentNode 544 | } 545 | 546 | /* 547 | * Set up window for Node.js 548 | */ 549 | 550 | var root = (typeof window !== 'undefined' ? window : {}); 551 | 552 | /* 553 | * Parsing HTML strings 554 | */ 555 | 556 | function canParseHTMLNatively () { 557 | var Parser = root.DOMParser; 558 | var canParse = false; 559 | 560 | // Adapted from https://gist.github.com/1129031 561 | // Firefox/Opera/IE throw errors on unsupported types 562 | try { 563 | // WebKit returns null on unsupported types 564 | if (new Parser().parseFromString('', 'text/html')) { 565 | canParse = true; 566 | } 567 | } catch (e) {} 568 | 569 | return canParse 570 | } 571 | 572 | function createHTMLParser () { 573 | var Parser = function () {}; 574 | 575 | { 576 | if (shouldUseActiveX()) { 577 | Parser.prototype.parseFromString = function (string) { 578 | var doc = new window.ActiveXObject('htmlfile'); 579 | doc.designMode = 'on'; // disable on-page scripts 580 | doc.open(); 581 | doc.write(string); 582 | doc.close(); 583 | return doc 584 | }; 585 | } else { 586 | Parser.prototype.parseFromString = function (string) { 587 | var doc = document.implementation.createHTMLDocument(''); 588 | doc.open(); 589 | doc.write(string); 590 | doc.close(); 591 | return doc 592 | }; 593 | } 594 | } 595 | return Parser 596 | } 597 | 598 | function shouldUseActiveX () { 599 | var useActiveX = false; 600 | try { 601 | document.implementation.createHTMLDocument('').open(); 602 | } catch (e) { 603 | if (window.ActiveXObject) useActiveX = true; 604 | } 605 | return useActiveX 606 | } 607 | 608 | var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser(); 609 | 610 | function RootNode (input) { 611 | var root; 612 | if (typeof input === 'string') { 613 | var doc = htmlParser().parseFromString( 614 | // DOM parsers arrange elements in the and . 615 | // Wrapping in a custom element ensures elements are reliably arranged in 616 | // a single element. 617 | '' + input + '', 618 | 'text/html' 619 | ); 620 | root = doc.getElementById('turndown-root'); 621 | } else { 622 | root = input.cloneNode(true); 623 | } 624 | collapseWhitespace({ 625 | element: root, 626 | isBlock: isBlock, 627 | isVoid: isVoid 628 | }); 629 | 630 | return root 631 | } 632 | 633 | var _htmlParser; 634 | function htmlParser () { 635 | _htmlParser = _htmlParser || new HTMLParser(); 636 | return _htmlParser 637 | } 638 | 639 | function Node (node) { 640 | node.isBlock = isBlock(node); 641 | node.isCode = node.nodeName.toLowerCase() === 'code' || node.parentNode.isCode; 642 | node.isBlank = isBlank(node); 643 | node.flankingWhitespace = flankingWhitespace(node); 644 | return node 645 | } 646 | 647 | function isBlank (node) { 648 | return ( 649 | !isVoid(node) && 650 | !isMeaningfulWhenBlank(node) && 651 | /^\s*$/i.test(node.textContent) && 652 | !hasVoid(node) && 653 | !hasMeaningfulWhenBlank(node) 654 | ) 655 | } 656 | 657 | function flankingWhitespace (node) { 658 | var leading = ''; 659 | var trailing = ''; 660 | 661 | if (!node.isBlock) { 662 | var hasLeading = /^\s/.test(node.textContent); 663 | var hasTrailing = /\s$/.test(node.textContent); 664 | var blankWithSpaces = node.isBlank && hasLeading && hasTrailing; 665 | 666 | if (hasLeading && !isFlankedByWhitespace('left', node)) { 667 | leading = ' '; 668 | } 669 | 670 | if (!blankWithSpaces && hasTrailing && !isFlankedByWhitespace('right', node)) { 671 | trailing = ' '; 672 | } 673 | } 674 | 675 | return { leading: leading, trailing: trailing } 676 | } 677 | 678 | function isFlankedByWhitespace (side, node) { 679 | var sibling; 680 | var regExp; 681 | var isFlanked; 682 | 683 | if (side === 'left') { 684 | sibling = node.previousSibling; 685 | regExp = / $/; 686 | } else { 687 | sibling = node.nextSibling; 688 | regExp = /^ /; 689 | } 690 | 691 | if (sibling) { 692 | if (sibling.nodeType === 3) { 693 | isFlanked = regExp.test(sibling.nodeValue); 694 | } else if (sibling.nodeType === 1 && !isBlock(sibling)) { 695 | isFlanked = regExp.test(sibling.textContent); 696 | } 697 | } 698 | return isFlanked 699 | } 700 | 701 | var reduce = Array.prototype.reduce; 702 | var leadingNewLinesRegExp = /^\n*/; 703 | var trailingNewLinesRegExp = /\n*$/; 704 | var escapes = [ 705 | [/\\/g, '\\\\'], 706 | [/\*/g, '\\*'], 707 | [/^-/g, '\\-'], 708 | [/^\+ /g, '\\+ '], 709 | [/^(=+)/g, '\\$1'], 710 | [/^(#{1,6}) /g, '\\$1 '], 711 | [/`/g, '\\`'], 712 | [/^~~~/g, '\\~~~'], 713 | [/\[/g, '\\['], 714 | [/\]/g, '\\]'], 715 | [/^>/g, '\\>'], 716 | [/_/g, '\\_'], 717 | [/^(\d+)\. /g, '$1\\. '] 718 | ]; 719 | 720 | function TurndownService (options) { 721 | if (!(this instanceof TurndownService)) return new TurndownService(options) 722 | 723 | var defaults = { 724 | rules: rules, 725 | headingStyle: 'setext', 726 | hr: '* * *', 727 | bulletListMarker: '*', 728 | codeBlockStyle: 'indented', 729 | fence: '```', 730 | emDelimiter: '_', 731 | strongDelimiter: '**', 732 | linkStyle: 'inlined', 733 | linkReferenceStyle: 'full', 734 | br: ' ', 735 | blankReplacement: function (content, node) { 736 | return node.isBlock ? '\n\n' : '' 737 | }, 738 | keepReplacement: function (content, node) { 739 | return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML 740 | }, 741 | defaultReplacement: function (content, node) { 742 | return node.isBlock ? '\n\n' + content + '\n\n' : content 743 | } 744 | }; 745 | this.options = extend({}, defaults, options); 746 | this.rules = new Rules(this.options); 747 | } 748 | 749 | TurndownService.prototype = { 750 | /** 751 | * The entry point for converting a string or DOM node to Markdown 752 | * @public 753 | * @param {String|HTMLElement} input The string or DOM node to convert 754 | * @returns A Markdown representation of the input 755 | * @type String 756 | */ 757 | 758 | turndown: function (input) { 759 | if (!canConvert(input)) { 760 | throw new TypeError( 761 | input + ' is not a string, or an element/document/fragment node.' 762 | ) 763 | } 764 | 765 | if (input === '') return '' 766 | 767 | var output = process.call(this, new RootNode(input)); 768 | return postProcess.call(this, output) 769 | }, 770 | 771 | /** 772 | * Add one or more plugins 773 | * @public 774 | * @param {Function|Array} plugin The plugin or array of plugins to add 775 | * @returns The Turndown instance for chaining 776 | * @type Object 777 | */ 778 | 779 | use: function (plugin) { 780 | if (Array.isArray(plugin)) { 781 | for (var i = 0; i < plugin.length; i++) this.use(plugin[i]); 782 | } else if (typeof plugin === 'function') { 783 | plugin(this); 784 | } else { 785 | throw new TypeError('plugin must be a Function or an Array of Functions') 786 | } 787 | return this 788 | }, 789 | 790 | /** 791 | * Adds a rule 792 | * @public 793 | * @param {String} key The unique key of the rule 794 | * @param {Object} rule The rule 795 | * @returns The Turndown instance for chaining 796 | * @type Object 797 | */ 798 | 799 | addRule: function (key, rule) { 800 | this.rules.add(key, rule); 801 | return this 802 | }, 803 | 804 | /** 805 | * Keep a node (as HTML) that matches the filter 806 | * @public 807 | * @param {String|Array|Function} filter The unique key of the rule 808 | * @returns The Turndown instance for chaining 809 | * @type Object 810 | */ 811 | 812 | keep: function (filter) { 813 | this.rules.keep(filter); 814 | return this 815 | }, 816 | 817 | /** 818 | * Remove a node that matches the filter 819 | * @public 820 | * @param {String|Array|Function} filter The unique key of the rule 821 | * @returns The Turndown instance for chaining 822 | * @type Object 823 | */ 824 | 825 | remove: function (filter) { 826 | this.rules.remove(filter); 827 | return this 828 | }, 829 | 830 | /** 831 | * Escapes Markdown syntax 832 | * @public 833 | * @param {String} string The string to escape 834 | * @returns A string with Markdown syntax escaped 835 | * @type String 836 | */ 837 | 838 | escape: function (string) { 839 | return escapes.reduce(function (accumulator, escape) { 840 | return accumulator.replace(escape[0], escape[1]) 841 | }, string) 842 | } 843 | }; 844 | 845 | /** 846 | * Reduces a DOM node down to its Markdown string equivalent 847 | * @private 848 | * @param {HTMLElement} parentNode The node to convert 849 | * @returns A Markdown representation of the node 850 | * @type String 851 | */ 852 | 853 | function process (parentNode) { 854 | var self = this; 855 | return reduce.call(parentNode.childNodes, function (output, node) { 856 | node = new Node(node); 857 | 858 | var replacement = ''; 859 | if (node.nodeType === 3) { 860 | replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue); 861 | } else if (node.nodeType === 1) { 862 | replacement = replacementForNode.call(self, node); 863 | } 864 | 865 | return join(output, replacement) 866 | }, '') 867 | } 868 | 869 | /** 870 | * Appends strings as each rule requires and trims the output 871 | * @private 872 | * @param {String} output The conversion output 873 | * @returns A trimmed version of the ouput 874 | * @type String 875 | */ 876 | 877 | function postProcess (output) { 878 | var self = this; 879 | this.rules.forEach(function (rule) { 880 | if (typeof rule.append === 'function') { 881 | output = join(output, rule.append(self.options)); 882 | } 883 | }); 884 | 885 | return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') 886 | } 887 | 888 | /** 889 | * Converts an element node to its Markdown equivalent 890 | * @private 891 | * @param {HTMLElement} node The node to convert 892 | * @returns A Markdown representation of the node 893 | * @type String 894 | */ 895 | 896 | function replacementForNode (node) { 897 | var rule = this.rules.forNode(node); 898 | var content = process.call(this, node); 899 | var whitespace = node.flankingWhitespace; 900 | if (whitespace.leading || whitespace.trailing) content = content.trim(); 901 | return ( 902 | whitespace.leading + 903 | rule.replacement(content, node, this.options) + 904 | whitespace.trailing 905 | ) 906 | } 907 | 908 | /** 909 | * Determines the new lines between the current output and the replacement 910 | * @private 911 | * @param {String} output The current conversion output 912 | * @param {String} replacement The string to append to the output 913 | * @returns The whitespace to separate the current output and the replacement 914 | * @type String 915 | */ 916 | 917 | function separatingNewlines (output, replacement) { 918 | var newlines = [ 919 | output.match(trailingNewLinesRegExp)[0], 920 | replacement.match(leadingNewLinesRegExp)[0] 921 | ].sort(); 922 | var maxNewlines = newlines[newlines.length - 1]; 923 | return maxNewlines.length < 2 ? maxNewlines : '\n\n' 924 | } 925 | 926 | function join (string1, string2) { 927 | var separator = separatingNewlines(string1, string2); 928 | 929 | // Remove trailing/leading newlines and replace with separator 930 | string1 = string1.replace(trailingNewLinesRegExp, ''); 931 | string2 = string2.replace(leadingNewLinesRegExp, ''); 932 | 933 | return string1 + separator + string2 934 | } 935 | 936 | /** 937 | * Determines whether an input can be converted 938 | * @private 939 | * @param {String|HTMLElement} input Describe this parameter 940 | * @returns Describe what it returns 941 | * @type String|Object|Array|Boolean|Number 942 | */ 943 | 944 | function canConvert (input) { 945 | return ( 946 | input != null && ( 947 | typeof input === 'string' || 948 | (input.nodeType && ( 949 | input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11 950 | )) 951 | ) 952 | ) 953 | } 954 | 955 | return TurndownService; 956 | 957 | }()); -------------------------------------------------------------------------------- /src/lib/webbrowser-polyfill.js: -------------------------------------------------------------------------------- 1 | // https://unpkg.com/webextension-polyfill@0.6.0/dist/browser-polyfill.js 2 | (function (global, factory) { 3 | if (typeof define === "function" && define.amd) { 4 | define("webextension-polyfill", ["module"], factory); 5 | } else if (typeof exports !== "undefined") { 6 | factory(module); 7 | } else { 8 | var mod = { 9 | exports: {} 10 | }; 11 | factory(mod); 12 | global.browser = mod.exports; 13 | } 14 | })(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) { 15 | /* webextension-polyfill - v0.6.0 - Mon Dec 23 2019 12:32:53 */ 16 | 17 | /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 18 | 19 | /* vim: set sts=2 sw=2 et tw=80: */ 20 | 21 | /* This Source Code Form is subject to the terms of the Mozilla Public 22 | * License, v. 2.0. If a copy of the MPL was not distributed with this 23 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 24 | "use strict"; 25 | 26 | if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.prototype) { 27 | const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; 28 | const SEND_RESPONSE_DEPRECATION_WARNING = "Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)"; // Wrapping the bulk of this polyfill in a one-time-use function is a minor 29 | // optimization for Firefox. Since Spidermonkey does not fully parse the 30 | // contents of a function until the first time it's called, and since it will 31 | // never actually need to be called, this allows the polyfill to be included 32 | // in Firefox nearly for free. 33 | 34 | const wrapAPIs = extensionAPIs => { 35 | // NOTE: apiMetadata is associated to the content of the api-metadata.json file 36 | // at build time by replacing the following "include" with the content of the 37 | // JSON file. 38 | const apiMetadata = { 39 | "alarms": { 40 | "clear": { 41 | "minArgs": 0, 42 | "maxArgs": 1 43 | }, 44 | "clearAll": { 45 | "minArgs": 0, 46 | "maxArgs": 0 47 | }, 48 | "get": { 49 | "minArgs": 0, 50 | "maxArgs": 1 51 | }, 52 | "getAll": { 53 | "minArgs": 0, 54 | "maxArgs": 0 55 | } 56 | }, 57 | "bookmarks": { 58 | "create": { 59 | "minArgs": 1, 60 | "maxArgs": 1 61 | }, 62 | "get": { 63 | "minArgs": 1, 64 | "maxArgs": 1 65 | }, 66 | "getChildren": { 67 | "minArgs": 1, 68 | "maxArgs": 1 69 | }, 70 | "getRecent": { 71 | "minArgs": 1, 72 | "maxArgs": 1 73 | }, 74 | "getSubTree": { 75 | "minArgs": 1, 76 | "maxArgs": 1 77 | }, 78 | "getTree": { 79 | "minArgs": 0, 80 | "maxArgs": 0 81 | }, 82 | "move": { 83 | "minArgs": 2, 84 | "maxArgs": 2 85 | }, 86 | "remove": { 87 | "minArgs": 1, 88 | "maxArgs": 1 89 | }, 90 | "removeTree": { 91 | "minArgs": 1, 92 | "maxArgs": 1 93 | }, 94 | "search": { 95 | "minArgs": 1, 96 | "maxArgs": 1 97 | }, 98 | "update": { 99 | "minArgs": 2, 100 | "maxArgs": 2 101 | } 102 | }, 103 | "browserAction": { 104 | "disable": { 105 | "minArgs": 0, 106 | "maxArgs": 1, 107 | "fallbackToNoCallback": true 108 | }, 109 | "enable": { 110 | "minArgs": 0, 111 | "maxArgs": 1, 112 | "fallbackToNoCallback": true 113 | }, 114 | "getBadgeBackgroundColor": { 115 | "minArgs": 1, 116 | "maxArgs": 1 117 | }, 118 | "getBadgeText": { 119 | "minArgs": 1, 120 | "maxArgs": 1 121 | }, 122 | "getPopup": { 123 | "minArgs": 1, 124 | "maxArgs": 1 125 | }, 126 | "getTitle": { 127 | "minArgs": 1, 128 | "maxArgs": 1 129 | }, 130 | "openPopup": { 131 | "minArgs": 0, 132 | "maxArgs": 0 133 | }, 134 | "setBadgeBackgroundColor": { 135 | "minArgs": 1, 136 | "maxArgs": 1, 137 | "fallbackToNoCallback": true 138 | }, 139 | "setBadgeText": { 140 | "minArgs": 1, 141 | "maxArgs": 1, 142 | "fallbackToNoCallback": true 143 | }, 144 | "setIcon": { 145 | "minArgs": 1, 146 | "maxArgs": 1 147 | }, 148 | "setPopup": { 149 | "minArgs": 1, 150 | "maxArgs": 1, 151 | "fallbackToNoCallback": true 152 | }, 153 | "setTitle": { 154 | "minArgs": 1, 155 | "maxArgs": 1, 156 | "fallbackToNoCallback": true 157 | } 158 | }, 159 | "browsingData": { 160 | "remove": { 161 | "minArgs": 2, 162 | "maxArgs": 2 163 | }, 164 | "removeCache": { 165 | "minArgs": 1, 166 | "maxArgs": 1 167 | }, 168 | "removeCookies": { 169 | "minArgs": 1, 170 | "maxArgs": 1 171 | }, 172 | "removeDownloads": { 173 | "minArgs": 1, 174 | "maxArgs": 1 175 | }, 176 | "removeFormData": { 177 | "minArgs": 1, 178 | "maxArgs": 1 179 | }, 180 | "removeHistory": { 181 | "minArgs": 1, 182 | "maxArgs": 1 183 | }, 184 | "removeLocalStorage": { 185 | "minArgs": 1, 186 | "maxArgs": 1 187 | }, 188 | "removePasswords": { 189 | "minArgs": 1, 190 | "maxArgs": 1 191 | }, 192 | "removePluginData": { 193 | "minArgs": 1, 194 | "maxArgs": 1 195 | }, 196 | "settings": { 197 | "minArgs": 0, 198 | "maxArgs": 0 199 | } 200 | }, 201 | "commands": { 202 | "getAll": { 203 | "minArgs": 0, 204 | "maxArgs": 0 205 | } 206 | }, 207 | "contextMenus": { 208 | "remove": { 209 | "minArgs": 1, 210 | "maxArgs": 1 211 | }, 212 | "removeAll": { 213 | "minArgs": 0, 214 | "maxArgs": 0 215 | }, 216 | "update": { 217 | "minArgs": 2, 218 | "maxArgs": 2 219 | } 220 | }, 221 | "cookies": { 222 | "get": { 223 | "minArgs": 1, 224 | "maxArgs": 1 225 | }, 226 | "getAll": { 227 | "minArgs": 1, 228 | "maxArgs": 1 229 | }, 230 | "getAllCookieStores": { 231 | "minArgs": 0, 232 | "maxArgs": 0 233 | }, 234 | "remove": { 235 | "minArgs": 1, 236 | "maxArgs": 1 237 | }, 238 | "set": { 239 | "minArgs": 1, 240 | "maxArgs": 1 241 | } 242 | }, 243 | "devtools": { 244 | "inspectedWindow": { 245 | "eval": { 246 | "minArgs": 1, 247 | "maxArgs": 2, 248 | "singleCallbackArg": false 249 | } 250 | }, 251 | "panels": { 252 | "create": { 253 | "minArgs": 3, 254 | "maxArgs": 3, 255 | "singleCallbackArg": true 256 | } 257 | } 258 | }, 259 | "downloads": { 260 | "cancel": { 261 | "minArgs": 1, 262 | "maxArgs": 1 263 | }, 264 | "download": { 265 | "minArgs": 1, 266 | "maxArgs": 1 267 | }, 268 | "erase": { 269 | "minArgs": 1, 270 | "maxArgs": 1 271 | }, 272 | "getFileIcon": { 273 | "minArgs": 1, 274 | "maxArgs": 2 275 | }, 276 | "open": { 277 | "minArgs": 1, 278 | "maxArgs": 1, 279 | "fallbackToNoCallback": true 280 | }, 281 | "pause": { 282 | "minArgs": 1, 283 | "maxArgs": 1 284 | }, 285 | "removeFile": { 286 | "minArgs": 1, 287 | "maxArgs": 1 288 | }, 289 | "resume": { 290 | "minArgs": 1, 291 | "maxArgs": 1 292 | }, 293 | "search": { 294 | "minArgs": 1, 295 | "maxArgs": 1 296 | }, 297 | "show": { 298 | "minArgs": 1, 299 | "maxArgs": 1, 300 | "fallbackToNoCallback": true 301 | } 302 | }, 303 | "extension": { 304 | "isAllowedFileSchemeAccess": { 305 | "minArgs": 0, 306 | "maxArgs": 0 307 | }, 308 | "isAllowedIncognitoAccess": { 309 | "minArgs": 0, 310 | "maxArgs": 0 311 | } 312 | }, 313 | "history": { 314 | "addUrl": { 315 | "minArgs": 1, 316 | "maxArgs": 1 317 | }, 318 | "deleteAll": { 319 | "minArgs": 0, 320 | "maxArgs": 0 321 | }, 322 | "deleteRange": { 323 | "minArgs": 1, 324 | "maxArgs": 1 325 | }, 326 | "deleteUrl": { 327 | "minArgs": 1, 328 | "maxArgs": 1 329 | }, 330 | "getVisits": { 331 | "minArgs": 1, 332 | "maxArgs": 1 333 | }, 334 | "search": { 335 | "minArgs": 1, 336 | "maxArgs": 1 337 | } 338 | }, 339 | "i18n": { 340 | "detectLanguage": { 341 | "minArgs": 1, 342 | "maxArgs": 1 343 | }, 344 | "getAcceptLanguages": { 345 | "minArgs": 0, 346 | "maxArgs": 0 347 | } 348 | }, 349 | "identity": { 350 | "launchWebAuthFlow": { 351 | "minArgs": 1, 352 | "maxArgs": 1 353 | } 354 | }, 355 | "idle": { 356 | "queryState": { 357 | "minArgs": 1, 358 | "maxArgs": 1 359 | } 360 | }, 361 | "management": { 362 | "get": { 363 | "minArgs": 1, 364 | "maxArgs": 1 365 | }, 366 | "getAll": { 367 | "minArgs": 0, 368 | "maxArgs": 0 369 | }, 370 | "getSelf": { 371 | "minArgs": 0, 372 | "maxArgs": 0 373 | }, 374 | "setEnabled": { 375 | "minArgs": 2, 376 | "maxArgs": 2 377 | }, 378 | "uninstallSelf": { 379 | "minArgs": 0, 380 | "maxArgs": 1 381 | } 382 | }, 383 | "notifications": { 384 | "clear": { 385 | "minArgs": 1, 386 | "maxArgs": 1 387 | }, 388 | "create": { 389 | "minArgs": 1, 390 | "maxArgs": 2 391 | }, 392 | "getAll": { 393 | "minArgs": 0, 394 | "maxArgs": 0 395 | }, 396 | "getPermissionLevel": { 397 | "minArgs": 0, 398 | "maxArgs": 0 399 | }, 400 | "update": { 401 | "minArgs": 2, 402 | "maxArgs": 2 403 | } 404 | }, 405 | "pageAction": { 406 | "getPopup": { 407 | "minArgs": 1, 408 | "maxArgs": 1 409 | }, 410 | "getTitle": { 411 | "minArgs": 1, 412 | "maxArgs": 1 413 | }, 414 | "hide": { 415 | "minArgs": 1, 416 | "maxArgs": 1, 417 | "fallbackToNoCallback": true 418 | }, 419 | "setIcon": { 420 | "minArgs": 1, 421 | "maxArgs": 1 422 | }, 423 | "setPopup": { 424 | "minArgs": 1, 425 | "maxArgs": 1, 426 | "fallbackToNoCallback": true 427 | }, 428 | "setTitle": { 429 | "minArgs": 1, 430 | "maxArgs": 1, 431 | "fallbackToNoCallback": true 432 | }, 433 | "show": { 434 | "minArgs": 1, 435 | "maxArgs": 1, 436 | "fallbackToNoCallback": true 437 | } 438 | }, 439 | "permissions": { 440 | "contains": { 441 | "minArgs": 1, 442 | "maxArgs": 1 443 | }, 444 | "getAll": { 445 | "minArgs": 0, 446 | "maxArgs": 0 447 | }, 448 | "remove": { 449 | "minArgs": 1, 450 | "maxArgs": 1 451 | }, 452 | "request": { 453 | "minArgs": 1, 454 | "maxArgs": 1 455 | } 456 | }, 457 | "runtime": { 458 | "getBackgroundPage": { 459 | "minArgs": 0, 460 | "maxArgs": 0 461 | }, 462 | "getPlatformInfo": { 463 | "minArgs": 0, 464 | "maxArgs": 0 465 | }, 466 | "openOptionsPage": { 467 | "minArgs": 0, 468 | "maxArgs": 0 469 | }, 470 | "requestUpdateCheck": { 471 | "minArgs": 0, 472 | "maxArgs": 0 473 | }, 474 | "sendMessage": { 475 | "minArgs": 1, 476 | "maxArgs": 3 477 | }, 478 | "sendNativeMessage": { 479 | "minArgs": 2, 480 | "maxArgs": 2 481 | }, 482 | "setUninstallURL": { 483 | "minArgs": 1, 484 | "maxArgs": 1 485 | } 486 | }, 487 | "sessions": { 488 | "getDevices": { 489 | "minArgs": 0, 490 | "maxArgs": 1 491 | }, 492 | "getRecentlyClosed": { 493 | "minArgs": 0, 494 | "maxArgs": 1 495 | }, 496 | "restore": { 497 | "minArgs": 0, 498 | "maxArgs": 1 499 | } 500 | }, 501 | "storage": { 502 | "local": { 503 | "clear": { 504 | "minArgs": 0, 505 | "maxArgs": 0 506 | }, 507 | "get": { 508 | "minArgs": 0, 509 | "maxArgs": 1 510 | }, 511 | "getBytesInUse": { 512 | "minArgs": 0, 513 | "maxArgs": 1 514 | }, 515 | "remove": { 516 | "minArgs": 1, 517 | "maxArgs": 1 518 | }, 519 | "set": { 520 | "minArgs": 1, 521 | "maxArgs": 1 522 | } 523 | }, 524 | "managed": { 525 | "get": { 526 | "minArgs": 0, 527 | "maxArgs": 1 528 | }, 529 | "getBytesInUse": { 530 | "minArgs": 0, 531 | "maxArgs": 1 532 | } 533 | }, 534 | "sync": { 535 | "clear": { 536 | "minArgs": 0, 537 | "maxArgs": 0 538 | }, 539 | "get": { 540 | "minArgs": 0, 541 | "maxArgs": 1 542 | }, 543 | "getBytesInUse": { 544 | "minArgs": 0, 545 | "maxArgs": 1 546 | }, 547 | "remove": { 548 | "minArgs": 1, 549 | "maxArgs": 1 550 | }, 551 | "set": { 552 | "minArgs": 1, 553 | "maxArgs": 1 554 | } 555 | } 556 | }, 557 | "tabs": { 558 | "captureVisibleTab": { 559 | "minArgs": 0, 560 | "maxArgs": 2 561 | }, 562 | "create": { 563 | "minArgs": 1, 564 | "maxArgs": 1 565 | }, 566 | "detectLanguage": { 567 | "minArgs": 0, 568 | "maxArgs": 1 569 | }, 570 | "discard": { 571 | "minArgs": 0, 572 | "maxArgs": 1 573 | }, 574 | "duplicate": { 575 | "minArgs": 1, 576 | "maxArgs": 1 577 | }, 578 | "executeScript": { 579 | "minArgs": 1, 580 | "maxArgs": 2 581 | }, 582 | "get": { 583 | "minArgs": 1, 584 | "maxArgs": 1 585 | }, 586 | "getCurrent": { 587 | "minArgs": 0, 588 | "maxArgs": 0 589 | }, 590 | "getZoom": { 591 | "minArgs": 0, 592 | "maxArgs": 1 593 | }, 594 | "getZoomSettings": { 595 | "minArgs": 0, 596 | "maxArgs": 1 597 | }, 598 | "highlight": { 599 | "minArgs": 1, 600 | "maxArgs": 1 601 | }, 602 | "insertCSS": { 603 | "minArgs": 1, 604 | "maxArgs": 2 605 | }, 606 | "move": { 607 | "minArgs": 2, 608 | "maxArgs": 2 609 | }, 610 | "query": { 611 | "minArgs": 1, 612 | "maxArgs": 1 613 | }, 614 | "reload": { 615 | "minArgs": 0, 616 | "maxArgs": 2 617 | }, 618 | "remove": { 619 | "minArgs": 1, 620 | "maxArgs": 1 621 | }, 622 | "removeCSS": { 623 | "minArgs": 1, 624 | "maxArgs": 2 625 | }, 626 | "sendMessage": { 627 | "minArgs": 2, 628 | "maxArgs": 3 629 | }, 630 | "setZoom": { 631 | "minArgs": 1, 632 | "maxArgs": 2 633 | }, 634 | "setZoomSettings": { 635 | "minArgs": 1, 636 | "maxArgs": 2 637 | }, 638 | "update": { 639 | "minArgs": 1, 640 | "maxArgs": 2 641 | } 642 | }, 643 | "topSites": { 644 | "get": { 645 | "minArgs": 0, 646 | "maxArgs": 0 647 | } 648 | }, 649 | "webNavigation": { 650 | "getAllFrames": { 651 | "minArgs": 1, 652 | "maxArgs": 1 653 | }, 654 | "getFrame": { 655 | "minArgs": 1, 656 | "maxArgs": 1 657 | } 658 | }, 659 | "webRequest": { 660 | "handlerBehaviorChanged": { 661 | "minArgs": 0, 662 | "maxArgs": 0 663 | } 664 | }, 665 | "windows": { 666 | "create": { 667 | "minArgs": 0, 668 | "maxArgs": 1 669 | }, 670 | "get": { 671 | "minArgs": 1, 672 | "maxArgs": 2 673 | }, 674 | "getAll": { 675 | "minArgs": 0, 676 | "maxArgs": 1 677 | }, 678 | "getCurrent": { 679 | "minArgs": 0, 680 | "maxArgs": 1 681 | }, 682 | "getLastFocused": { 683 | "minArgs": 0, 684 | "maxArgs": 1 685 | }, 686 | "remove": { 687 | "minArgs": 1, 688 | "maxArgs": 1 689 | }, 690 | "update": { 691 | "minArgs": 2, 692 | "maxArgs": 2 693 | } 694 | } 695 | }; 696 | 697 | if (Object.keys(apiMetadata).length === 0) { 698 | throw new Error("api-metadata.json has not been included in browser-polyfill"); 699 | } 700 | /** 701 | * A WeakMap subclass which creates and stores a value for any key which does 702 | * not exist when accessed, but behaves exactly as an ordinary WeakMap 703 | * otherwise. 704 | * 705 | * @param {function} createItem 706 | * A function which will be called in order to create the value for any 707 | * key which does not exist, the first time it is accessed. The 708 | * function receives, as its only argument, the key being created. 709 | */ 710 | 711 | 712 | class DefaultWeakMap extends WeakMap { 713 | constructor(createItem, items = undefined) { 714 | super(items); 715 | this.createItem = createItem; 716 | } 717 | 718 | get(key) { 719 | if (!this.has(key)) { 720 | this.set(key, this.createItem(key)); 721 | } 722 | 723 | return super.get(key); 724 | } 725 | 726 | } 727 | /** 728 | * Returns true if the given object is an object with a `then` method, and can 729 | * therefore be assumed to behave as a Promise. 730 | * 731 | * @param {*} value The value to test. 732 | * @returns {boolean} True if the value is thenable. 733 | */ 734 | 735 | 736 | const isThenable = value => { 737 | return value && typeof value === "object" && typeof value.then === "function"; 738 | }; 739 | /** 740 | * Creates and returns a function which, when called, will resolve or reject 741 | * the given promise based on how it is called: 742 | * 743 | * - If, when called, `chrome.runtime.lastError` contains a non-null object, 744 | * the promise is rejected with that value. 745 | * - If the function is called with exactly one argument, the promise is 746 | * resolved to that value. 747 | * - Otherwise, the promise is resolved to an array containing all of the 748 | * function's arguments. 749 | * 750 | * @param {object} promise 751 | * An object containing the resolution and rejection functions of a 752 | * promise. 753 | * @param {function} promise.resolve 754 | * The promise's resolution function. 755 | * @param {function} promise.rejection 756 | * The promise's rejection function. 757 | * @param {object} metadata 758 | * Metadata about the wrapped method which has created the callback. 759 | * @param {integer} metadata.maxResolvedArgs 760 | * The maximum number of arguments which may be passed to the 761 | * callback created by the wrapped async function. 762 | * 763 | * @returns {function} 764 | * The generated callback function. 765 | */ 766 | 767 | 768 | const makeCallback = (promise, metadata) => { 769 | return (...callbackArgs) => { 770 | if (extensionAPIs.runtime.lastError) { 771 | promise.reject(extensionAPIs.runtime.lastError); 772 | } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) { 773 | promise.resolve(callbackArgs[0]); 774 | } else { 775 | promise.resolve(callbackArgs); 776 | } 777 | }; 778 | }; 779 | 780 | const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; 781 | /** 782 | * Creates a wrapper function for a method with the given name and metadata. 783 | * 784 | * @param {string} name 785 | * The name of the method which is being wrapped. 786 | * @param {object} metadata 787 | * Metadata about the method being wrapped. 788 | * @param {integer} metadata.minArgs 789 | * The minimum number of arguments which must be passed to the 790 | * function. If called with fewer than this number of arguments, the 791 | * wrapper will raise an exception. 792 | * @param {integer} metadata.maxArgs 793 | * The maximum number of arguments which may be passed to the 794 | * function. If called with more than this number of arguments, the 795 | * wrapper will raise an exception. 796 | * @param {integer} metadata.maxResolvedArgs 797 | * The maximum number of arguments which may be passed to the 798 | * callback created by the wrapped async function. 799 | * 800 | * @returns {function(object, ...*)} 801 | * The generated wrapper function. 802 | */ 803 | 804 | 805 | const wrapAsyncFunction = (name, metadata) => { 806 | return function asyncFunctionWrapper(target, ...args) { 807 | if (args.length < metadata.minArgs) { 808 | throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); 809 | } 810 | 811 | if (args.length > metadata.maxArgs) { 812 | throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); 813 | } 814 | 815 | return new Promise((resolve, reject) => { 816 | if (metadata.fallbackToNoCallback) { 817 | // This API method has currently no callback on Chrome, but it return a promise on Firefox, 818 | // and so the polyfill will try to call it with a callback first, and it will fallback 819 | // to not passing the callback if the first call fails. 820 | try { 821 | target[name](...args, makeCallback({ 822 | resolve, 823 | reject 824 | }, metadata)); 825 | } catch (cbError) { 826 | console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); 827 | target[name](...args); // Update the API method metadata, so that the next API calls will not try to 828 | // use the unsupported callback anymore. 829 | 830 | metadata.fallbackToNoCallback = false; 831 | metadata.noCallback = true; 832 | resolve(); 833 | } 834 | } else if (metadata.noCallback) { 835 | target[name](...args); 836 | resolve(); 837 | } else { 838 | target[name](...args, makeCallback({ 839 | resolve, 840 | reject 841 | }, metadata)); 842 | } 843 | }); 844 | }; 845 | }; 846 | /** 847 | * Wraps an existing method of the target object, so that calls to it are 848 | * intercepted by the given wrapper function. The wrapper function receives, 849 | * as its first argument, the original `target` object, followed by each of 850 | * the arguments passed to the original method. 851 | * 852 | * @param {object} target 853 | * The original target object that the wrapped method belongs to. 854 | * @param {function} method 855 | * The method being wrapped. This is used as the target of the Proxy 856 | * object which is created to wrap the method. 857 | * @param {function} wrapper 858 | * The wrapper function which is called in place of a direct invocation 859 | * of the wrapped method. 860 | * 861 | * @returns {Proxy} 862 | * A Proxy object for the given method, which invokes the given wrapper 863 | * method in its place. 864 | */ 865 | 866 | 867 | const wrapMethod = (target, method, wrapper) => { 868 | return new Proxy(method, { 869 | apply(targetMethod, thisObj, args) { 870 | return wrapper.call(thisObj, target, ...args); 871 | } 872 | 873 | }); 874 | }; 875 | 876 | let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); 877 | /** 878 | * Wraps an object in a Proxy which intercepts and wraps certain methods 879 | * based on the given `wrappers` and `metadata` objects. 880 | * 881 | * @param {object} target 882 | * The target object to wrap. 883 | * 884 | * @param {object} [wrappers = {}] 885 | * An object tree containing wrapper functions for special cases. Any 886 | * function present in this object tree is called in place of the 887 | * method in the same location in the `target` object tree. These 888 | * wrapper methods are invoked as described in {@see wrapMethod}. 889 | * 890 | * @param {object} [metadata = {}] 891 | * An object tree containing metadata used to automatically generate 892 | * Promise-based wrapper functions for asynchronous. Any function in 893 | * the `target` object tree which has a corresponding metadata object 894 | * in the same location in the `metadata` tree is replaced with an 895 | * automatically-generated wrapper function, as described in 896 | * {@see wrapAsyncFunction} 897 | * 898 | * @returns {Proxy} 899 | */ 900 | 901 | const wrapObject = (target, wrappers = {}, metadata = {}) => { 902 | let cache = Object.create(null); 903 | let handlers = { 904 | has(proxyTarget, prop) { 905 | return prop in target || prop in cache; 906 | }, 907 | 908 | get(proxyTarget, prop, receiver) { 909 | if (prop in cache) { 910 | return cache[prop]; 911 | } 912 | 913 | if (!(prop in target)) { 914 | return undefined; 915 | } 916 | 917 | let value = target[prop]; 918 | 919 | if (typeof value === "function") { 920 | // This is a method on the underlying object. Check if we need to do 921 | // any wrapping. 922 | if (typeof wrappers[prop] === "function") { 923 | // We have a special-case wrapper for this method. 924 | value = wrapMethod(target, target[prop], wrappers[prop]); 925 | } else if (hasOwnProperty(metadata, prop)) { 926 | // This is an async method that we have metadata for. Create a 927 | // Promise wrapper for it. 928 | let wrapper = wrapAsyncFunction(prop, metadata[prop]); 929 | value = wrapMethod(target, target[prop], wrapper); 930 | } else { 931 | // This is a method that we don't know or care about. Return the 932 | // original method, bound to the underlying object. 933 | value = value.bind(target); 934 | } 935 | } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { 936 | // This is an object that we need to do some wrapping for the children 937 | // of. Create a sub-object wrapper for it with the appropriate child 938 | // metadata. 939 | value = wrapObject(value, wrappers[prop], metadata[prop]); 940 | } else if (hasOwnProperty(metadata, "*")) { 941 | // Wrap all properties in * namespace. 942 | value = wrapObject(value, wrappers[prop], metadata["*"]); 943 | } else { 944 | // We don't need to do any wrapping for this property, 945 | // so just forward all access to the underlying object. 946 | Object.defineProperty(cache, prop, { 947 | configurable: true, 948 | enumerable: true, 949 | 950 | get() { 951 | return target[prop]; 952 | }, 953 | 954 | set(value) { 955 | target[prop] = value; 956 | } 957 | 958 | }); 959 | return value; 960 | } 961 | 962 | cache[prop] = value; 963 | return value; 964 | }, 965 | 966 | set(proxyTarget, prop, value, receiver) { 967 | if (prop in cache) { 968 | cache[prop] = value; 969 | } else { 970 | target[prop] = value; 971 | } 972 | 973 | return true; 974 | }, 975 | 976 | defineProperty(proxyTarget, prop, desc) { 977 | return Reflect.defineProperty(cache, prop, desc); 978 | }, 979 | 980 | deleteProperty(proxyTarget, prop) { 981 | return Reflect.deleteProperty(cache, prop); 982 | } 983 | 984 | }; // Per contract of the Proxy API, the "get" proxy handler must return the 985 | // original value of the target if that value is declared read-only and 986 | // non-configurable. For this reason, we create an object with the 987 | // prototype set to `target` instead of using `target` directly. 988 | // Otherwise we cannot return a custom object for APIs that 989 | // are declared read-only and non-configurable, such as `chrome.devtools`. 990 | // 991 | // The proxy handlers themselves will still use the original `target` 992 | // instead of the `proxyTarget`, so that the methods and properties are 993 | // dereferenced via the original targets. 994 | 995 | let proxyTarget = Object.create(target); 996 | return new Proxy(proxyTarget, handlers); 997 | }; 998 | /** 999 | * Creates a set of wrapper functions for an event object, which handles 1000 | * wrapping of listener functions that those messages are passed. 1001 | * 1002 | * A single wrapper is created for each listener function, and stored in a 1003 | * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` 1004 | * retrieve the original wrapper, so that attempts to remove a 1005 | * previously-added listener work as expected. 1006 | * 1007 | * @param {DefaultWeakMap} wrapperMap 1008 | * A DefaultWeakMap object which will create the appropriate wrapper 1009 | * for a given listener function when one does not exist, and retrieve 1010 | * an existing one when it does. 1011 | * 1012 | * @returns {object} 1013 | */ 1014 | 1015 | 1016 | const wrapEvent = wrapperMap => ({ 1017 | addListener(target, listener, ...args) { 1018 | target.addListener(wrapperMap.get(listener), ...args); 1019 | }, 1020 | 1021 | hasListener(target, listener) { 1022 | return target.hasListener(wrapperMap.get(listener)); 1023 | }, 1024 | 1025 | removeListener(target, listener) { 1026 | target.removeListener(wrapperMap.get(listener)); 1027 | } 1028 | 1029 | }); // Keep track if the deprecation warning has been logged at least once. 1030 | 1031 | 1032 | let loggedSendResponseDeprecationWarning = false; 1033 | const onMessageWrappers = new DefaultWeakMap(listener => { 1034 | if (typeof listener !== "function") { 1035 | return listener; 1036 | } 1037 | /** 1038 | * Wraps a message listener function so that it may send responses based on 1039 | * its return value, rather than by returning a sentinel value and calling a 1040 | * callback. If the listener function returns a Promise, the response is 1041 | * sent when the promise either resolves or rejects. 1042 | * 1043 | * @param {*} message 1044 | * The message sent by the other end of the channel. 1045 | * @param {object} sender 1046 | * Details about the sender of the message. 1047 | * @param {function(*)} sendResponse 1048 | * A callback which, when called with an arbitrary argument, sends 1049 | * that value as a response. 1050 | * @returns {boolean} 1051 | * True if the wrapped listener returned a Promise, which will later 1052 | * yield a response. False otherwise. 1053 | */ 1054 | 1055 | 1056 | return function onMessage(message, sender, sendResponse) { 1057 | let didCallSendResponse = false; 1058 | let wrappedSendResponse; 1059 | let sendResponsePromise = new Promise(resolve => { 1060 | wrappedSendResponse = function (response) { 1061 | if (!loggedSendResponseDeprecationWarning) { 1062 | console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack); 1063 | loggedSendResponseDeprecationWarning = true; 1064 | } 1065 | 1066 | didCallSendResponse = true; 1067 | resolve(response); 1068 | }; 1069 | }); 1070 | let result; 1071 | 1072 | try { 1073 | result = listener(message, sender, wrappedSendResponse); 1074 | } catch (err) { 1075 | result = Promise.reject(err); 1076 | } 1077 | 1078 | const isResultThenable = result !== true && isThenable(result); // If the listener didn't returned true or a Promise, or called 1079 | // wrappedSendResponse synchronously, we can exit earlier 1080 | // because there will be no response sent from this listener. 1081 | 1082 | if (result !== true && !isResultThenable && !didCallSendResponse) { 1083 | return false; 1084 | } // A small helper to send the message if the promise resolves 1085 | // and an error if the promise rejects (a wrapped sendMessage has 1086 | // to translate the message into a resolved promise or a rejected 1087 | // promise). 1088 | 1089 | 1090 | const sendPromisedResult = promise => { 1091 | promise.then(msg => { 1092 | // send the message value. 1093 | sendResponse(msg); 1094 | }, error => { 1095 | // Send a JSON representation of the error if the rejected value 1096 | // is an instance of error, or the object itself otherwise. 1097 | let message; 1098 | 1099 | if (error && (error instanceof Error || typeof error.message === "string")) { 1100 | message = error.message; 1101 | } else { 1102 | message = "An unexpected error occurred"; 1103 | } 1104 | 1105 | sendResponse({ 1106 | __mozWebExtensionPolyfillReject__: true, 1107 | message 1108 | }); 1109 | }).catch(err => { 1110 | // Print an error on the console if unable to send the response. 1111 | console.error("Failed to send onMessage rejected reply", err); 1112 | }); 1113 | }; // If the listener returned a Promise, send the resolved value as a 1114 | // result, otherwise wait the promise related to the wrappedSendResponse 1115 | // callback to resolve and send it as a response. 1116 | 1117 | 1118 | if (isResultThenable) { 1119 | sendPromisedResult(result); 1120 | } else { 1121 | sendPromisedResult(sendResponsePromise); 1122 | } // Let Chrome know that the listener is replying. 1123 | 1124 | 1125 | return true; 1126 | }; 1127 | }); 1128 | 1129 | const wrappedSendMessageCallback = ({ 1130 | reject, 1131 | resolve 1132 | }, reply) => { 1133 | if (extensionAPIs.runtime.lastError) { 1134 | // Detect when none of the listeners replied to the sendMessage call and resolve 1135 | // the promise to undefined as in Firefox. 1136 | // See https://github.com/mozilla/webextension-polyfill/issues/130 1137 | if (extensionAPIs.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { 1138 | resolve(); 1139 | } else { 1140 | reject(extensionAPIs.runtime.lastError); 1141 | } 1142 | } else if (reply && reply.__mozWebExtensionPolyfillReject__) { 1143 | // Convert back the JSON representation of the error into 1144 | // an Error instance. 1145 | reject(new Error(reply.message)); 1146 | } else { 1147 | resolve(reply); 1148 | } 1149 | }; 1150 | 1151 | const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => { 1152 | if (args.length < metadata.minArgs) { 1153 | throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); 1154 | } 1155 | 1156 | if (args.length > metadata.maxArgs) { 1157 | throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); 1158 | } 1159 | 1160 | return new Promise((resolve, reject) => { 1161 | const wrappedCb = wrappedSendMessageCallback.bind(null, { 1162 | resolve, 1163 | reject 1164 | }); 1165 | args.push(wrappedCb); 1166 | apiNamespaceObj.sendMessage(...args); 1167 | }); 1168 | }; 1169 | 1170 | const staticWrappers = { 1171 | runtime: { 1172 | onMessage: wrapEvent(onMessageWrappers), 1173 | onMessageExternal: wrapEvent(onMessageWrappers), 1174 | sendMessage: wrappedSendMessage.bind(null, "sendMessage", { 1175 | minArgs: 1, 1176 | maxArgs: 3 1177 | }) 1178 | }, 1179 | tabs: { 1180 | sendMessage: wrappedSendMessage.bind(null, "sendMessage", { 1181 | minArgs: 2, 1182 | maxArgs: 3 1183 | }) 1184 | } 1185 | }; 1186 | const settingMetadata = { 1187 | clear: { 1188 | minArgs: 1, 1189 | maxArgs: 1 1190 | }, 1191 | get: { 1192 | minArgs: 1, 1193 | maxArgs: 1 1194 | }, 1195 | set: { 1196 | minArgs: 1, 1197 | maxArgs: 1 1198 | } 1199 | }; 1200 | apiMetadata.privacy = { 1201 | network: { 1202 | "*": settingMetadata 1203 | }, 1204 | services: { 1205 | "*": settingMetadata 1206 | }, 1207 | websites: { 1208 | "*": settingMetadata 1209 | } 1210 | }; 1211 | return wrapObject(extensionAPIs, staticWrappers, apiMetadata); 1212 | }; 1213 | 1214 | if (typeof chrome != "object" || !chrome || !chrome.runtime || !chrome.runtime.id) { 1215 | throw new Error("This script should only be loaded in a browser extension."); 1216 | } // The build process adds a UMD wrapper around this file, which makes the 1217 | // `module` variable available. 1218 | 1219 | 1220 | module.exports = wrapAPIs(chrome); 1221 | } else { 1222 | module.exports = browser; 1223 | } 1224 | }); 1225 | //# sourceMappingURL=browser-polyfill.js.map -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Obsidian Clipper", 3 | "version": "0.5.0", 4 | "manifest_version": 3, 5 | "description": "A small chrome plugin that let's you easily clip things to Obsidian", 6 | "homepage_url": "https://github.com/jplattel/obsidian-clipper", 7 | "background": { 8 | "service_worker": "background.js", 9 | "type": "module" 10 | }, 11 | "options_ui": { 12 | "page": "options.html", 13 | "open_in_tab": true 14 | }, 15 | "action": { 16 | "default_title": "Clip selection to Obsidian" 17 | }, 18 | "permissions": [ 19 | "activeTab", 20 | "clipboardWrite", 21 | "storage", 22 | "scripting" 23 | ], 24 | "host_permissions": [ 25 | "https://jplattel.github.io/obsidian-clipper/clip.html" 26 | ], 27 | "web_accessible_resources": [ 28 | { 29 | "resources": ["/lib/*"], 30 | "matches": [""] 31 | } 32 | ], 33 | "icons": { 34 | "16": "icons/favicon-16x16.png", 35 | "48": "icons/favicon-48x48.png", 36 | "128": "icons/favicon-128x128.png" 37 | }, 38 | "commands": { 39 | "_execute_browser_action": { 40 | "suggested_key": { 41 | "default": "Ctrl+Shift+O" 42 | } 43 | } 44 | }, 45 | "applications": { 46 | "gecko": { 47 | "id": "j@jplattel.nl", 48 | "strict_min_version": "53.0" 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/options.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: #202020; 3 | box-sizing: border-box; 4 | font-family: Arial, Helvetica, sans-serif; 5 | } 6 | 7 | main{ 8 | margin: 0 auto; 9 | display: block; 10 | width: 900px; 11 | color: white; 12 | font-size: 20px; 13 | text-align: center; 14 | padding-bottom: 50px; 15 | } 16 | 17 | p.description{ 18 | text-align: left; 19 | color: #999; 20 | line-height: 140%; 21 | } 22 | a { 23 | color: #7f6df2; 24 | text-decoration: none; 25 | } 26 | a:hover { 27 | color: #8875ff; 28 | text-decoration: underline; 29 | } 30 | 31 | .form-group label, .form-check label{ 32 | margin-top: 20px; 33 | text-align: left; 34 | float: left; 35 | margin-bottom: 10px; 36 | } 37 | .form-group label small, .form-check label small{ 38 | font-size: 14px; 39 | padding: 3px; 40 | border-radius: 4px; 41 | background-color: rgb(54, 54, 54); 42 | color: #7e7e7e 43 | } 44 | .form-group{ 45 | clear: both; 46 | } 47 | .form-text { 48 | float: left; 49 | } 50 | 51 | .grey{ 52 | color: #7e7e7e 53 | } 54 | 55 | #reset_format{ 56 | float: right; 57 | display: inline; 58 | cursor: pointer; 59 | } 60 | 61 | #select_as_markdown, #clip_as_new_note { 62 | /* text-align: left; */ 63 | margin-top: 25px; 64 | margin-right: 10px; 65 | float: left; 66 | width: inherit 67 | } 68 | 69 | 70 | .btn-half{ 71 | width: 50%; 72 | float: left; 73 | } 74 | input, textarea{ 75 | width:100%; 76 | padding: 15px; 77 | box-sizing: border-box; 78 | border: 0px; 79 | font-size: 18px; 80 | margin-bottom: 20px; 81 | border-radius: 3px; 82 | } 83 | 84 | #screencast{ 85 | border: 10px solid #333; 86 | border-radius: 10px; 87 | box-sizing: border-box; 88 | } 89 | 90 | h1 { 91 | font-size: 70px; 92 | line-height: 86px; 93 | margin-bottom: 24px; 94 | color: #f8f8f8; 95 | font-family: 'Playfair', sans-serif; 96 | font-weight: 800; 97 | } 98 | 99 | #status{ 100 | clear: both; 101 | } 102 | 103 | button{ 104 | margin-top: 20px; 105 | font-size: 22px; 106 | padding: 15px; 107 | width: 100%; 108 | border: 0px; 109 | border-radius: 3px; 110 | } 111 | 112 | button:hover{ 113 | background-color: #483699; 114 | color: white; 115 | cursor: pointer; 116 | } 117 | 118 | #status{ 119 | padding-top: 20px; 120 | } 121 | 122 | #faq{ 123 | float: left; 124 | text-align: left; 125 | } 126 | 127 | #faq dt{ 128 | 129 | } 130 | #faq dd{ 131 | margin-left: 0px; 132 | margin-top: 10px; 133 | color: #999; 134 | margin-bottom: 30px; 135 | } 136 | 137 | dt code{ 138 | background-color: #111; 139 | padding: 3px; 140 | color: rgb(237, 237, 102); 141 | border-radius: 4px; 142 | } 143 | 144 | 145 | 146 | 147 | .by-jplattel { 148 | font-family:"Helvetica Neue",sans-serif; 149 | right:0; 150 | bottom:0; 151 | position:fixed; 152 | z-index:100; 153 | border-top-left-radius: 5px; 154 | padding: 6px; 155 | border-top:1px solid #efefef; 156 | border-left:1px solid #efefef; 157 | background:#fff; 158 | color:#6f6f6f; 159 | text-decoration:none; 160 | } 161 | .by-jplattel:hover { 162 | background:#fff; 163 | color:#7f6df2; 164 | } 165 | .by-jplattel img { 166 | border-radius:100%; 167 | width:22px; 168 | vertical-align:middle; 169 | } 170 | .by-jplattel p { 171 | margin:0; 172 | vertical-align:middle; 173 | display:inline; 174 | margin-left:7px; 175 | font-weight:500; 176 | font-size:14px; 177 | } -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Obsidian Clipper 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |

Obsidian Clipper

15 |
16 |

An unoffical clipper

17 | 18 |

19 | This is an unofficial clipper for Obsidian that allows you to easily clip a selection to a note in 20 | Obsidian. Made by Joost Plattel. 21 | This plugin is available as open-source on Github. 22 |

23 | 24 |

At this page you can configure the vault & note to append the clip to. Please test the configuration

25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 | 33 | 37 |
38 | 39 |
40 | 41 | 42 | Use {date}, {datetime}, {year}, {month}, {day}, {time}, {title} and {zettel} as placeholders 43 |
44 | 45 |
46 | 47 | 48 | 51 | Use {clip}, {date}, {datetime}, {year}, {month}, {day}, {time}, {title}, {zettel}, {og:image} and {url} as placeholders 52 |
53 | 54 |
55 | 56 | 57 |
58 | 59 | 60 |
61 | 62 | 63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 | 71 |
72 | 73 |
74 | 75 | 76 | 77 | 78 |
79 | 80 | 81 | 82 | 83 |

by jplattel

84 | 85 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | 2 | // Saves options to chrome.storage 3 | function saveOptions() { 4 | var obsidianVaultName = document.getElementById('obsidian_vault_name').value; 5 | var obsidianNoteName = document.getElementById('obsidian_note_name').value; 6 | var obsidianNoteFormat = document.getElementById('obsidian_note_format').value; 7 | var selectAsMarkdown = document.getElementById('select_as_markdown').checked; 8 | var clipAsNewNote = document.getElementById('clip_as_new_note').checked; 9 | var datetimeFormat = document.getElementById('datetime_format').value; 10 | var dateFormat = document.getElementById('date_format').value; 11 | var timeFormat = document.getElementById('time_format').value; 12 | 13 | chrome.storage.sync.set({ 14 | obsidianVaultName: obsidianVaultName, 15 | obsidianNoteName: obsidianNoteName, 16 | selectAsMarkdown, selectAsMarkdown, 17 | obsidianNoteFormat, obsidianNoteFormat, 18 | clipAsNewNote: clipAsNewNote, 19 | datetimeFormat: datetimeFormat, 20 | dateFormat: dateFormat, 21 | timeFormat: timeFormat, 22 | }, function() { 23 | // Update status to let user know options were saved. 24 | var status = document.getElementById('status'); 25 | status.textContent = 'Options saved.'; 26 | setTimeout(function() { 27 | status.textContent = ''; 28 | }, 750); 29 | }); 30 | } 31 | 32 | // Restores select box and checkbox state using the preferences 33 | // stored in chrome.storage. 34 | function restoreOptions() { 35 | chrome.storage.sync.get({ 36 | obsidianVaultName: 'obsidian', 37 | obsidianNoteName: 'Chrome Clippings', 38 | selectAsMarkdown: false, 39 | obsidianNoteFormat: `> {clip}, 40 | 41 | Clipped from [{title}]({url}) at {date}.`, 42 | clipAsNewNote: true, 43 | datetimeFormat: "YYYY-MM-DD HH:mm:ss", 44 | dateFormat: "YYYY-MM-DD", 45 | timeFormat: "HH:mm:ss", 46 | }, function(options) { 47 | document.getElementById('obsidian_vault_name').value = options.obsidianVaultName; 48 | document.getElementById('obsidian_note_name').value = options.obsidianNoteName; 49 | document.getElementById('obsidian_note_format').value = options.obsidianNoteFormat; 50 | document.getElementById('select_as_markdown').checked = options.selectAsMarkdown; 51 | document.getElementById('clip_as_new_note').checked = options.clipAsNewNote; 52 | document.getElementById('datetime_format').value = options.datetimeFormat; 53 | document.getElementById('date_format').value = options.dateFormat; 54 | document.getElementById('time_format').value = options.timeFormat; 55 | }); 56 | } 57 | 58 | function resetFormat() { 59 | document.getElementById('obsidian_note_format').value = `> {clip} 60 | Clipped from [{title}]({url}) at {date}.` 61 | } 62 | 63 | async function testClipping() { 64 | // Save settings first 65 | saveOptions(); 66 | 67 | // Run the clipper 68 | const clipper = await import(chrome.runtime.getURL('lib/clip.js')); 69 | await clipper.createTest() 70 | } 71 | 72 | document.addEventListener('DOMContentLoaded', restoreOptions); 73 | document.getElementById('save').addEventListener('click', saveOptions); 74 | document.getElementById('reset').addEventListener('click', resetFormat); 75 | document.getElementById('test').addEventListener('click', testClipping); -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | // So Much For Subtlety 2 | ; (async () => { 3 | // Run the clipper 4 | const clipper = await import(chrome.runtime.getURL('lib/clip.js')); 5 | await clipper.create() 6 | })(); 7 | -------------------------------------------------------------------------------- /web-ext-artifacts/obsidian_clipper-0.5.0.xpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jplattel/obsidian-clipper/dd71904e867fb6b3f19e645e443100a086c58af6/web-ext-artifacts/obsidian_clipper-0.5.0.xpi --------------------------------------------------------------------------------