├── .coveragerc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── Pipfile ├── Pipfile.lock ├── README.md ├── md2notion ├── NotionPyRenderer.py ├── __init__.py ├── __main__.py └── upload.py ├── setup.py └── tests ├── COMPREHENSIVE_TEST.md ├── TEST IMAGE HAS SPACES.png ├── TEST.md ├── TEST_IMAGE.png ├── __init__.py ├── conftest.py ├── test_NotionPyRenderer.py └── test_upload.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = md2notion -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Package Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | strategy: 16 | max-parallel: 4 17 | matrix: 18 | python-version: [3.6, 3.7, 3.8] 19 | 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install pipenv 30 | pipenv install 31 | #Install manually to avoid all other dev deps 32 | - name: Test with pytest 33 | run: | 34 | pipenv install pytest 35 | pipenv run pytest -v -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | *.egg 6 | *.py[cod] 7 | __pycache__/ 8 | *.so 9 | *~ 10 | venv/ 11 | venv-ci/ 12 | .pytest_cache/ 13 | .cache/ 14 | .coverage 15 | htmlcov -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Here's how to run all the development stuff. 4 | 5 | ## Setup Development Environment 6 | * `pyenv global 3.6.8-amd64` 7 | * `pipenv install --dev` 8 | 9 | ## Testing 10 | * `pytest -v` in the root directory 11 | * It's best to run a test against Notion's API as well with `pipenv run python -m md2notion.upload [token] https://www.notion.so/TestPage-8937635afd984d2f953a1750dfce4d26 tests/COMPREHENSIVE_TEST.md` with your token and page. 12 | * To test coverage run `pipenv run coverage run -m pytest -v` 13 | * Then run `pipenv run coverage report` or `pipenv run coverage html` and browser the coverage (TODO: Figure out a way to make a badge for this??) 14 | 15 | ## Releasing 16 | Refer to [the python docs on packaging for clarification](https://packaging.python.org/tutorials/packaging-projects/). 17 | * Make sure you've updated `setup.py` 18 | * `python setup.py sdist bdist_wheel` - Create a source distribution and a binary wheel distribution into `dist/` 19 | * `twine upload dist/md2notion-x.x.x*` - Upload all `dist/` files to PyPI of a given version 20 | * Make sure to tag the commit you released! 21 | 22 | ## Useful tips 23 | Mistletoe comes with a helpful tokenizing parser called `ASTRenderer`. This gives us great insight into what `NotionPyRenderer` is going to be seeing while rendering. 24 | 25 | Example: 26 | ``` 27 | import mistletoe 28 | from mistletoe.ast_renderer import ASTRenderer 29 | 30 | print(mistletoe.markdown(f"#Owo what's this?", ASTRenderer)) 31 | ``` 32 | outputs 33 | ``` 34 | { 35 | "type": "Document", 36 | "footnotes": {}, 37 | "children": [ 38 | { 39 | "type": "Paragraph", 40 | "children": [ 41 | { 42 | "type": "RawText", 43 | "content": "#Owo what's this?" 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ``` -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Peter Fornari / "Cobertos" 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. -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pytest = "*" 8 | twine = "*" 9 | setuptools = "*" 10 | wheel = "*" 11 | coverage = "*" 12 | 13 | [packages] 14 | md2notion = {editable = true,path = "."} 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "9a876b59201c2978c51b625ba0ac5d4916046a36646126602950f65ecce9a14e" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "beautifulsoup4": { 18 | "hashes": [ 19 | "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35", 20 | "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25", 21 | "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666" 22 | ], 23 | "version": "==4.9.3" 24 | }, 25 | "bs4": { 26 | "hashes": [ 27 | "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" 28 | ], 29 | "version": "==0.0.1" 30 | }, 31 | "cached-property": { 32 | "hashes": [ 33 | "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", 34 | "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0" 35 | ], 36 | "version": "==1.5.2" 37 | }, 38 | "certifi": { 39 | "hashes": [ 40 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", 41 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" 42 | ], 43 | "version": "==2021.5.30" 44 | }, 45 | "chardet": { 46 | "hashes": [ 47 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 48 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 49 | ], 50 | "version": "==4.0.0" 51 | }, 52 | "commonmark": { 53 | "hashes": [ 54 | "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", 55 | "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" 56 | ], 57 | "version": "==0.9.1" 58 | }, 59 | "dictdiffer": { 60 | "hashes": [ 61 | "sha256:1adec0d67cdf6166bda96ae2934ddb5e54433998ceab63c984574d187cc563d2", 62 | "sha256:d79d9a39e459fe33497c858470ca0d2e93cb96621751de06d631856adfd9c390" 63 | ], 64 | "version": "==0.8.1" 65 | }, 66 | "idna": { 67 | "hashes": [ 68 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 69 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 70 | ], 71 | "version": "==2.10" 72 | }, 73 | "md2notion": { 74 | "editable": true, 75 | "path": "." 76 | }, 77 | "mistletoe": { 78 | "hashes": [ 79 | "sha256:24d0f18cc5f0381c2cfb8a24ef3de83eb9f7929cb7d0e71ecb164b671d86e6a3", 80 | "sha256:3e2d31b2fa6231ea2ee46981274ebac8d5e5736e3aad2d1cd449e2c053b7023b" 81 | ], 82 | "version": "==0.7.2" 83 | }, 84 | "notion": { 85 | "hashes": [ 86 | "sha256:a6c511e1055bb69f05d75220f3f6a5de377a52d42ae38186c3138557c6b4942a", 87 | "sha256:cc380ea2ffe70ee8d5f19244df910cc79ea56a577ca569553d30f8798f0b5b61" 88 | ], 89 | "version": "==0.0.28" 90 | }, 91 | "python-slugify": { 92 | "hashes": [ 93 | "sha256:6d8c5df75cd4a7c3a2d21e257633de53f52ab0265cd2d1dc62a730e8194a7380", 94 | "sha256:f13383a0b9fcbe649a1892b9c8eb4f8eab1d6d84b84bb7a624317afa98159cab" 95 | ], 96 | "version": "==5.0.2" 97 | }, 98 | "pytz": { 99 | "hashes": [ 100 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", 101 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" 102 | ], 103 | "version": "==2021.1" 104 | }, 105 | "requests": { 106 | "hashes": [ 107 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 108 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 109 | ], 110 | "version": "==2.25.1" 111 | }, 112 | "soupsieve": { 113 | "hashes": [ 114 | "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc", 115 | "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b" 116 | ], 117 | "markers": "python_version >= '3.0'", 118 | "version": "==2.2.1" 119 | }, 120 | "text-unidecode": { 121 | "hashes": [ 122 | "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", 123 | "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" 124 | ], 125 | "version": "==1.3" 126 | }, 127 | "tzlocal": { 128 | "hashes": [ 129 | "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", 130 | "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4" 131 | ], 132 | "version": "==2.1" 133 | }, 134 | "urllib3": { 135 | "hashes": [ 136 | "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", 137 | "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" 138 | ], 139 | "index": "pypi", 140 | "version": "==1.26.5" 141 | } 142 | }, 143 | "develop": { 144 | "attrs": { 145 | "hashes": [ 146 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", 147 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" 148 | ], 149 | "version": "==21.2.0" 150 | }, 151 | "bleach": { 152 | "hashes": [ 153 | "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", 154 | "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" 155 | ], 156 | "version": "==3.3.0" 157 | }, 158 | "certifi": { 159 | "hashes": [ 160 | "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", 161 | "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" 162 | ], 163 | "version": "==2021.5.30" 164 | }, 165 | "cffi": { 166 | "hashes": [ 167 | "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", 168 | "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373", 169 | "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69", 170 | "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f", 171 | "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", 172 | "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05", 173 | "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", 174 | "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", 175 | "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0", 176 | "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", 177 | "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7", 178 | "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f", 179 | "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", 180 | "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", 181 | "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76", 182 | "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", 183 | "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", 184 | "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed", 185 | "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", 186 | "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", 187 | "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", 188 | "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", 189 | "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", 190 | "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", 191 | "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", 192 | "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55", 193 | "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", 194 | "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", 195 | "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", 196 | "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", 197 | "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", 198 | "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", 199 | "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", 200 | "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", 201 | "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", 202 | "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", 203 | "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", 204 | "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", 205 | "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", 206 | "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", 207 | "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", 208 | "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", 209 | "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", 210 | "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc", 211 | "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", 212 | "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", 213 | "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333", 214 | "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", 215 | "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" 216 | ], 217 | "version": "==1.14.5" 218 | }, 219 | "chardet": { 220 | "hashes": [ 221 | "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", 222 | "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" 223 | ], 224 | "version": "==4.0.0" 225 | }, 226 | "colorama": { 227 | "hashes": [ 228 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 229 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 230 | ], 231 | "version": "==0.4.4" 232 | }, 233 | "coverage": { 234 | "hashes": [ 235 | "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", 236 | "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", 237 | "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", 238 | "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", 239 | "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", 240 | "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", 241 | "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", 242 | "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", 243 | "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", 244 | "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", 245 | "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", 246 | "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", 247 | "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", 248 | "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", 249 | "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", 250 | "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", 251 | "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", 252 | "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", 253 | "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", 254 | "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", 255 | "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", 256 | "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", 257 | "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", 258 | "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", 259 | "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", 260 | "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", 261 | "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", 262 | "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", 263 | "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", 264 | "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", 265 | "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", 266 | "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", 267 | "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", 268 | "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", 269 | "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", 270 | "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", 271 | "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", 272 | "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", 273 | "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", 274 | "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", 275 | "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", 276 | "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", 277 | "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", 278 | "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", 279 | "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", 280 | "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", 281 | "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", 282 | "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", 283 | "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" 284 | ], 285 | "index": "pypi", 286 | "version": "==5.4" 287 | }, 288 | "cryptography": { 289 | "hashes": [ 290 | "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", 291 | "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", 292 | "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", 293 | "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", 294 | "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", 295 | "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", 296 | "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", 297 | "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", 298 | "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", 299 | "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", 300 | "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", 301 | "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" 302 | ], 303 | "version": "==3.4.7" 304 | }, 305 | "docutils": { 306 | "hashes": [ 307 | "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", 308 | "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" 309 | ], 310 | "version": "==0.17.1" 311 | }, 312 | "idna": { 313 | "hashes": [ 314 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 315 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 316 | ], 317 | "version": "==2.10" 318 | }, 319 | "importlib-metadata": { 320 | "hashes": [ 321 | "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786", 322 | "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5" 323 | ], 324 | "version": "==4.4.0" 325 | }, 326 | "iniconfig": { 327 | "hashes": [ 328 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 329 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 330 | ], 331 | "version": "==1.1.1" 332 | }, 333 | "jeepney": { 334 | "hashes": [ 335 | "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", 336 | "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" 337 | ], 338 | "markers": "sys_platform == 'linux'", 339 | "version": "==0.6.0" 340 | }, 341 | "keyring": { 342 | "hashes": [ 343 | "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", 344 | "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" 345 | ], 346 | "version": "==23.0.1" 347 | }, 348 | "packaging": { 349 | "hashes": [ 350 | "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", 351 | "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" 352 | ], 353 | "version": "==20.9" 354 | }, 355 | "pkginfo": { 356 | "hashes": [ 357 | "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", 358 | "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" 359 | ], 360 | "version": "==1.7.0" 361 | }, 362 | "pluggy": { 363 | "hashes": [ 364 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 365 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 366 | ], 367 | "version": "==0.13.1" 368 | }, 369 | "py": { 370 | "hashes": [ 371 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 372 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 373 | ], 374 | "version": "==1.10.0" 375 | }, 376 | "pycparser": { 377 | "hashes": [ 378 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 379 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 380 | ], 381 | "version": "==2.20" 382 | }, 383 | "pygments": { 384 | "hashes": [ 385 | "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", 386 | "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" 387 | ], 388 | "version": "==2.9.0" 389 | }, 390 | "pyparsing": { 391 | "hashes": [ 392 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 393 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 394 | ], 395 | "version": "==2.4.7" 396 | }, 397 | "pytest": { 398 | "hashes": [ 399 | "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", 400 | "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" 401 | ], 402 | "index": "pypi", 403 | "version": "==6.2.2" 404 | }, 405 | "readme-renderer": { 406 | "hashes": [ 407 | "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", 408 | "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" 409 | ], 410 | "version": "==29.0" 411 | }, 412 | "requests": { 413 | "hashes": [ 414 | "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", 415 | "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" 416 | ], 417 | "version": "==2.25.1" 418 | }, 419 | "requests-toolbelt": { 420 | "hashes": [ 421 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 422 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 423 | ], 424 | "version": "==0.9.1" 425 | }, 426 | "rfc3986": { 427 | "hashes": [ 428 | "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", 429 | "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" 430 | ], 431 | "version": "==1.5.0" 432 | }, 433 | "secretstorage": { 434 | "hashes": [ 435 | "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", 436 | "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" 437 | ], 438 | "markers": "sys_platform == 'linux'", 439 | "version": "==3.3.1" 440 | }, 441 | "six": { 442 | "hashes": [ 443 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 444 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 445 | ], 446 | "version": "==1.16.0" 447 | }, 448 | "toml": { 449 | "hashes": [ 450 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 451 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 452 | ], 453 | "version": "==0.10.2" 454 | }, 455 | "tqdm": { 456 | "hashes": [ 457 | "sha256:736524215c690621b06fc89d0310a49822d75e599fcd0feb7cc742b98d692493", 458 | "sha256:cd5791b5d7c3f2f1819efc81d36eb719a38e0906a7380365c556779f585ea042" 459 | ], 460 | "version": "==4.61.0" 461 | }, 462 | "twine": { 463 | "hashes": [ 464 | "sha256:2f6942ec2a17417e19d2dd372fc4faa424c87ee9ce49b4e20c427eb00a0f3f41", 465 | "sha256:fcffa8fc37e8083a5be0728371f299598870ee1eccc94e9a25cef7b1dcfa8297" 466 | ], 467 | "index": "pypi", 468 | "version": "==3.3.0" 469 | }, 470 | "urllib3": { 471 | "hashes": [ 472 | "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", 473 | "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" 474 | ], 475 | "index": "pypi", 476 | "version": "==1.26.5" 477 | }, 478 | "webencodings": { 479 | "hashes": [ 480 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 481 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 482 | ], 483 | "version": "==0.5.1" 484 | }, 485 | "wheel": { 486 | "hashes": [ 487 | "sha256:78b5b185f0e5763c26ca1e324373aadd49182ca90e825f7853f4b2509215dc0e", 488 | "sha256:e11eefd162658ea59a60a0f6c7d493a7190ea4b9a85e335b33489d9f17e0245e" 489 | ], 490 | "index": "pypi", 491 | "version": "==0.36.2" 492 | }, 493 | "zipp": { 494 | "hashes": [ 495 | "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", 496 | "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" 497 | ], 498 | "version": "==3.4.1" 499 | } 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### This package is package is no longer maintained. I stopped using Notion.so some time ago and switched to Obsidian due to persistant problems with then Notion API. 2 | 3 | --- 4 | 5 |
10 | 11 | # Notion.so Markdown Importer 12 | 13 | An importer for Markdown files to [Notion.so](https://notion.so) using [`notion-py`](https://github.com/jamalex/notion-py) 14 | 15 | It provides these features over Notion.so's Markdown importer: 16 | 17 | * Picking a Notion.so page to upload to (instead of them all uploading to the root) 18 | * Code fences keep their original language (or as close as we can match it) 19 | * Code fences are formatted properly 20 | * Inline HTML is preserved 21 | * (Optionally) Upload images that are memtioned in the HTML `
228 |
229 | ### Code Blocks
230 |
231 | Pre-formatted code blocks are used for writing about programming or
232 | markup source code. Rather than forming normal paragraphs, the lines
233 | of a code block are interpreted literally. Markdown wraps a code block
234 | in both `` and `` tags.
235 |
236 | To produce a code block in Markdown, simply indent every line of the
237 | block by at least 4 spaces or 1 tab.
238 |
239 | This is a normal paragraph:
240 |
241 | This is a code block.
242 |
243 | Here is an example of AppleScript:
244 |
245 | tell application "Foo"
246 | beep
247 | end tell
248 |
249 | A code block continues until it reaches a line that is not indented
250 | (or the end of the article).
251 |
252 | Within a code block, ampersands (`&`) and angle brackets (`<` and `>`)
253 | are automatically converted into HTML entities. This makes it very
254 | easy to include example HTML source code using Markdown -- just paste
255 | it and indent it, and Markdown will handle the hassle of encoding the
256 | ampersands and angle brackets. For example, this:
257 |
258 |
tag'''
128 | #arrange/act
129 | output = mistletoe.markdown("head
tail",
130 | addHtmlImgTagExtension(NotionPyRenderer))
131 |
132 | #assert
133 | assert len(output) == 2
134 | assert isinstance(output[0], dict)
135 | assert output[0]['type'] == notion.block.TextBlock
136 | assert output[0]['title'] == "headtail" #Should extract the image
137 | assert isinstance(output[1], dict) #The ImageBlock can't be inline with anything else so it comes out
138 | assert output[1]['type'] == notion.block.ImageBlock
139 | assert output[1]['caption'] is None
140 |
141 | def test_imageInHtmlBlock():
142 | '''it should render an image that is mentioned in the html block'''
143 | output = mistletoe.markdown(\
144 | """
145 |
text in div
146 |
147 | tail
148 | """, addHtmlImgTagExtension(NotionPyRenderer))
149 |
150 | #assert
151 | assert len(output) == 3
152 | assert isinstance(output[0], dict)
153 | assert output[0]['type'] == notion.block.TextBlock
154 | assert output[0]['title'] == "text in div" #Should extract the image
155 | assert isinstance(output[1], dict)
156 | assert output[1]['type'] == notion.block.ImageBlock
157 | assert output[1]['caption'] == "ImCaption"
158 | assert isinstance(output[2], dict)
159 | assert output[2]['type'] == notion.block.TextBlock
160 | assert output[2]['title'] == "tail"
161 |
162 | def test_latex_inline():
163 | output = mistletoe.markdown(r"""
164 | # Test for latex blocks
165 | Text before $f(x) = \sigma(w \cdot x + b)$ Text after
166 | """, addLatexExtension(NotionPyRenderer))
167 |
168 | assert output[1] is not None
169 | assert output[1]['type'] == notion.block.TextBlock
170 | assert output[1]['title'] == r'Text before $$f(x) = \sigma(w \cdot x + b)$$ Text after'
171 |
172 | def test_latex_inline_with_underscores():
173 | '''it should render a latex block with underscores in it properly'''
174 | output = mistletoe.markdown(r'Text before $X^T=(X_1, X_2, \ldots, X_p)$ Text after', addLatexExtension(NotionPyRenderer))
175 |
176 | assert len(output) == 1
177 | output = output[0]
178 | assert output is not None
179 | assert output['type'] == notion.block.TextBlock
180 | assert output['title'] == r'Text before $$X^T=(X_1, X_2, \ldots, X_p)$$ Text after'
181 |
182 | def test_latex_block():
183 | output = mistletoe.markdown(r"""
184 | # Test for latex blocks
185 | Text before
186 |
187 | $$
188 | f(x) = \sigma(w \cdot x + b)
189 | $$
190 |
191 | Text after
192 | """, addLatexExtension(NotionPyRenderer))
193 |
194 | assert output[2] is not None
195 | assert output[2]['type'] == notion.block.EquationBlock
196 | assert output[2]['title_plaintext'] == 'f(x) = \\\\sigma(w \\\\cdot x + b)\n'
197 |
198 | def test_latex_block_with_underscores():
199 | '''it should render a latex block with underscores in it properly'''
200 | output = mistletoe.markdown(r"""
201 | $$
202 | \hat{Y} = \hat{\beta}_0 + \sum_{j=1}^p X_j \hat{\beta}_j
203 | $$""", addLatexExtension(NotionPyRenderer))
204 |
205 | assert len(output) == 1
206 | output = output[0]
207 | assert output is not None
208 | assert output['type'] == notion.block.EquationBlock
209 | assert output['title_plaintext'] == '\\\\hat{Y} = \\\\hat{\\\\beta}_0 + \\\\sum_{j=1}^p X_j \\\\hat{\\\\beta}_j\n'
210 |
211 | def test_escapeSequence():
212 | '''it should render out an escape sequence'''
213 | #arrange/act
214 | output = mistletoe.markdown("\\066", NotionPyRenderer)
215 |
216 | #assert
217 | assert len(output) == 1
218 | output = output[0]
219 | assert output['type'] == notion.block.TextBlock
220 | assert output['title'] == "\\066"
221 |
222 | def test_table():
223 | '''it should render a table'''
224 | #arrange/act
225 | output = mistletoe.markdown(\
226 | """
227 | | Awoo | Awooo | Awoooo |
228 | |---------|---------|---------|
229 | | Test100 | Test200 | Test300 |
230 | | | Test400 | |
231 | """, NotionPyRenderer)
232 |
233 | #assert
234 | assert len(output) == 1
235 | output = output[0]
236 | assert isinstance(output, dict)
237 | assert output['type'] == notion.block.CollectionViewBlock
238 |
239 | assert isinstance(output['schema'], dict)
240 | assert len(output['schema']) == 3 #3 properties
241 | assert list(output['schema'].keys())[2] == 'title' #Last one is 'title'
242 | assert list(output['schema'].values())[0] == {
243 | 'type': 'text',
244 | 'name': 'Awoo'
245 | }
246 | assert list(output['schema'].values())[1] == {
247 | 'type': 'text',
248 | 'name': 'Awooo'
249 | }
250 | assert list(output['schema'].values())[2] == {
251 | 'type': 'title',
252 | 'name': 'Awoooo'
253 | }
254 |
255 | assert isinstance(output['rows'], list)
256 | assert len(output['rows']) == 2 #2 rows
257 | assert output['rows'][0] == ['Test100', 'Test200', 'Test300']
258 | assert output['rows'][1] == ['', 'Test400', '']
259 |
260 | def test_nested_list():
261 | '''it should render nested lists'''
262 | #arrange/act
263 | output = mistletoe.markdown(\
264 | """
265 | * Awoo
266 | * Hewwo
267 | """, NotionPyRenderer)
268 |
269 | #assert
270 | assert len(output) == 1
271 | output = output[0]
272 | assert isinstance(output, dict)
273 | assert output['type'] == notion.block.BulletedListBlock
274 | assert output['title'] == 'Awoo'
275 |
276 | assert len(output['children']) == 1
277 | outputChild = output['children'][0]
278 | assert isinstance(outputChild, dict)
279 | assert outputChild['type'] == notion.block.BulletedListBlock
280 | assert outputChild['title'] == 'Hewwo'
281 |
282 | def test_code_block_with_language():
283 | '''it should render a fenced code block with explicit language'''
284 | #arrange/act
285 | raw =\
286 | """\
287 | ```python
288 | def get_favorite_fruit():
289 | return Watermelon
290 | ```"""
291 | expected = "def get_favorite_fruit():\n return Watermelon\n"
292 | output = mistletoe.markdown(raw, NotionPyRenderer)
293 |
294 | #assert
295 | assert len(output) == 1
296 | output = output[0]
297 | assert isinstance(output, dict)
298 | assert output['type'] == notion.block.CodeBlock
299 | assert output['title_plaintext'] == expected
300 | assert output['language'] == 'Python'
301 |
302 | def test_code_block_without_language():
303 | '''it should render a fenced code block with no language specified'''
304 | #arrange/act
305 | raw =\
306 | """\
307 | ```
308 | (f_ my_made_up_language a b)!
309 | ```"""
310 | expected = "(f_ my_made_up_language a b)!\n"
311 | output = mistletoe.markdown(raw, NotionPyRenderer)
312 |
313 | #assert
314 | assert len(output) == 1
315 | output = output[0]
316 | assert isinstance(output, dict)
317 | assert output['type'] == notion.block.CodeBlock
318 | assert output['title_plaintext'] == expected
319 | assert output['language'] == "Plain Text"
320 |
321 | def test_big_file():
322 | '''it should be able to render a full Markdown file'''
323 | #arrange/act
324 | #TODO: For now we just test that this doesn't file, not that it's correct
325 | mistletoe.markdown(open("tests/COMPREHENSIVE_TEST.md", "r", encoding="utf-8").read(), NotionPyRenderer)
326 |
--------------------------------------------------------------------------------
/tests/test_upload.py:
--------------------------------------------------------------------------------
1 | '''
2 | Tests NotionPyRenderer parsing
3 | '''
4 | import pytest
5 | from pathlib import Path
6 | import re
7 | import notion
8 | import sys
9 | from io import IOBase
10 | from md2notion.upload import filesFromPathsUrls, uploadBlock, cli, relativePathForMarkdownUrl
11 | from notion.block import TextBlock, ImageBlock, CollectionViewBlock, PageBlock
12 | from unittest.mock import Mock, patch, call
13 |
14 | def test_filesFromPathUrl_with_file():
15 | '''it can get a file name, path, and file object from a file'''
16 | #arrange/act
17 | filePath, fileName, file = next(filesFromPathsUrls(['tests/TEST.md']))
18 |
19 | #assert
20 | assert fileName == 'TEST.md'
21 | assert filePath == 'tests/TEST.md'
22 | assert isinstance(file, IOBase)
23 |
24 | def test_filesFromPathUrl_with_glob():
25 | '''it can get a file name, path, and file object from a file'''
26 | #arrange/act
27 | tuples = list(filesFromPathsUrls(['tests/TES*.md']))
28 |
29 | #assert
30 | assert len(tuples) == 1
31 |
32 | def test_filesFromPathUrl_with_url():
33 | '''it can get a file name, path, and file object from a file'''
34 | #arrange/act
35 | filePath, fileName, file = next(filesFromPathsUrls(['https://raw.githubusercontent.com/Cobertos/md2notion/master/README.md']))
36 |
37 | #assert
38 | assert fileName == 'README.md'
39 | assert filePath == 'https://raw.githubusercontent.com/Cobertos/md2notion/master/README.md'
40 | assert isinstance(file, IOBase)
41 |
42 | def test_relativePathForMarkdownUrl():
43 | '''gets relative path for simple file'''
44 | #arrange/act
45 | relPath = relativePathForMarkdownUrl('TEST_IMAGE.png', 'tests/TEST.md')
46 |
47 | #assert
48 | assert relPath == Path('tests/TEST_IMAGE.png')
49 |
50 | def test_relativePathForMarkdownUrl_http_url():
51 | '''gets relative path for simple file'''
52 | #arrange/act
53 | relPath = relativePathForMarkdownUrl('http://cobertos.com/non_exist.png', 'tests/TEST.md')
54 |
55 | #assert
56 | assert relPath == None
57 |
58 | def test_relativePathForMarkdownUrl_file_url():
59 | '''gets relative path for a url beginning with file://'''
60 | #arrange/act
61 | relPath = relativePathForMarkdownUrl('file://TEST%20IMAGE%20HAS%20SPACES.png', 'tests/TEST.md')
62 |
63 | #assert
64 | assert relPath == Path('tests/TEST IMAGE HAS SPACES.png')
65 |
66 | def test_relativePathForMarkdownUrl_encoded():
67 | '''gets relative path for a path that has encoding (which is kind of wonky by we'll support it)'''
68 | #arrange/act
69 | relPath = relativePathForMarkdownUrl('TEST%20IMAGE%20HAS%20SPACES.png', 'tests/TEST.md')
70 |
71 | #assert
72 | assert relPath == Path('tests/TEST IMAGE HAS SPACES.png')
73 |
74 | def test_relativePathForMarkdownUrl_non_exist():
75 | '''gets relative path for a file that doesn't exist should return None'''
76 | #arrange/act
77 | relPath = relativePathForMarkdownUrl('NON_EXIST.png', 'tests/TEST.md')
78 |
79 | #assert
80 | assert relPath == None
81 |
82 | def test_uploadBlock():
83 | '''uploads a simple block to Notion using add_new'''
84 | #arrange
85 | blockDescriptor = {
86 | 'type': TextBlock,
87 | 'title': 'This is a test of the test system'
88 | }
89 | notionBlock = Mock()
90 | notionBlock.children.add_new = Mock()
91 |
92 | #act
93 | uploadBlock(blockDescriptor, notionBlock, '')
94 |
95 | #assert
96 | notionBlock.children.add_new.assert_called_with(TextBlock, title='This is a test of the test system')
97 |
98 | def test_uploadBlock_image():
99 | '''uploads an external image block to Notion without uploading'''
100 | #arrange
101 | blockDescriptor = {
102 | 'type': ImageBlock,
103 | 'title': 'test',
104 | 'source': 'https://example.com'
105 | }
106 | notionBlock = Mock()
107 | newBlock = Mock(spec=blockDescriptor['type'])
108 | notionBlock.children.add_new = Mock(return_value=newBlock)
109 |
110 | #act
111 | uploadBlock(blockDescriptor, notionBlock, '')
112 |
113 | #assert
114 | notionBlock.children.add_new.assert_called_with(ImageBlock, title='test', source='https://example.com')
115 | newBlock.upload_file.assert_not_called()
116 |
117 | def test_uploadBlock_image_local():
118 | '''uploads an Image block with local image to Notion'''
119 | #arrange
120 | blockDescriptor = {
121 | 'type': ImageBlock,
122 | 'title': 'test',
123 | 'source': 'TEST_IMAGE.png'
124 | }
125 | notionBlock = Mock()
126 | notionBlock.children.add_new.return_value = newBlock = Mock(spec=blockDescriptor['type'])
127 |
128 | #act
129 | uploadBlock(blockDescriptor, notionBlock, 'tests/DUMMY.md')
130 |
131 | #assert
132 | notionBlock.children.add_new.assert_called_with(ImageBlock, title='test', source='TEST_IMAGE.png')
133 | newBlock.upload_file.assert_called_with(str(Path('tests/TEST_IMAGE.png')))
134 |
135 | def test_uploadBlock_image_local_file_scheme_url_encoded():
136 | '''uploads an Image block with local image to Notion if it has a file:// scheme'''
137 | #arrange
138 | blockDescriptor = {
139 | 'type': ImageBlock,
140 | 'title': 'test',
141 | 'source': 'file://TEST%20IMAGE%20HAS%20SPACES.png'
142 | }
143 | notionBlock = Mock()
144 | notionBlock.children.add_new.return_value = newBlock = Mock(spec=blockDescriptor['type'])
145 |
146 | #act
147 | uploadBlock(blockDescriptor, notionBlock, 'tests/DUMMY.md')
148 |
149 | #assert
150 | notionBlock.children.add_new.assert_called_with(ImageBlock, title='test', source='file://TEST%20IMAGE%20HAS%20SPACES.png')
151 | newBlock.upload_file.assert_called_with(str(Path('tests/TEST IMAGE HAS SPACES.png')))
152 |
153 | def test_uploadBlock_image_local_img_func():
154 | '''uploads an Image block with local image to Notion with a special transform'''
155 | #arrange
156 | blockDescriptor = {
157 | 'type': ImageBlock,
158 | 'title': 'test',
159 | 'source': 'NONEXIST_IMAGE.png'
160 | }
161 | notionBlock = Mock()
162 | notionBlock.children.add_new.return_value = newBlock = Mock(spec=blockDescriptor['type'])
163 | imagePathFunc = Mock(return_value=Path('tests/TEST_IMAGE.png'))
164 |
165 | #act
166 | uploadBlock(blockDescriptor, notionBlock, 'tests/DUMMY.md', imagePathFunc=imagePathFunc)
167 |
168 | #assert
169 | imagePathFunc.assert_called_with('NONEXIST_IMAGE.png', 'tests/DUMMY.md')
170 | notionBlock.children.add_new.assert_called_with(ImageBlock, title='test', source='NONEXIST_IMAGE.png')
171 | newBlock.upload_file.assert_called_with(str(Path('tests/TEST_IMAGE.png')))
172 |
173 | def test_uploadBlock_collection():
174 | #arrange
175 | blockDescriptor = {
176 | 'type': CollectionViewBlock,
177 | 'schema': {
178 | 'J=}2': {
179 | 'type': 'text',
180 | 'name': 'Awoooo'
181 | },
182 | 'J=}x': {
183 | 'type': 'text',
184 | 'name': 'Awooo'
185 | },
186 | 'title': {
187 | 'type': 'text',
188 | 'name': 'Awoo'
189 | }
190 | },
191 | 'rows': [
192 | ['Test100', 'Test200', 'Test300'],
193 | ['', 'Test400', '']
194 | ]
195 | }
196 | schema = blockDescriptor['schema']
197 | rows = blockDescriptor['rows']
198 | notionBlock = Mock()
199 | newBlock = Mock(spec=blockDescriptor['type'])
200 | notionBlock.children.add_new = Mock(return_value=newBlock)
201 |
202 | collection = Mock()
203 | notionBlock._client.create_record = Mock(return_value=collection)
204 | notionBlock._client.get_collection = Mock(return_value=collection)
205 |
206 | #act
207 | uploadBlock(blockDescriptor, notionBlock, '')
208 |
209 | #assert
210 | notionBlock.children.add_new.assert_called_with(CollectionViewBlock)
211 | notionBlock._client.create_record.assert_called_with("collection", parent=newBlock, schema=schema)
212 | notionBlock._client.get_collection.assert_called_with(collection)
213 | #TODO: This is incomplete...
214 |
215 | def MockClient():
216 | #No-op, seal doesn't exist in Python 3.6
217 | if sys.version_info >= (3,7,0):
218 | from unittest.mock import seal
219 | else:
220 | seal = lambda x: x
221 | notionClient = Mock()
222 | notionClient.return_value = notionClient
223 | getBlock = Mock(spec=PageBlock)
224 | class MockNodeList(list):
225 | def add_new(self, t, title=None):
226 | m = Mock(spec=t)
227 | def remove():
228 | self.remove(m)
229 | return None
230 | m.remove = Mock(return_value=None, side_effect=remove)
231 | m.title = title
232 | seal(m)
233 | self.append(m)
234 | return m
235 | getBlock.children = MockNodeList()
236 | getBlock.title = Mock(return_value="")
237 | notionClient.get_block = Mock(return_value=getBlock)
238 | seal(getBlock)
239 | seal(notionClient)
240 | return notionClient
241 |
242 | @patch('md2notion.upload.NotionClient', new_callable=MockClient)
243 | def test_cli_no_arguments(mockClient):
244 | '''should error when nothing is passed'''
245 | #act/assert
246 | with pytest.raises(SystemExit):
247 | cli([])
248 |
249 | @patch('md2notion.upload.upload')
250 | @patch('md2notion.upload.NotionClient', new_callable=MockClient)
251 | def test_cli_create_single_page(mockClient, upload):
252 | '''should create a single page'''
253 | #act
254 | cli(['token_v2', 'page_url', 'tests/TEST.md'])
255 |
256 | #assert
257 | #Should've been called with a file and the passed block
258 | args0, kwargs0 = upload.call_args
259 | assert isinstance(args0[0], IOBase)
260 | assert args0[0].name == 'tests/TEST.md'
261 | assert args0[1] == mockClient.get_block.return_value.children[0]
262 | assert args0[1].title == 'TEST.md'
263 |
264 | @patch('md2notion.upload.upload')
265 | @patch('md2notion.upload.NotionClient', new_callable=MockClient)
266 | def test_cli_create_multiple_pages(mockClient, upload):
267 | '''should create multiple pages'''
268 | #act
269 | cli(['token_v2', 'page_url', 'tests/TEST.md', 'tests/COMPREHENSIVE_TEST.md'])
270 | args0, kwargs0 = upload.call_args_list[0]
271 | assert isinstance(args0[0], IOBase)
272 | assert args0[0].name == 'tests/TEST.md'
273 | assert args0[1] == mockClient.get_block.return_value.children[0]
274 | assert args0[1].title == 'TEST.md'
275 | args1, kwargs1 = upload.call_args_list[1]
276 | assert isinstance(args1[0], IOBase)
277 | assert args1[0].name == 'tests/COMPREHENSIVE_TEST.md'
278 | assert args1[1] == mockClient.get_block.return_value.children[1]
279 | assert args1[1].title == 'COMPREHENSIVE_TEST.md'
280 |
281 | @patch('md2notion.upload.upload')
282 | @patch('md2notion.upload.NotionClient', new_callable=MockClient)
283 | def test_cli_append(mockClient, upload):
284 | '''should append when using that flag'''
285 | #act
286 | cli(['token_v2', 'page_url', 'tests/TEST.md', '--append'])
287 |
288 | #assert
289 | #Should've been called with a file and the passed block
290 | args0, kwargs0 = upload.call_args
291 | assert isinstance(args0[0], IOBase)
292 | assert args0[0].name == 'tests/TEST.md'
293 | assert args0[1] == mockClient.get_block.return_value
294 |
295 | @patch('md2notion.upload.upload')
296 | @patch('md2notion.upload.NotionClient', new_callable=MockClient)
297 | def test_cli_clear_previous(mockClient, upload):
298 | '''should clear previously title pages with the same name when passed that flag'''
299 | #arrange
300 | testNoBlock = mockClient.get_block().children.add_new(PageBlock, title='NO_TEST.md')
301 | testBlock = mockClient.get_block().children.add_new(PageBlock, title='TEST.md')
302 |
303 | #act
304 | cli(['token_v2', 'page_url', 'tests/TEST.md', '--clear-previous'])
305 |
306 | #assert
307 | testNoBlock.remove.assert_not_called()
308 | testBlock.remove.assert_called_with()
309 | args0, kwargs0 = upload.call_args
310 | assert isinstance(args0[0], IOBase)
311 | assert args0[0].name == 'tests/TEST.md'
312 | assert args0[1] == mockClient.get_block.return_value.children[1]
313 | assert args0[1].title == 'TEST.md'
314 |
315 | @patch('md2notion.upload.upload')
316 | @patch('md2notion.upload.NotionClient', new_callable=MockClient)
317 | def test_cli_html_img_tag(mockClient, upload):
318 | '''should enable the extension'''
319 |
320 | #act
321 | cli(['token_v2', 'page_url', 'tests/TEST.md', '--append', '--html-img'])
322 |
323 | #assert
324 | args0, kwargs0 = upload.call_args
325 | renderer = args0[3]()
326 | assert "HTMLSpan" in renderer.render_map
327 | assert "HTMLBlock" in renderer.render_map
328 |
329 | @patch('md2notion.upload.upload')
330 | @patch('md2notion.upload.NotionClient', new_callable=MockClient)
331 | def test_cli_latex(mockClient, upload):
332 | '''should enable the extension'''
333 |
334 | #act
335 | cli(['token_v2', 'page_url', 'tests/TEST.md', '--append', '--latex'])
336 |
337 | #assert
338 | args0, kwargs0 = upload.call_args
339 | renderer = args0[3]()
340 | assert "InlineEquation" in renderer.render_map
341 | assert "BlockEquation" in renderer.render_map
342 |
--------------------------------------------------------------------------------
261 |
262 | Regular Markdown syntax is not processed within code blocks. E.g.,
263 | asterisks are just literal asterisks within a code block. This means
264 | it's also easy to use Markdown to write about Markdown's own syntax.
265 |
266 | ```
267 | tell application "Foo"
268 | beep
269 | end tell
270 | ```
271 |
272 | There's also code fences with the language specified
273 |
274 | ```python
275 | def test():
276 | pass
277 | ```
278 |
279 | ## Span Elements
280 |
281 | ### Links
282 |
283 | Markdown supports two style of links: *inline* and *reference*.
284 |
285 | In both styles, the link text is delimited by [square brackets].
286 |
287 | To create an inline link, use a set of regular parentheses immediately
288 | after the link text's closing square bracket. Inside the parentheses,
289 | put the URL where you want the link to point, along with an *optional*
290 | title for the link, surrounded in quotes. For example:
291 |
292 | This is [an example](http://example.com/) inline link.
293 |
294 | [This link](http://example.net/) has no title attribute.
295 |
296 | ### Emphasis
297 |
298 | Markdown treats asterisks (`*`) and underscores (`_`) as indicators of
299 | emphasis. Text wrapped with one `*` or `_` will be wrapped with an
300 | HTML `` tag; double `*`'s or `_`'s will be wrapped with an HTML
301 | `` tag. E.g., this input:
302 |
303 | *single asterisks*
304 |
305 | _single underscores_
306 |
307 | **double asterisks**
308 |
309 | __double underscores__
310 |
311 | ### Code
312 |
313 | To indicate a span of code, wrap it with backtick quotes (`` ` ``).
314 | Unlike a pre-formatted code block, a code span indicates code within a
315 | normal paragraph. For example:
316 |
317 | Use the `printf()` function.
318 |
319 | ### Images
320 |
321 | Images use the image syntax with an ` ! `
322 |
323 | 
324 |
325 | We can also use images from externally
326 |
327 | 
328 |
329 | ## Other stuff
330 |
331 | ### Tables
332 |
333 | We can do tables too
334 |
335 | | tic | tac | toe |
336 | |-----|-----|-----|
337 | | x | o | o |
338 | | o | x | x |
339 | | o | x | o |
340 |
341 | ### TODOs
342 |
343 | * This is a normal list item
344 | * [] Still a normal list item
345 | * [ ] Empty todo
346 | * [x] Todo that's checked
347 | * [ x] Still a normal list item
348 |
349 | ### Math stuff
350 |
351 | This is inline $y = ax + b$
352 |
353 | This is a block of _math_
354 |
355 | $$y = ax + b$$
--------------------------------------------------------------------------------
/tests/TEST IMAGE HAS SPACES.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/md2notion/18ae9f1cc4511777329e9c6585db64b80ce65b1f/tests/TEST IMAGE HAS SPACES.png
--------------------------------------------------------------------------------
/tests/TEST.md:
--------------------------------------------------------------------------------
1 | # TEST
2 |
3 | | Awoo | Awooo | Awoooo |
4 | |---------|---------|---------|
5 | | Test100 | Test200 | Test300 |
6 | | | Test400 | |
7 |
--------------------------------------------------------------------------------
/tests/TEST_IMAGE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/md2notion/18ae9f1cc4511777329e9c6585db64b80ce65b1f/tests/TEST_IMAGE.png
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cobertos/md2notion/18ae9f1cc4511777329e9c6585db64b80ce65b1f/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | def pytest_generate_tests(metafunc):
2 | if "headerLevel" in metafunc.fixturenames:
3 | metafunc.parametrize("headerLevel", map(lambda n: n+1, range(6)))
--------------------------------------------------------------------------------
/tests/test_NotionPyRenderer.py:
--------------------------------------------------------------------------------
1 | '''
2 | Tests NotionPyRenderer parsing
3 | '''
4 | import re
5 | import mistletoe
6 | import notion
7 | from md2notion.NotionPyRenderer import NotionPyRenderer, addHtmlImgTagExtension, addLatexExtension
8 |
9 | def test_header(capsys, headerLevel):
10 | '''it renders a range of headers, warns if it cant render properly'''
11 | #arrange/act
12 | output = mistletoe.markdown(f"{'#'*headerLevel} Owo what's this?", NotionPyRenderer)
13 | captured = capsys.readouterr()
14 |
15 | #assert
16 | assert len(output) == 1
17 | output = output[0]
18 | assert isinstance(output, dict)
19 | if headerLevel > 3: #Should print error
20 | assert re.search(r"not support", captured.out, re.I) #Should print out warning
21 | if headerLevel == 1:
22 | assert output['type'] == notion.block.HeaderBlock
23 | elif headerLevel == 2:
24 | assert output['type'] == notion.block.SubheaderBlock
25 | else:
26 | assert output['type'] == notion.block.SubsubheaderBlock
27 | assert output['title'] == "Owo what's this?"
28 |
29 | def test_list():
30 | '''it should render a normal list'''
31 | #arrange/act
32 | output = mistletoe.markdown("* asdf", NotionPyRenderer)
33 |
34 | #assert
35 | assert len(output) == 1
36 | output = output[0]
37 | assert isinstance(output, dict)
38 | assert output['type'] == notion.block.BulletedListBlock
39 | assert output['title'] == 'asdf'
40 |
41 | def test_list_numbered():
42 | '''it should render a numbered list if given one'''
43 | #arrange/act
44 | output = mistletoe.markdown("1. asdf", NotionPyRenderer)
45 |
46 | #assert
47 | assert len(output) == 1
48 | output = output[0]
49 | assert isinstance(output, dict)
50 | assert output['type'] == notion.block.NumberedListBlock
51 | assert output['title'] == 'asdf'
52 |
53 | def test_list2():
54 | '''it should render a GFM list item'''
55 | #arrange/act
56 | output = mistletoe.markdown(\
57 | """
58 | * [] Really
59 | * [ ] big
60 | * [x] uwu
61 | """, NotionPyRenderer)
62 |
63 | #assert
64 | assert len(output) == 3
65 | assert isinstance(output[0], dict)
66 | assert output[0]['type'] == notion.block.BulletedListBlock
67 | assert output[0]['title'] == '[] Really'
68 | assert isinstance(output[1], dict)
69 | assert output[1]['type'] == notion.block.TodoBlock
70 | assert output[1]['title'] == 'big'
71 | assert output[1]['checked'] == False
72 | assert isinstance(output[2], dict)
73 | assert output[2]['type'] == notion.block.TodoBlock
74 | assert output[2]['title'] == 'uwu'
75 | assert output[2]['checked'] == True
76 |
77 | def test_quote():
78 | '''it should render a numbered list if given one'''
79 | #arrange/act
80 | output = mistletoe.markdown("> Quoth thee 'Mr. Obama... Hewwo? MR OBAMA??'", NotionPyRenderer)
81 |
82 | #assert
83 | assert len(output) == 1
84 | output = output[0]
85 | assert isinstance(output, dict)
86 | assert output['type'] == notion.block.QuoteBlock
87 | assert output['title'] == "Quoth thee 'Mr. Obama... Hewwo? MR OBAMA??'"
88 |
89 | def test_image():
90 | '''it should render an image'''
91 | #arrange/act
92 | output = mistletoe.markdown("", NotionPyRenderer)
93 |
94 | #assert
95 | assert len(output) == 1
96 | output = output[0]
97 | assert isinstance(output, dict)
98 | assert output['type'] == notion.block.ImageBlock
99 |
100 | def test_imageInLink():
101 | '''it should render an image in a link, but separately because notion doesnt support that'''
102 | #arrange/act
103 | output = mistletoe.markdown("[](https://cobertos.com)", NotionPyRenderer)
104 |
105 | #assert
106 | assert len(output) == 2
107 | assert isinstance(output[0], dict)
108 | assert output[0]['type'] == notion.block.TextBlock
109 | assert output[0]['title'] == "[](https://cobertos.com)" #Should extract the image
110 | assert isinstance(output[1], dict) #The ImageBlock can't be in a link in Notion, so we get it outside
111 | assert output[1]['type'] == notion.block.ImageBlock
112 |
113 | def test_imageBlockText():
114 | '''it should render an image in bold text'''
115 | #arrange/act
116 | output = mistletoe.markdown("**texttexttexttext**", NotionPyRenderer)
117 |
118 | #assert
119 | assert len(output) == 2
120 | assert isinstance(output[0], dict)
121 | assert output[0]['type'] == notion.block.TextBlock
122 | assert output[0]['title'] == "**texttexttexttext**" #Should extract the image
123 | assert isinstance(output[1], dict) #The ImageBlock can't be inline with anything else so it comes out
124 | assert output[1]['type'] == notion.block.ImageBlock
125 |
126 | def test_imageInHtml():
127 | '''it should render an image that is mentioned in the html