├── .gitignore ├── LICENSE ├── README.md ├── _sass ├── _base.scss ├── _layout.scss ├── custom-styles.scss ├── custom-variables.scss ├── fonts │ ├── PalatinoLinotype-Bold.eot │ ├── PalatinoLinotype-Bold.svg │ ├── PalatinoLinotype-Bold.ttf │ ├── PalatinoLinotype-Bold.woff │ ├── PalatinoLinotype-Bold.woff2 │ ├── PalatinoLinotype-BoldItalic.eot │ ├── PalatinoLinotype-BoldItalic.svg │ ├── PalatinoLinotype-BoldItalic.ttf │ ├── PalatinoLinotype-BoldItalic.woff │ ├── PalatinoLinotype-BoldItalic.woff2 │ ├── PalatinoLinotype-Italic.eot │ ├── PalatinoLinotype-Italic.svg │ ├── PalatinoLinotype-Italic.ttf │ ├── PalatinoLinotype-Italic.woff │ ├── PalatinoLinotype-Italic.woff2 │ ├── PalatinoLinotype-Roman.eot │ ├── PalatinoLinotype-Roman.svg │ ├── PalatinoLinotype-Roman.ttf │ ├── PalatinoLinotype-Roman.woff │ ├── PalatinoLinotype-Roman.woff2 │ ├── cmunbi.eot │ ├── cmunbi.svg │ ├── cmunbi.ttf │ ├── cmunbi.woff │ ├── cmunbx.eot │ ├── cmunbx.svg │ ├── cmunbx.ttf │ ├── cmunbx.woff │ ├── cmunrm.eot │ ├── cmunrm.svg │ ├── cmunrm.ttf │ ├── cmunrm.woff │ ├── cmunsi.eot │ ├── cmunsi.svg │ ├── cmunsi.ttf │ ├── cmunsi.woff │ ├── cmunso.eot │ ├── cmunso.svg │ ├── cmunso.ttf │ ├── cmunso.woff │ ├── cmunss.eot │ ├── cmunss.svg │ ├── cmunss.ttf │ ├── cmunss.woff │ ├── cmunsx.eot │ ├── cmunsx.svg │ ├── cmunsx.ttf │ ├── cmunsx.woff │ ├── cmunti.eot │ ├── cmunti.svg │ ├── cmunti.ttf │ └── cmunti.woff ├── main.scss └── skins │ ├── classic.scss │ ├── dark.scss │ ├── solarized-dark.scss │ └── solarized.scss ├── _templates ├── 404.html ├── _footer.html ├── _gallery.html ├── _head.html ├── _header.html ├── _list.html ├── _properties_table.html ├── archive.html ├── page.html └── test.css ├── notion4ever ├── __init__.py ├── __main__.py ├── markdown_parser.py ├── notion2json.py ├── site_generation.py └── structuring.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | *.pyc 4 | .vscode/settings.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Daniil Merkulov 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

NOTION4EVER

