├── .eslintrc.yaml ├── .github └── workflows │ ├── build.yml │ └── eslint.yml ├── .gitignore ├── .gitmodules ├── .yarnrc ├── LICENSE ├── Makefile ├── README.md ├── ext ├── background.js ├── builder.js ├── copy.svg ├── frontmatter.js ├── inject.js ├── markdown-mark.svg ├── menu.css ├── menu.png ├── mermaid.png ├── onboarding.css ├── onboarding.html ├── onboarding.js ├── options.css ├── options.html ├── options.js ├── page_action.png ├── renderer.js ├── spinner.css ├── sss │ ├── github.css │ ├── print.css │ └── sss.css ├── toolbar.png ├── view-md.css ├── view-md.html ├── view-md.js └── worker.js ├── manifest.json ├── package.json ├── test ├── LICENSE ├── MarkdownSample.PNG ├── sub │ └── hello-sub.md ├── test-file.md └── test-nobom.md └── yarn.lock /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 2022 3 | sourceType: script 4 | 5 | env: 6 | es6: true 7 | browser: true 8 | 9 | plugins: 10 | - no-unsanitized 11 | 12 | rules: 13 | constructor-super: error 14 | for-direction: error 15 | getter-return: error 16 | no-async-promise-executor: error 17 | no-case-declarations: error 18 | no-class-assign: error 19 | no-compare-neg-zero: error 20 | no-cond-assign: [error, except-parens] 21 | no-const-assign: error 22 | no-constant-condition: error 23 | no-control-regex: error 24 | no-debugger: error 25 | no-delete-var: error 26 | no-dupe-args: error 27 | no-dupe-class-members: error 28 | no-dupe-else-if: error 29 | no-dupe-keys: error 30 | no-duplicate-case: error 31 | no-empty: 32 | - error 33 | - allowEmptyCatch: true 34 | no-empty-character-class: error 35 | no-empty-pattern: error 36 | no-ex-assign: error 37 | no-extra-boolean-cast: error 38 | no-extra-semi: error 39 | no-fallthrough: error 40 | no-func-assign: error 41 | no-global-assign: error 42 | no-import-assign: error 43 | no-inner-declarations: error 44 | no-invalid-regexp: error 45 | no-irregular-whitespace: 46 | - error 47 | - skipRegExps: true 48 | no-misleading-character-class: error 49 | no-mixed-spaces-and-tabs: 50 | - error 51 | - smart-tabs 52 | no-new-symbol: error 53 | no-obj-calls: error 54 | no-octal: error 55 | no-prototype-builtins: error 56 | no-redeclare: error 57 | no-regex-spaces: error 58 | no-self-assign: error 59 | no-setter-return: error 60 | no-shadow-restricted-names: error 61 | no-sparse-arrays: error 62 | no-this-before-super: error 63 | no-undef: error 64 | no-unexpected-multiline: error 65 | no-unreachable: error 66 | no-unsafe-finally: error 67 | no-unsafe-negation: error 68 | no-unused-labels: error 69 | no-unused-vars: warn 70 | no-useless-catch: error 71 | no-useless-escape: error 72 | no-with: error 73 | require-yield: error 74 | use-isnan: error 75 | valid-typeof: error 76 | radix: error 77 | no-console: 78 | - error 79 | - allow: ["warn", "error"] 80 | no-promise-executor-return: error 81 | no-extra-parens: 82 | - error 83 | - all 84 | - conditionalAssign: false 85 | no-template-curly-in-string: error 86 | no-var: error 87 | prefer-const: warn 88 | prefer-template: warn 89 | prefer-destructuring: warn 90 | prefer-arrow-callback: warn 91 | prefer-numeric-literals: warn 92 | prefer-spread: warn 93 | strict: 94 | - error 95 | - global 96 | consistent-return: warn 97 | curly: error 98 | eqeqeq: error 99 | dot-notation: error 100 | dot-location: 101 | - error 102 | - property 103 | no-caller: error 104 | no-constructor-return: error 105 | no-empty-function: error 106 | no-eq-null: error 107 | no-eval: error 108 | guard-for-in: error 109 | no-extra-bind: error 110 | no-extra-label: error 111 | no-implied-eval: error 112 | no-invalid-this: error 113 | no-iterator: error 114 | no-labels: error 115 | no-lone-blocks: error 116 | no-multi-str: error 117 | no-new: error 118 | no-new-func: error 119 | no-new-wrappers: error 120 | no-proto: error 121 | no-restricted-properties: error 122 | no-return-assign: error 123 | no-self-compare: error 124 | no-throw-literal: error 125 | no-unused-expressions: 126 | - error 127 | - {allowShortCircuit: true, allowTernary: true} 128 | no-useless-call: error 129 | no-useless-concat: error 130 | no-void: error 131 | no-warning-comments: warn 132 | prefer-named-capture-group: warn 133 | prefer-promise-reject-errors: error 134 | prefer-regex-literals: error 135 | require-unicode-regexp: error 136 | no-undefined: error 137 | no-use-before-define: 138 | - error 139 | - functions: false 140 | camelcase: 141 | - error 142 | - properties: never 143 | consistent-this: error 144 | new-cap: error 145 | no-array-constructor: error 146 | func-style: 147 | - error 148 | - declaration 149 | - allowArrowFunctions: true 150 | func-names: 151 | - error 152 | - as-needed 153 | func-name-matching: error 154 | max-len: 155 | - error 156 | - 120 157 | max-depth: warn 158 | max-nested-callbacks: warn 159 | max-params: 160 | - warn 161 | - 5 162 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build signed addon on release or beta' 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | listed: 9 | description: 'Publish release on AMO' 10 | required: false 11 | type: boolean 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: current 20 | 21 | - name: Install dependencies 22 | run: | 23 | npm i -g web-ext yarn 24 | echo "`npm config get prefix`/bin" >> $GITHUB_PATH 25 | 26 | - uses: actions/checkout@v3 27 | with: 28 | submodules: true 29 | # We need tags and (some) history for describe 30 | fetch-depth: 0 31 | 32 | - name: Update version number for beta 33 | if: github.event_name != 'release' 34 | env: 35 | WEBEXT_SIGN_ARGS: ${{ secrets.AMO_API_CREDS }} 36 | run: | 37 | # NB. we are limited to a 4-number format and we use semantic versioning: major.minor.patch 38 | # so increment 4th number as release number 39 | addon=markdown-viewer-webext 40 | version=`jq -r .version manifest.json | sed -r 's/(([0-9]+\.){2}[0-9]+).*/\1/'` 41 | curl -sL -X GET -H "Authorization: JWT `make token`" -o versions.json \ 42 | "https://addons.mozilla.org/api/v5/addons/addon/$addon/versions/?filter=all_with_unlisted" 43 | 44 | # Get all releases in this version, get release number (fallback 0), and increment highest by 1 45 | # If no releases are found, last returns null, and null+1 = 1 46 | filter='[.[].results[].version | select(startswith($ver)) | (split(".")[3] // 0) | tonumber] | sort | last | .+1' 47 | next_rel=`jq -r -s --arg ver "$version" "$filter" versions.json` 48 | version=${version}.${next_rel} 49 | 50 | echo "Updating manifest with version=${version}" 51 | cp manifest.json original-manifest.json 52 | jq --arg ver "$version" '.version=$ver' original-manifest.json > manifest.json 53 | 54 | - name: Build extension 55 | run: make build 56 | 57 | - name: Make unsigned release available 58 | uses: actions/upload-artifact@v3 59 | with: 60 | path: artifacts/*.zip 61 | 62 | - name: Generate source zip 63 | run: make source 64 | 65 | - name: Generate source instructions 66 | run: | 67 | tee instructions.md <> $GITHUB_PATH 17 | 18 | - uses: actions/checkout@v3 19 | 20 | - name: Run linter 21 | run: make -k FORMAT=stylish 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /staging/ 2 | /artifacts/ 3 | /lib/* 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "srclib/katex"] 2 | path = srclib/katex 3 | url = https://github.com/katex/katex 4 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --modules-folder lib 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Keith L Robertson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | ############################################################################## 25 | 26 | This Sofware is based on: 27 | 28 | ------------------------------------------------------------------------------ 29 | 30 | Original markdown-viewer: i.e. some of ext/content.js (https://github.com/Thiht/markdown-viewer) 31 | 32 | Copyright (c) 2012-2013 Rousseau Thibaut 33 | 34 | Permission is hereby granted, free of charge, to any person obtaining a copy 35 | of this software and associated documentation files (the "Software"), to deal 36 | in the Software without restriction, including without limitation the rights 37 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 38 | copies of the Software, and to permit persons to whom the Software is 39 | furnished to do so, subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all 42 | copies or substantial portions of the Software. 43 | 44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 45 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 46 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 47 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 48 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 49 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 50 | SOFTWARE. 51 | 52 | ------------------------------------------------------------------------------ 53 | 54 | highlightjs (https://github.com/isagalaev/highlight.js) 55 | 56 | Copyright (c) 2006, Ivan Sagalaev 57 | All rights reserved. 58 | Redistribution and use in source and binary forms, with or without 59 | modification, are permitted provided that the following conditions are met: 60 | 61 | * Redistributions of source code must retain the above copyright 62 | notice, this list of conditions and the following disclaimer. 63 | * Redistributions in binary form must reproduce the above copyright 64 | notice, this list of conditions and the following disclaimer in the 65 | documentation and/or other materials provided with the distribution. 66 | * Neither the name of highlight.js nor the names of its contributors 67 | may be used to endorse or promote products derived from this software 68 | without specific prior written permission. 69 | 70 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY 71 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 72 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 73 | DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 74 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 75 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 76 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 77 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 78 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 79 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 80 | 81 | ------------------------------------------------------------------------------ 82 | 83 | markdown-it (https://github.com/markdown-it/markdown-it) 84 | 85 | Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin. 86 | 87 | Permission is hereby granted, free of charge, to any person 88 | obtaining a copy of this software and associated documentation 89 | files (the "Software"), to deal in the Software without 90 | restriction, including without limitation the rights to use, 91 | copy, modify, merge, publish, distribute, sublicense, and/or sell 92 | copies of the Software, and to permit persons to whom the 93 | Software is furnished to do so, subject to the following 94 | conditions: 95 | 96 | The above copyright notice and this permission notice shall be 97 | included in all copies or substantial portions of the Software. 98 | 99 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 100 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 101 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 102 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 103 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 104 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 105 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 106 | OTHER DEALINGS IN THE SOFTWARE. 107 | 108 | ------------------------------------------------------------------------------ 109 | 110 | markdown-it-checkbox (https://github.com/mcecot/markdown-it-checkbox) 111 | 112 | The MIT License (MIT) 113 | 114 | Copyright (c) 2015 Markus Cecot 115 | 116 | Permission is hereby granted, free of charge, to any person obtaining a copy 117 | of this software and associated documentation files (the "Software"), to deal 118 | in the Software without restriction, including without limitation the rights 119 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 120 | copies of the Software, and to permit persons to whom the Software is 121 | furnished to do so, subject to the following conditions: 122 | 123 | The above copyright notice and this permission notice shall be included in 124 | all copies or substantial portions of the Software. 125 | 126 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 127 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 128 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 129 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 130 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 131 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 132 | THE SOFTWARE. 133 | 134 | ------------------------------------------------------------------------------ 135 | 136 | markdown-mark (https://github.com/dcurtis/markdown-mark) 137 | 138 | DEDICATED TO THE PUBLIC DOMAIN BY DUSTIN CURTIS 139 | 140 | The Markdown Mark has been dedicated to the public domain. It is protected by the Creative Commons CC0 Universal Public Domain Dedication license. You can read the entire license below or at http://creativecommons.org/publicdomain/zero/1.0/deed.en. 141 | 142 | CC0 UNIVERSAL PUBLIC DOMAIN DEDICATION LICENSE 143 | 144 | Statement of Purpose 145 | 146 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). 147 | 148 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. 149 | 150 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 151 | 152 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: 153 | a. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; 154 | b. moral rights retained by the original author(s) and/or performer(s); 155 | c. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; 156 | rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; 157 | d. rights protecting the extraction, dissemination, use and reuse of data in a Work; 158 | e. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and 159 | f. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 160 | 161 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 162 | 163 | 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 164 | 165 | 4. Limitations and Disclaimers. 166 | 167 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. 168 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. 169 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. 170 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. 171 | 172 | ------------------------------------------------------------------------------ 173 | 174 | SSS Simple Style Sheet (https://github.com/Thiht/sss) 175 | 176 | The MIT License (MIT) 177 | 178 | Copyright (c) 2016 Thibaut Rousseau 179 | 180 | Permission is hereby granted, free of charge, to any person obtaining a copy 181 | of this software and associated documentation files (the "Software"), to deal 182 | in the Software without restriction, including without limitation the rights 183 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 184 | copies of the Software, and to permit persons to whom the Software is 185 | furnished to do so, subject to the following conditions: 186 | 187 | The above copyright notice and this permission notice shall be included in all 188 | copies or substantial portions of the Software. 189 | 190 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 191 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 192 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 193 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 194 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 195 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 196 | SOFTWARE. 197 | 198 | ------------------------------------------------------------------------------ 199 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ESLINT:=eslint --resolve-plugins-relative-to "$(shell npm config get prefix)/lib" 2 | FORMAT:=unix 3 | 4 | ENV=webextensions 5 | lint-worker: ENV=worker 6 | 7 | lint-worker: GLOBALS=Renderer 8 | lint-renderer: GLOBALS=markdownit fancyList texmath katex mermaid hljs markdownitCheckbox markdownitEmoji markdownitFootnote frontmatter yamltitle 9 | lint-builder: GLOBALS=Renderer mermaid 10 | lint-onboarding: GLOBALS=webext 11 | lint-inject lint-view-md: GLOBALS=webext renderInIframe renderInDocument addExtensionStylesheet pluginDefaults mermaid 12 | 13 | TARGETS:=$(patsubst ext/%.js,lint-%,$(wildcard ext/*.js)) 14 | lint: ${TARGETS} 15 | 16 | lint-%: ext/%.js 17 | @${ESLINT} -c .eslintrc.yaml -f ${FORMAT} --env=${ENV} $(addprefix --global=,${GLOBALS}) $< 18 | 19 | 20 | BUILDDIR:=staging 21 | OUTDIR:=artifacts 22 | VERSION:=$(shell jq -r .version manifest.json) 23 | SOURCE:=${OUTDIR}/markdown_viewer_webext-${VERSION}-src.zip 24 | TARGET:=${OUTDIR}/markdown_viewer_webext-${VERSION}.zip 25 | SIGNED:=${OUTDIR}/markdown_viewer_webext-${VERSION}.xpi 26 | 27 | FILES:=manifest.json \ 28 | $(filter-out ext/sss,$(wildcard ext/*)) \ 29 | lib/@highlightjs/cdn-assets/highlight.min.js \ 30 | $(wildcard lib/@highlightjs/cdn-assets/styles/*.min.css) \ 31 | $(wildcard lib/@highlightjs/cdn-assets/styles/base16/*.min.css) \ 32 | lib/markdown-it/dist/markdown-it.min.js \ 33 | lib/markdown-it-checkbox/dist/markdown-it-checkbox.js \ 34 | lib/markdown-it-emoji/dist/markdown-it-emoji.js \ 35 | lib/markdown-it-footnote/dist/markdown-it-footnote.js \ 36 | lib/markdown-it-fancy-lists/markdown-it-fancy-lists.js \ 37 | lib/markdown-it-texmath/texmath.js \ 38 | lib/markdown-it-texmath/css/texmath.css \ 39 | lib/mermaid/dist/mermaid.min.js \ 40 | srclib/katex/dist/katex.min.js \ 41 | srclib/katex/dist/katex.min.css \ 42 | ext/sss/sss.css \ 43 | ext/sss/print.css \ 44 | ext/sss/github.css 45 | 46 | STAGED_FILES:=$(addprefix ${BUILDDIR}/,${FILES}) 47 | 48 | ${BUILDDIR}: 49 | @mkdir -p ${BUILDDIR} 50 | 51 | $(filter lib/%,${FILES}): 52 | @yarn install --modules-folder lib/ --check-files 53 | 54 | srclib/katex/dist/katex.min.%: 55 | @cd srclib/katex && yarn install && USE_TTF=false USE_WOFF=false USE_WOFF2=false yarn build 56 | 57 | ${BUILDDIR}/%: % 58 | @mkdir -p "$(@D)" && cp "$<" "$@" 59 | 60 | build: ${TARGET} 61 | beta: ${SIGNED} 62 | release: ${SIGNED} 63 | source: ${SOURCE} 64 | 65 | # make beta -> private, make release -> public 66 | CHANNEL:=unlisted 67 | release: CHANNEL=listed 68 | 69 | # if running web-ext sign without credentials set, prompt 70 | ifneq ($(filter release beta ${SIGNED} token,$(MAKECMDGOALS)),) 71 | ifndef WEBEXT_SIGN_ARGS 72 | WEBEXT_SIGN_ARGS=$(shell kwallet-query -r 'API credentials for addons.mozilla.org' kdewallet) --channel ${CHANNEL} 73 | ifeq ($(firstword ${WEBEXT_SIGN_ARGS}),--channel) 74 | $(error API credentials for addons.mozilla.org not provided) 75 | endif 76 | endif 77 | endif 78 | 79 | ${OUTDIR}/%-src.zip: 80 | @git ls-files -z '*.json' yarn.lock ext/ | xargs -0 zip "$@" 81 | 82 | ${OUTDIR}/%.zip: ${STAGED_FILES} | ${BUILDDIR} 83 | @web-ext build -s "${BUILDDIR}" -a "${OUTDIR}" -o 84 | 85 | ${OUTDIR}/%.xpi: ${STAGED_FILES} | ${BUILDDIR} 86 | @web-ext sign -s "${BUILDDIR}" -a "${OUTDIR}" ${WEBEXT_SIGN_ARGS} 87 | 88 | clean: 89 | @rm -rf ${BUILDDIR} ${OUTDIR} 90 | 91 | token: 92 | @echo ${WEBEXT_SIGN_ARGS} | (read _ user _ secret;\ 93 | b64enc() { base64 -w0 | sed 'y|+/|-_|;s/=*$$//' ; } ;\ 94 | head=`printf '{"alg":"HS256","typ":"JWT"}' | b64enc`; \ 95 | data=`jq -nrc --arg user $$user --arg uuid "$(shell uuidgen)" --arg time "$(shell date +%s)" \ 96 | '($$time | tonumber) as $$time | {iss: $$user, jti: $$uuid, iat: $$time, exp: ($$time + 300)}' | b64enc` ; \ 97 | sign=`printf '%s.%s' "$$head" "$$data" | openssl dgst -sha256 -hmac "$$secret" -binary | b64enc` ;\ 98 | printf '%s.%s.%s' "$$head" "$$data" "$$sign") 99 | 100 | 101 | .PHONY: lint prep build source clean stage sign release 102 | .INTERMEDIATE: ${STAGED_FILES} 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-viewer 2 | 3 | Markdown (.md file) Viewer extension. 4 | Displays markdown documents beautified in your browser. 5 | 6 | Markdown is a lightweight markup language which uses plain text to describe formatting information, such as `# Heading`, `1. numbered lists`, `**bold**`, etc. 7 | This add-on identifies markdown documents by the extension in the URL (one of .markdown, .md, .mdown, .mdwn, .mkd, .mkdn). 8 | When you navigate to a markdown document, if the content is plain text, not already styled (by GitHub for example), this add-on formats 9 | it into HTML (with headings, ordered lists, bold text, etc.) using markup from the document and displays it directly in your browser. 10 | 11 | This add-on was initially a XUL/XPCOM addon by [@Thiht](https://github.com/Thiht/markdown-viewer), rewritten for the [WebExtensions API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) by [@KeithLRobertson](https://github.com/KeithLRobertson). 12 | 13 | ## Unicode Characters 14 | 15 | So, non-ASCII characters in your document aren't displaying correctly? Special characters like " **á** " appear as " **á** "? 16 | This is a problem of character encoding, which concerns converting a file (a sequence of octets) into text (a sequence of characters) and back. 17 | By the time Firefox activates the Markdown Viewer Web Extension for your document, the file is already converted into text, correctly or incorrectly. 18 | If the file begins with a Byte Order Marker (BOM), a pseudo-character which tells how the file is encoded, then Firefox will see it and use that encoding. 19 | Otherwise, Firefox may have decoded the file with the wrong encoding. 20 | Although UTF-8 is the modern _de facto_ standard encoding, Firefox defaults to using your system's regional encoding, yielding incorrect results. 21 | 22 | Some extensions have worked around this by re-loading the file and explicitly decoding it as UTF-8, but it is a pain to do so. 23 | Plus, this extension is intended to be a light and simple wrapper around the markdown-to-HTML converter, markdown-it. 24 | 25 | When the markdown document is obtained from a web site, the site should return a Content-Type header which specifies the encoding. 26 | If it does not and the file lacks a BOM, you're out of luck. Contact the web site owner. 27 | 28 | When the markdown is loaded from a local file, there are no HTML headers to lean on. 29 | Fortunately, Firefox now has a setting which allows specifying that you want to use UTF-8 for local files without a BOM. 30 | Go to about:config, search for `intl.charset.fallback.utf8_for_file`, and set its value to `true`. It should look better now. 31 | 32 | ## Viewing Original Markdown 33 | 34 | To keep it simple, the extension does not support on and off states. 35 | If the document has one of the supported extensions, it should convert. 36 | Some web sites however, like raw.githubusercontent.com, return CORS headers, in which case Firefox will not inject this extension's content scripts, so it cannot convert the document. 37 | On those pages, the extension will display a button in your address bar, and when clicked, take you to a page that can render the document. 38 | 39 | If you're viewing pretty markdown and you want to see the original source text, right-click and select "View Page Source". 40 | On the extension rendering page, you can instead open the drop-down menu on the page and click “View Source”. 41 | (Make sure you don't have any text selected to see that option.) 42 | Of course, you can also (Ctrl-S) save the document to a file and open it in any text editor. 43 | 44 | ## Saving Converted Markdown 45 | 46 | If you would like to save the HTML-converted text, it is possible to do so in the desktop versions of Firefox. 47 | * Open the drop-down menu on the page 48 | * Select “Download HTML” 49 | 50 | ## Custom Appearance 51 | 52 | You don't like how the styled markdown looks? 53 | There are currently 2 default styles to choose from (not from this repo − under MIT): 54 | - [SSS (Simple Style Sheet)](https://github.com/Thiht/sss) 55 | - [github markdown css](https://github.com/sindresorhus/github-markdown-css) 56 | 57 | There are also a number of [highlight.js](https://github.com/highlightjs/highlight.js) styles to choose from. 58 | Styles ending in `auto` will automatically pick the `dark` and `light` variant based on the `prefers-color-scheme` value of your browser. 59 | 60 | Using CSS, you can further customize the appearance any way you like it. 61 | View Add-ons (about:addons), and click Options for the Markdown Viewer. 62 | There is a box to enter your Custom CSS text. (Sorry, there is not currently a "user-friendly" drop-down option.) 63 | As the instructions there state, click or tab out of the text box to save your changes. 64 | The CSS is not validated, so you may want to create your CSS outside the options page (or lift it from a site that you like), and paste it in. 65 | If you have entered some changes that you don't want to keep, refresh the options page to discard changes. 66 | 67 | For example to assign a maximum width root element in the styled markdown: 68 | 69 | ```css 70 | .markdownRoot { 71 | margin: 0 auto; 72 | max-width: 888px; 73 | padding: 45px; 74 | border: lightgrey thin solid; 75 | } 76 | ``` 77 | 78 | ## "Can You Add This Feature?" 79 | 80 | This is an open-source project with the most liberal usage and change license there is. 81 | Please feel free to modify it to meet your needs, and to contribute your improvement back to the community. 82 | 83 | Think only experts can do that? 84 | Although [Keith](https://github.com/KeithLRobertson) is a programmer, he could only just make his way around JavaScript, and was a total beginner with the WebExtension browser plugin API. 85 | But Keith needed a markdown viewer, and the plugin API which the existing add-on had used was deprecated and removed, so he adapted that add-on to the new technology. 86 | 87 | The feature set is expected to grow, not from a single author, but by the contributions of users like you who need some additional capabilities. 88 | Several features have been added by community contributors who needed them, which include: 89 | 90 | * checkbox support, by [@RaffaeleMorganti](https://github.com/RaffaeleMorganti) 91 | * overriding CSP headers (and secure rendering) by [@KOLANICH](https://github.com/KOLANICH) 92 | * anchored headers 93 | * reload maintaining scroll location 94 | * custom CSS 95 | * many documentation fixes by various authors 96 | 97 | Many thanks to them and to our future contributors. Pull requests are welcomed. 98 | 99 | ## To Build The Extension 100 | 101 | * Required: 102 | * [nodejs](https://nodejs.org/) with npm or preferably yarn 103 | * [web-ext](https://github.com/mozilla/web-ext/) (npm install -g web-ext) 104 | * Clone the repo: 105 | ```sh 106 | git clone https://github.com/Cimbali/markdown-viewer.git 107 | cd markdown-viewer 108 | ``` 109 | * Get all pre-built javascript dependencies: 110 | ```sh 111 | yarn install 112 | ``` 113 | * Build katex from source (to avoid issues with fonts) 114 | ```sh 115 | git submodule update --init 116 | cd srclib/katex 117 | yarn install 118 | 119 | export USE_TTF=false 120 | export USE_WOFF=false 121 | export USE_WOFF2=false 122 | yarn build 123 | cd ../.. 124 | ``` 125 | 126 | ## To Test The Extension 127 | 128 | Firefox won't install the generated `.zip` file permanently until it's signed by Mozilla. 129 | You can test any changes using the cloned project files. 130 | 131 | Before that you’ll need to build katex from source (see [above](#to-build-the-extension)). 132 | 133 | * In a command prompt, navigate to the project root folder (containing the `manifest.json`) and run `web-ext run`. 134 | * Or to install the extension [temporarily in Firefox](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Temporary_Installation_in_Firefox): 135 | * First, uninstall this add-on if you have it already 136 | * Navigate to "about:debugging" 137 | * Click "Load Temporary Add-on" 138 | * Navigate to the project root folder and open the `manifest.json` file. 139 | 140 | ## Support for local files on Linux and macOS 141 | 142 | Firefox may not know how to handle markdown files by default (see #2), and if opening them it may suggest to download them instead, so that you may be unable to either automatically render them or access the address bar (“page action”) button. Until Firefox [implements mime-type handling for extensions](https://bugzilla.mozilla.org/show_bug.cgi?id=1457500), there are a number of possible workarounds for this. Here are the 3 options that work the best: 143 | 144 | 1) Open the page via the extension’s rendering page (since v2): 145 | 146 | The best way to do this is to go to `ext+view-markdown:file:///path/to/file.md`. (Note that you can do this also for online pages.) 147 | Firefox then requires us to have you confirm file access, through a file picker or drag-and-drop. 148 | 149 | Opening a file this way allows to both: 150 | - have a 1-click copy of the file path, that you can quickly copy directly into the file picker, 151 | - have Markdown Viewer remember the file path, e.g. when closing and reopening the tab, so you can see the same prompt again. 152 | 153 | Alternately, you can open the file prompt without providing the path, by clicking the “*Open local markdown file*” button in your toolbar or overflow menu or navigating to `ext+view-markdown:` in your address bar. Markdown Viewer will now never be aware of the actual path to the file. 154 | 155 | 2) Edit Firefox’s private mime types. 156 | 157 | These mime types are stored in a file indicated by `helpers.private_mime_types_file`, by default it is `~/.mime.types`. 158 | Create this file if it does not exist, otherwise edit it, and add the following line: 159 | 160 | ``` 161 | type=text/plain exts=md,mkd,mkdn,mdwn,mdown,markdown, desc="Markdown document" 162 | ``` 163 | 164 | Then restart firefox. 165 | 166 | --- 167 | **Important note:** On some systems, e.g. Ubuntu 21.10 or newer, firefox may be installed with a system like [snap](https://ubuntu.com/core/docs/snaps-in-ubuntu-core), which prevents it from reading files from your disk such as `~/.mime.types`, see [#86](https://github.com/Cimbali/markdown-viewer/issues/86). 168 | 169 | In that case you need to: 170 | - use a directory accessible to firefox, and 171 | - expand the `~` to the full path of your home 172 | 173 | E.g. use as filename and config value: `/home//snap/firefox/common/mime.types` − where `` is your username. 174 | A suitable directory is likely the parent of `.mozilla` in your profile path, which you can find in `about:profiles`. 175 | 176 | --- 177 | 178 | 3) On Linux only, a workaround is to define the MIME type of markdown file extensions as `text/plain`. 179 | 180 | Add the following XML to `~/.local/share/mime/packages/text-markdown.xml`: 181 | ```XML 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | ``` 194 | 195 | Then run 196 | ```bash 197 | $ update-mime-database ~/.local/share/mime 198 | ``` 199 | 200 | See [this SuperUser question](https://superuser.com/questions/696361/how-to-get-the-markdown-viewer-addon-of-firefox-to-work-on-linux) for further reading on the topic. 201 | -------------------------------------------------------------------------------- /ext/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webext = typeof browser === 'undefined' ? chrome : browser; 4 | 5 | // Watch the updates of URLs we requested in the manifest 6 | const urls = browser.runtime.getManifest().page_action.show_matches; 7 | 8 | // The list (and order) of scripts to render a page with injection 9 | const scripts = [ 10 | "lib/markdown-it/dist/markdown-it.min.js", 11 | "lib/markdown-it-checkbox/dist/markdown-it-checkbox.js", 12 | "lib/markdown-it-emoji/dist/markdown-it-emoji.js", 13 | "lib/markdown-it-footnote/dist/markdown-it-footnote.js", 14 | "lib/markdown-it-fancy-lists/markdown-it-fancy-lists.js", 15 | "lib/@highlightjs/cdn-assets/highlight.min.js", 16 | "srclib/katex/dist/katex.min.js", 17 | "lib/markdown-it-texmath/texmath.js", 18 | "lib/mermaid/dist/mermaid.min.js", 19 | "ext/frontmatter.js", 20 | "ext/renderer.js", 21 | "ext/builder.js", 22 | "ext/inject.js" 23 | ]; 24 | 25 | const checks = [ 26 | 'document.body.childNodes.length === 1', 27 | 'document.body.children.length === 1', 28 | 'document.body.children[0].nodeName === "PRE"' 29 | ].join(' && '); 30 | 31 | // Declare all event handlers at the top level 32 | webext.browserAction.onClicked.addListener(() => { 33 | webext.tabs.create({ url: webext.runtime.getURL('/ext/view-md.html') }); 34 | }); 35 | 36 | 37 | async function renderTab(tabId, url, loadReplace) { 38 | const { inject_local: inject = false } = await webext.storage.sync.get('inject_local'); 39 | 40 | if (url.protocol !== 'file:' || inject) { 41 | for (const path of scripts) { 42 | await webext.tabs.executeScript(tabId, { file: `/${path}` }); 43 | } 44 | webext.pageAction.hide(tabId); 45 | } else { 46 | // Default for local files is to redirect to our page 47 | const dest = new URL(webext.runtime.getURL('/ext/view-md.html?file=') + encodeURIComponent(url)); 48 | webext.tabs.update(tabId, {url: dest.href, loadReplace }).catch(console.error); 49 | } 50 | } 51 | 52 | async function tabUpdated(tabId, url) { 53 | try { 54 | const [isText] = await webext.tabs.executeScript(tabId, { code: checks }); 55 | 56 | if (isText) { 57 | renderTab(tabId, url, true); 58 | } 59 | } catch (e) { 60 | // Host permissions are likely not enabled 61 | console.error(e); 62 | return; 63 | } 64 | } 65 | 66 | webext.pageAction.onClicked.addListener(tab => renderTab(tab.id, new URL(tab.url), false)); 67 | 68 | browser.tabs.onUpdated.addListener((tabId, { 'status': update }, { url = '' }) => { 69 | if (update === 'complete' && url) { 70 | tabUpdated(tabId, new URL(url)); 71 | } 72 | }, { urls, properties: ['status'] }); 73 | 74 | 75 | browser.runtime.onInstalled.addListener(({ reason, previousVersion }) => { 76 | const url = browser.runtime.getURL('/ext/onboarding.html'); 77 | if (reason === 'update') { 78 | const prevMajor = parseInt(previousVersion.split('.')[0], 10); 79 | if (prevMajor === 1) { 80 | browser.tabs.create({ url: new URL(`?update=${previousVersion}`, url) }); 81 | } 82 | } 83 | else if (reason === 'install') { 84 | browser.tabs.create({ url }); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /ext/builder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webext = typeof browser === 'undefined' ? chrome : browser; 4 | const headerTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']; 5 | const pluginDefaults = {'hljs': true, 'checkbox': true, 'emojis': true, 'footnotes': false, 'fancy-lists': false, 6 | 'texmath': false, 'mermaid': false}; 7 | 8 | const mdcss = {'default': 'sss', 'github': 'github'} 9 | const hlcss = [ 10 | 'a11y-dark', 'a11y-light', 'a11y-auto', 'agate', 'androidstudio', 'an-old-hope', 'arduino-light', 'arta', 'ascetic', 11 | 'atom-one-dark', 'atom-one-dark-reasonable', 'atom-one-light', 'base16/3024', 'base16/apathy', 'base16/apprentice', 12 | 'base16/ashes', 'base16/atelier-cave-light', 'base16/atelier-cave', 'base16/atelier-cave-auto', 13 | 'base16/atelier-dune-light', 'base16/atelier-dune', 'base16/atelier-dune-auto', 'base16/atelier-estuary-light', 14 | 'base16/atelier-estuary', 'base16/atelier-estuary-auto', 'base16/atelier-forest-light', 'base16/atelier-forest', 15 | 'base16/atelier-forest-auto', 'base16/atelier-heath-light', 'base16/atelier-heath', 'base16/atelier-heath-auto', 16 | 'base16/atelier-lakeside-light', 'base16/atelier-lakeside', 'base16/atelier-lakeside-auto', 17 | 'base16/atelier-plateau-light', 'base16/atelier-plateau', 'base16/atelier-plateau-auto', 18 | 'base16/atelier-savanna-light', 'base16/atelier-savanna', 'base16/atelier-savanna-auto', 19 | 'base16/atelier-seaside-light', 'base16/atelier-seaside', 'base16/atelier-seaside-auto', 20 | 'base16/atelier-sulphurpool-light', 'base16/atelier-sulphurpool', 'base16/atelier-sulphurpool-auto', 'base16/atlas', 21 | 'base16/bespin', 'base16/black-metal-bathory', 'base16/black-metal-burzum', 'base16/black-metal-dark-funeral', 22 | 'base16/black-metal-gorgoroth', 'base16/black-metal-immortal', 'base16/black-metal-khold', 23 | 'base16/black-metal-marduk', 'base16/black-metal-mayhem', 'base16/black-metal-nile', 'base16/black-metal-venom', 24 | 'base16/black-metal', 'base16/brewer', 'base16/bright', 'base16/brogrammer', 'base16/brush-trees-dark', 25 | 'base16/brush-trees', 'base16/chalk', 'base16/circus', 'base16/classic-dark', 'base16/classic-light', 26 | 'base16/classic-auto', 'base16/codeschool', 'base16/colors', 'base16/cupcake', 'base16/cupertino', 'base16/danqing', 27 | 'base16/darcula', 'base16/dark-violet', 'base16/darkmoss', 'base16/darktooth', 'base16/decaf', 28 | 'base16/default-dark', 'base16/default-light', 'base16/default-auto', 'base16/dirtysea', 'base16/dracula', 29 | 'base16/edge-dark', 'base16/edge-light', 'base16/edge-auto', 'base16/eighties', 'base16/embers', 30 | 'base16/equilibrium-dark', 'base16/equilibrium-gray-dark', 'base16/equilibrium-gray-light', 31 | 'base16/equilibrium-gray-auto', 'base16/equilibrium-light', 'base16/espresso', 'base16/eva-dim', 'base16/eva', 32 | 'base16/flat', 'base16/framer', 'base16/fruit-soda', 'base16/gigavolt', 'base16/github', 'base16/google-dark', 33 | 'base16/google-light', 'base16/google-auto', 'base16/grayscale-dark', 'base16/grayscale-light', 34 | 'base16/grayscale-auto', 'base16/green-screen', 'base16/gruvbox-dark-hard', 'base16/gruvbox-dark-medium', 35 | 'base16/gruvbox-dark-pale', 'base16/gruvbox-dark-soft', 'base16/gruvbox-light-hard', 'base16/gruvbox-light-medium', 36 | 'base16/gruvbox-light-soft', 'base16/hardcore', 'base16/harmonic16-dark', 'base16/harmonic16-light', 37 | 'base16/harmonic16-auto', 'base16/heetch-dark', 'base16/heetch-light', 'base16/heetch-auto', 'base16/helios', 38 | 'base16/hopscotch', 'base16/horizon-dark', 'base16/horizon-light', 'base16/horizon-auto', 'base16/humanoid-dark', 39 | 'base16/humanoid-light', 'base16/humanoid-auto', 'base16/ia-dark', 'base16/ia-light', 'base16/ia-auto', 40 | 'base16/icy-dark', 'base16/ir-black', 'base16/isotope', 'base16/kimber', 'base16/london-tube', 'base16/macintosh', 41 | 'base16/marrakesh', 'base16/materia', 'base16/material-darker', 'base16/material-lighter', 42 | 'base16/material-palenight', 'base16/material-vivid', 'base16/material', 'base16/mellow-purple', 43 | 'base16/mexico-light', 'base16/mocha', 'base16/monokai', 'base16/nebula', 'base16/nord', 'base16/nova', 44 | 'base16/ocean', 'base16/oceanicnext', 'base16/one-light', 'base16/onedark', 'base16/outrun-dark', 45 | 'base16/papercolor-dark', 'base16/papercolor-light', 'base16/papercolor-auto', 'base16/paraiso', 'base16/pasque', 46 | 'base16/phd', 'base16/pico', 'base16/pop', 'base16/porple', 'base16/qualia', 'base16/railscasts', 'base16/rebecca', 47 | 'base16/ros-pine-dawn', 'base16/ros-pine-moon', 'base16/ros-pine', 'base16/sagelight', 'base16/sandcastle', 48 | 'base16/seti-ui', 'base16/shapeshifter', 'base16/silk-dark', 'base16/silk-light', 'base16/silk-auto', 49 | 'base16/snazzy', 'base16/solar-flare-light', 'base16/solar-flare', 'base16/solar-flare-auto', 50 | 'base16/solarized-dark', 'base16/solarized-light', 'base16/solarized-auto', 'base16/spacemacs', 'base16/summercamp', 51 | 'base16/summerfruit-dark', 'base16/summerfruit-light', 'base16/summerfruit-auto', 52 | 'base16/synth-midnight-terminal-dark', 'base16/synth-midnight-terminal-light', 53 | 'base16/synth-midnight-terminal-auto', 'base16/tango', 'base16/tender', 'base16/tomorrow-night', 'base16/tomorrow', 54 | 'base16/twilight', 'base16/unikitty-dark', 'base16/unikitty-light', 'base16/unikitty-auto', 'base16/vulcan', 55 | 'base16/windows-10-light', 'base16/windows-10', 'base16/windows-10-auto', 'base16/windows-95-light', 56 | 'base16/windows-95', 'base16/windows-95-auto', 'base16/windows-high-contrast-light', 'base16/windows-high-contrast', 57 | 'base16/windows-high-contrast-auto', 'base16/windows-nt-light', 'base16/windows-nt', 'base16/windows-nt-auto', 58 | 'base16/woodland', 'base16/xcode-dusk', 'base16/zenburn', 'brown-paper', 'codepen-embed', 'color-brewer', 'dark', 59 | 'default', 'devibeans', 'docco', 'far', 'felipec', 'foundation', 'github-dark-dimmed', 'github-dark', 'github', 60 | 'gml', 'googlecode', 'gradient-dark', 'gradient-light', 'gradient-auto', 'grayscale', 'hybrid', 'idea', 61 | 'intellij-light', 'ir-black', 'isbl-editor-dark', 'isbl-editor-light', 'isbl-editor-auto', 'kimbie-dark', 62 | 'kimbie-light', 'kimbie-auto', 'lightfair', 'lioshi', 'magula', 'mono-blue', 'monokai', 'monokai-sublime', 63 | 'night-owl', 'nnfx-dark', 'nnfx-light', 'nnfx-auto', 'nord', 'obsidian', 'panda-syntax-dark', 'panda-syntax-light', 64 | 'panda-syntax-auto', 'paraiso-dark', 'paraiso-light', 'paraiso-auto', 'pojoaque', 'purebasic', 'qtcreator-dark', 65 | 'qtcreator-light', 'qtcreator-auto', 'rainbow', 'routeros', 'school-book', 'shades-of-purple', 'srcery', 66 | 'stackoverflow-dark', 'stackoverflow-light', 'stackoverflow-auto', 'sunburst', 'tokyo-night-dark', 'xt256', 67 | 'tokyo-night-light', 'tokyo-night-auto', 'tomorrow-night-blue', 'tomorrow-night-bright', 'vs2015', 'vs', 'xcode', 68 | ] 69 | 70 | const parser = new DOMParser(); 71 | 72 | // Global flag to remember whether menu is opened on refresh 73 | let showMenu = false; 74 | 75 | 76 | function addStylesheet(doc, href, attributes) { 77 | const link = doc.createElement('link'); 78 | link.rel = 'stylesheet'; 79 | link.type = 'text/css'; 80 | link.href = href; 81 | for (const [attr, val] of Object.entries(attributes || {})) { 82 | link.setAttribute(attr, val); 83 | } 84 | doc.head.appendChild(link); 85 | return link 86 | } 87 | 88 | function addExtensionStylesheet(doc, href, attributes) { 89 | // If injecting (?), need full path to stylesheet 90 | addStylesheet(doc, new URL(href, webext.extension.getURL('/')).href, attributes); 91 | } 92 | 93 | function setExtensionStylesheet(href, sheet) { 94 | // If injecting (?), need full path to stylesheet 95 | const prevURL = new URL(sheet.href); 96 | if (prevURL.protocol === 'blob:') { 97 | URL.revokeObjectURL(prevURL.href); 98 | } 99 | sheet.href = new URL(href, webext.extension.getURL('/')).href; 100 | } 101 | 102 | function setExtensionStylesheetAuto(hrefDark, hrefLight, sheet) { 103 | const sheetContent = ` 104 | @import url("${webext.extension.getURL(hrefLight)}") (prefers-color-scheme: light); 105 | @import url("${webext.extension.getURL(hrefDark)}") (prefers-color-scheme: dark); 106 | ` 107 | const url = URL.createObjectURL(new Blob([sheetContent], {type: "text/css"})); 108 | setExtensionStylesheet(url, sheet); 109 | } 110 | 111 | function addCustomStylesheet(doc, attributes) { 112 | return webext.storage.sync.get({'custom_css': ''}).then(({custom_css: data}) => { 113 | const url = URL.createObjectURL(new Blob([data], {type: "text/css"})); 114 | addStylesheet(doc, url, attributes); 115 | }); 116 | } 117 | 118 | function makeAnchor(node, usedHeaders) { 119 | // From @ChenYingChou https://gist.github.com/asabaylus/3071099#gistcomment-1479328 120 | let anchor = node.textContent.trim().toLowerCase() 121 | // single chars that are removed 122 | .replace(/[`~!@#$%^&*()+=<>?,./:;"'|{}[\]\\–—]/gu, '') // ` 123 | // CJK punctuations that are removed 124 | .replace(/[ 。?!,、;:“”【】()〔〕[]﹃﹄“”‘’﹁﹂—…-~《》〈〉「」]/gu, '') 125 | .replace(/\s+/gu, '-').replace(/-+$/u, ''); 126 | 127 | if (usedHeaders.indexOf(anchor) !== -1) { 128 | let i = 1; 129 | for (; i <= 10; i++) { 130 | if (usedHeaders.indexOf(`${anchor}-${i}`) === -1) { 131 | break; 132 | } 133 | } 134 | anchor = `${anchor}-${i}`; 135 | } 136 | 137 | return anchor; 138 | } 139 | 140 | async function convertLinkToStylesheet(doc, node) { 141 | const style = doc.createElement('style'); 142 | const url = new URL(node.href); 143 | if (node.getAttribute('id') === '__markdown-viewer__custom_css') { 144 | const { custom_css: css } = await webext.storage.sync.get({'custom_css': ''}); 145 | style.textContent = css; 146 | } else if (node.getAttribute('id') === '__markdown-viewer__hljs_css' && url.protocol === 'blob:') { 147 | const autoStyle = node.getAttribute('data-style-auto'); 148 | const styleSheets = { 149 | dark: `/lib/@highlightjs/cdn-assets/styles/${autoStyle.slice(0, -4)}dark.min.css`, 150 | light: `/lib/@highlightjs/cdn-assets/styles/${autoStyle.slice(0, -4)}light.min.css`, 151 | } 152 | 153 | for (const [colorScheme, loc] of Object.entries(styleSheets)) { 154 | const content = await fetch(webext.extension.getURL(loc)).then(r => r.text()); 155 | style.textContent += `@media (prefers-color-scheme: ${colorScheme}) { ${content} }\n` 156 | } 157 | } else if (url.href === webext.extension.getURL(url.pathname)) { 158 | style.textContent = await fetch(node.href).then(r => r.text()); 159 | } else { 160 | throw new Error('Untrusted stylesheet'); 161 | } 162 | 163 | if (node.hasAttribute('media')) { 164 | style.setAttribute('media', node.getAttribute('media')); 165 | } 166 | 167 | return style.outerHTML; 168 | } 169 | 170 | async function createHTMLSourceBlob(doc) { 171 | const a = doc.getElementById('__markdown-viewer__download'); 172 | const opt = doc.getElementById('__markdown-viewer__styleselect'); 173 | if (a === null) { 174 | return 175 | } 176 | 177 | if (a.href) { 178 | URL.revokeObjectURL(a.href); 179 | } 180 | 181 | // create a string containing the html headers, but inline all the tags 182 | const head = []; 183 | for (const node of doc.head.children) { 184 | if (node.tagName === 'LINK' && node.hasAttribute('rel') && node.getAttribute('rel').includes('stylesheet')) { 185 | if (!node.hasAttribute('href') || new URL(node.href).protocol === 'resource:') { 186 | continue; 187 | } 188 | 189 | try { 190 | head.push(await convertLinkToStylesheet(doc, node)) 191 | } catch (e) { 192 | // Ignore bad URLs, fetch errors, untrusted sheets 193 | continue; 194 | } 195 | } else if (node.tagName !== 'SCRIPT') { 196 | head.push(node.outerHTML); 197 | } 198 | } 199 | 200 | // Hide the download button and style options, so they do not appear in the downloaded html. 201 | a.style.display = 'none'; 202 | opt.style.display = 'none'; 203 | 204 | const html = `${head.join('\n')}${doc.body.outerHTML}`; 205 | a.href = URL.createObjectURL(new Blob([html], {type: "text/html"})); 206 | 207 | a.style.display = 'inline-block'; 208 | opt.style.display = 'block'; 209 | } 210 | 211 | function makeDocHeader(doc) { 212 | const styleSheetsDone = Promise.all([ 213 | // Style the page and code highlights. 214 | addExtensionStylesheet(doc, '/ext/sss/sss.css', {media: 'screen', id: '__markdown-viewer__md_css'}), 215 | addExtensionStylesheet(doc, '/ext/sss/print.css', {media: 'print', id: '__markdown-viewer__md_print_css'}), 216 | addExtensionStylesheet(doc, '/lib/@highlightjs/cdn-assets/styles/default.min.css', 217 | {id: '__markdown-viewer__hljs_css'}), 218 | addExtensionStylesheet(doc, '/srclib/katex/dist/katex.min.css', {id: '__markdown-viewer__katex_css'}), 219 | addExtensionStylesheet(doc, '/lib/markdown-it-texmath/css/texmath.css', {id: '__markdown-viewer__texmath_css'}), 220 | addExtensionStylesheet(doc, '/ext/menu.css', {id: '__markdown-viewer__menu_css'}), 221 | // User-defined stylesheet. 222 | addCustomStylesheet(doc, {id: '__markdown-viewer__custom_css'}), 223 | ]) 224 | 225 | // This is considered a good practice for mobiles. 226 | const viewport = doc.createElement('meta'); 227 | viewport.name = 'viewport'; 228 | viewport.content = 'width=device-width, initial-scale=1'; 229 | doc.head.appendChild(viewport); 230 | 231 | return styleSheetsDone; 232 | } 233 | 234 | function makeDocTitle(markdownRoot, title) { 235 | // Set the page title. 236 | if (!title) { 237 | // Get first line if no header. 238 | title = markdownRoot.textContent.trim().split("\n", 1)[0].trim(); 239 | } 240 | if (title.length > 128) { 241 | // Limit its length. 242 | title = `${title.substr(0, 125) }...`; 243 | } 244 | 245 | return title; 246 | } 247 | 248 | function processRenderedMarkdown(html, title, pageUrl) { 249 | // Parse the element’s content Markdown to HTML, inside a div.markdownRoot 250 | const doc = parser.parseFromString(`
${html}
`, "text/html"); 251 | const markdownRoot = doc.body.removeChild(doc.body.firstChild); 252 | 253 | // Perform some cleanup and extract headers 254 | const documentAnchors = []; 255 | const jsLink = /^\s*javascript:/iu; 256 | const allElements = doc.createNodeIterator(markdownRoot, NodeFilter.SHOW_ELEMENT); 257 | for (let node = allElements.nextNode(); node; node = allElements.nextNode()) { 258 | const tagName = node.tagName.toUpperCase(); 259 | 260 | // Make anchor for headers; use first header text as page title. 261 | if (headerTags.includes(tagName)) { 262 | const anchor = makeAnchor(node, documentAnchors); 263 | documentAnchors.push(node.id = anchor); 264 | if (!title) { 265 | title = node.textContent.trim(); 266 | } 267 | } 268 | 269 | if (tagName === 'A' && !node.href.startsWith('#')) { 270 | node.target = '_top'; 271 | if (pageUrl) { 272 | node.href = new URL(node.getAttribute('href'), pageUrl); 273 | } 274 | } 275 | if (tagName === 'IMG' && pageUrl) { 276 | node.src = new URL(node.getAttribute('src'), pageUrl); 277 | } 278 | 279 | 280 | // Crush scripts. 281 | if (tagName === 'SCRIPT') { 282 | node.remove(); 283 | } 284 | // Trample JavaScript hrefs. 285 | if (node.getAttribute("href") && jsLink.test(node.href)) { 286 | node.removeAttribute("href"); 287 | } 288 | // Remove event handlers. 289 | for (const attr of node.attributes) { 290 | if (attr.name.toLowerCase().startsWith('on')) { 291 | node.removeAttribute(attr.name); 292 | } 293 | } 294 | } 295 | 296 | return { DOM: markdownRoot, title }; 297 | } 298 | 299 | function buildStyleOptions(doc) { 300 | const p = doc.createElement('p'); 301 | p.appendChild(doc.createTextNode('Pick a markdown and code style:')); 302 | p.appendChild(doc.createElement('br')); 303 | p.className = 'toggleable'; 304 | p.id = '__markdown-viewer__styleselect'; 305 | 306 | const mdselect = p.appendChild(doc.createElement('select')); 307 | mdselect.id = '__markdown-viewer__mdselect'; 308 | for (const [key, val] of Object.entries(mdcss)) { 309 | const opt = mdselect.appendChild(doc.createElement('option')); 310 | opt.textContent = key; 311 | opt.value = val; 312 | opt.selected = key === 'default'; 313 | } 314 | 315 | mdselect.addEventListener('change', () => { 316 | const mdchosen = mdselect.value; 317 | 318 | setExtensionStylesheet(`/ext/sss/${mdchosen}.css`, 319 | doc.getElementById('__markdown-viewer__md_css')); 320 | 321 | webext.storage.sync.set({ chosen_md_style: mdselect.value }); 322 | createHTMLSourceBlob(doc); 323 | }) 324 | 325 | const hlselect = p.appendChild(doc.createElement('select')); 326 | hlselect.id = '__markdown-viewer__hlselect'; 327 | for (const hlopt of hlcss) { 328 | const opt = hlselect.appendChild(doc.createElement('option')); 329 | opt.value = hlopt; 330 | opt.textContent = hlopt.substring(hlopt.lastIndexOf('/') + 1); 331 | opt.selected = hlopt === 'default'; 332 | } 333 | 334 | hlselect.addEventListener('change', () => { 335 | const sheet = doc.getElementById('__markdown-viewer__hljs_css'); 336 | if (hlselect.value.endsWith('-auto')) { 337 | const base = hlselect.value.slice(0, -5); 338 | const dark = hlcss.includes(`${base}-dark`) ? `${base}-dark` : base; 339 | const light = `${base}-light`; 340 | setExtensionStylesheetAuto(`/lib/@highlightjs/cdn-assets/styles/${dark}.min.css`, 341 | `/lib/@highlightjs/cdn-assets/styles/${light}.min.css`, sheet); 342 | sheet.setAttribute('data-style-auto', hlselect.value) 343 | } else { 344 | setExtensionStylesheet(`/lib/@highlightjs/cdn-assets/styles/${hlselect.value}.min.css`, sheet); 345 | } 346 | webext.storage.sync.set({chosen_hl_style: hlselect.value}); 347 | createHTMLSourceBlob(doc); 348 | }) 349 | 350 | return webext.storage.sync.get(['chosen_md_style', 'chosen_hl_style']).then((storage) => { 351 | if ('chosen_md_style' in storage && mdselect.value !== storage.chosen_md_style) { 352 | mdselect.value = storage.chosen_md_style; 353 | mdselect.dispatchEvent(new Event('change')); 354 | } 355 | 356 | if ('chosen_hl_style' in storage && hlselect.value !== storage.chosen_hl_style) { 357 | hlselect.value = storage.chosen_hl_style; 358 | hlselect.dispatchEvent(new Event('change')); 359 | } 360 | 361 | return p; 362 | }); 363 | } 364 | 365 | function buildDownloadButton(doc) { 366 | const a = doc.createElement('p').appendChild(doc.createElement('a')); 367 | a.parentNode.className = 'toggleable' 368 | a.id = '__markdown-viewer__download'; 369 | a.download = 'markdown.html'; 370 | a.innerText = 'Download as HTML'; 371 | a.style.display = 'none'; 372 | 373 | return Promise.resolve(a.parentNode); 374 | } 375 | 376 | function buildSourceLink(doc, url) { 377 | if (new URL(url).protocol === 'blob:') { 378 | return Promise.resolve(null); 379 | } 380 | 381 | const a = doc.createElement('p').appendChild(doc.createElement('a')); 382 | a.parentNode.className = 'toggleable' 383 | a.target = '_top'; 384 | a.href = `view-source:${url}`; 385 | a.id = '__markdown-viewer__source'; 386 | a.innerText = 'View Source'; 387 | 388 | return Promise.resolve(a.parentNode); 389 | } 390 | 391 | function buildTableOfContents(doc) { 392 | const allHeaders = doc.querySelectorAll(headerTags.join(',')); 393 | if (allHeaders.length) { 394 | const tocdiv = doc.createElement('div'); 395 | let level = 0; 396 | let list = tocdiv.appendChild(doc.createElement('ul')); 397 | for (const header of allHeaders) { 398 | /* Open/close the right amount of nested lists to fit tag level */ 399 | const headerLevel = headerTags.indexOf(header.tagName); 400 | for (; level < headerLevel; level++) { 401 | if (list.lastChild === null || list.lastChild.tagName !== 'LI') { 402 | list.appendChild(doc.createElement('li')) 403 | } 404 | list = list.lastChild.appendChild(doc.createElement('ul')); 405 | } 406 | for (; level > headerLevel; level--) { 407 | list = list.parentNode.parentNode; 408 | } 409 | 410 | /* Make a list item with a link to the heading */ 411 | const link = doc.createElement('a'); 412 | link.target = '_top'; 413 | link.textContent = header.textContent; 414 | link.href = `#${header.id}`; 415 | list.appendChild(doc.createElement('li')).appendChild(link); 416 | } 417 | 418 | /* Squash empty levels by moving its children to the parent list */ 419 | for (const deleteList of tocdiv.querySelectorAll('li > ul:only-child')) { 420 | const parentItem = deleteList.parentNode; 421 | const parentList = parentItem.parentNode; 422 | while (deleteList.children.length) { 423 | parentList.appendChild(deleteList.removeChild(deleteList.firstChild)); 424 | } 425 | parentList.removeChild(parentItem); 426 | } 427 | 428 | tocdiv.id = '__markdown-viewer__toc'; 429 | tocdiv.className = 'toggleable' 430 | return Promise.resolve(tocdiv); 431 | } 432 | else { 433 | return Promise.resolve(null); 434 | } 435 | } 436 | 437 | function addMarkdownViewerMenu(doc, url) { 438 | const toolsdiv = doc.createElement('div'); 439 | toolsdiv.id = '__markdown-viewer__tools'; 440 | toolsdiv.className = 'hidden'; 441 | const getMenuDisplay = webext.storage.sync.get({'display_menu': 'floating'}); 442 | 443 | const input = toolsdiv.appendChild(doc.createElement('input')); 444 | const label = toolsdiv.appendChild(doc.createElement('label')); 445 | input.type = 'checkbox'; 446 | input.id = '__markdown-viewer__show-tools'; 447 | input.checked = showMenu; 448 | label.setAttribute('for', input.id); 449 | 450 | return Promise.all([ 451 | getMenuDisplay, 452 | buildTableOfContents(doc), 453 | buildStyleOptions(doc), 454 | buildDownloadButton(doc), 455 | buildSourceLink(doc, url), 456 | ]).then(([{display_menu: menuDisplay}, ...nodes]) => { 457 | toolsdiv.className = menuDisplay; 458 | for (const node of nodes) { 459 | if (node) { 460 | toolsdiv.appendChild(node); 461 | } 462 | } 463 | doc.body.prepend(toolsdiv); 464 | }); 465 | } 466 | 467 | function revealDisclosures(doc, state) { 468 | state.splice(0, state.length); 469 | state.push(...Array.from(doc.getElementsByTagName('details')).map(tag => { 470 | const wasOpen = tag.getAttribute('open'); 471 | tag.setAttribute('open', true) 472 | return wasOpen; 473 | })) 474 | } 475 | 476 | function restoreDisclosures(doc, state) { 477 | Array.from(doc.getElementsByTagName('details')).forEach((tag, idx) => { 478 | if (state[idx] === null) { 479 | tag.removeAttribute('open') 480 | } else { 481 | tag.setAttribute('open', state[idx]) 482 | } 483 | }) 484 | } 485 | 486 | function render(doc, text, { inserter, url, displayUrl, skipHeader=false }) { 487 | const baseUrl = displayUrl || url || doc.defaultView.location.href; 488 | return webext.storage.sync.get({'plugins': {}}).then(storage => ({...pluginDefaults, ...storage.plugins})) 489 | .then(pluginPrefs => { 490 | return new Renderer(pluginPrefs).render(text) 491 | .then(({ html, title }) => processRenderedMarkdown(html, title, baseUrl)) 492 | .then(({ DOM: renderedDOM, title }) => { 493 | if (!skipHeader) { 494 | makeDocHeader(doc); 495 | } 496 | doc.title = makeDocTitle(renderedDOM, title); 497 | (inserter || doc.appendChild)(renderedDOM); 498 | 499 | if (pluginPrefs.mermaid) { 500 | for (const pre of doc.getElementsByClassName('mermaid')) { 501 | if (pre.tagName !== 'PRE') { 502 | continue; 503 | } 504 | // Mermaid rendering asychronously 505 | Promise.resolve().then(() => { 506 | const svg = mermaid.mermaidAPI.render('mermaid', pre.innerText); 507 | const img = parser.parseFromString(svg, 'image/svg+xml') 508 | pre.parentNode.replaceChild(img.firstChild, pre); 509 | }).catch(console.error) 510 | } 511 | } 512 | }) 513 | .then(() => addMarkdownViewerMenu(doc, baseUrl)) 514 | .then(() => createHTMLSourceBlob(doc)) 515 | }) 516 | .catch(console.error); 517 | } 518 | 519 | function preventRefresh(evt) { 520 | if (evt.altKey || evt.metaKey || evt.shiftKey || evt.repeat) { 521 | return false; 522 | } 523 | 524 | if (evt.key === 'F5' && !evt.ctrlKey || evt.key === 'r' && evt.ctrlKey) { 525 | evt.stopPropagation(); 526 | evt.preventDefault(); 527 | 528 | return true; 529 | } 530 | 531 | return false; 532 | } 533 | 534 | function replaceMarkdownDOM(doc) { 535 | return function replaceWith(node) { 536 | showMenu = doc.getElementById('__markdown-viewer__show-tools').checked; 537 | 538 | while (doc.body.children.length) { 539 | doc.body.removeChild(doc.body.firstChild); 540 | } 541 | 542 | doc.body.appendChild(node); 543 | } 544 | } 545 | 546 | function setupEvents(doc, win, { url, displayUrl }) { 547 | if (url) { 548 | win.addEventListener('keydown', e => { 549 | if (!preventRefresh(e)) { 550 | return; 551 | } 552 | 553 | fetch(url).then(r => r.text()).then(text => { 554 | render(doc, text, { inserter: replaceMarkdownDOM(doc), url, displayUrl, skipHeader: true }); 555 | }); 556 | }); 557 | } 558 | 559 | const disclosures = []; 560 | win.addEventListener('beforeprint', () => revealDisclosures(doc, disclosures)); 561 | win.addEventListener('afterprint', () => restoreDisclosures(doc, disclosures)); 562 | } 563 | 564 | /* exported renderInDocument */ 565 | function renderInDocument(doc, text, opts) { 566 | return render(doc, text, opts).then(() => { 567 | setupEvents(doc, window, opts); 568 | }); 569 | } 570 | 571 | /* exported renderInIframe */ 572 | function renderInIframe(parentDoc, text, { inserter, ...opts }) { 573 | const iframe = parentDoc.createElement("iframe"); 574 | iframe.sandbox = "allow-same-origin allow-top-navigation-by-user-activation"; 575 | iframe.referrerpolicy = "no-referrer"; 576 | iframe.name = "sandbox"; 577 | 578 | // An iframe cannot be fully initialized before it is embedded into the doc. 579 | inserter(iframe); 580 | 581 | return new Promise(resolve => { 582 | iframe.addEventListener("load", () => resolve(iframe.contentDocument)); 583 | iframe.srcdoc = ` 584 | 585 |
586 | `; 587 | }).then(doc => { 588 | const spinner = doc.getElementById('spinner'); 589 | // Render the document with an inserter that adds the markdown inside the iframe 590 | render(doc, text, { inserter: n => doc.body.replaceChild(n, spinner), ...opts }).then(() => { 591 | parentDoc.title = doc.title; 592 | setupEvents(doc, iframe.contentWindow, opts); 593 | 594 | // Listen for refresh keys and print events on parent window too 595 | setupEvents(doc, window, opts); 596 | 597 | // Can’t access the blobs from an iframe, so forward the same click 598 | // to an identical link outside the iframe 599 | buildDownloadButton(parentDoc).then(node => { 600 | node.display = 'hidden'; 601 | const button = doc.getElementById('__markdown-viewer__download'); 602 | const a = node.firstChild; 603 | button.addEventListener('click', e => { 604 | a.href = button.href; 605 | a.click(); 606 | e.preventDefault(); 607 | }, false); 608 | }); 609 | }); 610 | 611 | window.addEventListener('hashchange', () => { 612 | iframe.contentWindow.location.hash = window.location.hash; 613 | }); 614 | iframe.contentWindow.addEventListener('hashchange', () => { 615 | window.location.hash = iframe.contentWindow.location.hash; 616 | }); 617 | }); 618 | } 619 | -------------------------------------------------------------------------------- /ext/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ext/frontmatter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* exported yamltitle */ 4 | function yamltitle(text) { 5 | // Try (best-effort) to extract the value of a top-level “title” key 6 | const titleLine = text.split('\n').find(line => line.startsWith('title:')) 7 | if (!titleLine) { 8 | return null; 9 | } 10 | const title = titleLine.slice(6).trim(); 11 | if (!title || title === '|') { 12 | // We need a string, so don’t support array, object, or multi-line titles 13 | return null; 14 | } 15 | 16 | // Handle string quoting for prettyness 17 | const char0 = title.charAt(0); 18 | if (char0 !== title.charAt(title.length - 1)) { 19 | return title; 20 | } else if (char0 === "'") { 21 | return title.slice(1, title.length - 1).replaceAll("''", "'"); 22 | } else if (char0 === '"') { 23 | return title.slice(1, title.length - 1).replaceAll('\\n', "\n").replaceAll( 24 | /\\(?.)/gu, (match, escaped) => escaped 25 | ); 26 | } else { 27 | return title; 28 | } 29 | } 30 | 31 | /* exported frontmatter */ 32 | function frontmatter(callback = null) { 33 | const separator = '---', len = separator.length; 34 | return function block(state, startLine, endLine, silent) { 35 | if (startLine !== 0) { 36 | return false; 37 | } 38 | 39 | const { src, bMarks, eMarks, sCount } = state; 40 | 41 | if (src.slice(bMarks[startLine], eMarks[startLine]) !== separator) { 42 | return false; 43 | } 44 | 45 | let line = startLine; 46 | while (++line < endLine) { 47 | if (eMarks[line] - bMarks[line] !== len || sCount[line] !== 0) { 48 | continue; 49 | } 50 | if (src.slice(bMarks[line], eMarks[line]) === separator) { 51 | break; 52 | } 53 | } 54 | 55 | if (line === endLine) { 56 | return false; 57 | } 58 | if (silent) { 59 | return true; 60 | } 61 | 62 | const { parentType, lineMax } = state; 63 | // transform current token into container 64 | state.parentType = 'container'; 65 | state.lineMax = line; 66 | 67 | // insert front-matter token 68 | const token = state.push('front_matter', null, 0); 69 | token.hidden = true; 70 | token.markup = separator; 71 | token.block = true; 72 | token.map = [ startLine, line + 1 ]; 73 | token.meta = src.slice(state.bMarks[startLine + 1], state.eMarks[line - 1]); 74 | 75 | state.parentType = parentType; 76 | state.lineMax = lineMax; 77 | state.line = line + 1; 78 | 79 | if (callback) { 80 | callback(token.meta); 81 | } 82 | 83 | return true; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /ext/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // NB. webext defined in builder.js 4 | //const webext = typeof browser === 'undefined' ? chrome : browser; 5 | 6 | // Process only if document is unprocessed text. 7 | const {body} = document; 8 | if (body.childNodes.length === 1 && 9 | body.children.length === 1 && 10 | body.children[0].nodeName.toUpperCase() === 'PRE') 11 | { 12 | const inserter = renderedDOM => body.replaceChild(renderedDOM, body.firstChild); 13 | webext.storage.sync.get(['iframe_embed', 'plugins']).then(({ iframe_embed: embed = true, plugins }) => { 14 | const { mermaid: useMermaid = pluginDefaults.mermaid } = plugins; 15 | if (useMermaid) { 16 | mermaid.initialize(); 17 | } 18 | if (embed) { 19 | renderInIframe(document, body.firstChild.textContent, { inserter, url: window.location.href }); 20 | addExtensionStylesheet(document, '/ext/view-md.css', {}); 21 | } else { 22 | renderInDocument(document, body.firstChild.textContent, { inserter, url: window.location.href }) 23 | } 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /ext/markdown-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ext/menu.css: -------------------------------------------------------------------------------- 1 | /* Style for the menu, possible positions/visibility */ 2 | #__markdown-viewer__tools { 3 | margin:0; 4 | padding:0; 5 | background:#555; 6 | color:#eee; 7 | box-shadow:0 -1px rgba(0,0,0,.5) inset; 8 | border-radius: .5em; 9 | max-width: 25%; 10 | min-width: 2.8em; 11 | min-height: 3em; 12 | z-index: 2147483647; 13 | position: relative; 14 | } 15 | 16 | #__markdown-viewer__tools.floating { 17 | float:right; 18 | margin: 0 0 .5em .5em; 19 | } 20 | 21 | #__markdown-viewer__tools.fixed { 22 | max-height: calc(100vh - 1em); 23 | overflow-y: auto; 24 | position:fixed; 25 | top:.5em; 26 | right:1em; 27 | } 28 | 29 | #__markdown-viewer__tools.hidden { 30 | display:none; 31 | } 32 | 33 | @media print { 34 | #__markdown-viewer__tools { 35 | display: none; 36 | } 37 | } 38 | 39 | #__markdown-viewer__tools a[href]:after { 40 | content: ""; 41 | } 42 | 43 | /* Style for the menu top */ 44 | label[for=__markdown-viewer__show-tools] { 45 | display:block; 46 | padding:0 18px 0 12px; 47 | line-height:3em; 48 | background:#333; 49 | cursor:pointer; 50 | border-radius: .5em; 51 | min-height: 3em; 52 | width: .9em; 53 | position: absolute; 54 | right: 0; 55 | } 56 | 57 | input#__markdown-viewer__show-tools:not(:checked) ~ label { 58 | margin-left: -.9em; 59 | } 60 | 61 | input#__markdown-viewer__show-tools:checked ~ label { 62 | } 63 | 64 | label[for=__markdown-viewer__show-tools]:before{ 65 | } 66 | 67 | label[for=__markdown-viewer__show-tools]:after { 68 | content:""; 69 | display:inline-block; 70 | float:right; 71 | margin-top:1.5em; 72 | right:5px; 73 | width:0; 74 | height:0; 75 | border-style: solid; 76 | border-color: rgba(255,255,255,.5) transparent; 77 | border-width: 4px 4px 0 4px; 78 | transition:border-bottom .1s, border-top .1s .1s; 79 | } 80 | 81 | input#__markdown-viewer__show-tools:checked ~ label:after { 82 | border-top-width:0; 83 | border-bottom-width:4px; 84 | transition:border-top .1s, border-bottom .1s .1s; 85 | } 86 | 87 | /* hide the input that tracks the menu's visibility */ 88 | input#__markdown-viewer__show-tools { 89 | display:none; 90 | } 91 | 92 | /* style, and hide/show menu items based on the input being checked */ 93 | #__markdown-viewer__tools > .toggleable { 94 | overflow:hidden; 95 | transition-property:max-height, max-width, padding-top, padding-bottom, margin-top, margin-bottom; 96 | transition-duration:0.5s; 97 | } 98 | 99 | input#__markdown-viewer__show-tools:checked ~ .toggleable { 100 | /* maxes should be 'none' or infinite values, however those are not aniimatable, so just put something big enough. */ 101 | max-width: 2000px; 102 | max-height: 2000px; 103 | overflow-y: auto; 104 | transition-timing-function:ease-in; 105 | min-width: 15em; 106 | } 107 | 108 | input#__markdown-viewer__show-tools:not(:checked) ~ .toggleable { 109 | max-height:0; 110 | max-width:0; 111 | transition-timing-function:ease-out; 112 | padding-top:0; 113 | padding-bottom:0; 114 | margin-top:0; 115 | margin-bottom:0; 116 | } 117 | 118 | /* style the table of contents and its items */ 119 | #__markdown-viewer__toc { 120 | display:block; 121 | padding: .5em; 122 | border:0; 123 | } 124 | 125 | #__markdown-viewer__toc::before { 126 | content: "Table of Contents"; 127 | text-align: center; 128 | display: block; 129 | font-weight: bold; 130 | text-decoration: underline; 131 | } 132 | 133 | #__markdown-viewer__toc { 134 | margin-top: 2em; 135 | } 136 | 137 | #__markdown-viewer__tools.fixed input#__markdown-viewer__show-tools:checked ~ #__markdown-viewer__toc { 138 | max-height: calc(100vh - 16em); 139 | } 140 | 141 | input#__markdown-viewer__show-tools:checked ~ #__markdown-viewer__toc::before { 142 | position: absolute; 143 | top: .5em; 144 | left: 0; 145 | right: 3em; 146 | } 147 | 148 | #__markdown-viewer__tools select { 149 | max-width: 40%; 150 | margin: 0 .5em; 151 | } 152 | 153 | #__markdown-viewer__tools select, 154 | #__markdown-viewer__toc * { 155 | white-space: nowrap; 156 | overflow-x: hidden; 157 | text-overflow: ellipsis; 158 | } 159 | 160 | #__markdown-viewer__tools a { 161 | color: white; 162 | text-decoration: none; 163 | } 164 | 165 | #__markdown-viewer__toc ul { 166 | list-style: inside "• "; 167 | padding: 0; 168 | margin: 0; 169 | } 170 | 171 | #__markdown-viewer__toc ul ul { 172 | padding-left: 1.5em; 173 | } 174 | 175 | 176 | /* Style the "Download Source" button at the end of the menu */ 177 | #__markdown-viewer__tools > p { 178 | text-align:center; 179 | padding: 0 .5em; 180 | } 181 | 182 | #__markdown-viewer__download { 183 | /* appearance: button; */ 184 | -moz-appearance: button !important; 185 | display:inline-block; 186 | text-align: center; 187 | text-decoration:none; 188 | margin: .5em auto; 189 | } 190 | -------------------------------------------------------------------------------- /ext/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimbali/markdown-viewer/0a1d818b2a663a53c16da5001ca1705044075611/ext/menu.png -------------------------------------------------------------------------------- /ext/mermaid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimbali/markdown-viewer/0a1d818b2a663a53c16da5001ca1705044075611/ext/mermaid.png -------------------------------------------------------------------------------- /ext/onboarding.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 45px; 3 | min-width: 200px; 4 | } 5 | 6 | .foreword { 7 | max-width: 980px; 8 | margin: 0 auto; 9 | text-align: center; 10 | } 11 | 12 | .features { 13 | text-align: center; 14 | display: flex; 15 | flex-wrap: wrap; 16 | } 17 | 18 | #top::before { 19 | content: ''; 20 | background: no-repeat url('/ext/markdown-mark.svg'); 21 | background-size: 80%; 22 | background-position: center; 23 | width: 50px; 24 | height: 50px; 25 | display: inline-block; 26 | vertical-align: -40%; 27 | } 28 | 29 | .card { 30 | text-align: left; 31 | max-width: 20em; 32 | border-radius: 10px; 33 | padding: 0 10px 10px; 34 | margin: 10px; 35 | border: black thin solid; 36 | } 37 | 38 | .card h4 { 39 | text-align: center; 40 | } 41 | 42 | .card ul { 43 | padding-left: 2em; 44 | } 45 | 46 | .card img { 47 | max-width: 90%; 48 | padding-left: 5%; 49 | } 50 | 51 | .card img.float { 52 | max-width: 35%; 53 | float: right; 54 | } 55 | 56 | .card .center { 57 | text-align: center; 58 | } 59 | 60 | .card a.scheme { 61 | line-break: anywhere; 62 | } 63 | 64 | .card a.scheme strong { 65 | white-space: nowrap; 66 | } 67 | -------------------------------------------------------------------------------- /ext/onboarding.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Markdown Viewer Tour 8 | 9 | 10 |
11 |

Welcome to Markdown Viewer

12 | 13 |

Below are a few features and settings, you can always change later from the addon’s preferences page later.

14 |
15 | 16 |
17 |
18 |

Manual or automatic rendering

19 |

Markdown Viewer now allows the possibility to render pages manually through a button in the address bar.

20 |

Image of page action button in address bar

21 |

If you prefer pages being rendered automatically, Markdown Viewer will need access to all pages that match markdown file names to check they are renderable.

22 |

23 |

For pages that prevent checking whether the they contain renderable markdown, through e.g. security policies, the address bar button remains available.

24 |
25 | 26 |
27 |

Markdown Viewer extras

28 | Image of Markdown Viewer menu 29 |

The add-on can display a menu on the rendered page, with:

  • a table of contents
  • style selection,
  • an HTML download link,
  • and a link to view the page source.

30 |

Markdown Viewer also includes a number of markdown-it plugins to extend the markdown standard, that can be enabled or disabled in the settings page (mermaid diagrams, emojis, footnotes, and more!).

31 |

You can also specify custom CSS to apply on every page.

32 |

33 |
34 | 35 |
36 |

New Markdown Viewer features

37 |

Image of Markdown Viewer menu 38 | In addition to previous features (menu, table of contents, style selection) and plug-ins (footnotes, front matters, etc.), Markdown Viewer can now render mermaid diagrams

39 |

Markdown Viewer now also renders markdown in a sandboxed iframe, for improved privacy and security protection.

40 |

41 |
42 | 43 |
44 |

Extension page

45 |

In version 2, Markdown Viewer can render local markdown files* in an extension page, which: 46 |

    47 |
  • improves out-of-the-box local file support,
  • 48 |
  • avoids slowing down the browser on large files by using worker threads,
  • 49 |
  • supports a ext+view-markdown: protocol
  • 50 |
51 | 52 | * local markdown files are files stored on your computer and accessed with file:// URLs. Unfortunately, cross-origin permissions prevent accessing many markdown files on the web from the extension page. 53 |

54 |

Unfortunately due to limitations of add-on permissions, the extension page can not access markdown pages on the web with cross-origin headers, even when the access is manually triggered by a user.

55 | 56 |
57 | 58 |
59 |

Local file preference

60 |

Some functionalities are slightly reduced with the extension page: 61 |

    62 |
  • Opening a file requires granting access with a file picker
  • 63 |
  • Linking to page source is not possible
  • 64 |
  • Local images are not displayed
  • 65 |
66 |

Therefore, you can prefer injecting scripts into local markdown pages by default, without using the extension page.

67 |

For this, Markdown Viewer will need Firefox to consider markdown files as text 68 | (or Firefox will attempt to download these files instead of displaying them).

69 |

71 |

72 | 73 |
74 |

Manual access to extension page

75 |

The extension page is always accessible! To have Markdown Viewer prompt for a local file, use the toolbar (or overflow-menu) button, or go to 76 | ext+view-markdown:.

77 |

Image of extension button in tool bar

78 |

You can also go directly specify which page to open by providing the path, e.g.:
ext+view-markdown:file://C:/path/to/file.md

79 |

This is quite useful to link to local markdown pages when the path is known, but linking to actual local files is not allowed.

80 |
81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ext/onboarding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { version } = webext.runtime.getManifest(); 4 | const update = new URLSearchParams(window.location.search).get('update'); 5 | 6 | const topTitle = document.getElementById('top'); 7 | topTitle.innerText += ` v${version}` 8 | if (update) { 9 | document.querySelectorAll('.unchanged').forEach(node => { node.style.display = 'none' }); 10 | } else { 11 | document.querySelectorAll('.update').forEach(node => { node.style.display = 'none' }); 12 | } 13 | 14 | document.getElementById('to-settings').onclick = () => webext.runtime.openOptionsPage(); 15 | -------------------------------------------------------------------------------- /ext/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 1.25em; 3 | } 4 | h3 { 5 | font-size: 1.5em; 6 | margin: 1em 0 0.4em 0; 7 | } 8 | p { 9 | margin: 0; 10 | } 11 | .comment { 12 | font-size: small; 13 | opacity: .9; 14 | margin: 0; 15 | } 16 | #origins { 17 | list-style-type: none; 18 | margin: 0; 19 | } 20 | #origins li { 21 | display: inline-block; 22 | margin: 1ex; 23 | } 24 | #origins li button { 25 | padding: 1ex; 26 | } 27 | #all_origins { 28 | font-weight: bold; 29 | } 30 | #saved { 31 | color: green; 32 | font-weight: bold; 33 | display: none; 34 | } 35 | textarea { 36 | white-space: pre; 37 | overflow-wrap: normal; 38 | overflow-x: scroll; 39 | } 40 | label { 41 | display: block; 42 | } 43 | label input { 44 | display: inline-block; 45 | margin: 0 1em; 46 | } 47 | pre { 48 | font-size: .6em; 49 | margin: 15px; 50 | -moz-user-select: text; 51 | } 52 | -------------------------------------------------------------------------------- /ext/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Get Help

9 |

See this project's README page for help with:

10 |
    11 |
  • Unicode characters not displaying correctly,
  • 12 |
  • how to customize styled Markdown appearance,
  • 13 |
  • how to view the unstyled Markdown text,
  • 14 |
  • and more.
  • 15 |
16 | 17 |

Markdown-viewer menu

18 |

Select whether/how to display a Markdown-Viewer menu on rendered pages: 19 |
20 | 25 |

26 |

A fixed menu will stay at the same position when you scroll the page, but hide text underneath. 27 | A floating menu will reflow that page's text around it, but will scroll with the page.

28 | 29 |

Markdown-it plugins

30 |

Select which plugins to enable for the Markdown parser, markdown-it: 31 | 34 | 37 | 40 | 43 | 45 |

46 | 48 |

49 | 53 | 56 |

57 | 58 |

Custom CSS

59 |

Enter custom CSS to be applied to Markdown pages here.

60 | 61 | 62 |

Click or tab out of the text area to save changes. Refresh this page to revert changes. 63 | If you enable syncing Add-ons with your Firefox account, the CSS will synchronize across your devices.

64 |

Try for example:

65 |
 66 | @media screen {
 67 |   .markdownRoot {
 68 |     border: 1px solid;
 69 |     box-sizing: border-box;
 70 |     min-width: 200px;
 71 |     max-width: 980px;
 72 |     margin: 0 auto;
 73 |     padding: 45px;
 74 |   }
 75 | 
 76 |   @media (prefers-color-scheme: light) {
 77 |     :root {
 78 |       background-color: #f5f5f5;
 79 |     }
 80 |     .markdownRoot {
 81 |       background-color: #fff;
 82 |       border-color: #ccc;
 83 |     }
 84 |   }
 85 | 
 86 |   @media (prefers-color-scheme: dark) {
 87 |     :root {
 88 |       background-color: black;
 89 |     }
 90 |     .markdownRoot {
 91 |       background-color: #1C1B22;
 92 |       border-color: #333;
 93 |     }
 94 |   }
 95 | }
 96 | 	
97 |

Note the media queries (screen and prefers-color-scheme) which allow to not modify the print view or the wrong color scheme. 98 | Use @media print { … } to modify the print view.

99 | 100 |

Advanced Options

101 |

102 | 105 |

106 |

Requires host permissions for all URLs that match markdown files. 107 | Alternately, you can use the button in the address bar for manual rendition.

108 | 109 |

110 | 113 |

114 |

115 | Instead of using an extension page to render file:// pages. 116 |
Main advantages of the extension page: 117 |

118 |
    119 |
  • Avoids mime-type issues on Linux and macOS, causing Firefox to download the markdown files instead of displaying them
  • 120 |
  • Rendering done in a Worker (less laggy on large files)
  • 121 |
  • Safer operation
  • 122 |
123 |

Main disadvantages (for file:// URLs only):

124 |
    125 |
  • Requires a manual confirmation of file access on opening the file (refreshes are free)
  • 126 |
  • Built-in “view source” shortcut and menu “View Source” link are unavailable
  • 127 |
  • Local file images are not displayed
  • 128 |
129 | 130 |

131 | 134 |

135 |

Unticking restores the legacy behaviour, which inserts into the page directly. Use with care.

136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /ext/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webext = typeof browser === 'undefined' ? chrome : browser; 4 | if (webext === chrome) { 5 | document.querySelectorAll('.browser_name').forEach(span => { span.innerText = "Google" }); 6 | } 7 | 8 | let timer = null; 9 | const textarea = document.getElementById('custom_css'); 10 | function clearSavedMessage() { 11 | if (timer !== null) { window.clearTimeout(timer); timer = null; } 12 | textarea.onkeydown = null; 13 | document.getElementById('saved').style.display = 'none'; 14 | } 15 | 16 | // Load custom CSS and save it when changed by user 17 | webext.storage.sync.get({ custom_css: '' }).then(({custom_css: data}) => { 18 | if (!textarea) { 19 | return; 20 | } 21 | 22 | textarea.value = data; 23 | 24 | textarea.onchange = () => { 25 | webext.storage.sync.set({custom_css: textarea.value}, () => { 26 | document.getElementById('saved').style.display = 'inline'; 27 | timer = window.setTimeout(clearSavedMessage, 8000); 28 | textarea.onkeydown = clearSavedMessage; 29 | }); 30 | }; 31 | }); 32 | 33 | document.querySelectorAll('input, select').forEach(elt => { 34 | // Each setting key in sync storage matches the name attribute of its element, 35 | // and plugins are grouped together under the ”plugins” key. 36 | const prefName = elt.getAttribute('name'); 37 | if (!prefName) { 38 | return; 39 | } 40 | 41 | const isBoolPref = elt.tagName === 'INPUT' && elt.type === 'checkbox'; 42 | const setter = isBoolPref ? val => { elt.checked = val; } : val => { elt.value = val; }; 43 | const getter = isBoolPref ? () => elt.checked : () => elt.value ; 44 | 45 | if (elt.parentNode.classList.contains('permissions')) { 46 | const { optional_permissions: origins } = browser.runtime.getManifest(); 47 | const permissions = prefName === 'host-permissions' ? { origins } : { 48 | permissions: prefName.split(',').filter(perm => !perm.includes('://')), 49 | origins: prefName.split(',').filter(perm => perm.includes('://')), 50 | }; 51 | 52 | webext.permissions.contains(permissions).then(setter); 53 | elt.onchange = () => { 54 | if (getter()) { 55 | webext.permissions.request(permissions).then(granted => setter(granted)); 56 | } else { 57 | webext.permissions.remove(permissions).then(removed => setter(!removed)); 58 | } 59 | } 60 | } else if (elt.parentNode.classList.contains('plugins')) { 61 | webext.storage.sync.get({ plugins: {} }).then(({ plugins }) => { 62 | if (prefName in plugins) { 63 | setter(plugins[prefName]); 64 | } 65 | }); 66 | 67 | elt.onchange = () => { 68 | webext.storage.sync.get({ plugins: {} }).then(({ plugins }) => 69 | Object.assign(plugins, {[prefName]: getter()}) 70 | ).then(plugins => { 71 | webext.storage.sync.set({ plugins }) 72 | }); 73 | }; 74 | } else { 75 | webext.storage.sync.get(prefName).then(storage => { 76 | if (prefName in storage) { 77 | setter(storage[prefName]); 78 | } 79 | }); 80 | 81 | elt.onchange = () => { 82 | webext.storage.sync.set({[prefName]: getter()}) 83 | }; 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /ext/page_action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimbali/markdown-viewer/0a1d818b2a663a53c16da5001ca1705044075611/ext/page_action.png -------------------------------------------------------------------------------- /ext/renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* exported Renderer */ 4 | class Renderer { 5 | constructor(plugins) { 6 | this.plugins = plugins; 7 | } 8 | 9 | highlightCodeBlock(str, lang) { 10 | // Shameless copypasta https://github.com/markdown-it/markdown-it#syntax-highlighting 11 | if (lang && hljs.getLanguage(lang)) { 12 | try { 13 | return hljs.highlight(str, {language: lang}).value; 14 | } catch (e) {} 15 | } 16 | 17 | try { 18 | return hljs.highlightAuto(str).value; 19 | } catch (e) {} 20 | return ''; // use external default escaping 21 | } 22 | 23 | getRenderer(plugins) { 24 | const md = markdownit({ 25 | html: true, 26 | linkify: true, 27 | ...plugins.hljs ? {highlight: this.highlightCodeBlock} : {}, 28 | }) 29 | hljs.configure({ throwUnescapedHTML: true }); 30 | //markdown-it plugins: 31 | if (plugins.checkbox) {md.use(markdownitCheckbox);} 32 | if (plugins.emojis) {md.use(markdownitEmoji);} 33 | if (plugins.footnotes) {md.use(markdownitFootnote);} 34 | if (plugins.texmath) { 35 | const tm = texmath.use(katex); 36 | md.use(tm, { 37 | engine: katex, 38 | delimiters:'dollars', 39 | katexOptions: { macros: {"\\RR": "\\mathbb{R}"} } 40 | }) 41 | } 42 | if (plugins['fancy-lists']) { 43 | md.block.ruler.at('list', fancyList, { alt: [ 'paragraph', 'reference', 'blockquote' ] }); 44 | } 45 | if (plugins.frontmatter) { 46 | md.block.ruler.before('table', 'frontmatter', frontmatter(yaml => { 47 | this.title = yamltitle(yaml) 48 | })); 49 | } 50 | if (plugins.mermaid) { 51 | const origFenceRules = md.renderer.rules.fence.bind(md.renderer.rules); 52 | md.renderer.rules.fence = (tokens, idx, options, env, slf) => { 53 | const token = tokens[idx] 54 | if (token.info === 'mermaid') { 55 | return `
${token.content.trim()}
` 56 | } 57 | return origFenceRules(tokens, idx, options, env, slf) 58 | } 59 | } 60 | 61 | return md; 62 | } 63 | 64 | work(text) { 65 | const html = this.getRenderer(this.plugins).render(text); 66 | return Promise.resolve({ html, title: this.title }); 67 | } 68 | 69 | offload(text) { 70 | const worker = new Worker('./worker.js'); 71 | const { plugins } = this; 72 | 73 | return new Promise((resolve, reject) => { 74 | worker.addEventListener('message', e => { 75 | resolve(e.data); 76 | worker.terminate(); 77 | }); 78 | worker.addEventListener('error', reject); 79 | worker.postMessage({ plugins, text }); 80 | }); 81 | } 82 | 83 | // If we did not load the scripts we need to offload 84 | static prefersOffloading = typeof markdownit === 'undefined'; 85 | 86 | render(text) { 87 | if (Renderer.prefersOffloading) { 88 | return this.offload(text); 89 | } else { 90 | return this.work(text); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ext/spinner.css: -------------------------------------------------------------------------------- 1 | /* from loading.io */ 2 | #spinner { 3 | display: block; 4 | width: 80px; 5 | height: 80px; 6 | margin: 40vh auto 0; 7 | } 8 | 9 | #spinner:after { 10 | content: ' '; 11 | display: block; 12 | width: 64px; 13 | height: 64px; 14 | margin: 8px; 15 | border-width: 6px; 16 | border-style: solid; 17 | border-radius: 50%; 18 | animation: lds-dual-ring 1.2s linear infinite; 19 | } 20 | 21 | @keyframes lds-dual-ring { 22 | 0% { 23 | transform: rotate(0deg); 24 | } 25 | 100% { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-color-scheme: dark) { 31 | #spinner:after { 32 | border-color: #fff transparent #fff transparent; 33 | } 34 | } 35 | 36 | @media (prefers-color-scheme: light) { 37 | #spinner:after { 38 | border-color: #000 transparent #000 transparent; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /ext/sss/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Sindre Sorhus (https://sindresorhus.com) 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | * of the Software, and to permit persons to whom the Software is furnished to do 9 | * so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | @media (prefers-color-scheme: dark) { 24 | :root { 25 | --color-prettylights-syntax-comment: #8b949e; 26 | --color-prettylights-syntax-constant: #79c0ff; 27 | --color-prettylights-syntax-entity: #d2a8ff; 28 | --color-prettylights-syntax-storage-modifier-import: #c9d1d9; 29 | --color-prettylights-syntax-entity-tag: #7ee787; 30 | --color-prettylights-syntax-keyword: #ff7b72; 31 | --color-prettylights-syntax-string: #a5d6ff; 32 | --color-prettylights-syntax-variable: #ffa657; 33 | --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; 34 | --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; 35 | --color-prettylights-syntax-invalid-illegal-bg: #8e1519; 36 | --color-prettylights-syntax-carriage-return-text: #f0f6fc; 37 | --color-prettylights-syntax-carriage-return-bg: #b62324; 38 | --color-prettylights-syntax-string-regexp: #7ee787; 39 | --color-prettylights-syntax-markup-list: #f2cc60; 40 | --color-prettylights-syntax-markup-heading: #1f6feb; 41 | --color-prettylights-syntax-markup-italic: #c9d1d9; 42 | --color-prettylights-syntax-markup-bold: #c9d1d9; 43 | --color-prettylights-syntax-markup-deleted-text: #ffdcd7; 44 | --color-prettylights-syntax-markup-deleted-bg: #67060c; 45 | --color-prettylights-syntax-markup-inserted-text: #aff5b4; 46 | --color-prettylights-syntax-markup-inserted-bg: #033a16; 47 | --color-prettylights-syntax-markup-changed-text: #ffdfb6; 48 | --color-prettylights-syntax-markup-changed-bg: #5a1e02; 49 | --color-prettylights-syntax-markup-ignored-text: #c9d1d9; 50 | --color-prettylights-syntax-markup-ignored-bg: #1158c7; 51 | --color-prettylights-syntax-meta-diff-range: #d2a8ff; 52 | --color-prettylights-syntax-brackethighlighter-angle: #8b949e; 53 | --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58; 54 | --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; 55 | --color-fg-default: #c9d1d9; 56 | --color-fg-muted: #8b949e; 57 | --color-fg-subtle: #484f58; 58 | --color-canvas-default: #0d1117; 59 | --color-canvas-subtle: #161b22; 60 | --color-border-default: #30363d; 61 | --color-border-muted: #21262d; 62 | --color-neutral-muted: rgba(110,118,129,0.4); 63 | --color-accent-fg: #58a6ff; 64 | --color-accent-emphasis: #1f6feb; 65 | --color-danger-fg: #f85149; 66 | } 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | --color-prettylights-syntax-comment: #6e7781; 72 | --color-prettylights-syntax-constant: #0550ae; 73 | --color-prettylights-syntax-entity: #8250df; 74 | --color-prettylights-syntax-storage-modifier-import: #24292f; 75 | --color-prettylights-syntax-entity-tag: #116329; 76 | --color-prettylights-syntax-keyword: #cf222e; 77 | --color-prettylights-syntax-string: #0a3069; 78 | --color-prettylights-syntax-variable: #953800; 79 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; 80 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; 81 | --color-prettylights-syntax-invalid-illegal-bg: #82071e; 82 | --color-prettylights-syntax-carriage-return-text: #f6f8fa; 83 | --color-prettylights-syntax-carriage-return-bg: #cf222e; 84 | --color-prettylights-syntax-string-regexp: #116329; 85 | --color-prettylights-syntax-markup-list: #3b2300; 86 | --color-prettylights-syntax-markup-heading: #0550ae; 87 | --color-prettylights-syntax-markup-italic: #24292f; 88 | --color-prettylights-syntax-markup-bold: #24292f; 89 | --color-prettylights-syntax-markup-deleted-text: #82071e; 90 | --color-prettylights-syntax-markup-deleted-bg: #FFEBE9; 91 | --color-prettylights-syntax-markup-inserted-text: #116329; 92 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1; 93 | --color-prettylights-syntax-markup-changed-text: #953800; 94 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5; 95 | --color-prettylights-syntax-markup-ignored-text: #eaeef2; 96 | --color-prettylights-syntax-markup-ignored-bg: #0550ae; 97 | --color-prettylights-syntax-meta-diff-range: #8250df; 98 | --color-prettylights-syntax-brackethighlighter-angle: #57606a; 99 | --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f; 100 | --color-prettylights-syntax-constant-other-reference-link: #0a3069; 101 | --color-fg-default: #24292f; 102 | --color-fg-muted: #57606a; 103 | --color-fg-subtle: #6e7781; 104 | --color-canvas-default: #ffffff; 105 | --color-canvas-subtle: #f6f8fa; 106 | --color-border-default: #d0d7de; 107 | --color-border-muted: hsla(210,18%,87%,1); 108 | --color-neutral-muted: rgba(175,184,193,0.2); 109 | --color-accent-fg: #0969da; 110 | --color-accent-emphasis: #0969da; 111 | --color-danger-fg: #cf222e; 112 | } 113 | } 114 | 115 | :root { 116 | color: var(--color-fg-default); 117 | background-color: var(--color-canvas-default); 118 | } 119 | 120 | .markdownroot { 121 | -ms-text-size-adjust: 100%; 122 | -webkit-text-size-adjust: 100%; 123 | margin: 0; 124 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; 125 | font-size: 16px; 126 | line-height: 1.5; 127 | word-wrap: break-word; 128 | } 129 | 130 | .markdownroot .octicon { 131 | display: inline-block; 132 | fill: currentColor; 133 | vertical-align: text-bottom; 134 | } 135 | 136 | .markdownroot h1:hover .anchor .octicon-link:before, 137 | .markdownroot h2:hover .anchor .octicon-link:before, 138 | .markdownroot h3:hover .anchor .octicon-link:before, 139 | .markdownroot h4:hover .anchor .octicon-link:before, 140 | .markdownroot h5:hover .anchor .octicon-link:before, 141 | .markdownroot h6:hover .anchor .octicon-link:before { 142 | width: 16px; 143 | height: 16px; 144 | content: ' '; 145 | display: inline-block; 146 | background-color: currentColor; 147 | -webkit-mask-image: url("data:image/svg+xml,"); 148 | mask-image: url("data:image/svg+xml,"); 149 | } 150 | 151 | .markdownroot details, 152 | .markdownroot figcaption, 153 | .markdownroot figure { 154 | display: block; 155 | } 156 | 157 | .markdownroot summary { 158 | display: list-item; 159 | } 160 | 161 | .markdownroot a { 162 | background-color: transparent; 163 | color: var(--color-accent-fg); 164 | text-decoration: none; 165 | } 166 | 167 | .markdownroot a:active, 168 | .markdownroot a:hover { 169 | outline-width: 0; 170 | } 171 | 172 | .markdownroot abbr[title] { 173 | border-bottom: none; 174 | -webkit-text-decoration: underline dotted; 175 | text-decoration: underline dotted; 176 | } 177 | 178 | .markdownroot b, 179 | .markdownroot strong { 180 | font-weight: 600; 181 | } 182 | 183 | .markdownroot dfn { 184 | font-style: italic; 185 | } 186 | 187 | .markdownroot h1 { 188 | margin: .67em 0; 189 | font-weight: 600; 190 | padding-bottom: .3em; 191 | font-size: 2em; 192 | border-bottom: 1px solid var(--color-border-muted); 193 | } 194 | 195 | .markdownroot mark { 196 | background-color: #ff0; 197 | color: var(--color-text-primary); 198 | } 199 | 200 | .markdownroot small { 201 | font-size: 90%; 202 | } 203 | 204 | .markdownroot sub, 205 | .markdownroot sup { 206 | font-size: 75%; 207 | line-height: 0; 208 | position: relative; 209 | vertical-align: baseline; 210 | } 211 | 212 | .markdownroot sub { 213 | bottom: -0.25em; 214 | } 215 | 216 | .markdownroot sup { 217 | top: -0.5em; 218 | } 219 | 220 | .markdownroot img { 221 | border-style: none; 222 | max-width: 100%; 223 | box-sizing: content-box; 224 | background-color: var(--color-canvas-default); 225 | } 226 | 227 | .markdownroot code, 228 | .markdownroot kbd, 229 | .markdownroot pre, 230 | .markdownroot samp { 231 | font-family: monospace,monospace; 232 | font-size: 1em; 233 | } 234 | 235 | .markdownroot figure { 236 | margin: 1em 40px; 237 | } 238 | 239 | .markdownroot hr { 240 | box-sizing: content-box; 241 | overflow: hidden; 242 | background: transparent; 243 | border-bottom: 1px solid var(--color-border-muted); 244 | height: .25em; 245 | padding: 0; 246 | margin: 24px 0; 247 | background-color: var(--color-border-default); 248 | border: 0; 249 | } 250 | 251 | .markdownroot html [type=button], 252 | .markdownroot [type=reset], 253 | .markdownroot [type=submit] { 254 | -webkit-appearance: button; 255 | } 256 | 257 | .markdownroot [type=button]::-moz-focus-inner, 258 | .markdownroot [type=reset]::-moz-focus-inner, 259 | .markdownroot [type=submit]::-moz-focus-inner { 260 | border-style: none; 261 | padding: 0; 262 | } 263 | 264 | .markdownroot [type=button]:-moz-focusring, 265 | .markdownroot [type=reset]:-moz-focusring, 266 | .markdownroot [type=submit]:-moz-focusring { 267 | outline: 1px dotted ButtonText; 268 | } 269 | 270 | .markdownroot [type=checkbox], 271 | .markdownroot [type=radio] { 272 | box-sizing: border-box; 273 | padding: 0; 274 | } 275 | 276 | .markdownroot [type=number]::-webkit-inner-spin-button, 277 | .markdownroot [type=number]::-webkit-outer-spin-button { 278 | height: auto; 279 | } 280 | 281 | .markdownroot [type=search] { 282 | -webkit-appearance: textfield; 283 | outline-offset: -2px; 284 | } 285 | 286 | .markdownroot [type=search]::-webkit-search-cancel-button, 287 | .markdownroot [type=search]::-webkit-search-decoration { 288 | -webkit-appearance: none; 289 | } 290 | 291 | .markdownroot ::-webkit-input-placeholder { 292 | color: inherit; 293 | opacity: .54; 294 | } 295 | 296 | .markdownroot ::-webkit-file-upload-button { 297 | -webkit-appearance: button; 298 | font: inherit; 299 | } 300 | 301 | .markdownroot a:hover { 302 | text-decoration: underline; 303 | } 304 | 305 | .markdownroot hr::before { 306 | display: table; 307 | content: ""; 308 | } 309 | 310 | .markdownroot hr::after { 311 | display: table; 312 | clear: both; 313 | content: ""; 314 | } 315 | 316 | .markdownroot table { 317 | border-spacing: 0; 318 | border-collapse: collapse; 319 | display: block; 320 | width: max-content; 321 | max-width: 100%; 322 | overflow: auto; 323 | } 324 | 325 | .markdownroot td, 326 | .markdownroot th { 327 | padding: 0; 328 | } 329 | 330 | .markdownroot details summary { 331 | cursor: pointer; 332 | } 333 | 334 | .markdownroot details:not([open])>*:not(summary) { 335 | display: none !important; 336 | } 337 | 338 | .markdownroot kbd { 339 | display: inline-block; 340 | padding: 3px 5px; 341 | font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 342 | line-height: 10px; 343 | color: var(--color-fg-default); 344 | vertical-align: middle; 345 | background-color: var(--color-canvas-subtle); 346 | border: solid 1px var(--color-neutral-muted); 347 | border-bottom-color: var(--color-neutral-muted); 348 | border-radius: 6px; 349 | box-shadow: inset 0 -1px 0 var(--color-neutral-muted); 350 | } 351 | 352 | .markdownroot h1, 353 | .markdownroot h2, 354 | .markdownroot h3, 355 | .markdownroot h4, 356 | .markdownroot h5, 357 | .markdownroot h6 { 358 | margin-top: 24px; 359 | margin-bottom: 16px; 360 | font-weight: 600; 361 | line-height: 1.25; 362 | } 363 | 364 | .markdownroot h2 { 365 | font-weight: 600; 366 | padding-bottom: .3em; 367 | font-size: 1.5em; 368 | border-bottom: 1px solid var(--color-border-muted); 369 | } 370 | 371 | .markdownroot h3 { 372 | font-weight: 600; 373 | font-size: 1.25em; 374 | } 375 | 376 | .markdownroot h4 { 377 | font-weight: 600; 378 | font-size: 1em; 379 | } 380 | 381 | .markdownroot h5 { 382 | font-weight: 600; 383 | font-size: .875em; 384 | } 385 | 386 | .markdownroot h6 { 387 | font-weight: 600; 388 | font-size: .85em; 389 | color: var(--color-fg-muted); 390 | } 391 | 392 | .markdownroot p { 393 | margin-top: 0; 394 | margin-bottom: 10px; 395 | } 396 | 397 | .markdownroot blockquote { 398 | margin: 0; 399 | padding: 0 1em; 400 | color: var(--color-fg-muted); 401 | border-left: .25em solid var(--color-border-default); 402 | } 403 | 404 | .markdownroot ul, 405 | .markdownroot ol { 406 | margin-top: 0; 407 | margin-bottom: 0; 408 | padding-left: 2em; 409 | } 410 | 411 | .markdownroot ol ol, 412 | .markdownroot ul ol { 413 | list-style-type: lower-roman; 414 | } 415 | 416 | .markdownroot ul ul ol, 417 | .markdownroot ul ol ol, 418 | .markdownroot ol ul ol, 419 | .markdownroot ol ol ol { 420 | list-style-type: lower-alpha; 421 | } 422 | 423 | .markdownroot dd { 424 | margin-left: 0; 425 | } 426 | 427 | .markdownroot tt, 428 | .markdownroot code { 429 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 430 | font-size: 12px; 431 | } 432 | 433 | .markdownroot pre { 434 | margin-top: 0; 435 | margin-bottom: 0; 436 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; 437 | font-size: 12px; 438 | word-wrap: normal; 439 | } 440 | 441 | .markdownroot :-ms-input-placeholder { 442 | color: var(--color-fg-subtle); 443 | opacity: 1; 444 | } 445 | 446 | .markdownroot ::-ms-input-placeholder { 447 | color: var(--color-fg-subtle); 448 | opacity: 1; 449 | } 450 | 451 | .markdownroot ::placeholder { 452 | color: var(--color-fg-subtle); 453 | opacity: 1; 454 | } 455 | 456 | .markdownroot .pl-c { 457 | color: var(--color-prettylights-syntax-comment); 458 | } 459 | 460 | .markdownroot .pl-c1, 461 | .markdownroot .pl-s .pl-v { 462 | color: var(--color-prettylights-syntax-constant); 463 | } 464 | 465 | .markdownroot .pl-e, 466 | .markdownroot .pl-en { 467 | color: var(--color-prettylights-syntax-entity); 468 | } 469 | 470 | .markdownroot .pl-smi, 471 | .markdownroot .pl-s .pl-s1 { 472 | color: var(--color-prettylights-syntax-storage-modifier-import); 473 | } 474 | 475 | .markdownroot .pl-ent { 476 | color: var(--color-prettylights-syntax-entity-tag); 477 | } 478 | 479 | .markdownroot .pl-k { 480 | color: var(--color-prettylights-syntax-keyword); 481 | } 482 | 483 | .markdownroot .pl-s, 484 | .markdownroot .pl-pds, 485 | .markdownroot .pl-s .pl-pse .pl-s1, 486 | .markdownroot .pl-sr, 487 | .markdownroot .pl-sr .pl-cce, 488 | .markdownroot .pl-sr .pl-sre, 489 | .markdownroot .pl-sr .pl-sra { 490 | color: var(--color-prettylights-syntax-string); 491 | } 492 | 493 | .markdownroot .pl-v, 494 | .markdownroot .pl-smw { 495 | color: var(--color-prettylights-syntax-variable); 496 | } 497 | 498 | .markdownroot .pl-bu { 499 | color: var(--color-prettylights-syntax-brackethighlighter-unmatched); 500 | } 501 | 502 | .markdownroot .pl-ii { 503 | color: var(--color-prettylights-syntax-invalid-illegal-text); 504 | background-color: var(--color-prettylights-syntax-invalid-illegal-bg); 505 | } 506 | 507 | .markdownroot .pl-c2 { 508 | color: var(--color-prettylights-syntax-carriage-return-text); 509 | background-color: var(--color-prettylights-syntax-carriage-return-bg); 510 | } 511 | 512 | .markdownroot .pl-sr .pl-cce { 513 | font-weight: bold; 514 | color: var(--color-prettylights-syntax-string-regexp); 515 | } 516 | 517 | .markdownroot .pl-ml { 518 | color: var(--color-prettylights-syntax-markup-list); 519 | } 520 | 521 | .markdownroot .pl-mh, 522 | .markdownroot .pl-mh .pl-en, 523 | .markdownroot .pl-ms { 524 | font-weight: bold; 525 | color: var(--color-prettylights-syntax-markup-heading); 526 | } 527 | 528 | .markdownroot .pl-mi { 529 | font-style: italic; 530 | color: var(--color-prettylights-syntax-markup-italic); 531 | } 532 | 533 | .markdownroot .pl-mb { 534 | font-weight: bold; 535 | color: var(--color-prettylights-syntax-markup-bold); 536 | } 537 | 538 | .markdownroot .pl-md { 539 | color: var(--color-prettylights-syntax-markup-deleted-text); 540 | background-color: var(--color-prettylights-syntax-markup-deleted-bg); 541 | } 542 | 543 | .markdownroot .pl-mi1 { 544 | color: var(--color-prettylights-syntax-markup-inserted-text); 545 | background-color: var(--color-prettylights-syntax-markup-inserted-bg); 546 | } 547 | 548 | .markdownroot .pl-mc { 549 | color: var(--color-prettylights-syntax-markup-changed-text); 550 | background-color: var(--color-prettylights-syntax-markup-changed-bg); 551 | } 552 | 553 | .markdownroot .pl-mi2 { 554 | color: var(--color-prettylights-syntax-markup-ignored-text); 555 | background-color: var(--color-prettylights-syntax-markup-ignored-bg); 556 | } 557 | 558 | .markdownroot .pl-mdr { 559 | font-weight: bold; 560 | color: var(--color-prettylights-syntax-meta-diff-range); 561 | } 562 | 563 | .markdownroot .pl-ba { 564 | color: var(--color-prettylights-syntax-brackethighlighter-angle); 565 | } 566 | 567 | .markdownroot .pl-sg { 568 | color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); 569 | } 570 | 571 | .markdownroot .pl-corl { 572 | text-decoration: underline; 573 | color: var(--color-prettylights-syntax-constant-other-reference-link); 574 | } 575 | 576 | .markdownroot [data-catalyst] { 577 | display: block; 578 | } 579 | 580 | .markdownroot g-emoji { 581 | font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 582 | font-size: 1em; 583 | font-style: normal !important; 584 | font-weight: 400; 585 | line-height: 1; 586 | vertical-align: -0.075em; 587 | } 588 | 589 | .markdownroot g-emoji img { 590 | width: 1em; 591 | height: 1em; 592 | } 593 | 594 | .markdownroot::before { 595 | display: table; 596 | content: ""; 597 | } 598 | 599 | .markdownroot::after { 600 | display: table; 601 | clear: both; 602 | content: ""; 603 | } 604 | 605 | .markdownroot>*:first-child { 606 | margin-top: 0 !important; 607 | } 608 | 609 | .markdownroot>*:last-child { 610 | margin-bottom: 0 !important; 611 | } 612 | 613 | .markdownroot a:not([href]) { 614 | color: inherit; 615 | text-decoration: none; 616 | } 617 | 618 | .markdownroot .absent { 619 | color: var(--color-danger-fg); 620 | } 621 | 622 | .markdownroot .anchor { 623 | float: left; 624 | padding-right: 4px; 625 | margin-left: -20px; 626 | line-height: 1; 627 | } 628 | 629 | .markdownroot .anchor:focus { 630 | outline: none; 631 | } 632 | 633 | .markdownroot p, 634 | .markdownroot blockquote, 635 | .markdownroot ul, 636 | .markdownroot ol, 637 | .markdownroot dl, 638 | .markdownroot table, 639 | .markdownroot pre, 640 | .markdownroot details { 641 | margin-top: 0; 642 | margin-bottom: 16px; 643 | } 644 | 645 | .markdownroot blockquote>:first-child { 646 | margin-top: 0; 647 | } 648 | 649 | .markdownroot blockquote>:last-child { 650 | margin-bottom: 0; 651 | } 652 | 653 | .markdownroot sup>a::before { 654 | content: "["; 655 | } 656 | 657 | .markdownroot sup>a::after { 658 | content: "]"; 659 | } 660 | 661 | .markdownroot h1 .octicon-link, 662 | .markdownroot h2 .octicon-link, 663 | .markdownroot h3 .octicon-link, 664 | .markdownroot h4 .octicon-link, 665 | .markdownroot h5 .octicon-link, 666 | .markdownroot h6 .octicon-link { 667 | color: var(--color-fg-default); 668 | vertical-align: middle; 669 | visibility: hidden; 670 | } 671 | 672 | .markdownroot h1:hover .anchor, 673 | .markdownroot h2:hover .anchor, 674 | .markdownroot h3:hover .anchor, 675 | .markdownroot h4:hover .anchor, 676 | .markdownroot h5:hover .anchor, 677 | .markdownroot h6:hover .anchor { 678 | text-decoration: none; 679 | } 680 | 681 | .markdownroot h1:hover .anchor .octicon-link, 682 | .markdownroot h2:hover .anchor .octicon-link, 683 | .markdownroot h3:hover .anchor .octicon-link, 684 | .markdownroot h4:hover .anchor .octicon-link, 685 | .markdownroot h5:hover .anchor .octicon-link, 686 | .markdownroot h6:hover .anchor .octicon-link { 687 | visibility: visible; 688 | } 689 | 690 | .markdownroot h1 tt, 691 | .markdownroot h1 code, 692 | .markdownroot h2 tt, 693 | .markdownroot h2 code, 694 | .markdownroot h3 tt, 695 | .markdownroot h3 code, 696 | .markdownroot h4 tt, 697 | .markdownroot h4 code, 698 | .markdownroot h5 tt, 699 | .markdownroot h5 code, 700 | .markdownroot h6 tt, 701 | .markdownroot h6 code { 702 | padding: 0 .2em; 703 | font-size: inherit; 704 | } 705 | 706 | .markdownroot ul.no-list, 707 | .markdownroot ol.no-list { 708 | padding: 0; 709 | list-style-type: none; 710 | } 711 | 712 | .markdownroot ol[type="1"] { 713 | list-style-type: decimal; 714 | } 715 | 716 | .markdownroot ol[type=a] { 717 | list-style-type: lower-alpha; 718 | } 719 | 720 | .markdownroot ol[type=i] { 721 | list-style-type: lower-roman; 722 | } 723 | 724 | .markdownroot div>ol:not([type]) { 725 | list-style-type: decimal; 726 | } 727 | 728 | .markdownroot ul ul, 729 | .markdownroot ul ol, 730 | .markdownroot ol ol, 731 | .markdownroot ol ul { 732 | margin-top: 0; 733 | margin-bottom: 0; 734 | } 735 | 736 | .markdownroot li>p { 737 | margin-top: 16px; 738 | } 739 | 740 | .markdownroot li+li { 741 | margin-top: .25em; 742 | } 743 | 744 | .markdownroot dl { 745 | padding: 0; 746 | } 747 | 748 | .markdownroot dl dt { 749 | padding: 0; 750 | margin-top: 16px; 751 | font-size: 1em; 752 | font-style: italic; 753 | font-weight: 600; 754 | } 755 | 756 | .markdownroot dl dd { 757 | padding: 0 16px; 758 | margin-bottom: 16px; 759 | } 760 | 761 | .markdownroot table th { 762 | font-weight: 600; 763 | } 764 | 765 | .markdownroot table th, 766 | .markdownroot table td { 767 | padding: 6px 13px; 768 | border: 1px solid var(--color-border-default); 769 | } 770 | 771 | .markdownroot table tr { 772 | background-color: var(--color-canvas-default); 773 | border-top: 1px solid var(--color-border-muted); 774 | } 775 | 776 | .markdownroot table tr:nth-child(2n) { 777 | background-color: var(--color-canvas-subtle); 778 | } 779 | 780 | .markdownroot table img { 781 | background-color: transparent; 782 | } 783 | 784 | .markdownroot img[align=right] { 785 | padding-left: 20px; 786 | } 787 | 788 | .markdownroot img[align=left] { 789 | padding-right: 20px; 790 | } 791 | 792 | .markdownroot .emoji { 793 | max-width: none; 794 | vertical-align: text-top; 795 | background-color: transparent; 796 | } 797 | 798 | .markdownroot span.frame { 799 | display: block; 800 | overflow: hidden; 801 | } 802 | 803 | .markdownroot span.frame>span { 804 | display: block; 805 | float: left; 806 | width: auto; 807 | padding: 7px; 808 | margin: 13px 0 0; 809 | overflow: hidden; 810 | border: 1px solid var(--color-border-default); 811 | } 812 | 813 | .markdownroot span.frame span img { 814 | display: block; 815 | float: left; 816 | } 817 | 818 | .markdownroot span.frame span span { 819 | display: block; 820 | padding: 5px 0 0; 821 | clear: both; 822 | color: var(--color-fg-default); 823 | } 824 | 825 | .markdownroot span.align-center { 826 | display: block; 827 | overflow: hidden; 828 | clear: both; 829 | } 830 | 831 | .markdownroot span.align-center>span { 832 | display: block; 833 | margin: 13px auto 0; 834 | overflow: hidden; 835 | text-align: center; 836 | } 837 | 838 | .markdownroot span.align-center span img { 839 | margin: 0 auto; 840 | text-align: center; 841 | } 842 | 843 | .markdownroot span.align-right { 844 | display: block; 845 | overflow: hidden; 846 | clear: both; 847 | } 848 | 849 | .markdownroot span.align-right>span { 850 | display: block; 851 | margin: 13px 0 0; 852 | overflow: hidden; 853 | text-align: right; 854 | } 855 | 856 | .markdownroot span.align-right span img { 857 | margin: 0; 858 | text-align: right; 859 | } 860 | 861 | .markdownroot span.float-left { 862 | display: block; 863 | float: left; 864 | margin-right: 13px; 865 | overflow: hidden; 866 | } 867 | 868 | .markdownroot span.float-left span { 869 | margin: 13px 0 0; 870 | } 871 | 872 | .markdownroot span.float-right { 873 | display: block; 874 | float: right; 875 | margin-left: 13px; 876 | overflow: hidden; 877 | } 878 | 879 | .markdownroot span.float-right>span { 880 | display: block; 881 | margin: 13px auto 0; 882 | overflow: hidden; 883 | text-align: right; 884 | } 885 | 886 | .markdownroot code, 887 | .markdownroot tt { 888 | padding: .2em .4em; 889 | margin: 0; 890 | font-size: 85%; 891 | background-color: var(--color-neutral-muted); 892 | border-radius: 6px; 893 | } 894 | 895 | .markdownroot code br, 896 | .markdownroot tt br { 897 | display: none; 898 | } 899 | 900 | .markdownroot del code { 901 | text-decoration: inherit; 902 | } 903 | 904 | .markdownroot pre code { 905 | font-size: 100%; 906 | } 907 | 908 | .markdownroot pre>code { 909 | padding: 0; 910 | margin: 0; 911 | word-break: normal; 912 | white-space: pre; 913 | background: transparent; 914 | border: 0; 915 | } 916 | 917 | .markdownroot .highlight { 918 | margin-bottom: 16px; 919 | } 920 | 921 | .markdownroot .highlight pre { 922 | margin-bottom: 0; 923 | word-break: normal; 924 | } 925 | 926 | .markdownroot .highlight pre, 927 | .markdownroot pre { 928 | padding: 16px; 929 | overflow: auto; 930 | font-size: 85%; 931 | line-height: 1.45; 932 | background-color: var(--color-canvas-subtle); 933 | border-radius: 6px; 934 | } 935 | 936 | .markdownroot pre code, 937 | .markdownroot pre tt { 938 | display: inline; 939 | max-width: auto; 940 | padding: 0; 941 | margin: 0; 942 | overflow: visible; 943 | line-height: inherit; 944 | word-wrap: normal; 945 | background-color: transparent; 946 | border: 0; 947 | } 948 | 949 | .markdownroot .csv-data td, 950 | .markdownroot .csv-data th { 951 | padding: 5px; 952 | overflow: hidden; 953 | font-size: 12px; 954 | line-height: 1; 955 | text-align: left; 956 | white-space: nowrap; 957 | } 958 | 959 | .markdownroot .csv-data .blob-num { 960 | padding: 10px 8px 9px; 961 | text-align: right; 962 | background: var(--color-canvas-default); 963 | border: 0; 964 | } 965 | 966 | .markdownroot .csv-data tr { 967 | border-top: 0; 968 | } 969 | 970 | .markdownroot .csv-data th { 971 | font-weight: 600; 972 | background: var(--color-canvas-subtle); 973 | border-top: 0; 974 | } 975 | 976 | .markdownroot .footnotes { 977 | font-size: 12px; 978 | color: var(--color-fg-muted); 979 | border-top: 1px solid var(--color-border-default); 980 | } 981 | 982 | .markdownroot .footnotes ol { 983 | padding-left: 16px; 984 | } 985 | 986 | .markdownroot .footnotes li { 987 | position: relative; 988 | } 989 | 990 | .markdownroot .footnotes li:target::before { 991 | position: absolute; 992 | top: -8px; 993 | right: -8px; 994 | bottom: -8px; 995 | left: -24px; 996 | pointer-events: none; 997 | content: ""; 998 | border: 2px solid var(--color-accent-emphasis); 999 | border-radius: 6px; 1000 | } 1001 | 1002 | .markdownroot .footnotes li:target { 1003 | color: var(--color-fg-default); 1004 | } 1005 | 1006 | .markdownroot .footnotes .data-footnote-backref g-emoji { 1007 | font-family: monospace; 1008 | } 1009 | 1010 | .markdownroot [hidden] { 1011 | display: none !important; 1012 | } 1013 | 1014 | .markdownroot ::-webkit-calendar-picker-indicator { 1015 | filter: invert(50%); 1016 | } 1017 | -------------------------------------------------------------------------------- /ext/sss/print.css: -------------------------------------------------------------------------------- 1 | :root { 2 | background: white; 3 | color: black; 4 | font: 8pt serif; 5 | line-height: 1.5; 6 | vertical-align: baseline; 7 | } 8 | 9 | body, .markdownRoot { 10 | max-width: 100%; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | * { 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | p { 21 | margin: 0 0 .3rem 0; 22 | } 23 | 24 | ul, ol { 25 | padding-left: 2em 26 | } 27 | 28 | a { 29 | color: #666; 30 | text-decoration: underline; 31 | } 32 | 33 | a[href]:after { 34 | content: ' (' attr(href) ')'; 35 | font-size: 1rem; 36 | } 37 | 38 | img { 39 | max-width: 100%; 40 | } 41 | 42 | h1, h2, h3, h4, h5, h6 { 43 | padding: 0 0 .2em 0; 44 | } 45 | 46 | h1, h2, h3, h4, h5, h6, dt { 47 | /* break-after: avoid; */ 48 | break-inside: avoid; 49 | } 50 | 51 | /* Hack to fix break-after: https://stackoverflow.com/a/53742871 */ 52 | h1:after, h2:after, h3:after, h4:after, h5:after, h6:after, dt:after { 53 | content: ""; 54 | display: block; 55 | /* .2em of padding + 3em for 2 full lines */ 56 | height: 3.2em; 57 | margin-bottom: -3.2em; 58 | } 59 | 60 | blockquote, pre, li, dt, dd { 61 | break-inside: avoid; 62 | } 63 | -------------------------------------------------------------------------------- /ext/sss/sss.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Thibaut Rousseau 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | * of the Software, and to permit persons to whom the Software is furnished to do 9 | * so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | @media (prefers-color-scheme: light) { 24 | :root { 25 | --back: white; 26 | --text: #333333; 27 | --link: #0088CC; 28 | --alt-link: #005588; 29 | --alt-back: #EEEEEE; 30 | } 31 | } 32 | 33 | @media (prefers-color-scheme: dark) { 34 | :root { 35 | --back: #1C1B22; 36 | --text: #FBFBFE; 37 | --link: #55CCFF; 38 | --alt-link: #0088CC; 39 | --alt-back: #444444; 40 | } 41 | } 42 | 43 | :root { 44 | background: var(--back); 45 | color: var(--text); 46 | } 47 | 48 | body { 49 | font-family: 'Segoe UI', 'Lucida Grande', Helvetica, sans-serif; 50 | line-height: 1.5; 51 | margin: 2em; 52 | } 53 | 54 | h1, h2, h3, h4, h5, h6 { 55 | font-weight: normal; 56 | line-height: 1em; 57 | margin: 20px 0; 58 | } 59 | h1 { 60 | font-size: 2.25em; 61 | } 62 | h2 { 63 | font-size: 1.75em; 64 | } 65 | h3 { 66 | font-size: 1.5em; 67 | } 68 | h4, h5, h6 { 69 | font-size: 1.25em; 70 | } 71 | 72 | a { 73 | color: var(--link); 74 | text-decoration: none; 75 | } 76 | a:hover, a:focus { 77 | text-decoration: underline; 78 | } 79 | a:visited { 80 | color: var(--alt-link); 81 | } 82 | 83 | img { 84 | max-width: 100%; 85 | } 86 | 87 | li + li { 88 | margin-top: 3px; 89 | } 90 | dt { 91 | font-weight: bold; 92 | } 93 | 94 | code { 95 | background: var(--alt-back); 96 | font-family: "Consolas", "Lucida Console", monospace; 97 | padding: 1px 5px; 98 | } 99 | pre { 100 | background: var(--alt-back); 101 | padding: 5px 10px; 102 | white-space: pre-wrap; 103 | } 104 | pre code { 105 | padding: 0; 106 | } 107 | 108 | blockquote { 109 | border-left: 5px solid var(--alt-back); 110 | margin: 0; 111 | padding: 0 10px; 112 | } 113 | 114 | table { 115 | border-collapse: collapse; 116 | width: 100%; 117 | } 118 | table + table { 119 | margin-top: 1em; 120 | } 121 | thead { 122 | background: var(--alt-back); 123 | text-align: left; 124 | } 125 | th, td { 126 | border: 1px solid var(--alt-back); 127 | padding: 5px 10px; 128 | } 129 | 130 | hr { 131 | background: var(--alt-back); 132 | border: 0; 133 | height: 1px; 134 | } 135 | -------------------------------------------------------------------------------- /ext/toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimbali/markdown-viewer/0a1d818b2a663a53c16da5001ca1705044075611/ext/toolbar.png -------------------------------------------------------------------------------- /ext/view-md.css: -------------------------------------------------------------------------------- 1 | html, body, iframe { 2 | display: block; 3 | margin: 0px; 4 | padding: 0px; 5 | } 6 | 7 | @media (prefers-color-scheme: dark) { 8 | body { 9 | color: white; 10 | background: black; 11 | font-weight: 350; 12 | } 13 | .error span { 14 | font-weight: 650; 15 | } 16 | a { 17 | color: lightblue; 18 | } 19 | a:visited { 20 | opacity: lightskyblue; 21 | } 22 | 23 | span.copy { 24 | border-color: white; 25 | } 26 | span.copy:after { 27 | filter: invert(100%); 28 | } 29 | } 30 | 31 | @media (prefers-color-scheme: light) { 32 | span.copy { 33 | border-color: black; 34 | } 35 | } 36 | 37 | iframe { 38 | border: none; 39 | overflow: scroll; 40 | width: 100vw; 41 | height: 100vh; 42 | resize: both; 43 | } 44 | 45 | .request-local-access { 46 | text-align: center; 47 | display: block; 48 | margin: 20vh auto 0; 49 | padding: 20vh 15px; 50 | background-color: #8080FF80; 51 | border-radius: 10px; 52 | max-width: 75vw; 53 | line-height: 200%; 54 | } 55 | 56 | .error { 57 | text-align: center; 58 | display: block; 59 | margin: 20vh auto 0; 60 | padding: 15px; 61 | background-color: #FF000080; 62 | border-radius: 10px; 63 | max-width: 50vw; 64 | line-height: 200%; 65 | } 66 | 67 | .error span { 68 | font-weight: bold; 69 | } 70 | 71 | span.copy { 72 | position: relative; 73 | border-style: inset; 74 | border-width: 1px; 75 | border-radius: 3px; 76 | padding: 3px; 77 | cursor: pointer; 78 | font-family: monospace; 79 | } 80 | 81 | span.copy:after { 82 | content: ' '; 83 | background: url('./copy.svg') no-repeat right; 84 | background-size: .9em; 85 | width: 1.2em; 86 | height: .9em; 87 | display: inline-block; 88 | vertical-align: baseline; 89 | } 90 | 91 | span.copy span.feedback { 92 | display: none; 93 | position: absolute; 94 | right: 0; 95 | bottom: 2em; 96 | line-height: 100%; 97 | padding: 9px; 98 | border-radius: 6px; 99 | 100 | background: #000; 101 | color: white; 102 | } 103 | 104 | span.copy span.feedback::after { 105 | content: ''; 106 | position: absolute; 107 | width: 0; 108 | height: 0; 109 | top: 100%; 110 | right: 12px; 111 | border: 6px solid transparent; 112 | border-bottom: none; 113 | 114 | border-top-color: #000; 115 | } 116 | -------------------------------------------------------------------------------- /ext/view-md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ext/view-md.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function parseURI() { 4 | const params = new URLSearchParams(window.location.search); 5 | let file = params.get("file"); 6 | if (!file) { 7 | return null; 8 | } 9 | if (file.startsWith('ext+view-markdown:')) { 10 | file = file.slice('ext+view-markdown:'.length); 11 | } 12 | 13 | try { 14 | const url = new URL(file); 15 | if (url.protocol) { 16 | return url; 17 | } 18 | } catch (e) {} 19 | 20 | // Try to handle URLs without schemes. Does it look like a local file path? 21 | if (file.startsWith('/') || file.match(/^[A-Z]:/u)) { 22 | try { 23 | return new URL(`file://${file}`); 24 | } catch (e) {} 25 | } else { 26 | try { 27 | return new URL(`http://${file}`); 28 | } catch (e) {} 29 | } 30 | 31 | return null; 32 | } 33 | 34 | const file = parseURI(); 35 | 36 | function display(allowedUrl, displayUrl) { 37 | const spinner = document.body.appendChild(document.createElement('div')); 38 | spinner.id = 'spinner'; 39 | 40 | return fetch(allowedUrl).then(r => r.text()).then(text => { 41 | const inserter = rendered => document.body.replaceChild(rendered, spinner); 42 | webext.storage.sync.get(['iframe_embed', 'plugins']).then(({ 43 | iframe_embed: embed = true, 44 | plugins = pluginDefaults, 45 | }) => { 46 | const { mermaid: useMermaid } = plugins; 47 | if (useMermaid) { 48 | mermaid.initialize(); 49 | } 50 | if (embed) { 51 | renderInIframe(document, text, { inserter, url: allowedUrl.toString(), displayUrl }); 52 | } else { 53 | renderInDocument(document, text, { inserter, url: allowedUrl.toString(), displayUrl }); 54 | } 55 | }) 56 | }).catch(() => { 57 | const error = document.body.appendChild(document.createElement('p')); 58 | error.classList.add('error'); 59 | 60 | const span = error.appendChild(document.createElement('span')); 61 | span.innerText = 'Failed loading page '; 62 | 63 | const link = span.appendChild(document.createElement('a')); 64 | link.href = displayUrl; 65 | link.innerText = displayUrl; 66 | 67 | document.body.removeChild(spinner); 68 | }) 69 | } 70 | 71 | function stopEvent(e) { 72 | e.stopPropagation(); 73 | e.preventDefault(); 74 | } 75 | 76 | function accessGranted(e) { 77 | stopEvent(e); 78 | const [fileObj] = (e.target.tagName === 'INPUT' ? e.target : e.dataTransfer).files; 79 | 80 | let url, displayUrl; 81 | if (typeof fileObj !== 'undefined') { 82 | url = URL.createObjectURL(fileObj); 83 | displayUrl = file || `file:///C:/fakepath/${fileObj.name}`; 84 | } 85 | else if (e.dataTransfer) { 86 | // Someone dropped an URL here instead of a file 87 | try { 88 | url = new URL(e.dataTransfer.getData('text/plain')); 89 | displayUrl = url.toString() 90 | } catch (e) { 91 | return 92 | } 93 | } 94 | 95 | const msg = document.body.querySelector('p.request-local-access'); 96 | 97 | display(url, displayUrl).then(() => { 98 | document.body.removeChild(msg); 99 | }); 100 | } 101 | 102 | 103 | if (file === null || file.protocol === 'file:') { 104 | const msg = document.body.appendChild(document.createElement('p')); 105 | msg.classList.add('request-local-access'); 106 | msg.appendChild(document.createTextNode( 107 | 'For your security, Firefox requires you to select local markdown file explicitely ' 108 | + 'before markdown-viewer can open it.' 109 | )); 110 | msg.appendChild(document.createElement('br')); 111 | 112 | if (file) { 113 | msg.appendChild(document.createTextNode(`The requested file is `)); 114 | const copy = msg.appendChild(document.createElement('span')); 115 | msg.appendChild(document.createElement('br')); 116 | 117 | copy.appendChild(document.createTextNode(file)); 118 | copy.classList.add('copy'); 119 | copy.title = 'Click to copy'; 120 | 121 | const feedback = copy.appendChild(document.createElement('span')); 122 | feedback.classList.add('feedback'); 123 | feedback.appendChild(document.createTextNode('Copied to clipboard!')); 124 | 125 | copy.addEventListener('click', () => { 126 | navigator.clipboard.writeText(file); 127 | feedback.style.display = 'block'; 128 | setTimeout(() => { feedback.style.display = 'none'; }, 700) 129 | }); 130 | } 131 | 132 | msg.addEventListener('dragenter', stopEvent, false); 133 | msg.addEventListener('dragover', stopEvent, false); 134 | msg.addEventListener('drop', accessGranted, false); 135 | 136 | msg.appendChild(document.createElement('label')).appendChild( 137 | document.createTextNode('Select markdown file (or drag it onto this area): ') 138 | ); 139 | 140 | const input = msg.lastChild.appendChild(document.createElement('input')); 141 | input.type = 'file'; 142 | input.accept = 'text/markdown,text/plain,.md,.markdown,.mdown,.mdwn,.mkd,.mkdn'; 143 | input.addEventListener('change', accessGranted, false); 144 | 145 | document.title = 'Markdown Viewer file selector'; 146 | } else { 147 | display(file, file) 148 | } 149 | -------------------------------------------------------------------------------- /ext/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | importScripts('../lib/markdown-it/dist/markdown-it.min.js') 4 | importScripts('../lib/markdown-it-checkbox/dist/markdown-it-checkbox.js') 5 | importScripts('../lib/markdown-it-emoji/dist/markdown-it-emoji.js') 6 | importScripts('../lib/markdown-it-footnote/dist/markdown-it-footnote.js') 7 | importScripts('../lib/markdown-it-fancy-lists/markdown-it-fancy-lists.js') 8 | importScripts('../lib/@highlightjs/cdn-assets/highlight.min.js') 9 | importScripts('../srclib/katex/dist/katex.min.js') 10 | importScripts('../lib/markdown-it-texmath/texmath.js') 11 | importScripts('./frontmatter.js') 12 | importScripts('./renderer.js') 13 | 14 | onmessage = function(e) { 15 | const { text, plugins } = e.data; 16 | new Renderer(plugins).render(text).then(postMessage); 17 | } 18 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Markdown Viewer Webext", 4 | "short_name": "Markdown Viewer", 5 | "version": "2.0.2", 6 | "author": "Cimbali, Keith L Robertson", 7 | "homepage_url": "https://github.com/Cimbali/markdown-viewer", 8 | "description": "Displays markdown documents beautified in your browser, both on the web and local files (file:// URLs).", 9 | 10 | "browser_specific_settings": { 11 | "gecko": { 12 | "id": "{943b8007-a895-44af-a672-4f4ea548c95f}" 13 | } 14 | }, 15 | 16 | "background": { 17 | "scripts": ["ext/background.js"], 18 | "persistent": false 19 | }, 20 | 21 | "icons": { 22 | "48": "ext/markdown-mark.svg", 23 | "96": "ext/markdown-mark.svg" 24 | }, 25 | 26 | "permissions": [ 27 | "storage", 28 | "activeTab" 29 | ], 30 | 31 | "optional_permissions": [ 32 | "*://*/*.markdown", 33 | "*://*/*.MARKDOWN", 34 | "*://*/*.md", 35 | "*://*/*.MD", 36 | "*://*/*.mdown", 37 | "*://*/*.MDOWN", 38 | "*://*/*.mdwn", 39 | "*://*/*.MDWN", 40 | "*://*/*.mkd", 41 | "*://*/*.MKD", 42 | "*://*/*.mkdn", 43 | "*://*/*.MKDN", 44 | "file://*/*.markdown", 45 | "file://*/*.MARKDOWN", 46 | "file://*/*.md", 47 | "file://*/*.MD", 48 | "file://*/*.mdown", 49 | "file://*/*.MDOWN", 50 | "file://*/*.mdwn", 51 | "file://*/*.MDWN", 52 | "file://*/*.mkd", 53 | "file://*/*.MKD", 54 | "file://*/*.mkdn", 55 | "file://*/*.MKDN" 56 | ], 57 | 58 | "content_security_policy": "default-src: 'self'; object-src 'none'; frame-src 'self'; script-src 'self'; img-src blob: https: filesystem:; media-src blob: https: filesystem:; connect-src blob: https: filesystem:; style-src: 'self' blob:; upgrade-insecure-requests;", 59 | 60 | "protocol_handlers": [ 61 | { 62 | "protocol": "ext+view-markdown", 63 | "name": "Markdown Viewer", 64 | "uriTemplate": "/ext/view-md.html?file=%s" 65 | } 66 | ], 67 | 68 | "options_ui": { 69 | "page": "ext/options.html", 70 | "browser_style": true 71 | }, 72 | 73 | "web_accessible_resources": [ 74 | "lib/sss/*.css", 75 | "lib/highlightjs/build/styles/*.min.css", 76 | "lib/markdown-it-texmath/css/texmath.css", 77 | "lib/katex/dist/katex.min.css", 78 | "ext/menu.css" 79 | ], 80 | 81 | "page_action": { 82 | "browser_style": true, 83 | "default_icon": "ext/markdown-mark.svg", 84 | "default_title": "Force rendering Markdown", 85 | "show_matches": [ 86 | "*://*/*.markdown", 87 | "*://*/*.MARKDOWN", 88 | "*://*/*.md", 89 | "*://*/*.MD", 90 | "*://*/*.mdown", 91 | "*://*/*.MDOWN", 92 | "*://*/*.mdwn", 93 | "*://*/*.MDWN", 94 | "*://*/*.mkd", 95 | "*://*/*.MKD", 96 | "*://*/*.mkdn", 97 | "*://*/*.MKDN", 98 | "file://*/*.markdown", 99 | "file://*/*.MARKDOWN", 100 | "file://*/*.md", 101 | "file://*/*.MD", 102 | "file://*/*.mdown", 103 | "file://*/*.MDOWN", 104 | "file://*/*.mdwn", 105 | "file://*/*.MDWN", 106 | "file://*/*.mkd", 107 | "file://*/*.MKD", 108 | "file://*/*.mkdn", 109 | "file://*/*.MKDN" 110 | ] 111 | }, 112 | 113 | "browser_action": { 114 | "browser_style": true, 115 | "default_icon": "ext/markdown-mark.svg", 116 | "default_title": "Open local markdown file" 117 | }, 118 | 119 | "commands": { 120 | "_execute_browser_action": { 121 | "description": "Open local markdown file picker" 122 | }, 123 | "_execute_page_action": { 124 | "description": "Force rendering markdown in page" 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@highlightjs/cdn-assets": "^11.8.0", 4 | "markdown-it": "^13.0.1", 5 | "markdown-it-checkbox": "^1.1.0", 6 | "markdown-it-emoji": "^2.0.2", 7 | "markdown-it-fancy-lists": "https://github.com/mminer237/markdown-it-fancy-lists#v1.0", 8 | "markdown-it-footnote": "^3.0.3", 9 | "markdown-it-texmath": "https://github.com/Cimbali/markdown-it-texmath#v1.0.1", 10 | "mermaid": "9" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Rousseau Thibaut 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /test/MarkdownSample.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cimbali/markdown-viewer/0a1d818b2a663a53c16da5001ca1705044075611/test/MarkdownSample.PNG -------------------------------------------------------------------------------- /test/sub/hello-sub.md: -------------------------------------------------------------------------------- 1 | # Hello Sub-World! 2 | -------------------------------------------------------------------------------- /test/test-file.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Test document 3 | demo: this triple-dash separated block is a YAML front matter and should not be rendered 4 | --- 5 | # Heading 1 6 | ## Heading 2 7 | ### Heading 3 8 | #### Heading 4 9 | ##### Heading 5 10 | ###### Heading 6 11 | 12 | Markdown Viewer Webext now supports local styles in a file _markdown.css alongside the .md files. 13 | This is used here to format the top-level heading in bold blue text. 14 | 15 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 16 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 17 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 18 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 19 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 20 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 21 | 22 | Unicode: âêîôûŵŷ äëïöüẅÿ àèìòùẁỳ áéíóúẃý Вот какой-то текст. 23 | 24 | **bold** 25 | 26 | *italic* 27 | 28 | ~~strikethrough~~ 29 | 30 | multiple_underscores_in_text_without_parsing 31 | 32 | ![image demo](MarkdownSample.PNG "hover text") 33 | 34 | Let's try some :slightly_smiling_face: emoji[^1] :-) for your viewing :joy: pleasure^[footnotes can also be inline]. 35 | 36 | [^1]: even if you do not like emojis, you might still enjoy footnotes 37 | 38 | Unordered lists with: 39 | - asterisk 40 | - dash 41 | - or plus 42 | - [ ] Can contain checkboxes, unchecked 43 | - [X] or checked 44 | 45 | Ordered lists: 46 | 1. start with numbers. 47 | 2. any number will do 48 | 3. if you like 49 | 50 | Roman ordered lists: 51 | i. foo 52 | ii. bar 53 | iii. baz 54 | 55 | | Left-Aligned | Center Aligned | Right Aligned | 56 | | :------------ |:---------------:| -----:| 57 | | col 1 is | some wordy text | $1600 | 58 | | col 2 is | centered | $12 | 59 | | zebra stripes | are neat | $1 | 60 | 61 | [Link to Markdown Viewer (on AMO)](https://addons.mozilla.org/firefox/addon/markdown-viewer-webext/) 62 | 63 | https://addons.mozilla.org/firefox/addon/markdown-viewer-webext/ 64 | 65 | [relative link](test-nobom.md) 66 | 67 | [relative link to subdir](sub/hello-sub.md) 68 | 69 | ```js 70 | // Some Javascript code 71 | function myFunction() { 72 | console.log("Hello World!"); 73 | } 74 | ``` 75 | 76 | ![Small image](http://lorempixel.com/400/200/) 77 | 78 | ![Large image (should be resized)](http://lorempixel.com/1200/200/) 79 | 80 | HTML is supported 81 | 82 | But scripts (in script elements or from events) are not accepted 83 | 84 | 85 | 86 | 87 | 88 | JavaScript links are trampled out, too 89 | 90 | LaTeX rendering: $\sqrt 5$ 91 | 92 | # Anchor links tests for #49 93 | 94 | 1. [متطلبات التشغيل الأولي](#متطلبات-التشغيل-الأولي) 95 | 2. [اكتشاف الكاميرات](#اكتشاف-الكاميرات) 96 | 3. [A test with à é ñ](#a-test-with-à-é-ñ) 97 | 98 | ## متطلبات التشغيل الأولي 99 | * تجربة 100 | 101 | ## اكتشاف الكاميرات 102 | * تجربة 103 | 104 | ## A test with à é ñ 105 | * a test with à é ñ 106 | 107 | ## A mermaid diagram copied fom the mermaid docs 108 | 109 | ```mermaid 110 | sequenceDiagram 111 | participant Alice 112 | participant Bob 113 | Alice->>John: Hello John, how are you? 114 | loop Healthcheck 115 | John->>John: Fight against hypochondria 116 | end 117 | Note right of John: Rational thoughts prevail! 118 | John-->>Alice: Great! 119 | John->>Bob: How about you? 120 | Bob-->>John: Jolly good! 121 | ``` 122 | -------------------------------------------------------------------------------- /test/test-nobom.md: -------------------------------------------------------------------------------- 1 | # Heading 1 2 | ## Heading 2 3 | ### Heading 3 4 | #### Heading 4 5 | ##### Heading 5 6 | ###### Heading 6 7 | 8 | Markdown Viewer Webext now supports local styles in a file _markdown.css alongside the .md files. 9 | This is used here to format the top-level heading in bold blue text. 10 | 11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 12 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 13 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 14 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 15 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 16 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 17 | 18 | Unicode: âêîôûŵŷ äëïöüẅÿ àèìòùẁỳ áéíóúẃý Вот какой-то текст. 19 | 20 | á is á 21 | 22 | **bold** 23 | 24 | *italic* 25 | 26 | ~~strikethrough~~ 27 | 28 | ![image demo](MarkdownSample.PNG "hover text") 29 | 30 | multiple_underscores_in_text_without_parsing 31 | 32 | Unordered lists with: 33 | - asterisk 34 | - dash 35 | - or plus 36 | - [ ] Can contain checkboxes, unchecked 37 | - [X] or checked 38 | 39 | Ordered lists: 40 | 1. start with numbers. 41 | 2. any number will do 42 | 3. if you like 43 | 44 | | Left-Aligned | Center Aligned | Right Aligned | 45 | | :------------ |:---------------:| -----:| 46 | | col 1 is | some wordy text | $1600 | 47 | | col 2 is | centered | $12 | 48 | | zebra stripes | are neat | $1 | 49 | 50 | [Link to Markdown Viewer (on AMO)](https://addons.mozilla.org/firefox/addon/markdown-viewer-webext/) 51 | 52 | https://addons.mozilla.org/firefox/addon/markdown-viewer-webext/ 53 | 54 | [relative link](test-file.md) 55 | 56 | [relative link to subdir](sub/hello-sub.md) 57 | 58 | ```js 59 | // Some Javascript code 60 | function myFunction() { 61 | console.log("Hello World!"); 62 | } 63 | ``` 64 | 65 | ![Small image](http://lorempixel.com/400/200/) 66 | 67 | ![Large image (should be resized)](http://lorempixel.com/1200/200/) 68 | 69 | HTML is supported 70 | 71 | But scripts (in script elements or from events) are not accepted 72 | 73 | 74 | 75 | 76 | 77 | JavaScript links are trampled out, too 78 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@braintree/sanitize-url@^6.0.0": 6 | version "6.0.2" 7 | resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" 8 | integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg== 9 | 10 | "@highlightjs/cdn-assets@^11.8.0": 11 | version "11.8.0" 12 | resolved "https://registry.yarnpkg.com/@highlightjs/cdn-assets/-/cdn-assets-11.8.0.tgz#e3aa9f20bf742b50bd7b1d60a24c8e7d124a602f" 13 | integrity sha512-gkfCH4xGBGY9xPaW+t26WpgnfpDhNhB5RtVUDLx3MHkC7ZrmKeIxXsfjzOiuOnEgRk+vydlY6XeOeglh+eVhyg== 14 | 15 | argparse@^2.0.1: 16 | version "2.0.1" 17 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 18 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 19 | 20 | commander@7: 21 | version "7.2.0" 22 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" 23 | integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== 24 | 25 | cose-base@^1.0.0: 26 | version "1.0.3" 27 | resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a" 28 | integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg== 29 | dependencies: 30 | layout-base "^1.0.0" 31 | 32 | cose-base@^2.2.0: 33 | version "2.2.0" 34 | resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01" 35 | integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g== 36 | dependencies: 37 | layout-base "^2.0.0" 38 | 39 | cytoscape-cose-bilkent@^4.1.0: 40 | version "4.1.0" 41 | resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b" 42 | integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ== 43 | dependencies: 44 | cose-base "^1.0.0" 45 | 46 | cytoscape-fcose@^2.1.0: 47 | version "2.2.0" 48 | resolved "https://registry.yarnpkg.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471" 49 | integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ== 50 | dependencies: 51 | cose-base "^2.2.0" 52 | 53 | cytoscape@^3.23.0: 54 | version "3.25.0" 55 | resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.25.0.tgz#5289e9d18be0293b073bfe93f83bb95b908b2dc1" 56 | integrity sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q== 57 | dependencies: 58 | heap "^0.2.6" 59 | lodash "^4.17.21" 60 | 61 | "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: 62 | version "3.2.4" 63 | resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" 64 | integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== 65 | dependencies: 66 | internmap "1 - 2" 67 | 68 | d3-axis@3: 69 | version "3.0.0" 70 | resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" 71 | integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== 72 | 73 | d3-brush@3: 74 | version "3.0.0" 75 | resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" 76 | integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== 77 | dependencies: 78 | d3-dispatch "1 - 3" 79 | d3-drag "2 - 3" 80 | d3-interpolate "1 - 3" 81 | d3-selection "3" 82 | d3-transition "3" 83 | 84 | d3-chord@3: 85 | version "3.0.1" 86 | resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" 87 | integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== 88 | dependencies: 89 | d3-path "1 - 3" 90 | 91 | "d3-color@1 - 3", d3-color@3: 92 | version "3.1.0" 93 | resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" 94 | integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== 95 | 96 | d3-contour@4: 97 | version "4.0.2" 98 | resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" 99 | integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== 100 | dependencies: 101 | d3-array "^3.2.0" 102 | 103 | d3-delaunay@6: 104 | version "6.0.4" 105 | resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" 106 | integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== 107 | dependencies: 108 | delaunator "5" 109 | 110 | "d3-dispatch@1 - 3", d3-dispatch@3: 111 | version "3.0.1" 112 | resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" 113 | integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== 114 | 115 | "d3-drag@2 - 3", d3-drag@3: 116 | version "3.0.0" 117 | resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" 118 | integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== 119 | dependencies: 120 | d3-dispatch "1 - 3" 121 | d3-selection "3" 122 | 123 | "d3-dsv@1 - 3", d3-dsv@3: 124 | version "3.0.1" 125 | resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" 126 | integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== 127 | dependencies: 128 | commander "7" 129 | iconv-lite "0.6" 130 | rw "1" 131 | 132 | "d3-ease@1 - 3", d3-ease@3: 133 | version "3.0.1" 134 | resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" 135 | integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== 136 | 137 | d3-fetch@3: 138 | version "3.0.1" 139 | resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" 140 | integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== 141 | dependencies: 142 | d3-dsv "1 - 3" 143 | 144 | d3-force@3: 145 | version "3.0.0" 146 | resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" 147 | integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== 148 | dependencies: 149 | d3-dispatch "1 - 3" 150 | d3-quadtree "1 - 3" 151 | d3-timer "1 - 3" 152 | 153 | "d3-format@1 - 3", d3-format@3: 154 | version "3.1.0" 155 | resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" 156 | integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== 157 | 158 | d3-geo@3: 159 | version "3.1.0" 160 | resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e" 161 | integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA== 162 | dependencies: 163 | d3-array "2.5.0 - 3" 164 | 165 | d3-hierarchy@3: 166 | version "3.1.2" 167 | resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" 168 | integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== 169 | 170 | "d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: 171 | version "3.0.1" 172 | resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" 173 | integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== 174 | dependencies: 175 | d3-color "1 - 3" 176 | 177 | "d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: 178 | version "3.1.0" 179 | resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" 180 | integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== 181 | 182 | d3-polygon@3: 183 | version "3.0.1" 184 | resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" 185 | integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== 186 | 187 | "d3-quadtree@1 - 3", d3-quadtree@3: 188 | version "3.0.1" 189 | resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" 190 | integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== 191 | 192 | d3-random@3: 193 | version "3.0.1" 194 | resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" 195 | integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== 196 | 197 | d3-scale-chromatic@3: 198 | version "3.0.0" 199 | resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" 200 | integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== 201 | dependencies: 202 | d3-color "1 - 3" 203 | d3-interpolate "1 - 3" 204 | 205 | d3-scale@4: 206 | version "4.0.2" 207 | resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" 208 | integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== 209 | dependencies: 210 | d3-array "2.10.0 - 3" 211 | d3-format "1 - 3" 212 | d3-interpolate "1.2.0 - 3" 213 | d3-time "2.1.1 - 3" 214 | d3-time-format "2 - 4" 215 | 216 | "d3-selection@2 - 3", d3-selection@3: 217 | version "3.0.0" 218 | resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" 219 | integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== 220 | 221 | d3-shape@3: 222 | version "3.2.0" 223 | resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" 224 | integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== 225 | dependencies: 226 | d3-path "^3.1.0" 227 | 228 | "d3-time-format@2 - 4", d3-time-format@4: 229 | version "4.1.0" 230 | resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" 231 | integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== 232 | dependencies: 233 | d3-time "1 - 3" 234 | 235 | "d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: 236 | version "3.1.0" 237 | resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" 238 | integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== 239 | dependencies: 240 | d3-array "2 - 3" 241 | 242 | "d3-timer@1 - 3", d3-timer@3: 243 | version "3.0.1" 244 | resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" 245 | integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== 246 | 247 | "d3-transition@2 - 3", d3-transition@3: 248 | version "3.0.1" 249 | resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" 250 | integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== 251 | dependencies: 252 | d3-color "1 - 3" 253 | d3-dispatch "1 - 3" 254 | d3-ease "1 - 3" 255 | d3-interpolate "1 - 3" 256 | d3-timer "1 - 3" 257 | 258 | d3-zoom@3: 259 | version "3.0.0" 260 | resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" 261 | integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== 262 | dependencies: 263 | d3-dispatch "1 - 3" 264 | d3-drag "2 - 3" 265 | d3-interpolate "1 - 3" 266 | d3-selection "2 - 3" 267 | d3-transition "2 - 3" 268 | 269 | d3@^7.4.0, d3@^7.8.2: 270 | version "7.8.5" 271 | resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c" 272 | integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA== 273 | dependencies: 274 | d3-array "3" 275 | d3-axis "3" 276 | d3-brush "3" 277 | d3-chord "3" 278 | d3-color "3" 279 | d3-contour "4" 280 | d3-delaunay "6" 281 | d3-dispatch "3" 282 | d3-drag "3" 283 | d3-dsv "3" 284 | d3-ease "3" 285 | d3-fetch "3" 286 | d3-force "3" 287 | d3-format "3" 288 | d3-geo "3" 289 | d3-hierarchy "3" 290 | d3-interpolate "3" 291 | d3-path "3" 292 | d3-polygon "3" 293 | d3-quadtree "3" 294 | d3-random "3" 295 | d3-scale "4" 296 | d3-scale-chromatic "3" 297 | d3-selection "3" 298 | d3-shape "3" 299 | d3-time "3" 300 | d3-time-format "4" 301 | d3-timer "3" 302 | d3-transition "3" 303 | d3-zoom "3" 304 | 305 | dagre-d3-es@7.0.9: 306 | version "7.0.9" 307 | resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.9.tgz#aca12fccd9d09955a4430029ba72ee6934542a8d" 308 | integrity sha512-rYR4QfVmy+sR44IBDvVtcAmOReGBvRCWDpO2QjYwqgh9yijw6eSHBqaPG/LIOEy7aBsniLvtMW6pg19qJhq60w== 309 | dependencies: 310 | d3 "^7.8.2" 311 | lodash-es "^4.17.21" 312 | 313 | dayjs@^1.11.7: 314 | version "1.11.8" 315 | resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.8.tgz#4282f139c8c19dd6d0c7bd571e30c2d0ba7698ea" 316 | integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ== 317 | 318 | delaunator@5: 319 | version "5.0.0" 320 | resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b" 321 | integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw== 322 | dependencies: 323 | robust-predicates "^3.0.0" 324 | 325 | dompurify@2.4.3: 326 | version "2.4.3" 327 | resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03" 328 | integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ== 329 | 330 | elkjs@^0.8.2: 331 | version "0.8.2" 332 | resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e" 333 | integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ== 334 | 335 | entities@~3.0.1: 336 | version "3.0.1" 337 | resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" 338 | integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== 339 | 340 | heap@^0.2.6: 341 | version "0.2.7" 342 | resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" 343 | integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== 344 | 345 | iconv-lite@0.6: 346 | version "0.6.3" 347 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" 348 | integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== 349 | dependencies: 350 | safer-buffer ">= 2.1.2 < 3.0.0" 351 | 352 | "internmap@1 - 2": 353 | version "2.0.3" 354 | resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" 355 | integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== 356 | 357 | khroma@^2.0.0: 358 | version "2.0.0" 359 | resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.0.0.tgz#7577de98aed9f36c7a474c4d453d94c0d6c6588b" 360 | integrity sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g== 361 | 362 | layout-base@^1.0.0: 363 | version "1.0.2" 364 | resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" 365 | integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== 366 | 367 | layout-base@^2.0.0: 368 | version "2.0.1" 369 | resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285" 370 | integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg== 371 | 372 | linkify-it@^4.0.1: 373 | version "4.0.1" 374 | resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.1.tgz#01f1d5e508190d06669982ba31a7d9f56a5751ec" 375 | integrity sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw== 376 | dependencies: 377 | uc.micro "^1.0.1" 378 | 379 | lodash-es@^4.17.21: 380 | version "4.17.21" 381 | resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" 382 | integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== 383 | 384 | lodash@^4.17.21: 385 | version "4.17.21" 386 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 387 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 388 | 389 | markdown-it-checkbox@^1.1.0: 390 | version "1.1.0" 391 | resolved "https://registry.yarnpkg.com/markdown-it-checkbox/-/markdown-it-checkbox-1.1.0.tgz#20cff97f33d77d172f9dcf1bcfc92cecc5330fac" 392 | integrity sha512-NkZVjnXo5G+cLNdi7DPZxICypBuxFE9F8sx3YGMZn+Cfizr8EZ/1TFUKl7ZnefF6cr1aFHbnQ5iA3rc4cp7EyA== 393 | dependencies: 394 | underscore "^1.8.2" 395 | 396 | markdown-it-emoji@^2.0.2: 397 | version "2.0.2" 398 | resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz#cd42421c2fda1537d9cc12b9923f5c8aeb9029c8" 399 | integrity sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ== 400 | 401 | "markdown-it-fancy-lists@https://github.com/mminer237/markdown-it-fancy-lists#v1.0": 402 | version "0.0.0" 403 | resolved "https://github.com/mminer237/markdown-it-fancy-lists#f6ab3d44958a19477c1559b3e47af8bf993c2c68" 404 | 405 | markdown-it-footnote@^3.0.3: 406 | version "3.0.3" 407 | resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.3.tgz#e0e4c0d67390a4c5f0c75f73be605c7c190ca4d8" 408 | integrity sha512-YZMSuCGVZAjzKMn+xqIco9d1cLGxbELHZ9do/TSYVzraooV8ypsppKNmUJ0fVH5ljkCInQAtFpm8Rb3eXSrt5w== 409 | 410 | "markdown-it-texmath@https://github.com/Cimbali/markdown-it-texmath#v1.0.1": 411 | version "1.0.1" 412 | resolved "https://github.com/Cimbali/markdown-it-texmath#758ae9a432e4ba190c19b7fa545c8994ad711677" 413 | 414 | markdown-it@^13.0.1: 415 | version "13.0.1" 416 | resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-13.0.1.tgz#c6ecc431cacf1a5da531423fc6a42807814af430" 417 | integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== 418 | dependencies: 419 | argparse "^2.0.1" 420 | entities "~3.0.1" 421 | linkify-it "^4.0.1" 422 | mdurl "^1.0.1" 423 | uc.micro "^1.0.5" 424 | 425 | mdurl@^1.0.1: 426 | version "1.0.1" 427 | resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" 428 | integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== 429 | 430 | mermaid@9: 431 | version "9.4.3" 432 | resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-9.4.3.tgz#62cf210c246b74972ea98c19837519b6f03427f2" 433 | integrity sha512-TLkQEtqhRSuEHSE34lh5bCa94KATCyluAXmFnNI2PRZwOpXFeqiJWwZl+d2CcemE1RS6QbbueSSq9QIg8Uxcyw== 434 | dependencies: 435 | "@braintree/sanitize-url" "^6.0.0" 436 | cytoscape "^3.23.0" 437 | cytoscape-cose-bilkent "^4.1.0" 438 | cytoscape-fcose "^2.1.0" 439 | d3 "^7.4.0" 440 | dagre-d3-es "7.0.9" 441 | dayjs "^1.11.7" 442 | dompurify "2.4.3" 443 | elkjs "^0.8.2" 444 | khroma "^2.0.0" 445 | lodash-es "^4.17.21" 446 | non-layered-tidy-tree-layout "^2.0.2" 447 | stylis "^4.1.2" 448 | ts-dedent "^2.2.0" 449 | uuid "^9.0.0" 450 | web-worker "^1.2.0" 451 | 452 | non-layered-tidy-tree-layout@^2.0.2: 453 | version "2.0.2" 454 | resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" 455 | integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== 456 | 457 | robust-predicates@^3.0.0: 458 | version "3.0.2" 459 | resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" 460 | integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== 461 | 462 | rw@1: 463 | version "1.3.3" 464 | resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" 465 | integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== 466 | 467 | "safer-buffer@>= 2.1.2 < 3.0.0": 468 | version "2.1.2" 469 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 470 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 471 | 472 | stylis@^4.1.2: 473 | version "4.2.0" 474 | resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" 475 | integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== 476 | 477 | ts-dedent@^2.2.0: 478 | version "2.2.0" 479 | resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" 480 | integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== 481 | 482 | uc.micro@^1.0.1, uc.micro@^1.0.5: 483 | version "1.0.6" 484 | resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" 485 | integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== 486 | 487 | underscore@^1.8.2: 488 | version "1.13.6" 489 | resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" 490 | integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== 491 | 492 | uuid@^9.0.0: 493 | version "9.0.0" 494 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" 495 | integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== 496 | 497 | web-worker@^1.2.0: 498 | version "1.2.0" 499 | resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da" 500 | integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA== 501 | --------------------------------------------------------------------------------