├── .all-contributorsrc ├── .cz-config.js ├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .release-it.json ├── .vscode └── extensions.json ├── CHANGELOG.md ├── README.md ├── jest.config.js ├── jest.preset.js ├── libs ├── blocks-html-parser │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── blocks-html-parser.spec.ts │ │ │ ├── blocks-html-parser.ts │ │ │ └── interfaces.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── blocks-markdown-parser │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── blocks-markdown-parser.spec.ts │ │ │ ├── blocks-markdown-parser.ts │ │ │ └── external-video.util.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── scully-plugin-notion │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── plugin-options.ts │ │ │ ├── plugin.ts │ │ │ ├── process-page-properties.ts │ │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── v4-types │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── index.ts │ └── lib │ │ └── types.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── migrations.json ├── nx.json ├── package.json ├── pnpm-lock.yaml ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.base.json └── workspace.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "notion-stuff", 3 | "projectOwner": "nartc", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 75, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "nartc", 15 | "name": "Chau Tran", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/25516557?v=4", 17 | "profile": "https://nartc.me/", 18 | "contributions": [ 19 | "code", 20 | "doc" 21 | ] 22 | }, 23 | { 24 | "login": "barbados-clemens", 25 | "name": "Caleb Ukle", 26 | "avatar_url": "https://avatars.githubusercontent.com/u/23272162?v=4", 27 | "profile": "https://github.com/barbados-clemens", 28 | "contributions": [ 29 | "code" 30 | ] 31 | }, 32 | { 33 | "login": "MarcARoberge", 34 | "name": "MarcARoberge", 35 | "avatar_url": "https://avatars.githubusercontent.com/u/12281920?v=4", 36 | "profile": "https://github.com/MarcARoberge", 37 | "contributions": [ 38 | "code" 39 | ] 40 | }, 41 | { 42 | "login": "Panthaaaa", 43 | "name": "Panthaaaa", 44 | "avatar_url": "https://avatars.githubusercontent.com/u/1465893?v=4", 45 | "profile": "https://github.com/Panthaaaa", 46 | "contributions": [ 47 | "code" 48 | ] 49 | } 50 | ], 51 | "contributorsPerLine": 7 52 | } 53 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'feat', name: 'feat: A new feature' }, 4 | { value: 'fix', name: 'fix: A bug fix' }, 5 | { value: 'docs', name: 'docs: Documentation only changes' }, 6 | { 7 | value: 'cleanup', 8 | name: 'cleanup: A code change that neither fixes a bug nor adds a feature', 9 | }, 10 | { 11 | value: 'chore', 12 | name: "chore: Other changes that don't modify src or test files", 13 | }, 14 | ], 15 | 16 | scopes: [ 17 | { name: 'html', description: 'HTML parser' }, 18 | { name: 'markdown', description: 'Markdown parser' }, 19 | { name: 'scully', description: 'anything Scully plugin specific' }, 20 | { name: 'v4-types', description: 'anything related to Notion 0.4 Typings' }, 21 | ], 22 | // override the messages, defaults are as follows 23 | messages: { 24 | type: "Select the type of change that you're committing:", 25 | scope: '\nDenote the SCOPE of this change (optional):', 26 | // used if allowCustomScopes is true 27 | customScope: 'Denote the SCOPE of this change:', 28 | subject: 29 | 'Write a SHORT, IMPERATIVE (lowercase) description of the change:\n', 30 | body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', 31 | breaking: 'List any BREAKING CHANGES (optional):\n', 32 | footer: 33 | 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n', 34 | confirmCommit: 'Are you sure you want to proceed with the commit above?', 35 | }, 36 | 37 | allowCustomScopes: true, 38 | allowBreakingChanges: ['feat', 'fix'], 39 | // skip any questions you want 40 | skipQuestions: ['ticketNumber'], 41 | 42 | // limit subject length 43 | subjectLimit: 100, 44 | }; 45 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN=your github token 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | .env 42 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@release-it/conventional-changelog": { 4 | "preset": { 5 | "name": "conventionalcommits", 6 | "types": [ 7 | { 8 | "type": "feat", 9 | "section": "Features" 10 | }, 11 | { 12 | "type": "fix", 13 | "section": "Bug Fixes" 14 | }, 15 | { 16 | "type": "cleanup", 17 | "section": "Cleanup" 18 | }, 19 | { 20 | "type": "docs", 21 | "section": "Documentations" 22 | } 23 | ] 24 | }, 25 | "infile": "CHANGELOG.md" 26 | }, 27 | "@release-it/bumper": { 28 | "out": [ 29 | "libs/**/package*.json" 30 | ] 31 | } 32 | }, 33 | "git": { 34 | "commitMessage": "chore: release ${version}" 35 | }, 36 | "npm": { 37 | "publish": false 38 | }, 39 | "github": { 40 | "release": true, 41 | "releaseName": "Release ${version}" 42 | }, 43 | "hooks": { 44 | "after:bump": "pnpm run build" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [6.0.0](https://github.com/nartc/notion-stuff/compare/5.1.0...6.0.0) (2022-04-01) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * **repo:** @notionhq/client has been updated to 1.0.0 9 | 10 | ### Features 11 | 12 | * **repo:** update notion client to 1.0.0 ([497a88f](https://github.com/nartc/notion-stuff/commit/497a88f0f5fd1b3e30727eecc56207c6056e2581)) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **html:** only set mdToHtmlOptions.highlight if not already passed in ([0349311](https://github.com/nartc/notion-stuff/commit/034931181eb1ef70a0cf6dd7a8918510f3bc2627)), closes [#11](https://github.com/nartc/notion-stuff/issues/11) 18 | * **markdown:** adjust markdown parser type for 1.0.0 ([3163924](https://github.com/nartc/notion-stuff/commit/31639247f4afbea44bf4868ccbd01f5ad7b7f26d)) 19 | * **scully:** adjust scully plugin type for 1.0.0 ([cb462da](https://github.com/nartc/notion-stuff/commit/cb462dae75477c4009a8355cd50c0892ae5b8213)) 20 | * **v4-types:** add PostResult ([f15c938](https://github.com/nartc/notion-stuff/commit/f15c9381d6b39ea5a8e7050b6e531e5c1a7945b6)) 21 | 22 | 23 | ### Documentations 24 | 25 | * add @Panthaaaa as a contributor ([4edea81](https://github.com/nartc/notion-stuff/commit/4edea81349b9dfc5a92a3f5207f194153613cf2a))## [5.1.0](https://github.com/nartc/notion-stuff/compare/5.0.0...5.1.0) (2021-12-26) 26 | 27 | 28 | ### Features 29 | 30 | * **html:** render nested markdown in html blocks ([3e0f241](https://github.com/nartc/notion-stuff/commit/3e0f24115701097fb08f67360663ac168decc713)) 31 | 32 | ## [5.0.0](https://github.com/nartc/notion-stuff/compare/4.0.0...5.0.0) (2021-12-25) 33 | 34 | 35 | ### ⚠ BREAKING CHANGES 36 | 37 | * **v4-types:** - 0.4.11 adds aliases that do not conform with other existing aliases in QueryResult type leading to 38 | breaking changes 39 | 40 | ### Features 41 | 42 | * **v4-types:** update 0.4.11 ([caf4b59](https://github.com/nartc/notion-stuff/commit/caf4b5978e006f4103c58dc6f91e431050f65de4)) 43 | 44 | ## [4.0.0](https://github.com/nartc/notion-stuff/compare/3.2.0...4.0.0) (2021-11-11) 45 | 46 | ### ⚠ BREAKING CHANGES 47 | 48 | * **html:** - Marked 4 no longer provides default import 49 | * **v4-types:** - Update notionhq/client to 0.4.6 50 | 51 | ### Features 52 | 53 | * **html:** adapt to Marked 4 ([ad8f1fe](https://github.com/nartc/notion-stuff/commit/ad8f1fe09bca72cadf1dccf7fc344e0bef0b1c57)) 54 | * **markdown:** update notionhq/client 0.4.6 ([66c8f64](https://github.com/nartc/notion-stuff/commit/66c8f645fa5c12205b2ca35fd1ff20bf42248ea1)) 55 | * **scully:** update notionhq/client 0.4.6 ([cb7fc51](https://github.com/nartc/notion-stuff/commit/cb7fc51266e6aee96ec5812b055c7d89fbff2bf0)) 56 | * **v4-types:** add Column and ColumnList ([009ebaa](https://github.com/nartc/notion-stuff/commit/009ebaab605ec49f9f22560542a48f57bac8cf84)), closes [#8](https://github.com/nartc/notion-stuff/issues/8) 57 | 58 | ## [3.2.0](https://github.com/nartc/notion-stuff/compare/3.1.3...3.2.0) (2021-10-28) 59 | 60 | 61 | ### Features 62 | 63 | * **markdown:** add support for empty paragraph with non-breaking space ([4531ae0](https://github.com/nartc/notion-stuff/commit/4531ae0877fe70761cb32d277deff406bf9fddfe)) 64 | 65 | 66 | ### Cleanup 67 | 68 | * **markdown:** reformat ([543597f](https://github.com/nartc/notion-stuff/commit/543597f82e74953305deb36c1b2e40c9304a0978)) 69 | 70 | 71 | ### Documentations 72 | 73 | * add @MarcARoberge as a contributor ([0a62355](https://github.com/nartc/notion-stuff/commit/0a62355d538d67c9176a341059ccf148beeafb4d)) 74 | 75 | ### [3.1.3](https://github.com/nartc/notion-stuff/compare/3.1.2...3.1.3) (2021-10-21) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * **html:** handle undefined language param ([#2](https://github.com/nartc/notion-stuff/issues/2)) ([a77409a](https://github.com/nartc/notion-stuff/commit/a77409a2b4f1393d25d0d7982059d687269cd0d1)) 81 | 82 | 83 | ### Documentations 84 | 85 | * add [@barbados-clemens](https://github.com/barbados-clemens) as a contributor ([4b81cba](https://github.com/nartc/notion-stuff/commit/4b81cba2ad92f68d5525a1f9f631f074852f5301)) 86 | 87 | ### [3.1.2](https://github.com/nartc/notion-stuff/compare/3.1.1...3.1.2) (2021-10-21) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **scully:** null check cover and icon before processing ([c034ef6](https://github.com/nartc/notion-stuff/commit/c034ef686992f5152d352a4e734d828a4ddba1ff)) 93 | 94 | ### [3.1.1](https://github.com/nartc/notion-stuff/compare/3.1.0...3.1.1) (2021-10-21) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * **scully:** ensure to get the plugin options after plugin has been registered ([4e9571e](https://github.com/nartc/notion-stuff/commit/4e9571e5e88587e677231cd190fa0d74621fea76)) 100 | 101 | ## [3.1.0](https://github.com/nartc/notion-stuff/compare/3.0.0...3.1.0) (2021-10-20) 102 | 103 | 104 | ### Features 105 | 106 | * bump notionClient dep ([cca14f4](https://github.com/nartc/notion-stuff/commit/cca14f4556128bc997b24dbad0e30515ccb4c4e0)) 107 | 108 | ## [3.0.0](https://github.com/nartc/notion-stuff/compare/2.4.0...3.0.0) (2021-10-20) 109 | 110 | 111 | ### ⚠ BREAKING CHANGES 112 | 113 | * **scully:** - `NotionPluginOptions` -> `NotionDomPluginOptions` 114 | 115 | ### Features 116 | 117 | * **html:** expose MarkdownParser ([600ed9c](https://github.com/nartc/notion-stuff/commit/600ed9cf97e3b0cb8133f9dbe273ab9162797925)) 118 | * **markdown:** support Divider now ([37116e7](https://github.com/nartc/notion-stuff/commit/37116e74cb9b23c881403d746b90788e8363aefd)) 119 | * **scully:** support icon, cover, and more customization for Page Properties ([2b9e1ac](https://github.com/nartc/notion-stuff/commit/2b9e1ac27c0e3eb7d6da60ddaca372e9b18358a7)) 120 | 121 | 122 | ### Bug Fixes 123 | 124 | * **v4-types:** widen file type for cover ([26868bc](https://github.com/nartc/notion-stuff/commit/26868bc2fd408b4815643283942ee592dc575008)) 125 | 126 | 127 | ### Documentations 128 | 129 | * **scully:** add info about database column naming conversion/convention ([#1](https://github.com/nartc/notion-stuff/issues/1)) ([c52b178](https://github.com/nartc/notion-stuff/commit/c52b178f07275f89b6eb8e08406d20e8de2184fa)) 130 | 131 | ## [2.4.0](https://github.com/nartc/notion-stuff/compare/2.3.2...2.4.0) (2021-10-18) 132 | 133 | 134 | ### Features 135 | 136 | * **markdown:** add youtube video processor to process youtube url ([6e4f99c](https://github.com/nartc/notion-stuff/commit/6e4f99cd66ce2a2716f2b3b7e2fd7645be160b9b)) 137 | 138 | 139 | ### Bug Fixes 140 | 141 | * **markdown:** change paragraph parser to have EOL before and after ([2ab15c8](https://github.com/nartc/notion-stuff/commit/2ab15c86f64fb197bcbf7da817537274d2fe215e)) 142 | 143 | ### [2.3.2](https://github.com/nartc/notion-stuff/compare/2.3.1...2.3.2) (2021-10-16) 144 | 145 | 146 | ### Bug Fixes 147 | 148 | * **html:** coerce plain text language to none ([ca9057a](https://github.com/nartc/notion-stuff/commit/ca9057a243501636682c2e58aaf153023b0f51fa)) 149 | 150 | ### [2.3.1](https://github.com/nartc/notion-stuff/compare/2.3.0...2.3.1) (2021-10-16) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * **markdown:** parse callout with div ([50d0537](https://github.com/nartc/notion-stuff/commit/50d053745937d70f03361783fbcbfe2200136017)) 156 | 157 | ## [2.3.0](https://github.com/nartc/notion-stuff/compare/2.2.2...2.3.0) (2021-10-14) 158 | 159 | 160 | ### Features 161 | 162 | * **scully:** instantiate a global notion instead ([686dec7](https://github.com/nartc/notion-stuff/commit/686dec7914a6d9b8b36a69fed6d2430ae20a551b)) 163 | 164 | ### [2.2.2](https://github.com/nartc/notion-stuff/compare/2.2.1...2.2.2) (2021-10-14) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * **markdown:** add EOL_MD to before blockquote ([fcc90cf](https://github.com/nartc/notion-stuff/commit/fcc90cf751676450de0f7dedf1db4e3ba059d80a)) 170 | 171 | ### [2.2.1](https://github.com/nartc/notion-stuff/compare/2.2.0...2.2.1) (2021-10-13) 172 | 173 | 174 | ### Documentations 175 | 176 | * update README for v4-types ([69d7898](https://github.com/nartc/notion-stuff/commit/69d7898114654c8776354e8ffa1cab51ddd14158)) 177 | 178 | ## [2.2.0](https://github.com/nartc/notion-stuff/compare/2.1.1...2.2.0) (2021-10-13) 179 | 180 | 181 | ### Features 182 | 183 | * **html:** use v4-types and update to notion 0.4 ([82bf4d7](https://github.com/nartc/notion-stuff/commit/82bf4d70cc2eee10d6eeeeefd0743c119f7bfd85)) 184 | * **markdown:** use v4-types and update to notion 0.4 ([1431612](https://github.com/nartc/notion-stuff/commit/143161244c1543a119d517baa7646dc0bd9c5bc4)) 185 | * **scully:** use v4-types and update to notion 0.4 ([40c3e16](https://github.com/nartc/notion-stuff/commit/40c3e1603909b3130abf6a7e1a0f97d558af5080)) 186 | * **v4-types:** add v4-types ([9806b41](https://github.com/nartc/notion-stuff/commit/9806b41b0168764d7ff2586749e5164322313683)) 187 | 188 | ### [2.1.1](https://github.com/nartc/notion-stuff/compare/2.1.0...2.1.1) (2021-10-03) 189 | 190 | 191 | ### Bug Fixes 192 | 193 | * **markdown:** annotate text first before annotate link ([0159038](https://github.com/nartc/notion-stuff/commit/01590384b1ca420db9ca6c29e8594ebfadc400d5)) 194 | 195 | ## [2.1.0](https://github.com/nartc/notion-stuff/compare/2.0.5...2.1.0) (2021-10-02) 196 | 197 | 198 | ### Features 199 | 200 | * **markdown:** add imageAsFigure to parser options to parse image as figure element ([ffce05a](https://github.com/nartc/notion-stuff/commit/ffce05a94adb3d130d70572cdcdb8bb57de0e12f)) 201 | 202 | 203 | ### Bug Fixes 204 | 205 | * **html:** use Partial for MarkdownParserOptions instead ([0224a5e](https://github.com/nartc/notion-stuff/commit/0224a5ec3b55b3bdf9751c462a035cecf2238a04)) 206 | 207 | ### [2.0.5](https://github.com/nartc/notion-stuff/compare/2.0.4...2.0.5) (2021-10-02) 208 | 209 | 210 | ### Bug Fixes 211 | 212 | * **html:** add codeTransformer to return correct template for correct highlighter ([ef4a8fd](https://github.com/nartc/notion-stuff/commit/ef4a8fd9540f6d86aef5af4db846565153913597)) 213 | 214 | ### [2.0.4](https://github.com/nartc/notion-stuff/compare/2.0.3...2.0.4) (2021-10-02) 215 | 216 | 217 | ### Bug Fixes 218 | 219 | * **markdown:** adjust format of codeBlockParser ([7c881a5](https://github.com/nartc/notion-stuff/commit/7c881a5a24402ad9dc9e2da6833014d5ad3d6902)) 220 | 221 | ### [2.0.3](https://github.com/nartc/notion-stuff/compare/2.0.2...2.0.3) (2021-10-02) 222 | 223 | 224 | ### Bug Fixes 225 | 226 | * **markdown:** double EOL for paragraph and adjust CodeBlock Parser ([2a4f122](https://github.com/nartc/notion-stuff/commit/2a4f122897d9bd515f9a44f11af0c06fa10b1bf8)) 227 | 228 | ### [2.0.2](https://github.com/nartc/notion-stuff/compare/2.0.1...2.0.2) (2021-10-01) 229 | 230 | 231 | ### Bug Fixes 232 | 233 | * **markdown:** null check children before recusive call ([6d96a01](https://github.com/nartc/notion-stuff/commit/6d96a019ed4e51fbc8ab12bb6a70408cbaa718b1)) 234 | 235 | ### [2.0.1](https://github.com/nartc/notion-stuff/compare/2.0.0...2.0.1) (2021-10-01) 236 | 237 | 238 | ### Bug Fixes 239 | 240 | * **scully:** move scully to peerDep instead ([7171397](https://github.com/nartc/notion-stuff/commit/7171397f995f975f8f096124f84c02cd99762826)) 241 | 242 | 243 | ### Documentations 244 | 245 | * **html:** fix highlight.js dep ([45fa3db](https://github.com/nartc/notion-stuff/commit/45fa3db9874c872219d3d7d1fe9931a1dd7ca831)) 246 | 247 | ## [2.0.0](https://github.com/nartc/notion-stuff/compare/1.0.4...2.0.0) (2021-09-30) 248 | 249 | 250 | ### ⚠ BREAKING CHANGES 251 | 252 | * **scully:** Scully version has been bumped to v2 253 | * **html:** HTML Parser now uses Markdown parser. Please check README for details 254 | 255 | ### Features 256 | 257 | * **html:** use Markdown parser ([9882e90](https://github.com/nartc/notion-stuff/commit/9882e9071d9034aa5ba33306689ad2daa36ed8a8)) 258 | * **markdown:** add markdown parser ([6ed8398](https://github.com/nartc/notion-stuff/commit/6ed8398d7ef9c07c5676452b99f9427662b0ab35)) 259 | * **scully:** bump scully version ([025c3a1](https://github.com/nartc/notion-stuff/commit/025c3a1f1425e2d7e9986bfb65e428404db4682c)) 260 | 261 | ### [1.0.4](https://github.com/nartc/notion-stuff/compare/1.0.3...1.0.4) (2021-09-24) 262 | 263 | 264 | ### Bug Fixes 265 | 266 | * **scully:** adjust Formula and RollUp block type to match NotionAPi ([53e895c](https://github.com/nartc/notion-stuff/commit/53e895c1fdc00e78296376e4abc6a92ffa5616b5)) 267 | 268 | ### [1.0.3](https://github.com/nartc/notion-stuff/compare/1.0.2...1.0.3) (2021-09-08) 269 | 270 | 271 | ### Bug Fixes 272 | 273 | * **html:** fix list items being undefined ([1b95be1](https://github.com/nartc/notion-stuff/commit/1b95be1e5f382cef6ee79f068eba436c1d4f2b66)) 274 | 275 | ### [1.0.2](https://github.com/nartc/notion-stuff/compare/1.0.1...1.0.2) (2021-09-08) 276 | 277 | 278 | ### Bug Fixes 279 | 280 | * **html:** grab plain text as plain image caption instead of captionTransformer ([2e36175](https://github.com/nartc/notion-stuff/commit/2e36175f708200e33824dbd8b8005d4dd3a2b09a)) 281 | 282 | ### [1.0.1](https://github.com/nartc/notion-stuff/compare/1.0.0...1.0.1) (2021-09-08) 283 | 284 | 285 | ### Bug Fixes 286 | 287 | * **html:** use image caption as alt if available. fix "this" issue ([5831dac](https://github.com/nartc/notion-stuff/commit/5831dac6b3f53e570e69e488d62341f3881be780)) 288 | 289 | ## 1.0.0 (2021-09-08) 290 | 291 | 292 | ### Features 293 | 294 | * **html:** add HTML parser ([7fa11d9](https://github.com/nartc/notion-stuff/commit/7fa11d92404ba9ffcf115150a90cfe27a5be29c0)) 295 | * **scully:** add Scully plugin ([7c81cef](https://github.com/nartc/notion-stuff/commit/7c81cef1041499bf7889f84e0c649c21802745e2)) 296 | 297 | 298 | ### Documentations 299 | 300 | * add [@nartc](https://github.com/nartc) as a contributor ([e1ea76f](https://github.com/nartc/notion-stuff/commit/e1ea76f2b00847e7e4375d15b62511fb404a39f4)) 301 | * add README ([c981380](https://github.com/nartc/notion-stuff/commit/c981380ad1e7df2bbe7b60645eb0c74fb48b312a)) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notion Stuff 2 | 3 | - [Notion Blocks to Markdown parser](libs/blocks-markdown-parser/README.md) 4 | - [Notion Blocks to HTML parser](libs/blocks-html-parser/README.md) 5 | - [Scully plugin](libs/scully-plugin-notion/README.md) 6 | - [Notion 0.4 Types](libs/v4-types/README.md) 7 | 8 | ## Contributors ✨ 9 | 10 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