5 |
6 | 7 | Notion4ever is a small python tool that allows you to free your content and export it as a collection of markdown and HTML files via the official Notion API. 8 | 9 | # ✨ Features 10 | * Export ready to deploy static HTML pages from your Notion.so pages. 11 | ![root_page](https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/assets/root_page.png) 12 | * Supports nice urls. 13 | * Downloads all your Notion content, which is accessible via API to a raw JSON file. 14 | * Uses official Notion API (via [notion-sdk-py](https://github.com/ramnes/notion-sdk-py), but you can use curls if you want). 15 | * Supports arbitrary page hierarchy. 16 | ![breadcrumb](https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/assets/breadcrumb.png) 17 | * Supports galleries and lists 18 | ![gallery](https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/assets/gallery.png) 19 | 20 | ![list](https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/assets/list.png) 21 | 22 | Note that Notion API does not provide information about the database view yet. That is why notion4ever will render the database as a list if any database entries do not have a cover. Suppose all entries have covers, then it will be displayed as a gallery. 23 | * Lightweight and responsive. 24 | * Downloads all your images and files locally (you can turn this off if you prefer to store images\files somewhere else). 25 | 26 | # 💻 How to run it locally 27 | Just copy or clone the content of this repository and run. 28 | 29 | ```python 30 | python -m notion4ever -n NOTION_TOKEN -p NOTION_PAGE_ID -bl True 31 | ``` 32 | # 🤖 How to run it automatically with Github actions 33 | I will demonstrate it on the specific example of my site. 34 | [Notion page](https://fmin.notion.site/Danya-Merkulov-12e3d1659a444678b4e2b6a989a3c625) -> [Github repository](https://github.com/MerkulovDaniil/merkulovdaniil.github.io/) 35 | 36 | ## ✅ Step 1. Create/choose some page in Notion. 37 | 1. We will need the page ID. For example, the page with URL 38 | `https://fmin.notion.site/Danya-Merkulov-12e3d1659a444678b4e2b6a989a3c625` has the following ID: `12e3d1659a444678b4e2b6a989a3c625`. 39 | 1. Also, we will need to create a Notion API token. Go to [Notion.so/my-integrations](https://www.notion.so/my-integrations) -> `Create new integration`. Type the name of the integration and press `submit`. Now you can see your token, which starts with `secret_***` under the `Internal Integration Token` field. 40 | 1. Do not forget to grant access for your integration to edit your page. Go to `Share -> invite -> {YOUR INTEGRATION NAME}`. 41 | 42 | ## ✅ Step 2. Set up a repository for your static site. 43 | In my case, it is [github.com/MerkulovDaniil/merkulovdaniil.github.io/](https://github.com/MerkulovDaniil/merkulovdaniil.github.io/). 44 | 1. You need to specify your Notion settings in a Github action secret. Jump to the `Settings -> Secrets -> Actions -> New repository secret` and create two secrets: 45 | a. NOTION_PAGE_ID 46 | b. NOTION_TOKEN 47 | 48 | ![github_secret](https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/assets/github_secret.png) 49 | 50 | 1. Create and configure the following GitHub action in your repository: 51 | 52 |
publish.yml 53 | 54 | ```yaml 55 | name: Deploy from Notion to Pages 56 | 57 | # on: [workflow_dispatch] 58 | on: 59 | schedule: 60 | - cron: "0 */12 * * *" 61 | 62 | jobs: 63 | download_old-generate-push: 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | # Download packages 68 | - name: Submodule Update 69 | run: | 70 | wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 71 | sudo apt install ./google-chrome-stable_current_amd64.deb 72 | sudo apt-get update 73 | 74 | - name: Set up Python 75 | uses: actions/setup-python@v2 76 | with: 77 | python-version: 3.10.0 78 | 79 | - name: Download notion4ever 80 | uses: actions/checkout@v2 81 | with: 82 | repository: 'MerkulovDaniil/notion4ever' 83 | 84 | - name: Install packages 85 | run: pip install -r requirements.txt 86 | 87 | - name: Download current version of the site 88 | uses: actions/checkout@v2 89 | with: 90 | # HERE, YOU NEED TO PLACE YOUR REPOSITORY 91 | repository: 'MerkulovDaniil/merkulovdaniil.github.io' 92 | # TARGET BRANCH 93 | ref: main 94 | # THE FOLDER, WHERE NOTION4EVER EXPORTS YOUR CONTENT BY DEFAULT 95 | path: _site 96 | 97 | - name: Run notion4ever 98 | run: python -m notion4ever 99 | env: 100 | # HERE YOU NEED TO PLACE URL OF THE ROOT PAGE. PROBABLY IT IS "https://.github.io" 101 | SITE_URL: "https://merkulov.top" 102 | NOTION_TOKEN: ${{secrets.NOTION_TOKEN}} 103 | NOTION_PAGE_ID: ${{secrets.NOTION_PAGE_ID}} 104 | 105 | - name: Deploy to Pages 106 | uses: JamesIves/github-pages-deploy-action@3.7.1 107 | with: 108 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 109 | BRANCH: main 110 | FOLDER: _site 111 | COMMIT_MESSAGE: 🤖 Deployed via notion4ever. 112 | ``` 113 |
114 | 115 | This script will run every 12 hours, and you can change it. Note that the first run could be slow if your page contains a lot of content, but all the subsequent runs will not download existing files. 116 | Congratulations 🤗! 117 | 118 | # 🛠 How it works 119 | 1. Given your notion token and ID of some page, notion4ever downloads all your content from this page and all nested subpages and saves it in a JSON file, `notion_content.json`. 120 | 1. Given your raw Notion data, notion4ever structures the page's content and generates file `notion_structured.json` with markdown content of all pages and relations between them. Markdown parsing is done via modification of [notion2md](https://github.com/echo724/notion2md) library. 121 | 1. Given structured notion content, notion4ever generates site from [jinja](https://github.com/pallets/jinja/) templates located in `./_templates` directory. All styles are located in `./_sass` directory and compiled with [libsass-python](https://github.com/sass/libsass-python) library. By default, site is located in `./_site` directory 122 | 123 | # 🌈 Alternatives 124 | ## 🆓 Free 125 | * [loconotion](https://github.com/leoncvlt/loconotion) - Python tool to turn Notion.so pages into lightweight, customizable static websites. 126 | * [NotoCourse](https://github.com/MerkulovDaniil/NotoCourse) - properly configured github actions + structuring for loconotion. 127 | * [notablog](https://github.com/dragonman225/notablog) - blog-oriented static site generator from Notion database. 128 | * [popsy.co](popsy.co) - turns your Notion docs into a site with custom domain. 129 | 130 | ## 💰 Paid 131 | * [helpkit.so](helpkit.so) - turns your Notion docs into a hosted self-service knowledge base. 132 | * [float.so](float.so) - turns your docs in Notion into online course. 133 | * [super.so](super.so) - turns your Notion docs into a site. 134 | * [potion.so](https://potion.so/) - turns your Notion docs into a site. 135 | 136 | # 🦄 Examples 137 | Please, add your sites here if you are using Notion4ever. 138 | | Notion public page | Notion4ever web page | 139 | |---|---| 140 | | [My personal page](https://fmin.notion.site/Danya-Merkulov-12e3d1659a444678b4e2b6a989a3c625) | [My personal page](https://merkulov.top) | 141 | | [MIPT optimization course](https://fmin.notion.site/00ef4311866942fd8efd351cc976959c) | [MIPT optimization course](https://opt.mipt.ru) | 142 | 143 | 144 | # ToDo 145 | - [ ] Proper documentation. 146 | - [ ] Create pip package. 147 | - [ ] Add parallel files downloading. 148 | - [ ] Add search field. 149 | -------------------------------------------------------------------------------- /_sass/_base.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: $base-font-size; 3 | } 4 | 5 | /** 6 | * Reset some basic elements 7 | */ 8 | body, h1, h2, h3, h4, h5, h6, 9 | p, blockquote, pre, hr, 10 | dl, dd, ol, ul, figure { 11 | margin: 0; 12 | padding: 0; 13 | 14 | } 15 | 16 | 17 | 18 | /** 19 | * Basic styling 20 | */ 21 | body { 22 | font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family; 23 | color: $text-color; 24 | background-color: $background-color; 25 | -webkit-text-size-adjust: 100%; 26 | -webkit-font-feature-settings: "kern" 1; 27 | -moz-font-feature-settings: "kern" 1; 28 | -o-font-feature-settings: "kern" 1; 29 | font-feature-settings: "kern" 1; 30 | font-kerning: normal; 31 | display: flex; 32 | min-height: 100vh; 33 | flex-direction: column; 34 | overflow-wrap: break-word; 35 | } 36 | 37 | 38 | 39 | /** 40 | * Set `margin-bottom` to maintain vertical rhythm 41 | */ 42 | h1, h2, h3, h4, h5, h6, 43 | p, blockquote, pre, 44 | ul, ol, dl, figure, 45 | %vertical-rhythm { 46 | margin-bottom: $spacing-unit / 2; 47 | } 48 | 49 | hr { 50 | margin-top: $spacing-unit; 51 | margin-bottom: $spacing-unit; 52 | } 53 | 54 | /** 55 | * `main` element 56 | */ 57 | main { 58 | display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */ 59 | } 60 | 61 | 62 | 63 | /** 64 | * Images 65 | */ 66 | img { 67 | max-width: 100%; 68 | vertical-align: middle; 69 | } 70 | 71 | 72 | 73 | /** 74 | * Figures 75 | */ 76 | figure > img { 77 | display: block; 78 | } 79 | 80 | figcaption { 81 | font-size: $small-font-size; 82 | font-family: 'Computer Modern Sans'; 83 | margin-top: $spacing-unit / 4; 84 | } 85 | 86 | 87 | 88 | /** 89 | * Lists 90 | */ 91 | ul, ol { 92 | margin-left: $spacing-unit; 93 | } 94 | 95 | li { 96 | > ul, 97 | > ol { 98 | margin-bottom: 0; 99 | } 100 | } 101 | 102 | 103 | 104 | /** 105 | * Headings 106 | */ 107 | h1, h2, h3, h4, h5, h6 { 108 | font-family: $header-font-family; 109 | font-weight: bold; 110 | } 111 | 112 | 113 | 114 | /** 115 | * Links 116 | */ 117 | a { 118 | color: $link-base-color; 119 | text-decoration: none; 120 | 121 | &:visited { 122 | color: $link-visited-color; 123 | } 124 | 125 | &:hover { 126 | color: $link-hover-color; 127 | text-decoration: underline; 128 | } 129 | 130 | .social-media-list &:hover { 131 | text-decoration: none; 132 | 133 | .username { 134 | text-decoration: underline; 135 | } 136 | } 137 | } 138 | 139 | 140 | /** 141 | * Blockquotes 142 | */ 143 | blockquote { 144 | color: $brand-color; 145 | border-left: 4px solid $border-color-01; 146 | padding-left: $spacing-unit / 2; 147 | @include relative-font-size(1.125); 148 | font-style: italic; 149 | 150 | > :last-child { 151 | margin-bottom: 0; 152 | } 153 | 154 | i, em { 155 | font-style: normal; 156 | } 157 | } 158 | 159 | 160 | 161 | /** 162 | * Code formatting 163 | */ 164 | pre, 165 | code { 166 | font-family: $code-font-family; 167 | font-size: 0.9375em; 168 | border: 1px solid $border-color-01; 169 | border-radius: 3px; 170 | background-color: $code-background-color; 171 | } 172 | 173 | code { 174 | padding: 1px 5px; 175 | } 176 | 177 | pre { 178 | padding: 8px 12px; 179 | overflow-x: auto; 180 | 181 | > code { 182 | border: 0; 183 | padding-right: 0; 184 | padding-left: 0; 185 | } 186 | } 187 | 188 | .highlight { 189 | border-radius: 3px; 190 | background: $code-background-color; 191 | @extend %vertical-rhythm; 192 | 193 | .highlighter-rouge & { 194 | background: $code-background-color; 195 | } 196 | } 197 | 198 | 199 | 200 | /** 201 | * Wrapper 202 | */ 203 | .wrapper { 204 | max-width: calc(#{$content-width} - (#{$spacing-unit})); 205 | margin-right: auto; 206 | margin-left: auto; 207 | padding-right: $spacing-unit / 2; 208 | padding-left: $spacing-unit / 2; 209 | @extend %clearfix; 210 | 211 | @media screen and (min-width: $on-large) { 212 | max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); 213 | padding-right: $spacing-unit; 214 | padding-left: $spacing-unit; 215 | } 216 | } 217 | 218 | 219 | 220 | /** 221 | * Clearfix 222 | */ 223 | %clearfix:after { 224 | content: ""; 225 | display: table; 226 | clear: both; 227 | } 228 | 229 | 230 | 231 | /** 232 | * Icons 233 | */ 234 | 235 | .orange { 236 | color: #f66a0a; 237 | } 238 | 239 | .grey { 240 | color: #828282; 241 | } 242 | 243 | .svg-icon { 244 | width: 16px; 245 | height: 16px; 246 | display: inline-block; 247 | fill: currentColor; 248 | padding: 5px 3px 2px 5px; 249 | vertical-align: text-bottom; 250 | } 251 | 252 | 253 | /** 254 | * Tables 255 | */ 256 | table { 257 | margin-bottom: $spacing-unit; 258 | width: 100%; 259 | text-align: $table-text-align; 260 | color: $table-text-color; 261 | border-collapse: collapse; 262 | border: 1px solid $table-border-color; 263 | tr { 264 | &:nth-child(even) { 265 | background-color: $table-zebra-color; 266 | } 267 | } 268 | th, td { 269 | padding: ($spacing-unit / 3) ($spacing-unit / 2); 270 | } 271 | th { 272 | background-color: $table-header-bg-color; 273 | border: 1px solid $table-header-border; 274 | } 275 | td { 276 | border: 1px solid $table-border-color; 277 | } 278 | 279 | @include media-query($on-laptop) { 280 | display: block; 281 | overflow-x: auto; 282 | -webkit-overflow-scrolling: touch; 283 | -ms-overflow-style: -ms-autohiding-scrollbar; 284 | } 285 | } 286 | 287 | /** 288 | * Tasklist 289 | */ 290 | .task-list-item { 291 | list-style-type: none !important; 292 | } 293 | 294 | .task-list-item input[type="checkbox"] { 295 | margin: 0 4px 0.25em -20px; 296 | vertical-align: middle; 297 | } 298 | 299 | // Code block container 300 | pre { 301 | position: relative; 302 | } 303 | 304 | // Copy button 305 | .copy-button { 306 | position: absolute; 307 | top: 5px; 308 | right: 5px; 309 | padding: 4px 8px; 310 | font-size: 12px; 311 | font-family: $mini-text-font-family; 312 | color: #666; 313 | background-color: #f5f5f5; 314 | border: 1px solid #ccc; 315 | border-radius: 3px; 316 | cursor: pointer; 317 | opacity: 0; 318 | transition: opacity 0.2s; 319 | 320 | &:hover { 321 | background-color: #e5e5e5; 322 | } 323 | 324 | &:active { 325 | background-color: #d5d5d5; 326 | } 327 | } 328 | 329 | // Show copy button on hover 330 | pre:hover .copy-button { 331 | opacity: 1; 332 | } 333 | 334 | // Success state 335 | .copy-button.success { 336 | color: #2a7ae2; 337 | border-color: #2a7ae2; 338 | } -------------------------------------------------------------------------------- /_sass/_layout.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Site header 3 | */ 4 | .path-header{ 5 | width: 100%; 6 | height: $path-header-height; 7 | position: fixed; 8 | background-color: white; 9 | display: flex; 10 | align-items: center; /* Vertical center alignment */ 11 | font-size: $small-font-size; 12 | z-index: 15; 13 | 14 | } 15 | 16 | .cover{ 17 | padding-top: $path-header-height; 18 | min-height: 100px; 19 | img{ 20 | max-height: 200px; 21 | width: 100%; 22 | object-fit: cover; 23 | } 24 | } 25 | 26 | .icon{ 27 | margin-top: -39px; 28 | line-height: 78px; 29 | font-size: 78px; 30 | img{ 31 | width: 128px; 32 | height: 128px; 33 | object-fit: cover; 34 | } 35 | 36 | max-width: calc(#{$content-width} - (#{$spacing-unit})); 37 | margin-right: auto; 38 | margin-left: auto; 39 | padding-right: $spacing-unit / 2; 40 | padding-left: $spacing-unit / 2; 41 | @media screen and (min-width: $on-large) { 42 | max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); 43 | padding-right: $spacing-unit; 44 | padding-left: $spacing-unit; 45 | } 46 | } 47 | 48 | .header-nav{ 49 | margin: 0; 50 | margin-left: 15px; 51 | height: $path-header-height; 52 | line-height: $path-header-height; 53 | 54 | // Scrolling 55 | white-space: nowrap; 56 | overflow-x: auto; 57 | overflow-y: hidden; 58 | &::-webkit-scrollbar { 59 | display: none; 60 | } 61 | -ms-overflow-style: none; /* IE and Edge */ 62 | scrollbar-width: none; /* Firefox */ 63 | 64 | // // dots instead of scrolling 65 | // overflow: hidden; 66 | // text-overflow: ellipsis; 67 | 68 | & .header-title-block { 69 | font-family: $mini-text-font-family; 70 | a{ 71 | padding-left: 5px; 72 | padding-right: 5px; 73 | color: black; 74 | &:hover{ 75 | text-decoration: none; /* no underline */ 76 | background-color: rgba(55, 53, 47, 0.08); 77 | } 78 | } 79 | display: inline-flex; 80 | &:not(:last-of-type) { 81 | &::after { 82 | content: '/'; 83 | margin: 0; 84 | color: #c1d3de; 85 | } 86 | } 87 | &:last-of-type{ 88 | padding-left: 5px; 89 | padding-right: 5px; 90 | } 91 | } 92 | } 93 | 94 | .miniicon{ 95 | img{ 96 | width: 24px; 97 | height: 24px; 98 | object-fit: cover; 99 | margin-top: -2px; 100 | display: unset !important; 101 | } 102 | } 103 | 104 | /** 105 | * Page content 106 | */ 107 | .page-content { 108 | flex: 1 0 auto; 109 | img{ 110 | margin-left: auto; 111 | margin-right: auto; 112 | display: block; 113 | } 114 | } 115 | 116 | .page-heading { 117 | @include relative-font-size(2); 118 | } 119 | 120 | .page-list-heading { 121 | @include relative-font-size(1.75); 122 | } 123 | 124 | .page-list { 125 | margin-left: 0; 126 | list-style: none; 127 | 128 | > li { 129 | margin-bottom: $spacing-unit; 130 | } 131 | } 132 | 133 | .page-meta { 134 | font-size: $small-font-size; 135 | color: $brand-color; 136 | } 137 | 138 | .page-link { 139 | display: block; 140 | @include relative-font-size(1.5); 141 | } 142 | 143 | /** 144 | * Posts 145 | */ 146 | 147 | .page-title{ 148 | font-family: $header-font-family; 149 | font-weight: bold; 150 | @include relative-font-size(2.625); 151 | letter-spacing: -1px; 152 | line-height: 1.15; 153 | 154 | @media screen and (min-width: $on-large) { 155 | @include relative-font-size(2.625); 156 | } 157 | 158 | margin-bottom: $spacing-unit / 2; 159 | margin-top: 5px; 160 | } 161 | 162 | .page-content h1 { 163 | @include relative-font-size(2.5); 164 | letter-spacing: -1px; 165 | line-height: 1.15; 166 | 167 | @media screen and (min-width: $on-large) { 168 | @include relative-font-size(2.5); 169 | } 170 | } 171 | 172 | .page-content { 173 | margin-bottom: $spacing-unit; 174 | 175 | h1 { margin-top: $spacing-unit * 2 } 176 | h2 { margin-top: $spacing-unit * 1.25 } 177 | h3, h4, h5, h6 { margin-top: $spacing-unit } 178 | 179 | h2 { 180 | @include relative-font-size(1.75); 181 | 182 | @media screen and (min-width: $on-large) { 183 | @include relative-font-size(2); 184 | } 185 | } 186 | 187 | h3 { 188 | @include relative-font-size(1.375); 189 | 190 | @media screen and (min-width: $on-large) { 191 | @include relative-font-size(1.625); 192 | } 193 | } 194 | 195 | h4 { 196 | @include relative-font-size(1.25); 197 | } 198 | 199 | h5 { 200 | @include relative-font-size(1.125); 201 | } 202 | h6 { 203 | @include relative-font-size(1.0625); 204 | } 205 | } 206 | 207 | 208 | .social-media-list { 209 | display: table; 210 | margin: 0 auto; 211 | li { 212 | float: left; 213 | margin: 5px 10px 5px 0; 214 | &:last-of-type { margin-right: 0 } 215 | a { 216 | display: block; 217 | padding: $spacing-unit / 4; 218 | border: 1px solid $border-color-01; 219 | &:hover { border-color: $border-color-02 } 220 | } 221 | } 222 | } 223 | 224 | /** 225 | * Grid helpers 226 | */ 227 | @media screen and (min-width: $on-large) { 228 | .one-half { 229 | width: calc(50% - (#{$spacing-unit} / 2)); 230 | } 231 | } 232 | 233 | /** 234 | * Gallery 235 | */ 236 | 237 | .gallery { 238 | display: grid; 239 | grid-gap: 12px; 240 | grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 241 | grid-auto-rows: auto; 242 | grid-auto-flow: dense; 243 | } 244 | 245 | .card { 246 | border: 1px solid rgba(0, 0, 0, 0.25); 247 | border-radius: 5px; 248 | min-height: 100px; 249 | a{ 250 | color: black; 251 | &:hover{ 252 | text-decoration: none; /* no underline */ 253 | } 254 | } 255 | &:hover{ 256 | background-color: rgba(55, 53, 47, 0.08); 257 | } 258 | } 259 | 260 | .card img{ 261 | width: 100%; 262 | border-radius: 3px 3px 0 0; 263 | object-fit: cover; 264 | max-height: 120px; 265 | } 266 | 267 | .card-desc{ 268 | border-top: 1px; 269 | margin: 7px; 270 | font-size: $small-font-size; 271 | display: flex; 272 | flex-direction: column; 273 | width: auto; 274 | } 275 | 276 | .card-tags{ 277 | margin: 7px; 278 | font-size: 14px; 279 | display: flex; 280 | flex-direction: row; 281 | width: auto; 282 | } 283 | 284 | .card-tag{ 285 | display: inline; 286 | font-family: $mini-text-font-family; 287 | width: min-content; 288 | border-radius: 3px; 289 | padding: 1px 3px 1px 3px; 290 | margin-right: 3px; 291 | color: #fff; 292 | background-color: #5e76bf; 293 | } 294 | 295 | .properties_table{ 296 | display: table; 297 | background-color: $background-color; 298 | color: $text-color; 299 | font-family: $mini-text-font-family; 300 | font-size: $small-font-size; 301 | border:None; 302 | td{ 303 | background-color: $background-color; 304 | border: None; 305 | padding: 5px; 306 | white-space: pre-line; 307 | } 308 | } 309 | 310 | .db_list_scroller{ 311 | overflow-x: scroll; 312 | } 313 | 314 | .db_list { 315 | font-family: $mini-text-font-family; 316 | font-size: 14px; 317 | border:None; 318 | background-color: $background-color; 319 | color: $text-color; 320 | display: table; 321 | width: 100%; 322 | } 323 | 324 | @media screen and (max-width: 540px) { 325 | .db_list { 326 | display: block; 327 | } 328 | } 329 | 330 | .row { 331 | display: table-row; 332 | } 333 | 334 | .row:hover{ 335 | background-color: #e8f3ff; 336 | } 337 | 338 | 339 | .row.header { 340 | white-space: nowrap; 341 | } 342 | 343 | @media screen and (max-width: 540px) { 344 | .row { 345 | padding: 14px 0 7px; 346 | display: block; 347 | } 348 | .row.header { 349 | padding: 0; 350 | height: 0px; 351 | } 352 | .row:nth-of-type(odd) { 353 | background: #f1f1f1 354 | } 355 | .row.header .cell { 356 | display: none; 357 | } 358 | .row .cell { 359 | margin-bottom: 10px; 360 | } 361 | .row .cell:before { 362 | margin-bottom: 3px; 363 | content: attr(data-title); 364 | min-width: 98px; 365 | font-size: 10px; 366 | line-height: 10px; 367 | font-weight: bold; 368 | text-transform: uppercase; 369 | color: #969696; 370 | display: block; 371 | } 372 | } 373 | 374 | .cell { 375 | padding: 6px 12px; 376 | display: table-cell; 377 | } 378 | @media screen and (max-width: 540px) { 379 | .cell { 380 | padding: 2px 16px; 381 | display: block; 382 | } 383 | } 384 | 385 | .timeline { 386 | position: relative; 387 | 388 | &:after { 389 | content: ""; 390 | width: 2px; 391 | position: absolute; 392 | top: 5px; 393 | bottom: 5px; 394 | left: 60px; 395 | z-index: 1; 396 | background: #c5c5c5; 397 | } 398 | 399 | .number { 400 | font-family: $mini-text-font-family; 401 | font-size: 16px; 402 | position: -webkit-sticky; 403 | position: sticky; 404 | margin-bottom: 20px; 405 | top: $path-header-height; 406 | } 407 | 408 | .year { 409 | position: relative; 410 | 411 | .description { 412 | font-size: 80%; 413 | position: relative; 414 | padding: 0 0 0 75px; 415 | margin-bottom: 20px; 416 | } 417 | 418 | .post { 419 | position: relative; 420 | padding: 0 0 0 75px; 421 | 422 | &::before { 423 | content: ""; 424 | width: 10px; 425 | height: 10px; 426 | background: #c5c5c5; 427 | border: 2px solid #ffffff; 428 | -webkit-border-radius: 50%; 429 | -moz-border-radius: 50%; 430 | -ms-border-radius: 50%; 431 | border-radius: 50%; 432 | position: absolute; 433 | left: 54px; 434 | top: 3px; 435 | z-index: 2; 436 | } 437 | } 438 | } 439 | } 440 | 441 | footer{ 442 | ul{ 443 | display: flex; 444 | align-items: center; 445 | justify-content: space-between; 446 | padding-left: 2em; 447 | padding-right: 2em; 448 | li{ 449 | list-style-type: none; 450 | } 451 | } 452 | } 453 | 454 | /* responsive videos */ 455 | .res_emb_block { 456 | position: relative; 457 | padding-bottom: 56.25%; /* 16:9 */ 458 | height: 0; 459 | } 460 | .res_emb_block iframe { 461 | position: absolute; 462 | top: 0; 463 | left: 0; 464 | width: 100%; 465 | height: 100%; 466 | } 467 | video { 468 | width: 100% !important; 469 | height: auto !important; 470 | } 471 | 472 | .search-container { 473 | position: fixed; 474 | right: 20px; 475 | top: 0; 476 | height: $path-header-height; 477 | display: flex; 478 | align-items: center; 479 | z-index: 20; 480 | } 481 | 482 | .search-icon { 483 | cursor: pointer; 484 | color: #c3c3c3; 485 | font-size: 16px; 486 | padding: 8px; 487 | 488 | &:hover { 489 | color: #333; 490 | } 491 | } 492 | 493 | #search-input { 494 | position: absolute; 495 | right: 0; 496 | top: 50%; 497 | transform: translateY(-50%); 498 | height: 28px; 499 | width: 200px; 500 | padding: 0 30px 0 10px; 501 | border: 1px solid #ddd; 502 | border-radius: 4px; 503 | font-size: 14px; 504 | font-family: $mini-text-font-family; 505 | 506 | &:focus { 507 | outline: none; // Removes the focus outline 508 | } 509 | } 510 | 511 | #search-results { 512 | position: absolute; 513 | right: 0; 514 | top: 40px; 515 | width: 300px; 516 | max-height: 400px; 517 | overflow-y: auto; 518 | background: white; 519 | border: 1px solid #ddd; 520 | border-radius: 4px; 521 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 522 | z-index: 1000; 523 | } 524 | 525 | .search-result-item { 526 | padding: 10px; 527 | border-bottom: 1px solid #eee; 528 | cursor: pointer; 529 | 530 | &:hover { 531 | background-color: #f5f5f5; 532 | } 533 | 534 | .result-title { 535 | font-weight: bold; 536 | margin-bottom: 5px; 537 | } 538 | 539 | .result-snippet { 540 | font-size: 13px; 541 | color: #666; 542 | } 543 | } -------------------------------------------------------------------------------- /_sass/custom-styles.scss: -------------------------------------------------------------------------------- 1 | // Placeholder to allow defining custom styles that override everything else. 2 | // (Use `_sass/minima/custom-variables.scss` to override variable defaults) 3 | -------------------------------------------------------------------------------- /_sass/custom-variables.scss: -------------------------------------------------------------------------------- 1 | // Placeholder to allow overriding predefined variables smoothly. 2 | -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Bold.eot -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Bold.ttf -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Bold.woff -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Bold.woff2 -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-BoldItalic.eot -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-BoldItalic.ttf -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-BoldItalic.woff -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-BoldItalic.woff2 -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Italic.eot -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Italic.ttf -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Italic.woff -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Italic.woff2 -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Roman.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Roman.eot -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Roman.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Roman.ttf -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Roman.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Roman.woff -------------------------------------------------------------------------------- /_sass/fonts/PalatinoLinotype-Roman.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/PalatinoLinotype-Roman.woff2 -------------------------------------------------------------------------------- /_sass/fonts/cmunbi.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunbi.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunbi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunbi.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunbi.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunbi.woff -------------------------------------------------------------------------------- /_sass/fonts/cmunbx.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunbx.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunbx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunbx.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunbx.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunbx.woff -------------------------------------------------------------------------------- /_sass/fonts/cmunrm.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunrm.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunrm.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunrm.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunrm.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunrm.woff -------------------------------------------------------------------------------- /_sass/fonts/cmunsi.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunsi.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunsi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 18 | 20 | 23 | 26 | 27 | 28 | 29 | 32 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 43 | 45 | 46 | 48 | 50 | 51 | 53 | 55 | 56 | 57 | 59 | 61 | 63 | 65 | 68 | 69 | 71 | 73 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 87 | 88 | 90 | 91 | 93 | 94 | 96 | 97 | 99 | 100 | 101 | 102 | 103 | 105 | 106 | 107 | 108 | 109 | 111 | 113 | 115 | 117 | 119 | 121 | 124 | 125 | 126 | 127 | 128 | 129 | 131 | 132 | 134 | 136 | 138 | 139 | 141 | 143 | 144 | 145 | 147 | 148 | 150 | 151 | 154 | 155 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /_sass/fonts/cmunsi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunsi.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunsi.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunsi.woff -------------------------------------------------------------------------------- /_sass/fonts/cmunso.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunso.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunso.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunso.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunso.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunso.woff -------------------------------------------------------------------------------- /_sass/fonts/cmunss.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunss.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 18 | 20 | 23 | 26 | 27 | 28 | 29 | 32 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 43 | 45 | 46 | 48 | 50 | 51 | 53 | 56 | 57 | 58 | 60 | 62 | 64 | 66 | 68 | 69 | 71 | 73 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 90 | 91 | 93 | 94 | 96 | 97 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 110 | 112 | 114 | 116 | 118 | 119 | 122 | 123 | 124 | 125 | 126 | 127 | 129 | 130 | 131 | 133 | 135 | 136 | 139 | 140 | 141 | 142 | 143 | 144 | 146 | 147 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /_sass/fonts/cmunss.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunss.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunss.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunss.woff -------------------------------------------------------------------------------- /_sass/fonts/cmunsx.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunsx.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunsx.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunsx.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunsx.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunsx.woff -------------------------------------------------------------------------------- /_sass/fonts/cmunti.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunti.eot -------------------------------------------------------------------------------- /_sass/fonts/cmunti.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunti.ttf -------------------------------------------------------------------------------- /_sass/fonts/cmunti.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/_sass/fonts/cmunti.woff -------------------------------------------------------------------------------- /_sass/main.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | // @import url('fonts/https://fonts.googleapis.com/css2?family=Montserrat:wght@700&family=Source+Serif+Pro&family=Ubuntu&display=swap'); 3 | @font-face { 4 | font-family: 'Computer Modern Sans'; 5 | src: url('fonts/cmunss.eot'); 6 | src: url('fonts/cmunss.eot?#iefix') format('embedded-opentype'), 7 | url('fonts/cmunss.woff') format('woff'), 8 | url('fonts/cmunss.ttf') format('truetype'), 9 | url('fonts/cmunss.svg#cmunss') format('svg'); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | 14 | 15 | @font-face { 16 | font-family: 'Computer Modern Sans'; 17 | src: url('fonts/cmunsx.eot'); 18 | src: url('fonts/cmunsx.eot?#iefix') format('embedded-opentype'), 19 | url('fonts/cmunsx.woff') format('woff'), 20 | url('fonts/cmunsx.ttf') format('truetype'), 21 | url('fonts/cmunsx.svg#cmunsx') format('svg'); 22 | font-weight: bold; 23 | font-style: normal; 24 | } 25 | 26 | 27 | @font-face { 28 | font-family: 'Computer Modern Sans'; 29 | src: url('fonts/cmunsi.eot'); 30 | src: url('fonts/cmunsi.eot?#iefix') format('embedded-opentype'), 31 | url('fonts/cmunsi.woff') format('woff'), 32 | url('fonts/cmunsi.ttf') format('truetype'), 33 | url('fonts/cmunsi.svg#cmunsi') format('svg'); 34 | font-weight: normal; 35 | font-style: italic; 36 | } 37 | 38 | 39 | @font-face { 40 | font-family: 'Computer Modern Sans'; 41 | src: url('fonts/cmunso.eot'); 42 | src: url('fonts/cmunso.eot?#iefix') format('embedded-opentype'), 43 | url('fonts/cmunso.woff') format('woff'), 44 | url('fonts/cmunso.ttf') format('truetype'), 45 | url('fonts/cmunso.svg#cmunso') format('svg'); 46 | font-weight: bold; 47 | font-style: italic; 48 | } 49 | 50 | @font-face { 51 | font-family: 'Palatino Linotype'; 52 | src: url('fonts/PalatinoLinotype-Bold.eot'); 53 | src: url('fonts/PalatinoLinotype-Bold.eot?#iefix') format('embedded-opentype'), 54 | url('fonts/PalatinoLinotype-Bold.woff2') format('woff2'), 55 | url('fonts/PalatinoLinotype-Bold.woff') format('woff'), 56 | url('fonts/PalatinoLinotype-Bold.ttf') format('truetype'), 57 | url('fonts/PalatinoLinotype-Bold.svg#PalatinoLinotype-Bold') format('svg'); 58 | font-weight: bold; 59 | font-style: normal; 60 | } 61 | 62 | @font-face { 63 | font-family: 'Palatino Linotype'; 64 | src: url('fonts/PalatinoLinotype-BoldItalic.eot'); 65 | src: url('fonts/PalatinoLinotype-BoldItalic.eot?#iefix') format('embedded-opentype'), 66 | url('fonts/PalatinoLinotype-BoldItalic.woff2') format('woff2'), 67 | url('fonts/PalatinoLinotype-BoldItalic.woff') format('woff'), 68 | url('fonts/PalatinoLinotype-BoldItalic.ttf') format('truetype'), 69 | url('fonts/PalatinoLinotype-BoldItalic.svg#PalatinoLinotype-BoldItalic') format('svg'); 70 | font-weight: bold; 71 | font-style: italic; 72 | } 73 | 74 | @font-face { 75 | font-family: 'Palatino Linotype'; 76 | src: url('fonts/PalatinoLinotype-Italic.eot'); 77 | src: url('fonts/PalatinoLinotype-Italic.eot?#iefix') format('embedded-opentype'), 78 | url('fonts/PalatinoLinotype-Italic.woff2') format('woff2'), 79 | url('fonts/PalatinoLinotype-Italic.woff') format('woff'), 80 | url('fonts/PalatinoLinotype-Italic.ttf') format('truetype'), 81 | url('fonts/PalatinoLinotype-Italic.svg#PalatinoLinotype-Italic') format('svg'); 82 | font-weight: normal; 83 | font-style: italic; 84 | } 85 | 86 | @font-face { 87 | font-family: 'Palatino Linotype'; 88 | src: url('fonts/PalatinoLinotype-Roman.eot'); 89 | src: url('fonts/PalatinoLinotype-Roman.eot?#iefix') format('embedded-opentype'), 90 | url('fonts/PalatinoLinotype-Roman.woff2') format('woff2'), 91 | url('fonts/PalatinoLinotype-Roman.woff') format('woff'), 92 | url('fonts/PalatinoLinotype-Roman.ttf') format('truetype'), 93 | url('fonts/PalatinoLinotype-Roman.svg#PalatinoLinotype-Roman') format('svg'); 94 | font-weight: normal; 95 | font-style: normal; 96 | } 97 | 98 | 99 | // @font-face { 100 | // font-family: 'Computer Modern Serif'; 101 | // src: url('fonts/cmunrm.eot'); 102 | // src: url('fonts/cmunrm.eot?#iefix') format('embedded-opentype'), 103 | // url('fonts/cmunrm.woff') format('woff'), 104 | // url('fonts/cmunrm.ttf') format('truetype'), 105 | // url('fonts/cmunrm.svg#cmunrm') format('svg'); 106 | // font-weight: normal; 107 | // font-style: normal; 108 | // } 109 | 110 | 111 | // @font-face { 112 | // font-family: 'Computer Modern Serif'; 113 | // src: url('fonts/cmunbx.eot'); 114 | // src: url('fonts/cmunbx.eot?#iefix') format('embedded-opentype'), 115 | // url('fonts/cmunbx.woff') format('woff'), 116 | // url('fonts/cmunbx.ttf') format('truetype'), 117 | // url('fonts/cmunbx.svg#cmunbx') format('svg'); 118 | // font-weight: bold; 119 | // font-style: normal; 120 | // } 121 | 122 | 123 | // @font-face { 124 | // font-family: 'Computer Modern Serif'; 125 | // src: url('fonts/cmunti.eot'); 126 | // src: url('fonts/cmunti.eot?#iefix') format('embedded-opentype'), 127 | // url('fonts/cmunti.woff') format('woff'), 128 | // url('fonts/cmunti.ttf') format('truetype'), 129 | // url('fonts/cmunti.svg#cmunti') format('svg'); 130 | // font-weight: normal; 131 | // font-style: italic; 132 | // } 133 | 134 | 135 | // @font-face { 136 | // font-family: 'Computer Modern Serif'; 137 | // src: url('fonts/cmunbi.eot'); 138 | // src: url('fonts/cmunbi.eot?#iefix') format('embedded-opentype'), 139 | // url('fonts/cmunbi.woff') format('woff'), 140 | // url('fonts/cmunbi.ttf') format('truetype'), 141 | // url('fonts/cmunbi.svg#cmunbi') format('svg'); 142 | // font-weight: bold; 143 | // font-style: italic; 144 | // } 145 | 146 | 147 | // Define defaults for each variable. 148 | 149 | $base-font-family: "Palatino Linotype", serif !default; 150 | // $base-font-family: "Computer Modern Serif", serif !default; 151 | $header-font-family: "Computer Modern Sans", sans-serif !default; 152 | $mini-text-font-family: 'Computer Modern Sans', sans-serif; 153 | $code-font-family: "Menlo", "Inconsolata", "Consolas", "Roboto Mono", "Ubuntu Mono", "Liberation Mono", "Courier New", monospace; 154 | $base-font-size: 16px !default; 155 | $base-font-weight: 400 !default; 156 | $small-font-size: $base-font-size * 0.875 !default; 157 | $base-line-height: 1.5 !default; 158 | $path-header-height: 35px; 159 | 160 | $spacing-unit: 30px !default; 161 | 162 | $table-text-align: left !default; 163 | 164 | // Width of the content area 165 | $content-width: 800px !default; 166 | 167 | $on-palm: 600px !default; 168 | $on-laptop: 800px !default; 169 | 170 | $on-medium: $on-palm !default; 171 | $on-large: $on-laptop !default; 172 | 173 | // Use media queries like this: 174 | // @include media-query($on-palm) { 175 | // .wrapper { 176 | // padding-right: $spacing-unit / 2; 177 | // padding-left: $spacing-unit / 2; 178 | // } 179 | // } 180 | // Notice the following mixin uses max-width, in a deprecated, desktop-first 181 | // approach, whereas media queries used elsewhere now use min-width. 182 | @mixin media-query($device) { 183 | @media screen and (max-width: $device) { 184 | @content; 185 | } 186 | } 187 | 188 | @mixin relative-font-size($ratio) { 189 | font-size: #{$ratio}rem; 190 | } 191 | 192 | // Import pre-styling-overrides hook and style-partials. 193 | @import 194 | "./skins/classic", //Choose the theme 195 | "custom-variables", // Hook to override predefined variables. 196 | "base", // Defines element resets. 197 | "layout", // Defines structure and style based on CSS selectors. 198 | "custom-styles" // Hook to override existing styles. 199 | ; 200 | -------------------------------------------------------------------------------- /_sass/skins/classic.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | $brand-color: #828282 !default; 4 | $brand-color-light: lighten($brand-color, 40%) !default; 5 | $brand-color-dark: darken($brand-color, 25%) !default; 6 | 7 | $site-title-color: $brand-color-dark !default; 8 | 9 | $text-color: black !default; 10 | $background-color: white !default; 11 | $code-background-color: #eeeeff !default; 12 | 13 | $link-base-color: #2a7ae2 !default; 14 | $link-visited-color: darken($link-base-color, 15%) !default; 15 | $link-hover-color: $text-color !default; 16 | 17 | $border-color-01: $brand-color-light !default; 18 | $border-color-02: lighten($brand-color, 35%) !default; 19 | $border-color-03: $brand-color-dark !default; 20 | 21 | $table-text-color: lighten($text-color, 18%) !default; 22 | $table-zebra-color: lighten($brand-color, 46%) !default; 23 | $table-header-bg-color: lighten($brand-color, 43%) !default; 24 | $table-header-border: lighten($brand-color, 37%) !default; 25 | $table-border-color: $border-color-01 !default; 26 | 27 | 28 | // Syntax highlighting styles should be adjusted appropriately for every "skin" 29 | // ---------------------------------------------------------------------------- 30 | 31 | .highlight { 32 | .c { color: #998; font-style: italic } // Comment 33 | .err { color: #a61717; background-color: #e3d2d2 } // Error 34 | .k { font-weight: bold } // Keyword 35 | .o { font-weight: bold } // Operator 36 | .cm { color: #998; font-style: italic } // Comment.Multiline 37 | .cp { color: #999; font-weight: bold } // Comment.Preproc 38 | .c1 { color: #998; font-style: italic } // Comment.Single 39 | .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special 40 | .gd { color: #000; background-color: #fdd } // Generic.Deleted 41 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific 42 | .ge { font-style: italic } // Generic.Emph 43 | .gr { color: #a00 } // Generic.Error 44 | .gh { color: #999 } // Generic.Heading 45 | .gi { color: #000; background-color: #dfd } // Generic.Inserted 46 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific 47 | .go { color: #888 } // Generic.Output 48 | .gp { color: #555 } // Generic.Prompt 49 | .gs { font-weight: bold } // Generic.Strong 50 | .gu { color: #aaa } // Generic.Subheading 51 | .gt { color: #a00 } // Generic.Traceback 52 | .kc { font-weight: bold } // Keyword.Constant 53 | .kd { font-weight: bold } // Keyword.Declaration 54 | .kp { font-weight: bold } // Keyword.Pseudo 55 | .kr { font-weight: bold } // Keyword.Reserved 56 | .kt { color: #458; font-weight: bold } // Keyword.Type 57 | .m { color: #099 } // Literal.Number 58 | .s { color: #d14 } // Literal.String 59 | .na { color: #008080 } // Name.Attribute 60 | .nb { color: #0086B3 } // Name.Builtin 61 | .nc { color: #458; font-weight: bold } // Name.Class 62 | .no { color: #008080 } // Name.Constant 63 | .ni { color: #800080 } // Name.Entity 64 | .ne { color: #900; font-weight: bold } // Name.Exception 65 | .nf { color: #900; font-weight: bold } // Name.Function 66 | .nn { color: #555 } // Name.Namespace 67 | .nt { color: #000080 } // Name.Tag 68 | .nv { color: #008080 } // Name.Variable 69 | .ow { font-weight: bold } // Operator.Word 70 | .w { color: #bbb } // Text.Whitespace 71 | .mf { color: #099 } // Literal.Number.Float 72 | .mh { color: #099 } // Literal.Number.Hex 73 | .mi { color: #099 } // Literal.Number.Integer 74 | .mo { color: #099 } // Literal.Number.Oct 75 | .sb { color: #d14 } // Literal.String.Backtick 76 | .sc { color: #d14 } // Literal.String.Char 77 | .sd { color: #d14 } // Literal.String.Doc 78 | .s2 { color: #d14 } // Literal.String.Double 79 | .se { color: #d14 } // Literal.String.Escape 80 | .sh { color: #d14 } // Literal.String.Heredoc 81 | .si { color: #d14 } // Literal.String.Interpol 82 | .sx { color: #d14 } // Literal.String.Other 83 | .sr { color: #009926 } // Literal.String.Regex 84 | .s1 { color: #d14 } // Literal.String.Single 85 | .ss { color: #990073 } // Literal.String.Symbol 86 | .bp { color: #999 } // Name.Builtin.Pseudo 87 | .vc { color: #008080 } // Name.Variable.Class 88 | .vg { color: #008080 } // Name.Variable.Global 89 | .vi { color: #008080 } // Name.Variable.Instance 90 | .il { color: #099 } // Literal.Number.Integer.Long 91 | } 92 | -------------------------------------------------------------------------------- /_sass/skins/dark.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | $brand-color: #999999 !default; 4 | $brand-color-light: lighten($brand-color, 5%) !default; 5 | $brand-color-dark: darken($brand-color, 35%) !default; 6 | 7 | $site-title-color: $brand-color-light !default; 8 | 9 | $text-color: #bbbbbb !default; 10 | $background-color: #181818 !default; 11 | $code-background-color: #212121 !default; 12 | 13 | $link-base-color: #79b8ff !default; 14 | $link-visited-color: $link-base-color !default; 15 | $link-hover-color: $text-color !default; 16 | 17 | $border-color-01: $brand-color-dark !default; 18 | $border-color-02: $brand-color-light !default; 19 | $border-color-03: $brand-color !default; 20 | 21 | $table-text-color: $text-color !default; 22 | $table-zebra-color: lighten($background-color, 4%) !default; 23 | $table-header-bg-color: lighten($background-color, 10%) !default; 24 | $table-header-border: lighten($background-color, 21%) !default; 25 | $table-border-color: $border-color-01 !default; 26 | 27 | 28 | // Syntax highlighting styles should be adjusted appropriately for every "skin" 29 | // List of tokens: https://github.com/rouge-ruby/rouge/wiki/List-of-tokens 30 | // Some colors come from Material Theme Darker: 31 | // https://github.com/material-theme/vsc-material-theme/blob/master/scripts/generator/settings/specific/darker-hc.ts 32 | // https://github.com/material-theme/vsc-material-theme/blob/master/scripts/generator/color-set.ts 33 | // ---------------------------------------------------------------------------- 34 | 35 | .highlight { 36 | .c { color: #545454; font-style: italic } // Comment 37 | .err { color: #f07178; background-color: #e3d2d2 } // Error 38 | .k { color: #89DDFF; font-weight: bold } // Keyword 39 | .o { font-weight: bold } // Operator 40 | .cm { color: #545454; font-style: italic } // Comment.Multiline 41 | .cp { color: #545454; font-weight: bold } // Comment.Preproc 42 | .c1 { color: #545454; font-style: italic } // Comment.Single 43 | .cs { color: #545454; font-weight: bold; font-style: italic } // Comment.Special 44 | .gd { color: #000; background-color: #fdd } // Generic.Deleted 45 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific 46 | .ge { font-style: italic } // Generic.Emph 47 | .gr { color: #f07178 } // Generic.Error 48 | .gh { color: #999 } // Generic.Heading 49 | .gi { color: #000; background-color: #dfd } // Generic.Inserted 50 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific 51 | .go { color: #888 } // Generic.Output 52 | .gp { color: #555 } // Generic.Prompt 53 | .gs { font-weight: bold } // Generic.Strong 54 | .gu { color: #aaa } // Generic.Subheading 55 | .gt { color: #f07178 } // Generic.Traceback 56 | .kc { font-weight: bold } // Keyword.Constant 57 | .kd { font-weight: bold } // Keyword.Declaration 58 | .kp { font-weight: bold } // Keyword.Pseudo 59 | .kr { font-weight: bold } // Keyword.Reserved 60 | .kt { color: #FFCB6B; font-weight: bold } // Keyword.Type 61 | .m { color: #F78C6C } // Literal.Number 62 | .s { color: #C3E88D } // Literal.String 63 | .na { color: #008080 } // Name.Attribute 64 | .nb { color: #EEFFFF } // Name.Builtin 65 | .nc { color: #FFCB6B; font-weight: bold } // Name.Class 66 | .no { color: #008080 } // Name.Constant 67 | .ni { color: #800080 } // Name.Entity 68 | .ne { color: #900; font-weight: bold } // Name.Exception 69 | .nf { color: #82AAFF; font-weight: bold } // Name.Function 70 | .nn { color: #555 } // Name.Namespace 71 | .nt { color: #FFCB6B } // Name.Tag 72 | .nv { color: #EEFFFF } // Name.Variable 73 | .ow { font-weight: bold } // Operator.Word 74 | .w { color: #EEFFFF } // Text.Whitespace 75 | .mf { color: #F78C6C } // Literal.Number.Float 76 | .mh { color: #F78C6C } // Literal.Number.Hex 77 | .mi { color: #F78C6C } // Literal.Number.Integer 78 | .mo { color: #F78C6C } // Literal.Number.Oct 79 | .sb { color: #C3E88D } // Literal.String.Backtick 80 | .sc { color: #C3E88D } // Literal.String.Char 81 | .sd { color: #C3E88D } // Literal.String.Doc 82 | .s2 { color: #C3E88D } // Literal.String.Double 83 | .se { color: #EEFFFF } // Literal.String.Escape 84 | .sh { color: #C3E88D } // Literal.String.Heredoc 85 | .si { color: #C3E88D } // Literal.String.Interpol 86 | .sx { color: #C3E88D } // Literal.String.Other 87 | .sr { color: #C3E88D } // Literal.String.Regex 88 | .s1 { color: #C3E88D } // Literal.String.Single 89 | .ss { color: #C3E88D } // Literal.String.Symbol 90 | .bp { color: #999 } // Name.Builtin.Pseudo 91 | .vc { color: #FFCB6B } // Name.Variable.Class 92 | .vg { color: #EEFFFF } // Name.Variable.Global 93 | .vi { color: #EEFFFF } // Name.Variable.Instance 94 | .il { color: #F78C6C } // Literal.Number.Integer.Long 95 | } 96 | -------------------------------------------------------------------------------- /_sass/skins/solarized-dark.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | $sol-is-dark: true; 4 | @import "../skins/solarized"; 5 | -------------------------------------------------------------------------------- /_sass/skins/solarized.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | // Solarized skin 4 | // ============== 5 | // Created by Sander Voerman using the Solarized 6 | // color scheme by Ethan Schoonover . 7 | 8 | // This style sheet implements two options for the minima.skin setting: 9 | // "solarized" for light mode and "solarized-dark" for dark mode. 10 | $sol-is-dark: false !default; 11 | 12 | 13 | // Color scheme 14 | // ------------ 15 | // The inline comments show the canonical L*a*b values for each color. 16 | 17 | $sol-base03: #002b36; // 15 -12 -12 18 | $sol-base02: #073642; // 20 -12 -12 19 | $sol-base01: #586e75; // 45 -07 -07 20 | $sol-base00: #657b83; // 50 -07 -07 21 | $sol-base0: #839496; // 60 -06 -03 22 | $sol-base1: #93a1a1; // 65 -05 -02 23 | $sol-base2: #eee8d5; // 92 -00 10 24 | $sol-base3: #fdf6e3; // 97 00 10 25 | $sol-yellow: #b58900; // 60 10 65 26 | $sol-orange: #cb4b16; // 50 50 55 27 | $sol-red: #dc322f; // 50 65 45 28 | $sol-magenta: #d33682; // 50 65 -05 29 | $sol-violet: #6c71c4; // 50 15 -45 30 | $sol-blue: #268bd2; // 55 -10 -45 31 | $sol-cyan: #2aa198; // 60 -35 -05 32 | $sol-green: #859900; // 60 -20 65 33 | 34 | $sol-mono3: $sol-base3; 35 | $sol-mono2: $sol-base2; 36 | $sol-mono1: $sol-base1; 37 | $sol-mono00: $sol-base00; 38 | $sol-mono01: $sol-base01; 39 | 40 | @if $sol-is-dark { 41 | $sol-mono3: $sol-base03; 42 | $sol-mono2: $sol-base02; 43 | $sol-mono1: $sol-base01; 44 | $sol-mono00: $sol-base0; 45 | $sol-mono01: $sol-base1; 46 | } 47 | 48 | 49 | // Minima color variables 50 | // ---------------------- 51 | 52 | $brand-color: $sol-mono1 !default; 53 | $brand-color-light: mix($sol-mono1, $sol-mono3) !default; 54 | $brand-color-dark: $sol-mono00 !default; 55 | 56 | $site-title-color: $sol-mono00 !default; 57 | 58 | $text-color: $sol-mono01 !default; 59 | $background-color: $sol-mono3 !default; 60 | $code-background-color: $sol-mono2 !default; 61 | 62 | $link-base-color: $sol-blue !default; 63 | $link-visited-color: mix($sol-blue, $sol-mono00) !default; 64 | $link-hover-color: $sol-mono00 !default; 65 | 66 | $border-color-01: $brand-color-light !default; 67 | $border-color-02: $sol-mono1 !default; 68 | $border-color-03: $sol-mono00 !default; 69 | 70 | $table-text-color: $sol-mono00 !default; 71 | $table-zebra-color: mix($sol-mono2, $sol-mono3) !default; 72 | $table-header-bg-color: $sol-mono2 !default; 73 | $table-header-border: $sol-mono1 !default; 74 | $table-border-color: $sol-mono1 !default; 75 | 76 | 77 | // Syntax highlighting styles 78 | // -------------------------- 79 | 80 | .highlight { 81 | .c { color: $sol-mono1; font-style: italic } // Comment 82 | .err { color: $sol-red } // Error 83 | .k { color: $sol-mono01; font-weight: bold } // Keyword 84 | .o { color: $sol-mono01; font-weight: bold } // Operator 85 | .cm { color: $sol-mono1; font-style: italic } // Comment.Multiline 86 | .cp { color: $sol-mono1; font-weight: bold } // Comment.Preproc 87 | .c1 { color: $sol-mono1; font-style: italic } // Comment.Single 88 | .cs { color: $sol-mono1; font-weight: bold; font-style: italic } // Comment.Special 89 | .gd { color: $sol-red } // Generic.Deleted 90 | .gd .x { color: $sol-red } // Generic.Deleted.Specific 91 | .ge { color: $sol-mono00; font-style: italic } // Generic.Emph 92 | .gr { color: $sol-red } // Generic.Error 93 | .gh { color: $sol-mono1 } // Generic.Heading 94 | .gi { color: $sol-green } // Generic.Inserted 95 | .gi .x { color: $sol-green } // Generic.Inserted.Specific 96 | .go { color: $sol-mono00 } // Generic.Output 97 | .gp { color: $sol-mono00 } // Generic.Prompt 98 | .gs { color: $sol-mono01; font-weight: bold } // Generic.Strong 99 | .gu { color: $sol-mono1 } // Generic.Subheading 100 | .gt { color: $sol-red } // Generic.Traceback 101 | .kc { color: $sol-mono01; font-weight: bold } // Keyword.Constant 102 | .kd { color: $sol-mono01; font-weight: bold } // Keyword.Declaration 103 | .kp { color: $sol-mono01; font-weight: bold } // Keyword.Pseudo 104 | .kr { color: $sol-mono01; font-weight: bold } // Keyword.Reserved 105 | .kt { color: $sol-violet; font-weight: bold } // Keyword.Type 106 | .m { color: $sol-cyan } // Literal.Number 107 | .s { color: $sol-magenta } // Literal.String 108 | .na { color: $sol-cyan } // Name.Attribute 109 | .nb { color: $sol-blue } // Name.Builtin 110 | .nc { color: $sol-violet; font-weight: bold } // Name.Class 111 | .no { color: $sol-cyan } // Name.Constant 112 | .ni { color: $sol-violet } // Name.Entity 113 | .ne { color: $sol-violet; font-weight: bold } // Name.Exception 114 | .nf { color: $sol-blue; font-weight: bold } // Name.Function 115 | .nn { color: $sol-mono00 } // Name.Namespace 116 | .nt { color: $sol-blue } // Name.Tag 117 | .nv { color: $sol-cyan } // Name.Variable 118 | .ow { color: $sol-mono01; font-weight: bold } // Operator.Word 119 | .w { color: $sol-mono1 } // Text.Whitespace 120 | .mf { color: $sol-cyan } // Literal.Number.Float 121 | .mh { color: $sol-cyan } // Literal.Number.Hex 122 | .mi { color: $sol-cyan } // Literal.Number.Integer 123 | .mo { color: $sol-cyan } // Literal.Number.Oct 124 | .sb { color: $sol-magenta } // Literal.String.Backtick 125 | .sc { color: $sol-magenta } // Literal.String.Char 126 | .sd { color: $sol-magenta } // Literal.String.Doc 127 | .s2 { color: $sol-magenta } // Literal.String.Double 128 | .se { color: $sol-magenta } // Literal.String.Escape 129 | .sh { color: $sol-magenta } // Literal.String.Heredoc 130 | .si { color: $sol-magenta } // Literal.String.Interpol 131 | .sx { color: $sol-magenta } // Literal.String.Other 132 | .sr { color: $sol-green } // Literal.String.Regex 133 | .s1 { color: $sol-magenta } // Literal.String.Single 134 | .ss { color: $sol-magenta } // Literal.String.Symbol 135 | .bp { color: $sol-mono1 } // Name.Builtin.Pseudo 136 | .vc { color: $sol-cyan } // Name.Variable.Class 137 | .vg { color: $sol-cyan } // Name.Variable.Global 138 | .vi { color: $sol-cyan } // Name.Variable.Instance 139 | .il { color: $sol-cyan } // Literal.Number.Integer.Long 140 | } 141 | -------------------------------------------------------------------------------- /_templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'page.html' %} 2 | {% set page = {} %} 3 | {% block tab_favicon %} 4 | 5 | {% endblock tab_favicon %} 6 | 7 | {% block tab_title %} 8 | SAPERE AUDE! 9 | {% endblock tab_title %} 10 | 11 | {% block path_header %} 12 | 17 | {% endblock path_header %} 18 | 19 | {% block page_date %} 20 | {% endblock page_date %} 21 | 22 | {% block cover %} 23 |
24 |
25 | {% endblock cover%} 26 | 27 | {% block icon %} 28 |
29 | 🤷‍♂️ 30 |
31 | {% endblock icon%} 32 | 33 | {% block page_title %} 34 |
Page not found
35 | {% endblock page_title %} 36 | 37 | {% block page_content %} 38 |

The page you are looking for is not found. This can happen because of the redesign, even though I try to preserve the content on this site.

39 |

{% set page = site['pages'][site['root_page_id']] %} 40 | Consider visiting main page {% if page.emoji %} 41 | {{page.emoji}} {{page.title}} 42 | {% elif page.icon %} 43 | {{page.title}} 44 | {% else %} 45 | {{page.title}} 46 | {% endif %}

47 |

Or take a look at the archive of all posts 🏛 Archive

48 | {% endblock page_content %} -------------------------------------------------------------------------------- /_templates/_footer.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /_templates/_gallery.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_templates/_head.html: -------------------------------------------------------------------------------- 1 | 2 | {% block head %} 3 | 4 | 5 | 6 | 7 | 8 | {% if 'md_content' in page.keys() %}{% endif %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 25 | 28 | {% block tab_favicon %} 29 | {% if page.emoji %} 30 | 31 | {% elif page.icon %} 32 | 33 | {% else %} 34 | 35 | {% endif %} 36 | {% endblock tab_favicon %} 37 | {% block tab_title %} 38 | {{page.title}} 39 | {% endblock tab_title %} 40 | {% endblock head%} 41 | 42 | -------------------------------------------------------------------------------- /_templates/_header.html: -------------------------------------------------------------------------------- 1 | {% block path_header %} 2 | 32 | {% endblock path_header%} 33 | 34 |
35 | {% block cover %} 36 |
37 | {% if page.cover %} {% endif %} 38 |
39 | {% endblock cover%} 40 | {% block icon %} 41 | {% if page.emoji %} 42 |
43 | {{page.emoji}} 44 |
45 | {% elif page.icon %} 46 |
47 | 48 |
49 | {% endif %} 50 | {% endblock icon%} 51 |
52 | -------------------------------------------------------------------------------- /_templates/_list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% set properties = site.pages[page['children'][0]]['properties_md'] %} 4 | {% if properties != {} %} 5 |
6 |
7 | {% for property_title, property in properties.items() %} 8 | {% set property_type = site.pages[page['children'][0]]['properties'][property_title]["type"] %} 9 | {% if property_type == 'rich_text' %} 10 |
 {{property_title}}