Chau Tran

💻 📖

Caleb Ukle

💻

MarcARoberge

💻

Panthaaaa

💻
23 | 24 | 25 | 26 | 27 | 28 | 29 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 30 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | module.exports = { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/README.md: -------------------------------------------------------------------------------- 1 | # @notion-stuff/blocks-html-parser 2 | 3 | This is a parser to parse Notion blocks to HTML parser. If you're here, you are probably aware of the [Notion API](https://developers.notion.com/), which is currently in beta. Hence, this parser is being updated (trying to) as the Notion API is updated. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install @notion-stuff/blocks-html-parser 9 | ``` 10 | 11 | > `@notion-stuff/blocks-html-parser` depends on `@notionhq/client` and `@notion-stuff/blocks-markdown-parser` 12 | 13 | If you are using `NotionBlocksHtmlParser` with default configuration, you need to also install `marked` and `hljs`. More info [here](#usage) 14 | 15 | ```bash 16 | npm install marked highlight.js 17 | ``` 18 | 19 | ## Usage 20 | 21 | 1. Get the parser instance with `NotionBlocksHtmlParser.getInstance()` 22 | 1. Optionally pass in a `NotionBlocksHtmlParserOptions` object to customize the parser. The passed-in option will be merged with the default options. 23 | 2. Call `instance.parse(blocks)` with `blocks` being `Block[]` that you'd get from a Notion page. 24 | 25 | `NotionBlocksHtmlParser` uses `NotionBlocksMarkdownParser` under the hood to parse Notion blocks to Markdown first. 26 | 27 | By default, `NotionBlocksHtmlParser` will use [marked](https://github.com/markedjs/marked) to compile Markdown to HTML. The default `marked` configuration is as follow: 28 | 29 | ```ts 30 | export const defaultMarkedOptions = { 31 | pedantic: false, 32 | gfm: true, 33 | breaks: false, 34 | sanitize: false, 35 | smartLists: true, 36 | smartypants: false, 37 | xhtml: false, 38 | }; 39 | ``` 40 | > `defaultMarkedOptions` is exported so you can merge it with your custom configuration. 41 | 42 | For highlighting, `hljs` will be used by default. 43 | 44 | You can customize: 45 | 46 | - Markdown to HTML compilation with `NotionBlocksHtmlParserOptions#mdToHtmlOptions` 47 | - If you pass in a function, you are taking over the compilation 48 | - Otherwise, you can pass in an `MarkedOptions` object to customize `marked` 49 | > Make sure to `npm install marked` 50 | - Markdown Highlighting with `NotionBlocksHtmlParserOptions#mdHighlightingOptions` 51 | - If you pass in a function, you are taking over the highlighting 52 | - If you pass in `hljs`, the parser will use `highlight.js` to highlight the code blocks. 53 | > Make sure to `npm install highlight.js` 54 | - If you pass in `prism`, the parse will use `prismjs` to highlight the code blocks 55 | > Make sure to `npm install prismjs` 56 | 57 | ## Configuration 58 | 59 | ```ts 60 | export interface NotionBlocksHtmlParserOptions { 61 | mdParserOptions?: NotionBlocksMarkdownParserOptions; 62 | mdToHtmlOptions?: 63 | | (( 64 | markdown: string, 65 | htmlParserOptions: NotionBlocksHtmlParserOptions 66 | ) => string) 67 | | MarkedOptions; 68 | /** 69 | * Only applicable with @link{mdToHtmlOptions} as @link{MarkedOptions} 70 | * @default 'hljs' 71 | */ 72 | mdHighlightingOptions?: 'hljs' | 'prismjs' | MarkedOptions['highlight']; 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'blocks-html-parser', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/libs/blocks-html-parser', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notion-stuff/blocks-html-parser", 3 | "version": "6.0.0", 4 | "dependencies": { 5 | "@notionhq/client": "~1.0.0" 6 | }, 7 | "publishConfig": { 8 | "access": "public" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/blocks-html-parser'; 2 | export * from './lib/interfaces'; 3 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/src/lib/blocks-html-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotionBlocksHtmlParser } from './blocks-html-parser'; 2 | import type { NotionBlocksHtmlParserOptions } from './interfaces'; 3 | 4 | describe(NotionBlocksHtmlParser.name, () => { 5 | let parser: NotionBlocksHtmlParser; 6 | 7 | function setup(options: NotionBlocksHtmlParserOptions = {}) { 8 | parser = NotionBlocksHtmlParser.getInstance(options); 9 | } 10 | 11 | it('should create instance', () => { 12 | setup(); 13 | expect(parser).toBeTruthy(); 14 | }); 15 | 16 | it('should create ONLY one instance', () => { 17 | setup(); 18 | const instance = NotionBlocksHtmlParser.getInstance(); 19 | 20 | expect(instance).toBe(parser); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/src/lib/blocks-html-parser.ts: -------------------------------------------------------------------------------- 1 | import { NotionBlocksMarkdownParser } from '@notion-stuff/blocks-markdown-parser'; 2 | import type { Blocks } from '@notion-stuff/v4-types'; 3 | import type { marked as Marked } from 'marked'; 4 | import type { NotionBlocksHtmlParserOptions } from './interfaces'; 5 | 6 | export const defaultMarkedOptions = { 7 | pedantic: false, 8 | gfm: true, 9 | breaks: false, 10 | sanitize: false, 11 | smartLists: true, 12 | smartypants: false, 13 | xhtml: false, 14 | }; 15 | 16 | let marked, hljs, prism; 17 | 18 | export class NotionBlocksHtmlParser { 19 | private static instance: NotionBlocksHtmlParser; 20 | private static markdownParser: NotionBlocksMarkdownParser; 21 | private readonly parserOptions: Required; 22 | 23 | private constructor(parserOptions: NotionBlocksHtmlParserOptions = {}) { 24 | this.parserOptions = NotionBlocksHtmlParser.assignDefaultOptions( 25 | parserOptions 26 | ) as Required; 27 | } 28 | 29 | static getInstance(parserOptions: NotionBlocksHtmlParserOptions = {}) { 30 | if (!this.instance) { 31 | this.instance = new this(parserOptions); 32 | if (typeof this.instance.parserOptions.mdToHtmlOptions === 'object') { 33 | this.setupMarked(this.instance.parserOptions); 34 | } 35 | } 36 | 37 | if (!this.markdownParser) { 38 | this.markdownParser = NotionBlocksMarkdownParser.getInstance( 39 | this.instance.parserOptions.mdParserOptions 40 | ); 41 | } 42 | 43 | return this.instance; 44 | } 45 | 46 | static getMarkdownParser(): NotionBlocksMarkdownParser { 47 | if (!this.markdownParser) { 48 | throw new Error( 49 | 'MarkdownParser is not available until HtmlParser instance has been created' 50 | ); 51 | } 52 | 53 | return this.markdownParser; 54 | } 55 | 56 | parse(blocks: Blocks) { 57 | const markdown = NotionBlocksHtmlParser.markdownParser.parse(blocks); 58 | const { mdToHtmlOptions } = this.parserOptions; 59 | 60 | if (typeof mdToHtmlOptions === 'object') { 61 | return marked(markdown, mdToHtmlOptions); 62 | } 63 | 64 | return mdToHtmlOptions(markdown, this.parserOptions); 65 | } 66 | 67 | private static assignDefaultOptions( 68 | parserOptions: NotionBlocksHtmlParserOptions 69 | ): NotionBlocksHtmlParserOptions { 70 | return { 71 | mdParserOptions: {}, 72 | mdHighlightingOptions: 'hljs', 73 | mdToHtmlOptions: defaultMarkedOptions, 74 | ...parserOptions, 75 | }; 76 | } 77 | 78 | private static setupMarked({ 79 | mdToHtmlOptions, 80 | mdHighlightingOptions, 81 | }: Required) { 82 | try { 83 | // eslint-disable-next-line @typescript-eslint/no-var-requires 84 | const _marked = require('marked'); 85 | marked = _marked.marked || _marked; 86 | } catch (e) { 87 | const message = `Error importing package: marked. Please install "marked" package.`; 88 | console.error(message); 89 | throw new Error(message); 90 | } 91 | 92 | const renderer = new marked.Renderer(); 93 | 94 | const codeTransformer = (code: unknown, language: string) => { 95 | const langClass = 96 | 'language-' + 97 | (!language || language.includes('plain') ? 'none' : language); 98 | if (mdHighlightingOptions === 'hljs') { 99 | return `
${
100 |           (code as Record).value
101 |         }
`; 102 | } 103 | 104 | // prism 105 | if (!language) { 106 | return `
${code}
`; 107 | } 108 | // e.g. "language-js" 109 | return `
${code}
`; 110 | }; 111 | 112 | renderer.code = function (this: typeof renderer, code, language) { 113 | code = this.options.highlight(code, language); 114 | return codeTransformer(code, code.language || language); 115 | }; 116 | 117 | renderer.html = function (this: typeof renderer, mixedHtml: string) { 118 | return mixedHtml.replace(/[^<>]+?(?=<\/[figcaption|span])/g, (match) => { 119 | const tokens = (marked as typeof Marked).lexer(match); 120 | return (marked as typeof Marked).parser(tokens); 121 | }); 122 | }; 123 | 124 | (mdToHtmlOptions as Marked.MarkedOptions).renderer = renderer; 125 | 126 | if (!(mdToHtmlOptions as Marked.MarkedOptions).highlight) { 127 | if (mdHighlightingOptions === 'hljs') { 128 | if (!hljs) { 129 | try { 130 | hljs = require('highlight.js'); 131 | } catch (e) { 132 | const message = `Error importing package: highlight.js. Please install "highlight.js" package.`; 133 | console.error(message); 134 | throw new Error(message); 135 | } 136 | } 137 | (mdToHtmlOptions as Marked.MarkedOptions).highlight = (code, lang) => { 138 | const language = hljs.getLanguage(lang) ? lang : 'plaintext'; 139 | return hljs.highlight(code, { language }); 140 | }; 141 | } else if (mdHighlightingOptions === 'prismjs') { 142 | if (!prism) { 143 | try { 144 | prism = require('prismjs'); 145 | require('prismjs/components/prism-bash'); 146 | require('prismjs/components/prism-css'); 147 | require('prismjs/components/prism-javascript'); 148 | require('prismjs/components/prism-json'); 149 | require('prismjs/components/prism-markup'); 150 | require('prismjs/components/prism-markdown'); 151 | require('prismjs/components/prism-typescript'); 152 | require('prismjs/components/prism-jsx'); 153 | require('prismjs/components/prism-tsx'); 154 | require('prismjs/components/prism-docker'); 155 | } catch (e) { 156 | const message = `Error importing package: prismjs. Please install "prismjs" package.`; 157 | console.error(message); 158 | throw new Error(message); 159 | } 160 | } 161 | 162 | (mdToHtmlOptions as Marked.MarkedOptions).highlight = (code, lang) => { 163 | if (!prism.languages[lang]) { 164 | return code; 165 | } 166 | 167 | return prism.highlight(code, prism.languages[lang]); 168 | }; 169 | } else { 170 | (mdToHtmlOptions as Marked.MarkedOptions).highlight = 171 | mdHighlightingOptions; 172 | } 173 | } 174 | 175 | 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/src/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { NotionBlocksMarkdownParserOptions } from '@notion-stuff/blocks-markdown-parser'; 2 | import type { marked } from 'marked'; 3 | 4 | export interface NotionBlocksHtmlParserOptions { 5 | mdParserOptions?: Partial; 6 | mdToHtmlOptions?: 7 | | (( 8 | markdown: string, 9 | htmlParserOptions: NotionBlocksHtmlParserOptions 10 | ) => string) 11 | | marked.MarkedOptions; 12 | /** 13 | * Only applicable with @link{mdToHtmlOptions} as @link{MarkedOptions} 14 | * @default 'hljs' 15 | */ 16 | mdHighlightingOptions?: 17 | | 'hljs' 18 | | 'prismjs' 19 | | marked.MarkedOptions['highlight']; 20 | } 21 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/blocks-html-parser/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/README.md: -------------------------------------------------------------------------------- 1 | # @notion-stuff/blocks-html-parser 2 | 3 | This is a parser to parse Notion blocks to Markdown. If you're here, you are probably aware of the [Notion API](https://developers.notion.com/), which is currently in beta. Hence, this parser is being updated (trying to) as the Notion API is updated. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install @notion-stuff/blocks-markdown-parser 9 | ``` 10 | 11 | > `@notion-stuff/blocks-markdown-parser` depends on `@notionhq/client` 12 | 13 | ## Usage 14 | 15 | 1. Get the parser instance with `NotionBlocksMarkdownParser.getInstance()` 16 | 1. Optionally pass in a `NotionBlocksMarkdownParserOptions` object to customize the parser. The passed-in option will be merged with the default options. 17 | 2. Call `instance.parse(blocks)` with `blocks` being `Block[]` that you'd get from a Notion page. 18 | 19 | ## Configuration 20 | 21 | ```ts 22 | export interface NotionBlocksMarkdownParserOptions {} 23 | ``` 24 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'blocks-markdown-parser', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/libs/blocks-markdown-parser', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notion-stuff/blocks-markdown-parser", 3 | "version": "6.0.0", 4 | "dependencies": { 5 | "@notionhq/client": "~1.0.0" 6 | }, 7 | "publishConfig": { 8 | "access": "public" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/blocks-markdown-parser'; 2 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/src/lib/blocks-markdown-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { NotionBlocksMarkdownParser } from './blocks-markdown-parser'; 2 | 3 | describe(NotionBlocksMarkdownParser.name, () => { 4 | it('should work', () => { 5 | expect(NotionBlocksMarkdownParser.getInstance()).toBeTruthy(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/src/lib/blocks-markdown-parser.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Annotations, 3 | AudioBlock, 4 | Blocks, 5 | BulletedListItemBlock, 6 | CalloutBlock, 7 | CalloutIconEmoji, 8 | CalloutIconExternal, 9 | CalloutIconFile, 10 | CodeBlock, 11 | EmbedBlock, 12 | ExternalFileWithCaption, 13 | FileBlock, 14 | FileWithCaption, 15 | HeadingBlock, 16 | ImageBlock, 17 | NumberedListItemBlock, 18 | ParagraphBlock, 19 | PDFBlock, 20 | QuoteBlock, 21 | RichText, 22 | RichTextEquation, 23 | RichTextMention, 24 | RichTextText, 25 | ToDoBlock, 26 | ToggleBlock, 27 | VideoBlock, 28 | } from '@notion-stuff/v4-types'; 29 | import { processExternalVideoUrl } from './external-video.util'; 30 | 31 | const EOL_MD = '\n'; 32 | 33 | export interface NotionBlocksMarkdownParserOptions { 34 | /** 35 | * Use
and
36 | */ 37 | imageAsFigure: boolean; 38 | /** 39 | * When a paragraphBlock#text is empty, render a   40 | */ 41 | emptyParagraphToNonBreakingSpace: boolean; 42 | } 43 | 44 | export class NotionBlocksMarkdownParser { 45 | private static instance: NotionBlocksMarkdownParser; 46 | private readonly parserOptions: Required; 47 | 48 | private constructor(options?: Partial) { 49 | this.parserOptions = { 50 | imageAsFigure: true, 51 | emptyParagraphToNonBreakingSpace: false, 52 | ...(options || {}), 53 | }; 54 | } 55 | 56 | static getInstance(options?: Partial) { 57 | if (!this.instance) { 58 | this.instance = new this(options); 59 | } 60 | 61 | return this.instance; 62 | } 63 | 64 | parse(blocks: Blocks, depth = 0): string { 65 | return blocks 66 | .reduce((markdown, childBlock) => { 67 | let childBlockString = ''; 68 | if (childBlock.has_children && childBlock[childBlock.type].children) { 69 | childBlockString = ' ' 70 | .repeat(depth) 71 | .concat( 72 | childBlockString, 73 | this.parse(childBlock[childBlock.type].children, depth + 2) 74 | ); 75 | } 76 | 77 | if (childBlock.type === 'unsupported') { 78 | markdown += 'NotionAPI Unsupported'.concat( 79 | EOL_MD.repeat(2), 80 | childBlockString 81 | ); 82 | } 83 | 84 | if (childBlock.type === 'paragraph') { 85 | markdown += this.parseParagraph(childBlock).concat(childBlockString); 86 | } 87 | 88 | if (childBlock.type === 'code') { 89 | markdown += this.parseCodeBlock(childBlock).concat(childBlockString); 90 | } 91 | 92 | if (childBlock.type === 'quote') { 93 | markdown += this.parseQuoteBlock(childBlock).concat(childBlockString); 94 | } 95 | 96 | if (childBlock.type === 'callout') { 97 | markdown += 98 | this.parseCalloutBlock(childBlock).concat(childBlockString); 99 | } 100 | 101 | if (childBlock.type.startsWith('heading_')) { 102 | const headingLevel = Number(childBlock.type.split('_')[1]); 103 | markdown += this.parseHeading( 104 | childBlock as HeadingBlock, 105 | headingLevel 106 | ).concat(childBlockString); 107 | } 108 | 109 | if (childBlock.type === 'bulleted_list_item') { 110 | markdown += 111 | this.parseBulletedListItems(childBlock).concat(childBlockString); 112 | } 113 | 114 | if (childBlock.type === 'numbered_list_item') { 115 | markdown += 116 | this.parseNumberedListItems(childBlock).concat(childBlockString); 117 | } 118 | 119 | if (childBlock.type === 'to_do') { 120 | markdown += this.parseTodoBlock(childBlock).concat(childBlockString); 121 | } 122 | 123 | if (childBlock.type === 'toggle') { 124 | markdown += this.parseToggleBlock(childBlock).replace( 125 | '{{childBlock}}', 126 | childBlockString 127 | ); 128 | } 129 | 130 | if (childBlock.type === 'image') { 131 | markdown += this.parseImageBlock(childBlock).concat(childBlockString); 132 | } 133 | 134 | if (childBlock.type === 'embed') { 135 | markdown += this.parseEmbedBlock(childBlock).concat(childBlockString); 136 | } 137 | 138 | if (childBlock.type === 'audio') { 139 | markdown += this.parseAudioBlock(childBlock).concat(childBlockString); 140 | } 141 | 142 | if (childBlock.type === 'video') { 143 | markdown += this.parseVideoBlock(childBlock).concat(childBlockString); 144 | } 145 | 146 | if (childBlock.type === 'file') { 147 | markdown += this.parseFileBlock(childBlock).concat(childBlockString); 148 | } 149 | 150 | if (childBlock.type === 'pdf') { 151 | markdown += this.parsePdfBlock(childBlock).concat(childBlockString); 152 | } 153 | 154 | if (childBlock.type === 'divider') { 155 | markdown += EOL_MD.concat('---', EOL_MD, childBlockString); 156 | } 157 | 158 | return markdown; 159 | }, '') 160 | .concat(EOL_MD); 161 | } 162 | 163 | parseParagraph(paragraphBlock: ParagraphBlock): string { 164 | let text: string; 165 | 166 | if ( 167 | this.parserOptions.emptyParagraphToNonBreakingSpace && 168 | paragraphBlock.paragraph.rich_text.length === 0 169 | ) { 170 | text = ' '; 171 | } else { 172 | text = this.parseRichTexts(paragraphBlock.paragraph.rich_text); 173 | } 174 | 175 | return EOL_MD.concat(text, EOL_MD); 176 | } 177 | 178 | parseCodeBlock(codeBlock: CodeBlock): string { 179 | return `\`\`\`${codeBlock.code.language.toLowerCase() || ''} 180 | ${(codeBlock.code.rich_text[0] as RichTextText).text.content} 181 | \`\`\``.concat(EOL_MD); 182 | } 183 | 184 | parseQuoteBlock(quoteBlock: QuoteBlock): string { 185 | return EOL_MD.concat( 186 | `> ${this.parseRichTexts(quoteBlock.quote.rich_text)}`, 187 | EOL_MD 188 | ); 189 | } 190 | 191 | parseCalloutBlock(calloutBlock: CalloutBlock) { 192 | const callout = `
193 | {{icon}} 194 | 195 | ${this.parseRichTexts(calloutBlock.callout.rich_text)} 196 | 197 |
`; 198 | 199 | function getCalloutIcon( 200 | icon: CalloutIconEmoji | CalloutIconExternal | CalloutIconFile 201 | ) { 202 | switch (icon.type) { 203 | case 'emoji': 204 | return `${icon.emoji}`; 205 | case 'external': 206 | return `notion-callout-external-link`; 207 | case 'file': 208 | // TODO: add support for Callout File 209 | return `notion-callout-file`; 210 | } 211 | } 212 | 213 | return EOL_MD.concat( 214 | callout.replace('{{icon}}', getCalloutIcon(calloutBlock.callout.icon)), 215 | EOL_MD 216 | ); 217 | } 218 | 219 | parseHeading(headingBlock: HeadingBlock, headingLevel: number): string { 220 | return EOL_MD.concat( 221 | '#'.repeat(headingLevel), 222 | ' ', 223 | this.parseRichTexts(headingBlock[headingBlock.type].rich_text), 224 | EOL_MD 225 | ); 226 | } 227 | 228 | parseBulletedListItems(bulletedListItemBlock: BulletedListItemBlock): string { 229 | return '* '.concat( 230 | this.parseRichTexts(bulletedListItemBlock.bulleted_list_item.rich_text), 231 | EOL_MD 232 | ); 233 | } 234 | 235 | parseNumberedListItems(numberedListItemBlock: NumberedListItemBlock): string { 236 | return '1. '.concat( 237 | this.parseRichTexts(numberedListItemBlock.numbered_list_item.rich_text), 238 | EOL_MD 239 | ); 240 | } 241 | 242 | parseTodoBlock(todoBlock: ToDoBlock): string { 243 | return `- [${todoBlock.to_do.checked ? 'x' : ' '}] `.concat( 244 | this.parseRichTexts(todoBlock.to_do.rich_text), 245 | EOL_MD 246 | ); 247 | } 248 | 249 | parseToggleBlock(toggleBlock: ToggleBlock): string { 250 | return `
${this.parseRichTexts( 251 | toggleBlock.toggle.rich_text 252 | )}{{childBlock}}
`; 253 | } 254 | 255 | parseImageBlock(imageBlock: ImageBlock): string { 256 | const { url, caption } = this.parseFile(imageBlock.image); 257 | if (this.parserOptions.imageAsFigure) { 258 | return ` 259 |
260 | ${caption} 261 |
${caption}
262 |
263 | `.concat(EOL_MD); 264 | } 265 | return `![${caption}](${url})`.concat(EOL_MD); 266 | } 267 | 268 | parseAudioBlock(audioBlock: AudioBlock): string { 269 | const { url, caption } = this.parseFile(audioBlock.audio); 270 | return `![${caption}](${url})`; 271 | } 272 | 273 | parseVideoBlock(videoBlock: VideoBlock): string { 274 | const { url, caption } = this.parseFile(videoBlock.video); 275 | 276 | const [processed, iframeOrUrl] = processExternalVideoUrl(url); 277 | 278 | if (processed) { 279 | return EOL_MD.concat(iframeOrUrl, EOL_MD); 280 | } 281 | 282 | return `To be supported: ${url} with ${caption}`.concat(EOL_MD); 283 | } 284 | 285 | parseFileBlock(fileBlock: FileBlock): string { 286 | const { url, caption } = this.parseFile(fileBlock.file); 287 | return `To be supported: ${url} with ${caption}`.concat(EOL_MD); 288 | } 289 | 290 | parsePdfBlock(pdfBlock: PDFBlock): string { 291 | const { url, caption } = this.parseFile(pdfBlock.pdf); 292 | return ` 293 |
294 | 295 |
${caption}
296 |
297 | `.concat(EOL_MD); 298 | } 299 | 300 | parseEmbedBlock(embedBlock: EmbedBlock): string { 301 | const embedded = ``; 302 | 303 | if (embedBlock.embed.caption) { 304 | return ` 305 |
306 | ${embedded} 307 |
${this.parseRichTexts(embedBlock.embed.caption)}
308 |
`.concat(EOL_MD); 309 | } 310 | 311 | return embedded.concat(EOL_MD); 312 | } 313 | 314 | parseRichTexts(richTexts: RichText[]): string { 315 | return richTexts.reduce((parsedContent, richText) => { 316 | switch (richText.type) { 317 | case 'text': 318 | parsedContent += this.parseText(richText); 319 | break; 320 | case 'mention': 321 | parsedContent += this.parseMention(richText); 322 | break; 323 | case 'equation': 324 | parsedContent += this.parseEquation(richText); 325 | break; 326 | } 327 | 328 | return parsedContent; 329 | }, ''); 330 | } 331 | 332 | parseText(richText: RichTextText): string { 333 | const content = this.annotate(richText.annotations, richText.text.content); 334 | 335 | return richText.text.link 336 | ? this.annotateLink(richText.text, content) 337 | : content; 338 | } 339 | 340 | // TODO: support mention when we know what it actually means 341 | 342 | parseMention(mention: RichTextMention): string { 343 | switch (mention.mention.type) { 344 | case 'user': 345 | break; 346 | case 'page': 347 | break; 348 | case 'database': 349 | break; 350 | case 'date': 351 | break; 352 | } 353 | return this.annotate(mention.annotations, mention.plain_text); 354 | } 355 | 356 | parseEquation(equation: RichTextEquation): string { 357 | return this.annotate( 358 | equation.annotations, 359 | `$${equation.equation.expression}$` 360 | ); 361 | } 362 | 363 | parseFile(file: ExternalFileWithCaption | FileWithCaption): { 364 | caption: string; 365 | url: string; 366 | } { 367 | const fileContent = { 368 | caption: '', 369 | url: '', 370 | }; 371 | 372 | switch (file.type) { 373 | case 'external': 374 | fileContent.url = file.external.url; 375 | break; 376 | case 'file': 377 | fileContent.url = file.file.url; 378 | break; 379 | } 380 | 381 | fileContent.caption = file.caption 382 | ? this.parseRichTexts(file.caption) 383 | : fileContent.url; 384 | 385 | return fileContent; 386 | } 387 | 388 | private annotate(annotations: Annotations, originalContent: string): string { 389 | return Object.entries(annotations).reduce( 390 | ( 391 | annotatedContent, 392 | [modifier, isOnOrColor]: [ 393 | keyof Annotations, 394 | boolean | Annotations['color'] 395 | ] 396 | ) => 397 | isOnOrColor 398 | ? this.annotateModifier( 399 | modifier, 400 | annotatedContent, 401 | isOnOrColor as Annotations['color'] 402 | ) 403 | : annotatedContent, 404 | originalContent 405 | ); 406 | } 407 | 408 | private annotateLink( 409 | text: RichTextText['text'], 410 | annotatedContent: string 411 | ): string { 412 | return `[${annotatedContent}](${ 413 | text.link.url ? text.link.url : text.link 414 | })`; 415 | } 416 | 417 | private annotateModifier( 418 | modifier: keyof Annotations, 419 | originalContent: string, 420 | color?: Annotations['color'] 421 | ): string { 422 | switch (modifier) { 423 | case 'bold': 424 | return `**${originalContent}**`; 425 | case 'italic': 426 | return `_${originalContent}_`; 427 | case 'strikethrough': 428 | return `~~${originalContent}~~`; 429 | case 'underline': 430 | return `${originalContent}`; 431 | case 'code': 432 | return `\`${originalContent}\``; 433 | case 'color': 434 | if (color !== 'default') { 435 | return `${originalContent}`; 436 | } 437 | return originalContent; 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/src/lib/external-video.util.ts: -------------------------------------------------------------------------------- 1 | export function processExternalVideoUrl(url: string): [boolean, string] { 2 | if (url.includes('youtu')) { 3 | return processYoutubeUrl(url); 4 | } 5 | 6 | return [false, url]; 7 | } 8 | 9 | function processYoutubeUrl(youtubeUrl: string): [boolean, string] { 10 | const lastPart = youtubeUrl.split('/').pop(); 11 | 12 | const youtubeIframe = ``; 13 | 14 | if (lastPart.includes('watch')) { 15 | const [, queryString] = lastPart.split('watch'); 16 | const queryParams = new URLSearchParams(queryString); 17 | if (queryParams.has('v')) { 18 | return [true, youtubeIframe.replace('{{videoId}}', queryParams.get('v'))]; 19 | } 20 | 21 | return [false, youtubeUrl]; 22 | } 23 | 24 | return [true, youtubeIframe.replace('{{videoId}}', lastPart)]; 25 | } 26 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/blocks-markdown-parser/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/README.md: -------------------------------------------------------------------------------- 1 | # @notion-stuff/scully-plugin-notion 2 | 3 | This is a [Scully](https://scully.io) plugin to convert [Notion](https://notion.so) pages to static HTML pages 4 | utilizing [@notion-stuff/blocks-html-parser](../blocks-html-parser/README.md) 5 | 6 | > [Notion API](https://developers.notion.com/) is currently in beta. There are content types that aren't being supported. Check the Notion API documentations for details 7 | 8 | ## Features 9 | 10 | - [x] Convert Notion pages to static HTML 11 | - [x] Access to Notion page properties as `Frontmatter` (should be `ScullyRoute` from `ScullyRoutesService`) 12 | - [ ] Customize Notion page properties 13 | - [x] Customize HTML parsers. Please check out `NotionBlocksHtmlParser` documentations for more details. 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm install --save-dev @notion-stuff/scully-plugin-notion 19 | ``` 20 | 21 | > `@notion-stuff/scully-plugin-notion` depends on `@notionhq/client` 22 | 23 | ## Usage 24 | 25 | > Notion integration steps courtesy of [Gatsby Source Notion Plugin author](https://www.gatsbyjs.com/plugins/gatsby-source-notion-api/) 26 | 27 | 1. Created a Notion integration (sign in to Notion, go 28 | to `Settings & Memberships → Integrations → Develop your own integrations` 29 | , [short link to the Integrations creation section](https://www.notion.so/my-integrations)). It’s OK to use an 30 | internal one. Don’t forget to copy the token: 31 | ![gif-1](https://files.readme.io/2ec137d-093ad49-create-integration.gif) 32 | 33 | 2. Go to the database you want to have access to from Scully, and share it with the integration (`Share` → Select the 34 | integration in the `Invite` dropdown). Don’t forget the database in the URL. It’s a series of characters after the 35 | last slash and before the question mark. 36 | ![gif-2](https://files.readme.io/0a267dd-share-database-with-integration.gif) 37 | 38 | > Here’s a reference: https://www.notion.so/{USER}/**{DATABASE_ID}**?{someotherirrelevantstuff} 39 | > If you only have 1 ID before the question mark in the URL, then the first ID is the Database ID 40 | > i.e. https://www.notion.so/**{DATABASE_ID}**?{OtherIdThatDoesNotMatter} 41 | 42 | 3. Your database needs to have the following Page Properties (Table column headers) 43 | 44 | | Title | Status | Slug | 45 | | ---------------------- | ----------------------------------------- | ---- | 46 | | This should be default | Select (a `Published` option is required) | Text | 47 | 48 | - `Status` is required 49 | - `Published` status is required because this is being used by the plugin to set the `published` flag for a 50 | specific `ScullyRoute`. Although this is required for the plugin to work as expected, your intention (as plugin 51 | consumers) to use the `published` flag is totally up to you. 52 | - `Slug` is utilized to setup the route to the post which is needed to be set manually as of the moment. 53 | - You can call `Slug` whatever you want but this needs to match `slugKey` 54 | 55 | 4. Configure the plugin in `scully.your-app.config.ts` 56 | 57 | ## Frontmatter (route metadata) 58 | 59 | All columns in the table will be added to the route data which can be consumed 60 | via [ScullyRouteService](https://scully.io/docs/Reference/ngLib/scully-routes-service/) along with the additional items from notion 61 | 62 | > All column headers will be turned into [camelCase keys](https://github.com/nartc/notion-stuff/blob/main/libs/scully-plugin-notion/src/lib/utils.ts#L16) to be used in the frontmatter/config of the plugin 63 | 64 | ```ts 65 | interface AdditionalNotionProperties { 66 | // link to the notion page cover image or null if one isn't set. Note: [someKey] is commonly external or file 67 | cover: null | { type: string, [someKey: string]: { url: string } }; 68 | // url to the notion page 69 | notionUrl: string; 70 | // GUID of the notion page 71 | id: string; 72 | } 73 | ``` 74 | 75 | ```ts 76 | import { ScullyConfig, setPluginConfig } from '@scullyio/scully'; 77 | import { 78 | NotionDom, 79 | NotionDomRouter, 80 | NotionDomPluginOptions, 81 | NotionDomRouterPluginOptions 82 | } from '@notion-stuff/scully-plugin-notion'; 83 | 84 | setPluginConfig(NotionDom, { 85 | notionBlocksHtmlParserOptions: { 86 | /** 87 | ... customer the parser ... 88 | */ 89 | }, 90 | } as NotionDomPluginOptions); 91 | 92 | export const config: ScullyConfig = { 93 | projectRoot: './src', 94 | projectName: 'my-project', 95 | outDir: './dist/static', 96 | routes: { 97 | '/blog/:slug': { 98 | type: NotionDomRouter, 99 | postRenderers: [NotionDom], 100 | databaseId: 'your-database-id', // required 101 | notionApiKey: 'your-integration-secret', // if NOTION_API_KEY is set in Environment Variables, it is used instead of notionApiKey 102 | basePath: '/blog', // optional, should match your route here 103 | titleSuffix: '', // optional 104 | slugKey: 'slug', // optional 105 | } as NotionDomRouterPluginOptions, 106 | }, 107 | }; 108 | ``` 109 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'scully-plugin-notion', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/libs/scully-plugin-notion', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notion-stuff/scully-plugin-notion", 3 | "version": "6.0.0", 4 | "dependencies": { 5 | "@notionhq/client": "~1.0.0" 6 | }, 7 | "peerDependencies": { 8 | "@scullyio/scully": "^2.0.0" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/plugin'; 2 | export * from './lib/plugin-options'; 3 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/src/lib/plugin-options.ts: -------------------------------------------------------------------------------- 1 | import type { NotionBlocksHtmlParserOptions } from '@notion-stuff/blocks-html-parser'; 2 | import type { PostResult } from '@notion-stuff/v4-types'; 3 | import type { RouteConfig } from '@scullyio/scully'; 4 | import type { GetPagePropertyResponse } from '@notionhq/client/build/src/api-endpoints'; 5 | 6 | export interface NotionDomRouterPluginOptions 7 | extends Omit { 8 | type: string; 9 | /** 10 | * @requires string Notion DatabaseID 11 | */ 12 | databaseId: string; 13 | 14 | /** 15 | * Notion API Key. This should be provided via NOTION_API_KEY environment variable instead 16 | */ 17 | notionApiKey?: string; 18 | 19 | /** 20 | * A custom function that will process the {PostResult} from Notion. You take over the Frontmatter with this function 21 | */ 22 | postResultProcessor?>( 23 | post: PostResult, 24 | options: NotionDomRouterPluginOptions, 25 | propertyValueParser: (propertyValue: GetPagePropertyResponse) => unknown 26 | ): TObject; 27 | 28 | /** 29 | * A custom function that will resolve the `published` flag that Scully needs. 30 | */ 31 | isPublished?>( 32 | frontmatter: TObject 33 | ): boolean; 34 | 35 | /** 36 | * Default icon for your post 37 | */ 38 | defaultPostIcon?: string; 39 | 40 | /** 41 | * The key that you use as your "Slug" in your Notion table. 42 | * @default "slug" (requires you to have a "Slug" property in your table) 43 | */ 44 | slugKey?: string; 45 | 46 | /** 47 | * The base path of the route when setup in Scully config 48 | * @example 49 | * { 50 | * "/basePath/:slug": { 51 | * basePath: "/basePath" // needs to match /basePath 52 | * } 53 | * } 54 | * 55 | * @default "/blog" 56 | */ 57 | basePath?: string; 58 | titleSuffix?: string; 59 | } 60 | 61 | export interface NotionDomPluginOptions { 62 | notionBlocksHtmlParserOptions?: NotionBlocksHtmlParserOptions; 63 | } 64 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/src/lib/plugin.ts: -------------------------------------------------------------------------------- 1 | import { NotionBlocksHtmlParser } from '@notion-stuff/blocks-html-parser'; 2 | import type { Blocks, PostResult } from '@notion-stuff/v4-types'; 3 | import { Client } from '@notionhq/client/build/src'; 4 | import type { HandledRoute, RouteConfig } from '@scullyio/scully'; 5 | import { 6 | getPluginConfig, 7 | log, 8 | red, 9 | registerPlugin, 10 | yellow, 11 | } from '@scullyio/scully'; 12 | import { injectHtml } from '@scullyio/scully/src/lib/renderPlugins/content-render-utils/injectHtml'; 13 | import type { 14 | NotionDomPluginOptions, 15 | NotionDomRouterPluginOptions, 16 | } from './plugin-options'; 17 | import { processPageProperties } from './process-page-properties'; 18 | 19 | export const NotionDom = 'notionDom'; 20 | export const NotionDomRouter = 'notionDomRouter'; 21 | 22 | let notion: Client; 23 | let htmlParser: NotionBlocksHtmlParser; 24 | let pluginOptions: NotionDomPluginOptions; 25 | 26 | function setupParserAndPluginOptions() { 27 | if (!pluginOptions) { 28 | pluginOptions = getPluginConfig(NotionDom, 'postProcessByDom') || {}; 29 | } 30 | 31 | if (!htmlParser) { 32 | htmlParser = NotionBlocksHtmlParser.getInstance( 33 | pluginOptions.notionBlocksHtmlParserOptions 34 | ); 35 | } 36 | } 37 | 38 | try { 39 | notion = new Client({ auth: process.env.NOTION_API_KEY }); 40 | } catch { 41 | log( 42 | yellow( 43 | 'No NOTION_API_KEY in Environment Variables. Attempting to use API key from the route config' 44 | ) 45 | ); 46 | } 47 | 48 | async function notionDomRouterPlugin( 49 | route: string | undefined, 50 | config: NotionDomRouterPluginOptions 51 | ) { 52 | const mergedConfig = { 53 | ...{ 54 | slugKey: 'slug', 55 | basePath: '/blog', 56 | titleSuffix: '', 57 | defaultPostIcon: '', 58 | }, 59 | ...config, 60 | }; 61 | 62 | try { 63 | if (!notion) { 64 | if (!mergedConfig.notionApiKey) { 65 | return Promise.reject( 66 | new Error( 67 | 'Notion Client has not been instantiated. Please provide the Notion API Key' 68 | ) 69 | ); 70 | } 71 | 72 | notion = new Client({ 73 | auth: mergedConfig.notionApiKey, 74 | }); 75 | } 76 | 77 | const posts = await notion.databases.query({ 78 | database_id: mergedConfig.databaseId, 79 | }); 80 | 81 | setupParserAndPluginOptions(); 82 | 83 | return Promise.resolve( 84 | posts.results.map((result) => { 85 | const postResult = result as unknown as PostResult; 86 | const frontmatter = processPageProperties(postResult, mergedConfig); 87 | 88 | let cover = ''; 89 | let icon = mergedConfig.defaultPostIcon; 90 | 91 | if (postResult.cover) { 92 | cover = NotionBlocksHtmlParser.getMarkdownParser().parseFile( 93 | postResult.cover 94 | ).url; 95 | } 96 | 97 | if (postResult.icon) { 98 | switch (postResult.icon.type) { 99 | case 'emoji': 100 | icon = postResult.icon.emoji; 101 | break; 102 | case 'external': 103 | case 'file': 104 | icon = NotionBlocksHtmlParser.getMarkdownParser().parseFile( 105 | postResult.icon 106 | ).url; 107 | break; 108 | } 109 | } 110 | 111 | return { 112 | type: mergedConfig.type, 113 | route: `${mergedConfig.basePath}/${ 114 | frontmatter[mergedConfig.slugKey] 115 | }`, 116 | title: mergedConfig.titleSuffix 117 | ? `${frontmatter.title} | ${mergedConfig.titleSuffix}` 118 | : frontmatter.title, 119 | data: { 120 | ...frontmatter, 121 | id: postResult.id, 122 | notionUrl: postResult.url, 123 | cover, 124 | icon, 125 | }, 126 | } as HandledRoute; 127 | }) 128 | ); 129 | } catch (e) { 130 | throw new Error(`Something went wrong. ${e}`); 131 | } 132 | } 133 | 134 | const notionDomRouterValidator = (config: RouteConfig) => { 135 | const errors: string[] = []; 136 | 137 | if (!config.databaseId) { 138 | errors.push('Missing "databaseId"'); 139 | } 140 | 141 | return errors; 142 | }; 143 | 144 | async function notionDomPlugin(dom: unknown, route: HandledRoute | undefined) { 145 | if (!route) return dom; 146 | 147 | const postId = route.data?.['id']; 148 | 149 | if (!notion) { 150 | log(yellow(`Notion Client not found. Skipping ${route.route}`)); 151 | return Promise.resolve(dom); 152 | } 153 | 154 | if (!postId) { 155 | log(yellow(`Post ID not found. Skipping ${route.route}`)); 156 | return Promise.resolve(dom); 157 | } 158 | 159 | try { 160 | const blocks = await notion.blocks.children.list({ 161 | block_id: postId, 162 | }); 163 | if (!blocks || !blocks.results.length) { 164 | log(yellow(`Post does not have any blocks. Skipping ${route.route}`)); 165 | return Promise.resolve(dom); 166 | } 167 | 168 | return injectHtml(dom, htmlParser.parse(blocks.results as Blocks), route); 169 | } catch (e) { 170 | log(red(`Something went wrong. ${e}`)); 171 | return Promise.resolve(dom); 172 | } 173 | } 174 | 175 | registerPlugin( 176 | 'router', 177 | NotionDomRouter, 178 | notionDomRouterPlugin, 179 | notionDomRouterValidator 180 | ); 181 | 182 | registerPlugin('postProcessByDom', NotionDom, notionDomPlugin); 183 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/src/lib/process-page-properties.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PostResult, 3 | PropertyValueFormula, 4 | PropertyValueRollup, 5 | PropertyValueSelect, 6 | PropertyValueUser, 7 | } from '@notion-stuff/v4-types'; 8 | import type { NotionDomRouterPluginOptions } from './plugin-options'; 9 | 10 | import { camelize } from './utils'; 11 | import { GetPagePropertyResponse } from '@notionhq/client/build/src/api-endpoints'; 12 | 13 | export function processPageProperties( 14 | post: PostResult, 15 | options: NotionDomRouterPluginOptions 16 | ) { 17 | if (options.postResultProcessor) { 18 | return options.postResultProcessor(post, options, parsePropertyValue); 19 | } 20 | 21 | const frontmatter: Record = { published: false }; 22 | 23 | for (const [propertyKey, propertyValue] of Object.entries(post.properties)) { 24 | const camelizedKey = camelize(propertyKey); 25 | frontmatter[camelizedKey] = parsePropertyValue( 26 | propertyValue as unknown as GetPagePropertyResponse 27 | ); 28 | 29 | if (!options.isPublished && propertyKey.toLowerCase() === 'status') { 30 | frontmatter.published = 31 | ( 32 | propertyValue as unknown as PropertyValueSelect 33 | ).select?.name.toLowerCase() === 'published'; 34 | } 35 | } 36 | 37 | if (options.isPublished) { 38 | frontmatter.published = options.isPublished(frontmatter); 39 | } 40 | 41 | return frontmatter; 42 | } 43 | 44 | function parsePropertyValue(propertyValue: GetPagePropertyResponse) { 45 | switch (propertyValue.type) { 46 | case 'title': 47 | return propertyValue.title[0].plain_text; 48 | case 'rich_text': 49 | return propertyValue.rich_text[0].plain_text; 50 | case 'number': 51 | return propertyValue.number; 52 | case 'select': 53 | return propertyValue.select?.name; 54 | case 'multi_select': 55 | return propertyValue.multi_select.map( 56 | (multiSelectOption) => multiSelectOption.name 57 | ); 58 | case 'date': 59 | if (!propertyValue?.date) return null; 60 | if (propertyValue.date.end) { 61 | return [ 62 | new Date(propertyValue.date.start), 63 | new Date(propertyValue.date.end), 64 | ]; 65 | } 66 | return new Date(propertyValue.date.start); 67 | case 'formula': 68 | return formularize(propertyValue); 69 | case 'rollup': 70 | return rollup(propertyValue); 71 | case 'people': 72 | return personify(propertyValue.people as PropertyValueUser); 73 | case 'files': 74 | return propertyValue.files.map((fileWithName) => fileWithName.name); 75 | case 'checkbox': 76 | return propertyValue.checkbox; 77 | case 'url': 78 | return propertyValue.url; 79 | case 'email': 80 | return propertyValue.email; 81 | case 'phone_number': 82 | return propertyValue.phone_number; 83 | case 'created_time': 84 | return new Date(propertyValue.created_time); 85 | case 'created_by': 86 | return personify(propertyValue.created_by as PropertyValueUser); 87 | case 'last_edited_time': 88 | return new Date(propertyValue.last_edited_time); 89 | case 'last_edited_by': 90 | return personify(propertyValue.last_edited_by as PropertyValueUser); 91 | } 92 | } 93 | 94 | function formularize(formulaValue: PropertyValueFormula) { 95 | let value: unknown; 96 | 97 | switch (formulaValue.formula.type) { 98 | case 'string': 99 | value = formulaValue.formula.string; 100 | break; 101 | case 'number': 102 | value = formulaValue.formula.number; 103 | break; 104 | case 'boolean': 105 | value = formulaValue.formula.boolean; 106 | break; 107 | case 'date': 108 | if (formulaValue.formula.date?.end) { 109 | value = [ 110 | formulaValue.formula.date?.start, 111 | formulaValue.formula.date?.end, 112 | ]; 113 | } else { 114 | value = formulaValue.formula.date?.start; 115 | } 116 | break; 117 | } 118 | return value; 119 | } 120 | 121 | function rollup(rollupValue: PropertyValueRollup) { 122 | let value: unknown; 123 | 124 | switch (rollupValue.rollup.type) { 125 | case 'number': 126 | value = rollupValue.rollup.number; 127 | break; 128 | case 'date': 129 | if (rollupValue.rollup.date?.end) { 130 | value = [rollupValue.rollup.date?.start, rollupValue.rollup.date?.end]; 131 | } else { 132 | value = rollupValue.rollup.date?.start; 133 | } 134 | break; 135 | case 'array': 136 | value = rollupValue.rollup.array.map((propertyValue) => 137 | parsePropertyValue(propertyValue as unknown as GetPagePropertyResponse) 138 | ); 139 | break; 140 | } 141 | 142 | return value; 143 | } 144 | 145 | function personify(user: PropertyValueUser) { 146 | return { 147 | name: user.name, 148 | avatar: user.avatar_url, 149 | }; 150 | } 151 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | const STRING_CAMELIZE_REGEXP = /(-|_|\.|\s)+(.)?/g; 2 | 3 | /** 4 | Returns the lowerCamelCase form of a string. 5 | ```javascript 6 | camelize('innerHTML'); // 'innerHTML' 7 | camelize('action_name'); // 'actionName' 8 | camelize('css-class-name'); // 'cssClassName' 9 | camelize('my favorite items'); // 'myFavoriteItems' 10 | camelize('My Favorite Items'); // 'myFavoriteItems' 11 | ``` 12 | @method camelize 13 | @param {String} str The string to camelize. 14 | @return {String} the camelized string. 15 | */ 16 | export function camelize(str: string): string { 17 | return str 18 | .replace( 19 | STRING_CAMELIZE_REGEXP, 20 | (_match: string, _separator: string, chr: string) => { 21 | return chr ? chr.toUpperCase() : ''; 22 | } 23 | ) 24 | .replace(/^([A-Z])/, (match: string) => match.toLowerCase()); 25 | } 26 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/scully-plugin-notion/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/v4-types/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] 3 | } 4 | -------------------------------------------------------------------------------- /libs/v4-types/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/v4-types/README.md: -------------------------------------------------------------------------------- 1 | # @notion-stuff/v4-types 2 | 3 | 4 | This package is to cover the typings of `@notionhq/client@0.4`. 5 | 6 | Since v0.4, `@notionhq/client` uses generated typings for their API Responses which breaks all the Interfaces/Types downstream for community packages that depend on those Interfaces/Types. This package aims to address this issue by construct Type Aliases from `@notion/client` API Responses Types. 7 | 8 | ## Installation 9 | 10 | ```bash 11 | npm i --save-dev @notion-stuff/v4-types 12 | ``` 13 | or 14 | ```bash 15 | yarn add --dev @notion-stuff/v4-types 16 | ``` 17 | 18 | ## Feature 19 | - [x] Blocks 20 | - [x] Page Properties 21 | 22 | If you find anything missing, feel free to create an issue. 23 | -------------------------------------------------------------------------------- /libs/v4-types/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'v4-types', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 14 | coverageDirectory: '../../coverage/libs/v4-types', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/v4-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notion-stuff/v4-types", 3 | "version": "6.0.0", 4 | "dependencies": { 5 | "@notionhq/client": "~1.0.0" 6 | }, 7 | "peerDependencies": { 8 | "@scullyio/scully": "^2.0.0" 9 | }, 10 | "publishConfig": { 11 | "access": "public" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /libs/v4-types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/types'; 2 | -------------------------------------------------------------------------------- /libs/v4-types/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GetBlockResponse, 3 | GetDatabaseResponse, 4 | GetPagePropertyResponse, 5 | } from '@notionhq/client/build/src/api-endpoints'; 6 | 7 | /** Property **/ 8 | 9 | export type PostResult = Extract; 10 | export type PropertyValueType = GetPagePropertyResponse['type']; 11 | 12 | export type ExtractedPropertyValue = Extract< 13 | GetPagePropertyResponse, 14 | { type: TType } 15 | >; 16 | 17 | export type PropertyValuePropertyItem = ExtractedPropertyValue<'property_item'>; 18 | export type PropertyValueTitle = ExtractedPropertyValue<'title'>; 19 | export type PropertyValueRichText = ExtractedPropertyValue<'rich_text'>; 20 | export type PropertyValueNumber = ExtractedPropertyValue<'number'>; 21 | export type PropertyValueUrl = ExtractedPropertyValue<'url'>; 22 | export type PropertyValueSelect = ExtractedPropertyValue<'select'>; 23 | export type PropertyValueMultiSelect = ExtractedPropertyValue<'multi_select'>; 24 | export type PropertyValuePeople = ExtractedPropertyValue<'people'>; 25 | export type PropertyValueEmail = ExtractedPropertyValue<'email'>; 26 | export type PropertyValuePhoneNumber = ExtractedPropertyValue<'phone_number'>; 27 | export type PropertyValueDate = ExtractedPropertyValue<'date'>; 28 | export type PropertyValueFiles = ExtractedPropertyValue<'files'>; 29 | export type PropertyValueFormula = ExtractedPropertyValue<'formula'>; 30 | export type PropertyValueRelation = ExtractedPropertyValue<'relation'>; 31 | export type PropertyValueRollup = ExtractedPropertyValue<'rollup'>; 32 | export type PropertyValueCreatedTime = ExtractedPropertyValue<'created_time'>; 33 | export type PropertyValueCreatedBy = ExtractedPropertyValue<'created_by'>; 34 | export type PropertyValueEditedTime = 35 | ExtractedPropertyValue<'last_edited_time'>; 36 | export type PropertyValueEditedBy = ExtractedPropertyValue<'last_edited_by'>; 37 | export type PropertyValueCheckbox = ExtractedPropertyValue<'checkbox'>; 38 | 39 | /** People **/ 40 | export type PropertyValueUser = Extract< 41 | PropertyValuePeople['people'], 42 | { type: string } 43 | >; 44 | export type PropertyValueUserType = PropertyValueUser['type']; 45 | 46 | export type PropertyValueUserPerson = Extract< 47 | PropertyValueUser, 48 | { type: 'person' } 49 | >; 50 | export type PropertyValueUserBot = Extract; 51 | 52 | /** Block **/ 53 | export type Block = Extract; 54 | 55 | export type BlockType = Block['type']; 56 | 57 | export type ExtractedBlockType = Extract< 58 | Block, 59 | { type: TType } 60 | >; 61 | 62 | export type Blocks = Block[]; 63 | 64 | export type ParagraphBlock = ExtractedBlockType<'paragraph'>; 65 | 66 | export type HeadingOneBlock = ExtractedBlockType<'heading_1'>; 67 | export type HeadingTwoBlock = ExtractedBlockType<'heading_2'>; 68 | export type HeadingThreeBlock = ExtractedBlockType<'heading_3'>; 69 | 70 | export type HeadingBlock = 71 | | HeadingOneBlock 72 | | HeadingTwoBlock 73 | | HeadingThreeBlock; 74 | 75 | export type BulletedListItemBlock = ExtractedBlockType<'bulleted_list_item'>; 76 | export type NumberedListItemBlock = ExtractedBlockType<'numbered_list_item'>; 77 | 78 | export type TableBlock = ExtractedBlockType<'table'>; 79 | export type TableRowBlock = ExtractedBlockType<'table_row'>; 80 | 81 | export type QuoteBlock = ExtractedBlockType<'quote'>; 82 | export type EquationBlock = ExtractedBlockType<'equation'>; 83 | export type CodeBlock = ExtractedBlockType<'code'>; 84 | export type CalloutBlock = ExtractedBlockType<'callout'>; 85 | export type ToDoBlock = ExtractedBlockType<'to_do'>; 86 | export type BookmarkBlock = ExtractedBlockType<'bookmark'>; 87 | export type ToggleBlock = ExtractedBlockType<'toggle'>; 88 | export type TemplateBlock = ExtractedBlockType<'template'>; 89 | export type SyncedBlock = ExtractedBlockType<'synced_block'>; 90 | export type BreadcrumbBlock = ExtractedBlockType<'breadcrumb'>; 91 | 92 | export type ChildPageBlock = ExtractedBlockType<'child_page'>; 93 | export type ChildDatabaseBlock = ExtractedBlockType<'child_database'>; 94 | 95 | export type EmbedBlock = ExtractedBlockType<'embed'>; 96 | export type ImageBlock = ExtractedBlockType<'image'>; 97 | export type VideoBlock = ExtractedBlockType<'video'>; 98 | export type PDFBlock = ExtractedBlockType<'pdf'>; 99 | export type FileBlock = ExtractedBlockType<'file'>; 100 | export type AudioBlock = ExtractedBlockType<'audio'>; 101 | 102 | export type TocBlock = ExtractedBlockType<'table_of_contents'>; 103 | export type DividerBlock = ExtractedBlockType<'divider'>; 104 | 105 | export type ColumnBlock = ExtractedBlockType<'column'>; 106 | export type ColumnListBlock = ExtractedBlockType<'column_list'>; 107 | 108 | export type LinkPreviewBlock = ExtractedBlockType<'link_preview'>; 109 | export type LinkToPageBlock = ExtractedBlockType<'link_to_page'>; 110 | 111 | export type UnsupportedBlock = ExtractedBlockType<'unsupported'>; 112 | 113 | /** RichText **/ 114 | export type RichText = ParagraphBlock['paragraph']['rich_text'][number]; 115 | 116 | export type Annotations = RichText['annotations']; 117 | 118 | export type RichTextType = RichText['type']; 119 | 120 | export type ExtractedRichText = Extract< 121 | RichText, 122 | { type: TType } 123 | >; 124 | 125 | export type RichTextText = ExtractedRichText<'text'>; 126 | export type RichTextMention = ExtractedRichText<'mention'>; 127 | export type RichTextEquation = ExtractedRichText<'equation'>; 128 | 129 | /** File **/ 130 | export type File = ImageBlock['image']; 131 | 132 | export type FileType = File['type']; 133 | 134 | export type ExtractedFile = Extract< 135 | File, 136 | { type: TType } 137 | >; 138 | 139 | export type ExternalFileWithCaption = Omit< 140 | ExtractedFile<'external'>, 141 | 'caption' 142 | > & { caption?: ExtractedFile<'external'>['caption'] }; 143 | export type FileWithCaption = Omit, 'caption'> & { 144 | caption?: ExtractedFile<'file'>['caption']; 145 | }; 146 | 147 | /** Callout */ 148 | export type CalloutIcon = CalloutBlock['callout']['icon']; 149 | 150 | export type CalloutIconType = CalloutIcon['type']; 151 | 152 | export type ExtractedCalloutIcon = Extract< 153 | CalloutIcon, 154 | { type: TType } 155 | >; 156 | 157 | export type CalloutIconEmoji = ExtractedCalloutIcon<'emoji'>; 158 | export type CalloutIconExternal = ExtractedCalloutIcon<'external'>; 159 | export type CalloutIconFile = ExtractedCalloutIcon<'file'>; 160 | -------------------------------------------------------------------------------- /libs/v4-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/v4-types/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/v4-types/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "version": "13.6.0-beta.0", 5 | "description": "Remove old options that are no longer used", 6 | "cli": "nx", 7 | "implementation": "./src/migrations/update-13-6-0/remove-old-task-runner-options", 8 | "package": "@nrwl/workspace", 9 | "name": "13-6-0-remove-old-task-runner-options" 10 | }, 11 | { 12 | "version": "13.9.0-beta.0", 13 | "description": "Update the decorate-angular-cli script to require nx instead of @nrwl/cli", 14 | "cli": "nx", 15 | "implementation": "./src/migrations/update-13-9-0/update-decorate-cli", 16 | "package": "@nrwl/workspace", 17 | "name": "13-9-0-update-decorate-cli" 18 | }, 19 | { 20 | "version": "13.9.0-beta.0", 21 | "description": "Replace @nrwl/tao with nx", 22 | "cli": "nx", 23 | "implementation": "./src/migrations/update-13-9-0/replace-tao-with-nx", 24 | "package": "@nrwl/workspace", 25 | "name": "13-9-0-replace-tao-with-nx" 26 | }, 27 | { 28 | "version": "13.4.4-beta.0", 29 | "cli": "nx", 30 | "description": "Create a root babel config file if it doesn't exist and using babel-jest in jest.config.js and add @nrwl/web as needed", 31 | "factory": "./src/migrations/update-13-4-4/add-missing-root-babel-config", 32 | "package": "@nrwl/jest", 33 | "name": "add-missing-root-babel-config" 34 | }, 35 | { 36 | "cli": "nx", 37 | "version": "13.8.5-beta.1", 38 | "description": "Renames @nrwl/node:build to @nrwl/node:webpack", 39 | "factory": "./src/migrations/update-13-8-5/rename-build-to-webpack", 40 | "package": "@nrwl/node", 41 | "name": "rename-build-to-webpack" 42 | }, 43 | { 44 | "cli": "nx", 45 | "version": "13.8.5-beta.1", 46 | "description": "Renames @nrwl/node:execute to @nrwl/node:node", 47 | "factory": "./src/migrations/update-13-8-5/rename-execute-to-node", 48 | "package": "@nrwl/node", 49 | "name": "rename-execute-to-node" 50 | }, 51 | { 52 | "cli": "nx", 53 | "version": "13.8.5-beta.1", 54 | "description": "Renames @nrwl/node:package to @nrwl/js:tsc", 55 | "factory": "./src/migrations/update-13-8-5/update-package-to-tsc", 56 | "package": "@nrwl/node", 57 | "name": "update-package-to-tsc" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "notion-stuff", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "implicitDependencies": { 7 | "package.json": { 8 | "dependencies": "*", 9 | "devDependencies": "*" 10 | }, 11 | ".eslintrc.json": "*" 12 | }, 13 | "tasksRunnerOptions": { 14 | "default": { 15 | "runner": "@nrwl/workspace/tasks-runners/default", 16 | "options": { 17 | "cacheableOperations": [ 18 | "build", 19 | "lint", 20 | "test", 21 | "e2e" 22 | ], 23 | "parallel": 1 24 | } 25 | } 26 | }, 27 | "targetDependencies": { 28 | "build": [ 29 | { 30 | "target": "build", 31 | "projects": "dependencies" 32 | } 33 | ] 34 | }, 35 | "cli": { 36 | "defaultCollection": "@nrwl/workspace" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notion-stuff", 3 | "version": "6.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "nx serve", 7 | "test": "nx test", 8 | "commit": "git-cz", 9 | "release": "dotenv release-it --", 10 | "build": "nx run-many --target=build --all --verbose", 11 | "publish": "nx run-many --target=publish --all --parallel", 12 | "contributors:init": "all-contributors init", 13 | "contributors:add": "all-contributors add" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "tslib": "^2.3.1" 18 | }, 19 | "devDependencies": { 20 | "@notionhq/client": "~1.0.4", 21 | "@nrwl/cli": "13.9.5", 22 | "@nrwl/eslint-plugin-nx": "13.9.5", 23 | "@nrwl/jest": "13.9.5", 24 | "@nrwl/js": "13.9.5", 25 | "@nrwl/linter": "13.9.5", 26 | "@nrwl/node": "13.9.5", 27 | "@nrwl/workspace": "13.9.5", 28 | "@release-it/bumper": "3.0.1", 29 | "@release-it/conventional-changelog": "4.2.2", 30 | "@scullyio/scully": "2.1.29", 31 | "@types/jest": "27.4.1", 32 | "@types/marked": "4.0.3", 33 | "@types/node": "16.11.7", 34 | "@typescript-eslint/eslint-plugin": "5.17.0", 35 | "@typescript-eslint/parser": "5.17.0", 36 | "all-contributors-cli": "6.20.0", 37 | "commitizen": "4.2.4", 38 | "cz-conventional-changelog": "3.3.0", 39 | "cz-customizable": "6.3.0", 40 | "dotenv-cli": "5.1.0", 41 | "eslint": "8.12.0", 42 | "eslint-config-prettier": "8.5.0", 43 | "highlight.js": "11.5.0", 44 | "jest": "27.5.1", 45 | "marked": "4.0.12", 46 | "nx": "13.9.5", 47 | "prettier": "2.6.1", 48 | "prismjs": "1.27.0", 49 | "release-it": "14.14.0", 50 | "ts-jest": "27.1.4", 51 | "typescript": "~4.6.3" 52 | }, 53 | "config": { 54 | "commitizen": { 55 | "path": "./node_modules/cz-customizable" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nartc/notion-stuff/905af932a1e376997775e0288d5c833f90298260/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@notion-stuff/blocks-html-parser": [ 19 | "libs/blocks-html-parser/src/index.ts" 20 | ], 21 | "@notion-stuff/blocks-markdown-parser": [ 22 | "libs/blocks-markdown-parser/src/index.ts" 23 | ], 24 | "@notion-stuff/scully-plugin-notion": [ 25 | "libs/scully-plugin-notion/src/index.ts" 26 | ], 27 | "@notion-stuff/v4-types": ["libs/v4-types/src/index.ts"] 28 | } 29 | }, 30 | "exclude": ["node_modules", "tmp"] 31 | } 32 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "blocks-html-parser": { 5 | "root": "libs/blocks-html-parser", 6 | "sourceRoot": "libs/blocks-html-parser/src", 7 | "projectType": "library", 8 | "targets": { 9 | "build": { 10 | "executor": "@nrwl/js:tsc", 11 | "outputs": ["{options.outputPath}"], 12 | "options": { 13 | "outputPath": "dist/libs/blocks-html-parser", 14 | "tsConfig": "libs/blocks-html-parser/tsconfig.lib.json", 15 | "packageJson": "libs/blocks-html-parser/package.json", 16 | "main": "libs/blocks-html-parser/src/index.ts", 17 | "assets": ["libs/blocks-html-parser/*.md"] 18 | } 19 | }, 20 | "lint": { 21 | "executor": "@nrwl/linter:eslint", 22 | "outputs": ["{options.outputFile}"], 23 | "options": { 24 | "lintFilePatterns": ["libs/blocks-html-parser/**/*.ts"] 25 | } 26 | }, 27 | "test": { 28 | "executor": "@nrwl/jest:jest", 29 | "outputs": ["coverage/libs/blocks-html-parser"], 30 | "options": { 31 | "jestConfig": "libs/blocks-html-parser/jest.config.js", 32 | "passWithNoTests": true 33 | } 34 | }, 35 | "publish": { 36 | "executor": "@nrwl/workspace:run-commands", 37 | "options": { 38 | "command": "npm publish", 39 | "cwd": "dist/libs/blocks-html-parser" 40 | } 41 | } 42 | }, 43 | "tags": [] 44 | }, 45 | "blocks-markdown-parser": { 46 | "root": "libs/blocks-markdown-parser", 47 | "sourceRoot": "libs/blocks-markdown-parser/src", 48 | "projectType": "library", 49 | "targets": { 50 | "build": { 51 | "executor": "@nrwl/js:tsc", 52 | "outputs": ["{options.outputPath}"], 53 | "options": { 54 | "outputPath": "dist/libs/blocks-markdown-parser", 55 | "tsConfig": "libs/blocks-markdown-parser/tsconfig.lib.json", 56 | "packageJson": "libs/blocks-markdown-parser/package.json", 57 | "main": "libs/blocks-markdown-parser/src/index.ts", 58 | "assets": ["libs/blocks-markdown-parser/*.md"] 59 | } 60 | }, 61 | "lint": { 62 | "executor": "@nrwl/linter:eslint", 63 | "outputs": ["{options.outputFile}"], 64 | "options": { 65 | "lintFilePatterns": ["libs/blocks-markdown-parser/**/*.ts"] 66 | } 67 | }, 68 | "test": { 69 | "executor": "@nrwl/jest:jest", 70 | "outputs": ["coverage/libs/blocks-markdown-parser"], 71 | "options": { 72 | "jestConfig": "libs/blocks-markdown-parser/jest.config.js", 73 | "passWithNoTests": true 74 | } 75 | }, 76 | "publish": { 77 | "executor": "@nrwl/workspace:run-commands", 78 | "options": { 79 | "command": "npm publish", 80 | "cwd": "dist/libs/blocks-markdown-parser" 81 | } 82 | } 83 | }, 84 | "tags": [] 85 | }, 86 | "scully-plugin-notion": { 87 | "root": "libs/scully-plugin-notion", 88 | "sourceRoot": "libs/scully-plugin-notion/src", 89 | "projectType": "library", 90 | "targets": { 91 | "build": { 92 | "executor": "@nrwl/js:tsc", 93 | "outputs": ["{options.outputPath}"], 94 | "options": { 95 | "outputPath": "dist/libs/scully-plugin-notion", 96 | "tsConfig": "libs/scully-plugin-notion/tsconfig.lib.json", 97 | "packageJson": "libs/scully-plugin-notion/package.json", 98 | "main": "libs/scully-plugin-notion/src/index.ts", 99 | "assets": ["libs/scully-plugin-notion/*.md"] 100 | } 101 | }, 102 | "lint": { 103 | "executor": "@nrwl/linter:eslint", 104 | "outputs": ["{options.outputFile}"], 105 | "options": { 106 | "lintFilePatterns": ["libs/scully-plugin-notion/**/*.ts"] 107 | } 108 | }, 109 | "test": { 110 | "executor": "@nrwl/jest:jest", 111 | "outputs": ["coverage/libs/scully-plugin-notion"], 112 | "options": { 113 | "jestConfig": "libs/scully-plugin-notion/jest.config.js", 114 | "passWithNoTests": true 115 | } 116 | }, 117 | "publish": { 118 | "executor": "@nrwl/workspace:run-commands", 119 | "options": { 120 | "command": "npm publish", 121 | "cwd": "dist/libs/scully-plugin-notion" 122 | } 123 | } 124 | }, 125 | "tags": [] 126 | }, 127 | "v4-types": { 128 | "root": "libs/v4-types", 129 | "sourceRoot": "libs/v4-types/src", 130 | "projectType": "library", 131 | "targets": { 132 | "build": { 133 | "executor": "@nrwl/js:tsc", 134 | "outputs": ["{options.outputPath}"], 135 | "options": { 136 | "outputPath": "dist/libs/v4-types", 137 | "tsConfig": "libs/v4-types/tsconfig.lib.json", 138 | "packageJson": "libs/v4-types/package.json", 139 | "main": "libs/v4-types/src/index.ts", 140 | "assets": ["libs/v4-types/*.md"] 141 | } 142 | }, 143 | "lint": { 144 | "executor": "@nrwl/linter:eslint", 145 | "outputs": ["{options.outputFile}"], 146 | "options": { 147 | "lintFilePatterns": ["libs/v4-types/**/*.ts"] 148 | } 149 | }, 150 | "test": { 151 | "executor": "@nrwl/jest:jest", 152 | "outputs": ["coverage/libs/v4-types"], 153 | "options": { 154 | "jestConfig": "libs/v4-types/jest.config.js", 155 | "passWithNoTests": true 156 | } 157 | }, 158 | "publish": { 159 | "executor": "@nrwl/workspace:run-commands", 160 | "options": { 161 | "command": "npm publish", 162 | "cwd": "dist/libs/v4-types" 163 | } 164 | } 165 | }, 166 | "tags": ["scope:v4-types", "type:lib"] 167 | } 168 | } 169 | } 170 | --------------------------------------------------------------------------------