11 | {% elif property_type == 'number' %} 12 |
 {{property_title}}
13 | {% elif property_type == 'select' %} 14 |
 {{property_title}}
15 | {% elif property_type == 'multi_select' %} 16 |
 {{property_title}}
17 | {% elif property_type == 'date' %} 18 |
 {{property_title}}
19 | {% elif property_type == 'people' %} 20 |
 {{property_title}}
21 | {% elif property_type == 'files' %} 22 |
 {{property_title}}
23 | {% elif property_type == 'checkbox' %} 24 |
 {{property_title}}
25 | {% elif property_type == 'url' %} 26 |
 {{property_title}}
27 | {% elif property_type == 'email' %} 28 |
 {{property_title}}
29 | {% elif property_type == 'phone_number' %} 30 |
 {{property_title}}
31 | {% elif property_type == 'created_time' %} 32 |
 {{property_title}}
33 | {% endif %} 34 | {% endfor %} 35 |
36 | {% endif %} 37 | 38 | {% for child_id in page.children %} 39 | {% set db_entry = site.pages[child_id] %} 40 |
41 | 42 | {% if db_entry.emoji %} 43 |
{{db_entry.emoji}} {{db_entry.title}}
44 | {% elif db_entry.icon %} 45 |
{{db_entry.title}}
46 | {% else %} 47 |
{{db_entry.title}}
48 | {% endif %} 49 |
50 | 51 | {% for property_title, property_md in db_entry['properties_md'].items() %} 52 | {% if db_entry['properties'][property_title]['type'] in ['files', 'url'] %} 53 | {% if property_md != '' %} 54 | 55 | {% else %} 56 |
{{property_md}}
57 | {% endif %} 58 | {% else %} 59 |
{{property_md}}
60 | {% endif %} 61 | {% endfor %} 62 |
63 | {% endfor %} 64 |
65 |
-------------------------------------------------------------------------------- /_templates/_properties_table.html: -------------------------------------------------------------------------------- 1 | 2 | {% for property_title, property_md in page['properties_md'].items() %} 3 | {% if property_md != '' %} 4 | {% if page['properties'][property_title]['type'] == 'rich_text' %} 5 | 6 | 7 | 8 | 9 | {% elif page['properties'][property_title]['type'] == 'number' %} 10 | 11 | 12 | 13 | 14 | {% elif page['properties'][property_title]['type'] == 'select' %} 15 | 16 | 17 | 18 | 19 | {% elif page['properties'][property_title]['type'] == 'multi_select' %} 20 | 21 | 22 | 23 | 24 | {% elif page['properties'][property_title]['type'] == 'date' %} 25 | 26 | 27 | 28 | 29 | {% elif page['properties'][property_title]['type'] == 'people' %} 30 | 31 | 32 | 33 | 34 | {% elif page['properties'][property_title]['type'] == 'files' %} 35 | 36 | 37 | 38 | 39 | {% elif page['properties'][property_title]['type'] == 'checkbox' %} 40 | 41 | 42 | 43 | 44 | {% elif page['properties'][property_title]['type'] == 'url' %} 45 | 46 | 47 | 48 | 49 | {% elif page['properties'][property_title]['type'] == 'email' %} 50 | 51 | 52 | 53 | 54 | {% elif page['properties'][property_title]['type'] == 'phone_number' %} 55 | 56 | 57 | 58 | 59 | {% elif page['properties'][property_title]['type'] == 'created_time' %} 60 | 61 | 62 | 63 | 64 | {% endif %} 65 | {% endif %} 66 | {% endfor %} 67 |
{{property_title}} {{property_md}}
{{property_title}} {{property_md}}
{{property_title}} {{property_md}}
{{property_title}} {% for tag in property_md.split(';') %}{{tag}} {% endfor %}
{{property_title}} {{property_md}}
{{property_title}} {{property_md}}
{{property_title}} {{property_md.split('](')[0][1:]}}
{{property_title}}
{{property_title}} {{property_md.split('](')[0][1:]}}
{{property_title}} {{property_md}}
{{property_title}} {{property_md}}
{{property_title}} {{property_md}}
-------------------------------------------------------------------------------- /_templates/archive.html: -------------------------------------------------------------------------------- 1 | {% extends 'page.html' %} 2 | {% set page = {} %} 3 | {% block tab_favicon %} 4 | 5 | {% endblock tab_favicon %} 6 | 7 | {% block tab_title %} 8 | Archive 9 | {% endblock tab_title %} 10 | 11 | {% block path_header %} 12 | 25 | {% endblock path_header %} 26 | 27 | 28 | {% block page_date %} 29 | {% endblock page_date %} 30 | 31 | {% block cover %} 32 |
33 |
34 | {% endblock cover%} 35 | {% block icon %} 36 |
37 | 🏛 38 |
39 | {% endblock icon%} 40 | 41 | {% block page_title %} 42 |
Archive
43 | {% endblock page_title %} 44 | 45 | {% block page_content %} 46 |
47 | {% for year in site['sorted_id_by_year'] %} 48 |
49 |
{{year}}
50 | {% for page_id in site['sorted_id_by_year'][year] %} 51 | {% set page = site.pages[page_id] %} 52 |
53 | {% if page.emoji %} 54 | {{page.emoji}} {{page.title}} 55 | {% elif page.icon %} 56 | {{page.title}} 57 | {% else %} 58 | {{page.title}} 59 | {% endif %} 60 |
61 |
62 | {{ page['date'].strftime("%d, %b") }} in {{ site["pages"][page["parent"]]["title"] }} 63 |
64 | {% endfor %} 65 |
66 | {% endfor %} 67 |
68 | {% endblock page_content %} -------------------------------------------------------------------------------- /_templates/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include '_head.html' %} 4 | 5 | {% include '_header.html' %} 6 |
7 |
8 |
9 | 36 |
37 | {% block page_content %} 38 | {% if 'db_list' in page.keys() %} 39 | {% include '_list.html' %} 40 | {% elif page.type == 'database' %} 41 | {% include '_gallery.html' %} 42 | {% else %} 43 | {{ content }} 44 | {% endif %} 45 | {% endblock page_content %} 46 |
47 |
48 |
49 |
50 | {% block footer %} 51 | {% if site["include_footer"] %} 52 | {% include '_footer.html' %} 53 | {% endif %} 54 | {% endblock footer %} 55 | 94 | {% if site.search_index %} 95 | 96 | 178 | {% endif %} 179 | 180 | -------------------------------------------------------------------------------- /_templates/test.css: -------------------------------------------------------------------------------- 1 | .timeline { 2 | position: relative; 3 | } 4 | .timeline:after { 5 | content: ""; 6 | width: 2px; 7 | position: absolute; 8 | top: 5px; 9 | bottom: 5px; 10 | left: 60px; 11 | z-index: 1; 12 | background: #c5c5c5; 13 | } 14 | .timeline .number { 15 | position: -webkit-sticky; 16 | position: sticky; 17 | margin-bottom: 20px; 18 | top: 20px; 19 | } 20 | .timeline .year { 21 | position: relative; 22 | } 23 | 24 | .timeline .year .description { 25 | position: relative; 26 | padding: 0 0 0 75px; 27 | margin-bottom: 20px; 28 | } 29 | .timeline .year .post { 30 | position: relative; 31 | padding: 0 0 0 75px; 32 | margin-bottom: 20px; 33 | } 34 | 35 | .timeline .year .post::before{ 36 | content: ""; 37 | width: 10px; 38 | height: 10px; 39 | background: #c5c5c5; 40 | border: 2px solid #ffffff; 41 | -webkit-border-radius: 50%; 42 | -moz-border-radius: 50%; 43 | -ms-border-radius: 50%; 44 | border-radius: 50%; 45 | position: absolute; 46 | left: 54px; 47 | top: 3px; 48 | z-index: 2; 49 | } -------------------------------------------------------------------------------- /notion4ever/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MerkulovDaniil/notion4ever/298cf0592bbaa28e372f9f2e7005ee68264bb320/notion4ever/__init__.py -------------------------------------------------------------------------------- /notion4ever/__main__.py: -------------------------------------------------------------------------------- 1 | from notion4ever import notion2json 2 | from notion4ever import structuring 3 | from notion4ever import site_generation 4 | 5 | import logging 6 | import json 7 | from pathlib import Path 8 | import shutil 9 | import argparse 10 | import os 11 | 12 | from notion_client import Client 13 | 14 | # Helper function to handle boolean arguments 15 | def str_to_bool(value): 16 | if isinstance(value, bool): 17 | return value 18 | if value.lower() in {'true', 't', 'yes', 'y', '1'}: 19 | return True 20 | elif value.lower() in {'false', 'f', 'no', 'n', '0'}: 21 | return False 22 | else: 23 | raise argparse.ArgumentTypeError(f"Boolean value expected, got {value}") 24 | 25 | def main(): 26 | parser = argparse.ArgumentParser(description=( 27 | "Notion4ever: Export all your Notion content to markdown and HTML," 28 | "and serve it as a static site." 29 | )) 30 | parser.add_argument('--notion_token', '-n', 31 | type=str, help="Set your Notion API token.", 32 | default=os.environ.get("NOTION_TOKEN")) 33 | parser.add_argument('--notion_page_id', '-p', 34 | type=str, help="Set page_id of the target page.", 35 | default=os.environ.get("NOTION_PAGE_ID")) 36 | parser.add_argument('--output_dir', '-od', 37 | type=str, default="./_site", 38 | help="Directory to save the generated site.") 39 | parser.add_argument('--templates_dir', '-td', 40 | type=str, default="./_templates", 41 | help="Directory containing site templates.") 42 | parser.add_argument('--sass_dir', '-sd', 43 | type=str, default="./_sass", 44 | help="Directory for SASS files.") 45 | parser.add_argument('--build_locally', '-bl', 46 | type=str_to_bool, default=False, 47 | help="Build the site locally. (true/false)") 48 | parser.add_argument('--download_files', '-df', 49 | type=str_to_bool, default=True, 50 | help="Download files. (true/false)") 51 | parser.add_argument('--site_url', '-su', 52 | type=str, default=os.environ.get("SITE_URL"), 53 | help="Base URL of the site.") 54 | parser.add_argument('--remove_before', '-rb', 55 | type=str_to_bool, default=False, 56 | help="Remove existing files before generating the site. (true/false)") 57 | parser.add_argument('--include_footer', '-if', 58 | type=str_to_bool, default=os.environ.get("INCLUDE_FOOTER"), 59 | help="Include a footer in the site. (true/false)") 60 | parser.add_argument('--logging_level', '-ll', 61 | type=str, default="INFO", choices=["INFO", "DEBUG"], 62 | help="Logging level.") 63 | parser.add_argument('--include_search', '-is', 64 | type=str_to_bool, default=os.environ.get("INCLUDE_SEARCH"), 65 | help="Include a search feature in the site. (true/false)") 66 | 67 | config = vars(parser.parse_args()) 68 | 69 | if config["logging_level"] == "DEBUG": 70 | llevel = logging.DEBUG 71 | else: 72 | llevel = logging.INFO 73 | 74 | logging.basicConfig(format="%(asctime)s %(levelname)s: %(message)s", 75 | level=llevel) 76 | 77 | if config["remove_before"]: 78 | if Path(config["output_dir"]).exists(): 79 | shutil.rmtree(config["output_dir"]) 80 | logging.debug("🤖 Removed old site files") 81 | 82 | notion = Client(auth=config["notion_token"]) 83 | logging.info("🤖 Notion authentification completed successfully.") 84 | 85 | # It will rewrite this file 86 | raw_notion = {} 87 | filename = "./notion_content.json" 88 | filename_structured = "./notion_structured.json" 89 | 90 | # Stage 1. Downloading (reading) raw notion content and save it to json file 91 | if Path(filename).exists(): 92 | logging.info("🤖 Reading existing raw notion content.") 93 | with open(filename, "r") as f: 94 | raw_notion = json.load(f) 95 | else: 96 | logging.info("🤖 Started raw notion content parsing.") 97 | notion2json.notion_page_parser(config["notion_page_id"], 98 | notion=notion, 99 | filename=filename, 100 | notion_json=raw_notion) 101 | logging.info(f"🤖 Downloaded raw notion content. Saved at {filename}") 102 | 103 | # Stage 2. Structuring data 104 | logging.info(f"🤖 Started structuring notion data") 105 | structured_notion = structuring.structurize_notion_content(raw_notion, 106 | config) 107 | with open(filename_structured, "w+", encoding="utf-8") as f: 108 | json.dump(structured_notion, f, ensure_ascii=False, indent=4) 109 | 110 | logging.info(f"🤖 Finished structuring notion data") 111 | 112 | if Path(filename_structured).exists(): 113 | logging.info("🤖 Reading existing raw notion content.") 114 | with open(filename_structured, "r") as f: 115 | structured_notion = json.load(f) 116 | 117 | # Stage 3. Generating site from template and data 118 | if config["build_locally"]: 119 | structured_notion['base_url'] = \ 120 | str(Path(config["output_dir"]).resolve()) 121 | else: 122 | structured_notion['base_url'] = config["site_url"] 123 | 124 | logging.info(("🤖 Started generating site " 125 | f"{'locally' if config['build_locally'] else ''} " 126 | f"to {config['output_dir']}")) 127 | 128 | site_generation.generate_site(structured_notion, config) 129 | 130 | logging.info("🤖 Finished generating site.") 131 | 132 | if __name__ == "__main__": 133 | main() -------------------------------------------------------------------------------- /notion4ever/markdown_parser.py: -------------------------------------------------------------------------------- 1 | # Most of the code was taken from the Notion2md repository 2 | # https://github.com/echo724/notion2md/tree/main/notion2md 3 | 4 | from pathlib import Path 5 | from urllib.parse import urljoin 6 | from urllib.parse import urlparse 7 | from urllib.parse import unquote 8 | 9 | def paragraph(information:dict) -> str: 10 | return information['text'] 11 | 12 | def heading_1(information:dict) -> str: 13 | return f"# {information['text']}" 14 | 15 | def heading_2(information:dict) -> str: 16 | return f"## {information['text']}" 17 | 18 | def heading_3(information:dict) -> str: 19 | return f"### {information['text']}" 20 | 21 | def callout(information:dict) -> str: 22 | return f"{information['icon']} {information['text']}" 23 | 24 | def quote(information:dict) -> str: 25 | return f"> {information['text']}" 26 | 27 | #toggle item will be changed as bulleted list item 28 | def bulleted_list_item(information:dict) -> str: 29 | return f"* {information['text']}" 30 | 31 | # numbering is not supported 32 | def numbered_list_item(information:dict) -> str: 33 | """ 34 | input: item:dict = {"number":int, "text":str} 35 | """ 36 | return f"1. {information['text']}" 37 | 38 | def to_do(information:dict) -> str: 39 | """ 40 | input: item:dict = {"checked":bool, "test":str} 41 | """ 42 | return f"- {'[x]' if information['checked'] else '[ ]'} {information['text']}" 43 | 44 | def code(information:dict) -> str: 45 | """ 46 | input: item:dict = {"language":str,"text":str,"caption":str} 47 | """ 48 | code_block = f"```{information['language'].replace(' ', '_')}\n{information['text']}\n```" 49 | 50 | if 'caption' in information and information['caption']: 51 | return f"
{information['caption']}
\n{code_block}" 52 | return code_block 53 | 54 | def embed(information:dict) -> str: 55 | """ 56 | input: item:dict ={"url":str,"text":str} 57 | """ 58 | embed_link = information["url"] 59 | 60 | block_md =f"""

61 | 62 |

""" 63 | 64 | return block_md 65 | 66 | def image(information:dict) -> str: 67 | """ 68 | input: item:dict ={"url":str,"text":str,"caption":str} 69 | """ 70 | image_name = information['url'] 71 | 72 | if information['caption']: 73 | return f"![{information['caption']}]({image_name})" 74 | else: 75 | return f"![]({image_name})" 76 | 77 | def file(information:dict) -> str: 78 | filename = information['url'] 79 | clean_url = urljoin(filename, urlparse(filename).path) 80 | return f"[📎 {unquote(Path(clean_url).name)}]({filename})" 81 | 82 | def bookmark(information:dict) -> str: 83 | """ 84 | input: item:dict ={"url":str,"text":str,"caption":str} 85 | """ 86 | if information['caption']: 87 | return f"![{information['caption']}]({information['url']})" 88 | else: 89 | return f"![]({information['url']})" 90 | 91 | def equation(information:dict) -> str: 92 | return f"$$ {information['text']} $$" 93 | 94 | def divider(information:dict) -> str: 95 | return f"---" 96 | 97 | def blank() -> str: 98 | return "
" 99 | 100 | def table_row(information:list) -> list: 101 | """ 102 | input: item:list = [[richtext],....] 103 | """ 104 | column_list = [] 105 | for column in information['cells']: 106 | column_list.append(richtext_convertor(column)) 107 | return column_list 108 | 109 | def video(information:dict) -> str: 110 | youtube_link = information["url"] 111 | clean_url = \ 112 | urljoin(youtube_link, urlparse( youtube_link).path) 113 | is_webm = clean_url.endswith(".webm") or clean_url.endswith(".mp4") 114 | if is_webm: 115 | block_md =f"""

""" 116 | else: 117 | youtube_link = youtube_link.replace("http://", "https://") 118 | 119 | block_md =f"""

120 | 121 |

""" 122 | 123 | return block_md 124 | 125 | block_type_map = { 126 | "paragraph": paragraph, 127 | "heading_1": heading_1, 128 | "heading_2": heading_2, 129 | "heading_3": heading_3, 130 | "callout": callout, 131 | "toggle":bulleted_list_item, 132 | "quote": quote, 133 | "bulleted_list_item": bulleted_list_item, 134 | "numbered_list_item": numbered_list_item, 135 | "to_do": to_do, 136 | # "child_page": child_page, 137 | "code": code, 138 | "embed": embed, 139 | "image": image, 140 | "bookmark": bookmark, 141 | "equation": equation, 142 | "divider": divider, 143 | "file": file, 144 | 'table_row': table_row, 145 | "video": video 146 | } 147 | 148 | def blocks_convertor(blocks:object, structured_notion, page_id) -> str: 149 | results = [] 150 | for block in blocks: 151 | block_md = block_convertor(block,0, structured_notion, page_id) 152 | results.append(block_md) 153 | 154 | outcome_blocks = "".join([result for result in results]) 155 | return outcome_blocks 156 | 157 | def information_collector(payload:dict, structured_notion: dict, page_id) -> dict: 158 | information = dict() 159 | if "text" in payload: 160 | information['text'] = richtext_convertor(payload['text']) 161 | if "icon" in payload: 162 | information['icon'] = payload['icon']['emoji'] 163 | if "checked" in payload: 164 | information['checked'] = payload['checked'] 165 | if "expression" in payload: 166 | information['text'] = payload['expression'] 167 | if "url" in payload: 168 | information['url'] = payload['url'] 169 | if "dont_download" not in payload: 170 | structured_notion["pages"][page_id]["files"].append(payload['url']) 171 | if "caption" in payload: 172 | information['caption'] = richtext_convertor(payload['caption']) 173 | if "external" in payload: 174 | information['url'] = payload['external']['url'] 175 | if "dont_download" not in payload: 176 | structured_notion["pages"][page_id]["files"].append(payload['external']['url']) 177 | if "language" in payload: 178 | information['language'] = payload['language'] 179 | 180 | # internal url 181 | if "file" in payload: 182 | information['url'] = payload['file']['url'] 183 | clean_url = \ 184 | urljoin(information['url'], urlparse( information['url']).path) 185 | is_webm = clean_url.endswith(".webm") or clean_url.endswith(".mp4") 186 | if "dont_download" not in payload or is_webm: 187 | structured_notion["pages"][page_id]["files"].append(payload['file']['url']) 188 | 189 | # table cells 190 | if "cells" in payload: 191 | information['cells'] = payload['cells'] 192 | 193 | return information 194 | 195 | def block_convertor(block:object,depth=0, structured_notion={}, page_id='') -> str: 196 | outcome_block:str = "" 197 | block_type = block['type'] 198 | #Special Case: Block is blank 199 | if block_type == "paragraph" and not block['has_children'] and not block[block_type]['text']: 200 | outcome_block = blank() +"\n\n" 201 | else: 202 | if block_type in ["child_page", "child_database", "db_entry"]: 203 | title = structured_notion['pages'][block['id']]['title'] 204 | url = structured_notion['pages'][block['id']]['url'] 205 | outcome_block = f"{title}]({url})\n\n" 206 | if structured_notion['pages'][block['id']]['emoji']: 207 | emoji = structured_notion['pages'][block['id']]['emoji'] 208 | outcome_block = f"[{emoji} {outcome_block}" 209 | elif structured_notion['pages'][block['id']]['icon']: 210 | icon = structured_notion['pages'][block['id']]['icon'] 211 | outcome_block = f"""[ {outcome_block}""" 212 | else: 213 | outcome_block = f"[{outcome_block}" 214 | 215 | else: 216 | if block_type in block_type_map: 217 | if block_type in ["embed", "video"]: 218 | block[block_type]["dont_download"] = True 219 | outcome_block = \ 220 | block_type_map[block_type](information_collector(block[block_type], 221 | structured_notion, page_id)) + "\n\n" 222 | else: 223 | outcome_block = f"[{block_type} is not supported]\n\n" 224 | 225 | if block_type == "code": 226 | outcome_block = outcome_block.rstrip('\n').replace('\n', '\n'+'\t'*depth) 227 | outcome_block += '\n\n' 228 | 229 | if block['has_children']: 230 | if block_type == 'table': 231 | depth += 1 232 | child_blocks = block["children"] 233 | table_list = [] 234 | for cell_block in child_blocks: 235 | cell_block_type = cell_block['type'] 236 | table_list.append(block_type_map[cell_block_type]( 237 | information_collector( 238 | cell_block[cell_block_type], 239 | structured_notion, 240 | page_id))) 241 | # convert to markdown table 242 | for index,value in enumerate(table_list): 243 | if index == 0: 244 | outcome_block = " | " + " | ".join(value) + " | " + "\n" 245 | outcome_block += " | " + " | ".join(['----'] * len(value)) + " | " + "\n" 246 | continue 247 | outcome_block += " | " + " | ".join(value) + " | " + "\n" 248 | outcome_block += "\n" 249 | else: 250 | depth += 1 251 | child_blocks = block["children"] 252 | for block in child_blocks: 253 | # This is needed, because notion thinks, that if 254 | # the page contains numbered list, header 1 will be the 255 | # child block for it, which is strange. 256 | if block['type'] == "heading_1": 257 | print(f"DEPTH {depth}") 258 | depth = 0 259 | block_md = block_convertor(block, depth, structured_notion, page_id) 260 | outcome_block += "\t"*depth + block_md 261 | 262 | return outcome_block 263 | 264 | #Link 265 | def text_link(item:dict): 266 | """ 267 | input: item:dict ={"content":str,"link":str} 268 | """ 269 | return f"[{item['content']}]({item['link']['url']})" 270 | 271 | #Annotations 272 | def bold(content:str): 273 | return f"**{content}**" 274 | 275 | def italic(content:str): 276 | return f"*{content}*" 277 | 278 | def strikethrough(content:str): 279 | return f"~~{content}~~" 280 | 281 | def underline(content:str): 282 | return f"{content}" 283 | 284 | def code(content:str): 285 | return f"`{content}`" 286 | 287 | def color(content:str,color): 288 | return f"{content}" 289 | 290 | def equation(content:str): 291 | return f"$ {content} $" 292 | 293 | annotation_map = { 294 | "bold": bold, 295 | "italic": italic, 296 | "strikethrough": strikethrough, 297 | "underline": underline, 298 | "code": code, 299 | } 300 | 301 | #Mentions 302 | def _mention_link(content,url): 303 | if "https://github.com/" in url: 304 | repo = Path(url).name 305 | return f' {repo} ' 306 | else: 307 | return f"[{content}]({url})" 308 | 309 | def user(information:dict): 310 | return f"({information['content']})" 311 | 312 | def page(information:dict): 313 | return _mention_link(information['content'], information['url']) 314 | 315 | def date(information:dict): 316 | return f"({information['content']})" 317 | 318 | def database(information:dict): 319 | return _mention_link(information['content'], information['url']) 320 | 321 | def link_preview(information:dict): 322 | return _mention_link(information['content'], information['url']) 323 | 324 | def mention_information(payload:dict): 325 | information = dict() 326 | if payload['href']: 327 | information['url'] = payload['href'] 328 | if payload['plain_text'] != "Untitled": 329 | information['content'] = payload['plain_text'] 330 | else: 331 | information['content'] = payload['href'] 332 | else: 333 | information['content'] = payload['plain_text'] 334 | 335 | return information 336 | 337 | mention_map = { 338 | "user": user, 339 | "page": page, 340 | "database": database, 341 | "date": date, 342 | "link_preview": link_preview 343 | } 344 | 345 | def richtext_word_converter(richtext:dict, title_mode=False) -> str: 346 | outcome_word = "" 347 | plain_text = richtext["plain_text"] 348 | if richtext['type'] == "equation": 349 | outcome_word = equation(plain_text) 350 | if title_mode: 351 | return outcome_word 352 | elif richtext['type'] == "mention": 353 | mention_type = richtext['mention']['type'] 354 | if mention_type in mention_map: 355 | outcome_word = mention_map[mention_type](mention_information(richtext)) 356 | else: 357 | if title_mode: 358 | outcome_word = plain_text 359 | return outcome_word 360 | if "href" in richtext: 361 | if richtext["href"]: 362 | outcome_word = text_link(richtext["text"]) 363 | else: 364 | outcome_word = plain_text 365 | else: 366 | outcome_word = plain_text 367 | annot = richtext["annotations"] 368 | for key,transfer in annotation_map.items(): 369 | if richtext["annotations"][key]: 370 | outcome_word = transfer(outcome_word) 371 | if annot["color"] != "default": 372 | outcome_word = color(outcome_word,annot["color"]) 373 | return outcome_word 374 | 375 | 376 | def richtext_convertor(richtext_list:list, title_mode=False) -> str: 377 | """ 378 | title_mode: bool flag is needed for headers parsing (in case they contain) 379 | any latex expressions. 380 | """ 381 | outcome_sentence = "" 382 | for richtext in richtext_list: 383 | outcome_sentence += richtext_word_converter(richtext, title_mode) 384 | return outcome_sentence 385 | 386 | def grouping(page_md: str) -> str: 387 | page_md_fixed = [] 388 | prev_line_type = '' 389 | for line in page_md.splitlines(): 390 | line_type = '' 391 | norm_line = line.lstrip('\t').lstrip() 392 | if norm_line.startswith('- [ ]') or norm_line.startswith('- [x]'): 393 | line_type = 'checkbox' 394 | elif norm_line.startswith('* '): 395 | line_type = 'bullet' 396 | elif norm_line.startswith('1. '): 397 | line_type = 'numbered' 398 | 399 | if prev_line_type != '': 400 | if line == '': 401 | continue 402 | 403 | if line_type != prev_line_type: 404 | page_md_fixed.append('') 405 | 406 | page_md_fixed.append(line) 407 | prev_line_type = line_type 408 | return "\n".join(page_md_fixed) 409 | 410 | def parse_markdown(raw_notion: dict, structured_notion: dict): 411 | for page_id, page in raw_notion.items(): 412 | structured_notion["pages"][page_id]["md_content"] = "" 413 | page_md = blocks_convertor(raw_notion[page_id]["blocks"], structured_notion, page_id) 414 | page_md = grouping(page_md) 415 | page_md = page_md.replace("\n\n\n", "\n\n") 416 | # page_md = code_aligner(page_md) 417 | structured_notion["pages"][page_id]["md_content"] = page_md -------------------------------------------------------------------------------- /notion4ever/notion2json.py: -------------------------------------------------------------------------------- 1 | from notion_client import APIResponseError 2 | import notion_client 3 | import json 4 | import logging 5 | 6 | def update_notion_file(filename:str, notion_json:dict): 7 | """Writes notion_json dictionary to a json file.""" 8 | with open(filename, 'w+', encoding='utf-8') as f: 9 | json.dump(notion_json, f, ensure_ascii=False, indent=4) 10 | 11 | def block_parser(block: dict, notion: "notion_client.client.Client")-> dict: 12 | """Parses block for obtaining all nested blocks 13 | 14 | This function does recursive search over all nested blocks in a given block. 15 | 16 | Args: 17 | block (dict): Notion block, which is obtained from a list returned by 18 | function notion.blocks.children.list(). 19 | notion (notion_client.client.Client): Client for python API for 20 | Notion from https://github.com/ramnes/notion-sdk-py is used here. 21 | 22 | Returns: 23 | block (dict): Notion block, which contains additional "children" key, 24 | which is a list of nested blocks of a given block. 25 | """ 26 | 27 | if block["has_children"]: 28 | block["children"] = [] 29 | start_cursor = None 30 | while True: 31 | if start_cursor is None: 32 | blocks = notion.blocks.children.list(block["id"]) 33 | start_cursor = blocks["next_cursor"] 34 | block["children"].extend(blocks['results']) 35 | if start_cursor is None: 36 | break 37 | 38 | for child_block in block["children"]: 39 | block_parser(child_block, notion) 40 | return block 41 | 42 | def notion_page_parser(page_id: str, notion: "notion_client.client.Client", 43 | filename: str, notion_json: dict): 44 | """Parses notion page with all its nested content and subpages 45 | 46 | This function does recursive search over all nested subpages and databases. 47 | The result of parsing incrementally saves in 'notion_json' dict and locally 48 | in the 'filename' file. 49 | 50 | Args: 51 | page_id (str): ID of the Notion page for parsing 52 | notion (notion_client.client.Client): Client for python API for 53 | Notion from https://github.com/ramnes/notion-sdk-py is used here. 54 | filename (str): Name of the JSON file to be saved. 55 | notion_json (dict): Dictionary with raw Notion data. Keys of this 56 | dictionary is the unique ID for each notion page. Each page contains 57 | a key 'blocks' which is a list of blocks with a content inside the 58 | page. Some blocks may be nested pages and databases. 59 | """ 60 | try: 61 | page = notion.pages.retrieve(page_id) 62 | page_type = 'page' 63 | 64 | except APIResponseError: 65 | page = notion.databases.retrieve(page_id) 66 | page_type = 'database' 67 | pass 68 | 69 | notion_json[page['id']] = page 70 | logging.debug(f"🤖 Retrieved {page['id']} of type {page_type}.") 71 | update_notion_file(filename, notion_json) 72 | start_cursor = None 73 | notion_json[page['id']]['blocks'] = [] 74 | 75 | while True: 76 | if start_cursor is None: 77 | if page_type == 'page': 78 | blocks = notion.blocks.children.list(page_id) 79 | elif page_type == 'database': 80 | blocks = notion.databases.query(page_id) 81 | else: 82 | if page_type == 'page': 83 | blocks = notion.blocks.children.list(page_id, 84 | start_cursor=start_cursor) 85 | elif page_type == 'database': 86 | blocks = notion.databases.query(page_id, 87 | start_cursor=start_cursor) 88 | 89 | start_cursor = blocks['next_cursor'] 90 | notion_json[page['id']]['blocks'].extend(blocks['results']) 91 | update_notion_file(filename, notion_json) 92 | if start_cursor is None: 93 | break 94 | 95 | logging.debug(f"🤖 Parsed content of {page['id']}.") 96 | 97 | for i_block, block in enumerate(notion_json[page['id']]['blocks']): 98 | if page_type == 'page': 99 | if block["type"] in ['page', 'child_page', 'child_database']: 100 | notion_page_parser(block['id'], notion, filename, notion_json) 101 | else: 102 | block = block_parser(block, notion) 103 | notion_json[page['id']]['blocks'][i_block] = block 104 | update_notion_file(filename, notion_json) 105 | elif page_type == 'database': 106 | block["type"] = "db_entry" 107 | notion_json[page['id']]['blocks'][i_block] = block 108 | update_notion_file(filename, notion_json) 109 | if block["object"] in ['page', 'child_page', 'child_database']: 110 | notion_page_parser(block['id'], notion, filename, notion_json) -------------------------------------------------------------------------------- /notion4ever/site_generation.py: -------------------------------------------------------------------------------- 1 | import sass 2 | import markdown 3 | import shutil 4 | import jinja2 5 | from pathlib import Path 6 | import logging 7 | import dateutil.parser as dt_parser 8 | from urllib.parse import urljoin 9 | import json 10 | # pip install mdx_truly_sane_lists 11 | # required pip install markdown-captions, pip install markdown-checklist 12 | # pip install pymdown-extensions 13 | 14 | def verify_templates(config: dict): 15 | """Verifies existense and content of sass and templates dirs.""" 16 | if Path(config["sass_dir"]).is_dir() and any(Path(config["sass_dir"]).iterdir()): 17 | logging.debug("🤖 Sass directory is OK") 18 | else: 19 | logging.critical("🤖 Sass directory is not found or empty.") 20 | 21 | if (Path(config["templates_dir"]).is_dir() and 22 | any(Path(config["templates_dir"]).iterdir())): 23 | logging.debug("🤖 Templates directory is OK") 24 | else: 25 | logging.critical("🤖 Templates directory is not found or empty.") 26 | 27 | def generate_css(config: dict): 28 | """Generates css file (compiling sass files in the output_dir folder).""" 29 | sass.compile(dirname=(config["sass_dir"], Path(config["output_dir"]) / 'css')) 30 | 31 | def generate_404(structured_notion: dict, config: dict): 32 | """Generates 404 html page.""" 33 | with open(Path(config["output_dir"]) / '404.html', 'w+', encoding='utf-8') as f: 34 | tml = (Path(config["templates_dir"] ) / '404.html').read_text() 35 | jinja_loader = jinja2.FileSystemLoader(config["templates_dir"]) 36 | jtml = jinja2.Environment(loader=jinja_loader).from_string(tml) 37 | html_page = jtml.render(content='', site=structured_notion) 38 | f.write(html_page) 39 | 40 | def generate_archive(structured_notion: dict, config: dict): 41 | """Generates archive page.""" 42 | if config["build_locally"]: 43 | archive_link = 'Archive.html' 44 | structured_notion['archive_url'] = str((Path(config["output_dir"]).resolve() / archive_link)) 45 | else: 46 | archive_link = 'Archive/index.html' 47 | structured_notion['archive_url'] = urljoin(structured_notion['base_url'], archive_link) 48 | (Path(config["output_dir"]) / "Archive").mkdir(exist_ok=True) 49 | 50 | with open(Path(config["output_dir"]) / archive_link, 'w+', encoding='utf-8') as f: 51 | # Specify template folder 52 | tml = (Path(config["templates_dir"] ) / 'archive.html').read_text() 53 | jinja_loader = jinja2.FileSystemLoader(config["templates_dir"]) 54 | jtemplate = jinja2.Environment(loader=jinja_loader).from_string(tml) 55 | html_page = jtemplate.render(content='', site=structured_notion) 56 | f.write(html_page) 57 | 58 | def str_to_dt(structured_notion: dict): 59 | for page_id, page in structured_notion["pages"].items(): 60 | for field in ['date', 'date_end', 'last_edited_time']: 61 | if field in page.keys(): 62 | structured_notion["pages"][page_id][field] = dt_parser.isoparse(page[field]) 63 | 64 | def generate_page(page_id: str, structured_notion: dict, config: dict): 65 | page = structured_notion["pages"][page_id] 66 | page_url = page["url"] 67 | md_filename = page["title"] + '.md' 68 | 69 | if config["build_locally"]: 70 | folder = urljoin(page_url, '.') 71 | local_file_location = str(Path(folder).relative_to(Path(config["output_dir"]).resolve())) 72 | html_filename = Path(page_url).name 73 | else: 74 | local_file_location = page_url.lstrip(config["site_url"]) 75 | html_filename = 'index.html' 76 | 77 | logging.debug(f"🤖 MD {Path(local_file_location) / md_filename}; HTML {Path(local_file_location) / html_filename}") 78 | 79 | (config["output_dir"] / Path(local_file_location)).mkdir(parents=True, exist_ok=True) 80 | with open((config["output_dir"] / Path(local_file_location) / md_filename).resolve(), 'w+', encoding='utf-8') as f: 81 | metadata = ("---\n" 82 | f"title: {page['title']}\n" 83 | f"cover: {page['cover']}\n" 84 | f"icon: {page['icon']}\n" 85 | f"emoji: {page['emoji']}\n") 86 | if "properties_md" in page.keys(): 87 | for p_title, p_md in page["properties_md"].items(): 88 | metadata += f"{p_title}: {p_md}\n" 89 | metadata += f"---\n\n" 90 | ### Complex part here 91 | md_content = page['md_content'] 92 | md_content = metadata + md_content 93 | 94 | f.write(md_content) 95 | 96 | html_content = markdown.markdown(md_content, extensions=["meta", 97 | "tables", 98 | "mdx_truly_sane_lists", 99 | "markdown_captions", 100 | "pymdownx.tilde", 101 | "pymdownx.tasklist", 102 | "pymdownx.superfences"], 103 | extension_configs={ 104 | 'mdx_truly_sane_lists': { 105 | 'nested_indent': 4, 106 | 'truly_sane': True, 107 | }, 108 | "pymdownx.tasklist":{ 109 | "clickable_checkbox": True, 110 | } 111 | }) 112 | 113 | tml = (Path(config["templates_dir"] ) / 'page.html').read_text() 114 | with open((config["output_dir"] / Path(local_file_location) / html_filename).resolve(), 'w+', encoding='utf-8')as f: 115 | # Specify template folder 116 | jinja_loader = jinja2.FileSystemLoader(config["templates_dir"]) 117 | jtemplate = jinja2.Environment(loader=jinja_loader).from_string(tml) 118 | html_page = jtemplate.render(content=html_content, page=page, site=structured_notion) 119 | f.write(html_page) 120 | 121 | def generate_pages(structured_notion: dict, config: dict): 122 | for page_id, page in structured_notion["pages"].items(): 123 | generate_page(page_id, structured_notion, config) 124 | 125 | def generate_search_index(structured_notion: dict, config: dict): 126 | """Generates search index file if building for server""" 127 | if not config["build_locally"] and structured_notion["search_index"]: 128 | search_index_path = Path(config["output_dir"]) / "search_index.json" 129 | with open(search_index_path, 'w', encoding='utf-8') as f: 130 | json.dump(structured_notion["search_index"], f, ensure_ascii=False) 131 | # Update the search_index to just contain the path 132 | structured_notion["search_index"] = "search_index.json" 133 | 134 | def generate_site(structured_notion: dict, config: dict): 135 | verify_templates(config) 136 | logging.debug("🤖 SASS and templates are verified.") 137 | 138 | generate_css(config) 139 | logging.debug("🤖 SASS translated to CSS folder.") 140 | 141 | generate_search_index(structured_notion, config) 142 | logging.debug("🤖 Generated search index file.") 143 | 144 | if (Path(config["output_dir"]) / "css" / "fonts").exists(): 145 | shutil.rmtree(Path(config["output_dir"]) / "css" / "fonts") 146 | shutil.copytree(Path(config["sass_dir"]) / "fonts", Path(config["output_dir"]) / "css" / "fonts") 147 | logging.debug("🤖 Copied fonts.") 148 | 149 | str_to_dt(structured_notion) 150 | logging.debug("🤖 Changed string in dates to datetime objects.") 151 | 152 | generate_archive(structured_notion, config) 153 | logging.info("🤖 Archive page generated.") 154 | 155 | generate_404(structured_notion, config) 156 | logging.info("🤖 404.html page generated.") 157 | 158 | generate_pages(structured_notion, config) 159 | logging.info("🤖 All html and md pages generated.") -------------------------------------------------------------------------------- /notion4ever/structuring.py: -------------------------------------------------------------------------------- 1 | import dateutil.parser as dt_parser 2 | import logging 3 | from urllib.parse import urljoin 4 | from urllib.parse import urlparse 5 | from urllib.parse import unquote 6 | from urllib.error import HTTPError 7 | from pathlib import Path 8 | from notion4ever import markdown_parser 9 | from urllib import request 10 | from itertools import groupby 11 | import re 12 | import html 13 | 14 | def strip_html_tags(text): 15 | """Remove HTML tags and normalize whitespace while preserving Unicode characters""" 16 | if not text: 17 | return "" 18 | 19 | # Use a regular expression to remove HTML tags 20 | clean = re.compile('<.*?>') 21 | # Remove HTML tags 22 | text = re.sub(clean, ' ', text) 23 | # Convert HTML entities 24 | text = html.unescape(text) 25 | # Normalize whitespace 26 | text = ' '.join(text.split()) 27 | return text 28 | 29 | def clean_url_string(string): 30 | replacements = ["$", "\\", ":", " "] 31 | for char in replacements: 32 | string = string.replace(char, "_") 33 | return string 34 | 35 | def recursive_search(key, dictionary): 36 | """This function does recursive search for the 'key' in the 'dictionary' 37 | 38 | Args: 39 | key (str): key for searching. 40 | dictionary: dictionary with nested dictionaries and lists. 41 | 42 | Returns: 43 | value of a target key. 44 | """ 45 | if hasattr(dictionary,"items"): 46 | for k, v in dictionary.items(): 47 | if k == key: 48 | yield v 49 | if isinstance(v, dict): 50 | for result in recursive_search(key, v): 51 | yield result 52 | elif isinstance(v, list): 53 | for d in v: 54 | for result in recursive_search(key, d): 55 | yield result 56 | 57 | def parse_headers(raw_notion: dict) -> dict: 58 | """Parses raw notion dict and returns dict with keys equal to each page_id, 59 | with values of dicts with the following fields: 60 | "type" (str): "page", "database" or "db_entry", 61 | "files" (list): list of urls for nested, 62 | "title" (str): title of corresponding page, 63 | "last_edited_time" (str): last edited time in iso format, 64 | "date" (str): date start in iso format, 65 | "date_end" (str): date end in iso format, 66 | "parent" (str): id of parent page, 67 | "children" (list): list of ids of children page, 68 | "cover" (str): cover url, 69 | "emoji" (str): emoji symbol, 70 | "icon" (str): icon url. 71 | Returns: Example 72 | { 73 | "12e3d165-9a44-4678-b4e2-b6a989a3c625": 74 | { 75 | "files": [ 76 | "https://merkulov.top/ineq_constr_10.svg", 77 | "https://merkulov.top/dm_on_fire.jpg" 78 | ], 79 | "type": "page", 80 | "title": "Danya Merkulov", 81 | "last_edited_time": "2022-01-25T22:35:00.000Z", 82 | "parent": null, 83 | "children": [ 84 | "89ae66ca-44a5-4819-9797-5bf321572676", 85 | "a2964f56-f0d3-43be-abf6-46cf508dff04", 86 | "c131810a-ca9b-41af-a5cb-8aa8dfd86971", 87 | "d4a7fb6a-fcb7-45f8-8a35-09c6c4f5c408" 88 | ], 89 | "cover": "https://merkulov.top/ineq_constr_10.svg", 90 | "icon": "https://merkulov.top/dm_on_fire.jpg", 91 | "emoji": null, 92 | }, 93 | "89ae66ca-44a5-4819-9797-5bf321572676": 94 | { 95 | "files": [], 96 | "type": "database", 97 | "title": "Papers", 98 | "last_edited_time": "2022-01-26T20:10:00.000Z", 99 | "parent": "12e3d165-9a44-4678-b4e2-b6a989a3c625", 100 | "children": [ 101 | "88f6b858-14b3-4d51-baad-5c0cf7da52d0", 102 | "0649ea83-e30e-4bd5-8601-4fdcb2369378", 103 | "d6d08b27-3754-4f8d-97d0-8d0a8bac9ac3" 104 | ], 105 | "cover": null, 106 | "emoji": "📜", 107 | "icon": null, 108 | }, 109 | "88f6b858-14b3-4d51-baad-5c0cf7da52d0": 110 | { 111 | "files": [ 112 | "https://merkulov.top/Papers/Empirical_Study_of_Extreme_Overfitting_Points_of_Neural_Networks/ResNet_CIFAR10.svg" 113 | ], 114 | "type": "db_entry", 115 | "title": "Empirical Study of Extreme Overfitting Points of Neural Networks", 116 | "last_edited_time": "2022-01-26T20:12:00.000Z", 117 | "parent": "89ae66ca-44a5-4819-9797-5bf321572676", 118 | "children": [], 119 | "cover": "https://merkulov.top/Papers/Empirical_Study_of_Extreme_Overfitting_Points_of_Neural_Networks/ResNet_CIFAR10.svg", 120 | "emoji": "🧠", 121 | "icon": null, 122 | } 123 | } 124 | """ 125 | notion_pages = {} 126 | for page_id, page in raw_notion.items(): 127 | notion_pages[page_id] = {} 128 | notion_pages[page_id]["files"] = [] 129 | 130 | # Page type. Could be "page", "database" or "db_entry" 131 | notion_pages[page_id]["type"] = page["object"] 132 | if page["parent"]["type"] in ["database_id"]: 133 | notion_pages[page_id]["type"] = "db_entry" 134 | 135 | # Title 136 | if notion_pages[page_id]["type"] == "page": 137 | if len(page["properties"]["title"]["title"]) > 0: 138 | notion_pages[page_id]["title"] = \ 139 | page["properties"]["title"]["title"][0]["plain_text"] 140 | else: 141 | notion_pages[page_id]["title"] = None 142 | elif notion_pages[page_id]["type"] == "database": 143 | if len(page["title"]) > 0: 144 | notion_pages[page_id]["title"] = \ 145 | page["title"][0]["text"]["content"] 146 | else: 147 | notion_pages[page_id]["title"] = None 148 | elif notion_pages[page_id]["type"] == "db_entry": 149 | res = recursive_search("title", page["properties"]) 150 | res = list(res)[0] 151 | if len(res) > 0: 152 | # notion_pages[page_id]["title"] = res[0]["plain_text"] 153 | notion_pages[page_id]["title"] = \ 154 | markdown_parser.richtext_convertor(res, title_mode=True) 155 | else: 156 | notion_pages[page_id]["title"] = None 157 | logging.warning(f"🤖Empty database entries could break the site building 😫.") 158 | 159 | 160 | # Time 161 | notion_pages[page_id]["last_edited_time"] = \ 162 | page["last_edited_time"] 163 | if notion_pages[page_id]["type"] == "db_entry": 164 | if "Date" in page["properties"].keys(): 165 | if page["properties"]["Date"]["date"] is not None: 166 | notion_pages[page_id]["date"] = \ 167 | page["properties"]["Date"]["date"]["start"] 168 | if page["properties"]["Date"]["date"]["end"] is not None: 169 | notion_pages[page_id]["date_end"] = \ 170 | page["properties"]["Date"]["date"]["end"] 171 | 172 | # Parent 173 | if "workspace" in page["parent"].keys(): 174 | parent_id = None 175 | notion_pages[page_id]["parent"] = parent_id 176 | elif notion_pages[page_id]["type"] in ["page", "database"]: 177 | parent_id = page["parent"]["page_id"] 178 | notion_pages[page_id]["parent"] = parent_id 179 | elif notion_pages[page_id]["type"] == "db_entry": 180 | parent_id = page["parent"]["database_id"] 181 | notion_pages[page_id]["parent"] = parent_id 182 | 183 | # Children 184 | if "children" not in notion_pages[page_id].keys(): 185 | notion_pages[page_id]["children"] = [] 186 | 187 | if parent_id is not None: 188 | notion_pages[parent_id]["children"].append(page_id) 189 | 190 | # Cover 191 | if page["cover"] is not None: 192 | cover = list(recursive_search("url", page["cover"]))[0] 193 | notion_pages[page_id]["cover"] = cover 194 | notion_pages[page_id]["files"].append(cover) 195 | 196 | else: 197 | notion_pages[page_id]["cover"] = None 198 | 199 | # Icon 200 | if type(page["icon"]) is dict: 201 | if "emoji" in page["icon"].keys(): 202 | notion_pages[page_id]["emoji"] = \ 203 | page["icon"]["emoji"] 204 | notion_pages[page_id]["icon"] = None 205 | else: 206 | icon = page["icon"]["file"]["url"] 207 | notion_pages[page_id]["icon"] = icon 208 | notion_pages[page_id]["files"].append(icon) 209 | notion_pages[page_id]["emoji"] = None 210 | else: 211 | notion_pages[page_id]["icon"] = None 212 | notion_pages[page_id]["emoji"] = None 213 | 214 | return notion_pages 215 | 216 | def find_lists_in_dbs(structured_notion: dict): 217 | """Determines the rule for considering database as list rather than gallery. 218 | 219 | Each database by default is treated as gallery, but if any child page does 220 | not have a cover, we will treat it as list. 221 | """ 222 | for page_id, page in structured_notion["pages"].items(): 223 | if page["type"] == 'database': 224 | for child_id in page["children"]: 225 | if structured_notion["pages"][child_id]["cover"] is None: 226 | structured_notion["pages"][page_id]["db_list"] = True 227 | break 228 | 229 | def parse_family_line(page_id: str, family_line: list, structured_notion: dict): 230 | """Parses the whole parental line for page with 'page_id'""" 231 | if structured_notion['pages'][page_id]["parent"] is not None: 232 | par_id = structured_notion["pages"][page_id]["parent"] 233 | family_line.insert(0, par_id) 234 | family_line = parse_family_line(par_id, family_line, structured_notion) 235 | 236 | return family_line 237 | 238 | def parse_family_lines(structured_notion: dict): 239 | for page_id, page in structured_notion["pages"].items(): 240 | page["family_line"] = parse_family_line(page_id, [], structured_notion) 241 | 242 | def generate_urls(page_id:str, structured_notion: dict, config: dict): 243 | """Generates url for each page nested in page with 'page_id'""" 244 | if page_id == structured_notion["root_page_id"]: 245 | if config["build_locally"]: 246 | f_name = clean_url_string(structured_notion["pages"][page_id]["title"]) 247 | else: 248 | f_name = 'index' 249 | 250 | f_name += '.html' 251 | 252 | if config["build_locally"]: 253 | f_url = str(Path(config["output_dir"]).resolve() / f_name) 254 | else: 255 | f_url = config["site_url"] 256 | structured_notion["pages"][page_id]["url"] = f_url 257 | structured_notion["urls"].append(f_url) 258 | else: 259 | if config["build_locally"]: 260 | parent_id = structured_notion["pages"][page_id]["parent"] 261 | parent_url = structured_notion["pages"][parent_id]["url"] 262 | f_name = clean_url_string(structured_notion["pages"][page_id]["title"]) 263 | 264 | f_url = Path(parent_url).parent.resolve() 265 | f_url = f_url / f_name / f_name 266 | f_url = str(f_url.resolve()) + '.html' 267 | while f_url in structured_notion["urls"]: 268 | f_name += "_" 269 | f_url = Path(parent_url).parent 270 | f_url = f_url / f_name / f_name 271 | f_url = str(f_url.resolve()) + '.html' 272 | structured_notion["pages"][page_id]["url"] = f_url 273 | structured_notion["urls"].append(f_url) 274 | else: 275 | parent_id = structured_notion["pages"][page_id]["parent"] 276 | parent_url = structured_notion["pages"][parent_id]["url"] 277 | parent_url += '/' 278 | f_name = clean_url_string(structured_notion["pages"][page_id]["title"]) 279 | f_url = urljoin(parent_url, f_name) 280 | while f_url in structured_notion["urls"]: 281 | f_name += "_" 282 | f_url = urljoin(parent_url, f_name) 283 | structured_notion["pages"][page_id]["url"] = f_url 284 | structured_notion["urls"].append(f_url) 285 | 286 | for child_id in structured_notion["pages"][page_id]["children"]: 287 | generate_urls(child_id, structured_notion, config) 288 | 289 | # ====================== 290 | # Properties handlers 291 | # ====================== 292 | 293 | def p_rich_text(property:dict)->str: 294 | md_property = markdown_parser.richtext_convertor(property['rich_text']) 295 | return md_property 296 | 297 | def p_number(property:dict)->str: 298 | md_property = '' 299 | logging.debug('🤖 Only number in the number block is supported') 300 | if property['number'] is not None: 301 | md_property = str(property['number']) 302 | return md_property 303 | 304 | def p_select(property:dict)->str: 305 | md_property = '' 306 | if property['select'] is not None: 307 | md_property += str(property['select']['name']) 308 | return md_property 309 | 310 | def p_multi_select(property:dict)->str: 311 | md_property = '' 312 | for tag in property['multi_select']: 313 | md_property += tag['name'] + '; ' 314 | return md_property.rstrip('; ') 315 | 316 | def p_date(property:dict)->str: 317 | md_property = '' 318 | if property['date'] is not None: 319 | dt = property['date']['start'] 320 | md_property += dt_parser.isoparse(dt).strftime("%d %b, %Y") 321 | if property['date']['end'] is not None: 322 | dt = property['date']['end'] 323 | md_property += ' - ' + dt_parser.isoparse(dt).strftime("%d %b, %Y") 324 | return md_property 325 | 326 | def p_people(property:dict)->str: 327 | md_property = '' 328 | for tag in property['people']: 329 | md_property += tag['name'] + '; ' 330 | return md_property.rstrip('; ') 331 | 332 | def p_files(property:dict)->str: 333 | md_property = '' 334 | for file in property['files']: 335 | md_property += f"[📎]({file['file']['url']})" + "; " 336 | return md_property.rstrip('; ') 337 | 338 | def p_checkbox(property:dict)->str: 339 | return f"- {'[x]' if property['checkbox'] else '[ ]'}" 340 | 341 | def p_url(property:dict)->str: 342 | md_property = '' 343 | if property['url'] is not None: 344 | md_property = f"[🕸]({property['url']})" 345 | return md_property 346 | 347 | def p_email(property:dict)->str: 348 | md_property = '' 349 | if property['email'] is not None: 350 | md_property = property['email'] 351 | return md_property 352 | 353 | def p_phone_number(property:dict)->str: 354 | md_property = '' 355 | if property['phone_number'] is not None: 356 | md_property = property['phone_number'] 357 | return md_property 358 | 359 | # def p_formula(property:dict)->str: 360 | # md_property = '' 361 | # return md_property 362 | 363 | # def p_relation(property:dict)->str: 364 | # md_property = '' 365 | # return md_property 366 | 367 | # def p_rollup(property:dict)->str: 368 | # md_property = '' 369 | # return md_property 370 | 371 | def p_created_time(property:dict)->str: 372 | md_property = '' 373 | if property['created_time'] is not None: 374 | dt = property['created_time'] 375 | md_property += dt_parser.isoparse(dt).strftime("%d %b, %Y") 376 | return md_property 377 | 378 | # def p_created_by(property:dict)->str: 379 | # md_property = '' 380 | # return md_property 381 | 382 | def p_last_edited_time(property:dict)->str: 383 | md_property = '' 384 | if property['last_edited_time'] is not None: 385 | dt = property['last_edited_time'] 386 | md_property += dt_parser.isoparse(dt).strftime("%d %b, %Y") 387 | return md_property 388 | 389 | # def p_last_edited_by(property:dict)->str: 390 | # md_property = '' 391 | # return md_property 392 | 393 | 394 | def parse_db_entry_properties(raw_notion: dict, structured_notion:dict): 395 | properties_map = { 396 | "rich_text": p_rich_text, 397 | "number": p_number, 398 | "select": p_select, 399 | "multi_select": p_multi_select, 400 | "date": p_date, 401 | "people": p_people, 402 | "files": p_files, 403 | "checkbox": p_checkbox, 404 | "url": p_url, 405 | "email": p_email, 406 | "phone_number": p_phone_number, 407 | # "formula": p_formula, 408 | # "relation": p_relation, 409 | # "rollup": p_rollup, 410 | "created_time": p_created_time, 411 | # "created_by": p_created_by, 412 | "last_edited_time": p_last_edited_time, 413 | # "last_edited_by": p_last_edited_by 414 | } 415 | for page_id, page in structured_notion["pages"].items(): 416 | if page["type"] == "db_entry": 417 | structured_notion["pages"][page_id]['properties'] = \ 418 | raw_notion[page_id]['properties'] 419 | structured_notion["pages"][page_id]['properties_md'] = {} 420 | for property_title, property in structured_notion["pages"][page_id]['properties'].items(): 421 | if property['type'] == "title": 422 | continue # We already have the title 423 | structured_notion["pages"][page_id]['properties_md'][property_title] = '' 424 | if property['type'] in properties_map: 425 | if property['type'] == "files": 426 | for file in property['files']: 427 | structured_notion["pages"][page_id]["files"].append(file['file']['url']) 428 | structured_notion["pages"][page_id]['properties_md'][property_title] = \ 429 | properties_map[property['type']](property) 430 | else: 431 | if property['type'] != "title": # We already have the title 432 | logging.debug(f"{property['type']} is not supported yet") 433 | 434 | def download_and_replace_paths(structured_notion:dict, config: dict): 435 | for page_id, page in structured_notion["pages"].items(): 436 | for i_file, file_url in enumerate(page["files"]): 437 | # Download file 438 | clean_url = urljoin(file_url, urlparse(file_url).path) 439 | 440 | if config["build_locally"]: 441 | folder = urljoin(page["url"], '.') 442 | filename = unquote(Path(clean_url).name) 443 | new_url = urljoin(folder, filename) 444 | local_file_location = str(Path(new_url).relative_to(Path(config["output_dir"]).resolve())) 445 | else: 446 | filename = unquote(Path(clean_url).name) 447 | new_url = urljoin(page["url"] + '/', filename) 448 | 449 | local_file_location = new_url.replace(config["site_url"], '', 1) 450 | local_file_location = local_file_location.lstrip("/") 451 | 452 | (config["output_dir"] / Path(local_file_location).parent).mkdir(parents=True, exist_ok=True) 453 | full_local_name = (Path(config["output_dir"]).resolve() / local_file_location) 454 | if Path(full_local_name).exists(): 455 | logging.debug(f"🤖 {filename} already exists.") 456 | else: 457 | try: 458 | request.urlretrieve(file_url, full_local_name) 459 | logging.debug(f"🤖 Downloaded {filename}") 460 | except HTTPError: 461 | logging.warning(f"🤖Cannot download {filename} from link {file_url}.") 462 | except ValueError: 463 | continue 464 | 465 | # Replace url in structured_data 466 | structured_notion["pages"][page_id]["files"][i_file] = new_url 467 | 468 | # Replace url in markdown 469 | md_content = structured_notion["pages"][page_id]["md_content"] 470 | structured_notion["pages"][page_id]["md_content"] = md_content.replace(file_url, new_url) 471 | 472 | # Add short description for sites 473 | clean_content = strip_html_tags(md_content) 474 | structured_notion["pages"][page_id]["description"] = clean_content[:150] 475 | 476 | # Replace url in header 477 | for asset in ['icon', 'cover']: 478 | if page[asset] == file_url: 479 | structured_notion["pages"][page_id][asset] = new_url 480 | 481 | # Replace url in files property: 482 | if page["type"] == "db_entry": 483 | for prop_name, prop_value in page["properties_md"].items(): 484 | if file_url in prop_value: 485 | new_value = prop_value.replace(file_url, new_url) 486 | structured_notion["pages"][page_id]["properties_md"][prop_name] = new_value 487 | 488 | def sorting_db_entries(structured_notion: dict): 489 | for page_id, page in structured_notion["pages"].items(): 490 | if page["type"] == "database": 491 | if len(page["children"]) > 1: 492 | first_child_id = page["children"][0] 493 | if "date" in structured_notion["pages"][first_child_id]: 494 | structured_notion["pages"][page_id]["children"] =\ 495 | sorted(page['children'], key=lambda item: structured_notion["pages"][item]["date"]) 496 | 497 | 498 | def sorting_page_by_year(structured_notion: dict): 499 | structured_notion['sorted_pages'] = \ 500 | {k: dt_parser.isoparse(v['date']) for k,v in structured_notion['pages'].items() if 'date' in v.keys()} 501 | structured_notion['sorted_pages'] = \ 502 | {k: v for k, v in sorted(structured_notion['sorted_pages'].items(), key=lambda item: item[1], reverse=True)} 503 | # grouping by year 504 | structured_notion['sorted_id_by_year'] = {} 505 | for year, year_pages in groupby(structured_notion['sorted_pages'].items(), key=lambda item: item[1].year): 506 | structured_notion['sorted_id_by_year'][year] = [] 507 | for page in year_pages: 508 | structured_notion['sorted_id_by_year'][year].append(page[0]) 509 | del structured_notion['sorted_pages'] 510 | 511 | def create_search_index(structured_notion: dict): 512 | """Creates a search index for all pages""" 513 | search_index = [] 514 | 515 | for page_id, page in structured_notion["pages"].items(): 516 | if "md_content" in page: 517 | clean_content = strip_html_tags(page["md_content"]) 518 | # Debug log to check content 519 | logging.debug(f"🤖 Indexing content for {page['title']}: {clean_content[:200]}...") 520 | 521 | search_index.append({ 522 | "title": page["title"], 523 | "content": clean_content, 524 | "url": page["url"] 525 | }) 526 | 527 | structured_notion["search_index"] = search_index 528 | 529 | def structurize_notion_content(raw_notion: dict, config: dict) -> dict: 530 | structured_notion = {} 531 | structured_notion["pages"] = {} 532 | structured_notion["urls"] = [] 533 | structured_notion["root_page_id"] = list(raw_notion.keys())[0] 534 | structured_notion["pages"] = parse_headers(raw_notion) 535 | structured_notion["include_footer"] = config["include_footer"] 536 | structured_notion["include_search"] = config["include_search"] 537 | structured_notion["build_locally"] = config["build_locally"] 538 | find_lists_in_dbs(structured_notion) 539 | logging.debug(f"🤖 Structurized headers") 540 | 541 | parse_family_lines(structured_notion) 542 | logging.debug(f"🤖 Structurized family lines") 543 | 544 | generate_urls(structured_notion["root_page_id"], structured_notion, config) 545 | logging.debug(f"🤖 Generated urls") 546 | 547 | markdown_parser.parse_markdown(raw_notion, structured_notion) 548 | logging.debug(f"🤖 Parsed markdown content") 549 | 550 | parse_db_entry_properties(raw_notion, structured_notion) 551 | logging.debug(f"🤖 Parsed db_entries properties") 552 | 553 | if config["download_files"]: 554 | download_and_replace_paths(structured_notion, config) 555 | logging.debug(f"🤖 Downloaded files and replaced paths") 556 | 557 | sorting_db_entries(structured_notion) 558 | sorting_page_by_year(structured_notion) 559 | logging.debug(f"🤖 Sorted pages by date and grouped by year.") 560 | if config["include_search"]: 561 | create_search_index(structured_notion) 562 | logging.debug(f"🤖 Created search index.") 563 | else: 564 | structured_notion["search_index"] = [] 565 | 566 | return structured_notion -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==3.0.3 2 | libsass==0.21.0 3 | Markdown==3.3.6 4 | notion_client==0.8.0 5 | python_dateutil==2.8.2 6 | mdx_truly_sane_lists 7 | pymdown-extensions 8 | markdown-captions 9 | markdown-checklist 10 | Pygments 11 | --------------------------------------------------------------------------------