Showing preview only (238K chars total). Download the full file or copy to clipboard to get everything.
Repository: asnunes/notion-page-to-html
Branch: main
Commit: 0bd3d1d0c9ff
Files: 138
Total size: 203.2 KB
Directory structure:
gitextract_8y8r4zl3/
├── .eslintignore
├── .eslintrc.js
├── .github/
│ └── workflows/
│ ├── publish.yml
│ └── test.yml
├── .gitignore
├── .husky/
│ ├── pre-commit
│ └── pre-push
├── .lintstagedrc.json
├── .npmignore
├── .nvmrc
├── .prettierrc.js
├── Dockerfile.test
├── LICENSE
├── Makefile
├── README.md
├── docker-compose.test.yml
├── jest.config.js
├── package.json
├── src/
│ ├── __tests__/
│ │ └── mocks/
│ │ ├── blocks.ts
│ │ ├── html.ts
│ │ ├── img/
│ │ │ └── base64.ts
│ │ └── notion-api-responses.ts
│ ├── data/
│ │ ├── helpers/
│ │ │ ├── block-to-inner-html.ts
│ │ │ ├── block-to-inner-text.ts
│ │ │ ├── blocks-to-html.ts
│ │ │ ├── color-to-hex.ts
│ │ │ └── replace-line-break-to-br-tag.ts
│ │ ├── protocols/
│ │ │ ├── blocks/
│ │ │ │ ├── block.ts
│ │ │ │ ├── decorable-text.ts
│ │ │ │ ├── decoration.ts
│ │ │ │ ├── format.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── list-blocks-wrapper.ts
│ │ │ ├── html-options/
│ │ │ │ └── html-options.ts
│ │ │ ├── http-request/
│ │ │ │ ├── http-get-client.ts
│ │ │ │ ├── http-post-client.ts
│ │ │ │ ├── http-response.ts
│ │ │ │ └── index.ts
│ │ │ └── page-props/
│ │ │ ├── image-cover.ts
│ │ │ ├── index.ts
│ │ │ └── page-props.ts
│ │ └── use-cases/
│ │ ├── blocks-to-html-converter/
│ │ │ ├── block-dispatcher.ts
│ │ │ ├── block-parsers/
│ │ │ │ ├── callout.ts
│ │ │ │ ├── code.ts
│ │ │ │ ├── decorations/
│ │ │ │ │ ├── decoration-dispatcher.ts
│ │ │ │ │ ├── decoration-parsers/
│ │ │ │ │ │ ├── bold.ts
│ │ │ │ │ │ ├── code.ts
│ │ │ │ │ │ ├── color.test.ts
│ │ │ │ │ │ ├── color.ts
│ │ │ │ │ │ ├── equation.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── italic.ts
│ │ │ │ │ │ ├── link.ts
│ │ │ │ │ │ ├── strikethrough.ts
│ │ │ │ │ │ ├── underline.ts
│ │ │ │ │ │ └── unknown.ts
│ │ │ │ │ └── decorator.ts
│ │ │ │ ├── divider.ts
│ │ │ │ ├── equation.ts
│ │ │ │ ├── header.ts
│ │ │ │ ├── image.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── list/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── list-item.ts
│ │ │ │ │ └── list.ts
│ │ │ │ ├── page.ts
│ │ │ │ ├── quote.ts
│ │ │ │ ├── sub-header.ts
│ │ │ │ ├── sub-sub-header.ts
│ │ │ │ ├── text.ts
│ │ │ │ ├── to-do.ts
│ │ │ │ ├── toggle.ts
│ │ │ │ ├── unknown.ts
│ │ │ │ └── youtube-video.ts
│ │ │ ├── blocks-to-html-converter.test.ts
│ │ │ ├── blocks-to-html-converter.ts
│ │ │ ├── index.ts
│ │ │ └── list-blocks-wrapper.ts
│ │ ├── format-to-style/
│ │ │ ├── format-to-style.ts
│ │ │ └── index.ts
│ │ ├── html-wrapper/
│ │ │ ├── header-from-template.test.ts
│ │ │ ├── header-from-template.ts
│ │ │ ├── options-html-wrapper.ts
│ │ │ ├── scripts.ts
│ │ │ └── styles.ts
│ │ └── page-block-to-page-props/
│ │ ├── index.ts
│ │ ├── page-block-to-cover-image-block.ts
│ │ ├── page-block-to-icon.ts
│ │ ├── page-block-to-page-props.test.ts
│ │ ├── page-block-to-page-props.ts
│ │ └── page-block-to-title.ts
│ ├── domain/
│ │ └── use-cases/
│ │ ├── html-wrapper.ts
│ │ └── to-html.ts
│ ├── index.ts
│ ├── infra/
│ │ ├── errors/
│ │ │ ├── index.ts
│ │ │ ├── invalid-page-url.ts
│ │ │ ├── missing-content.ts
│ │ │ ├── missing-page-id.ts
│ │ │ ├── notion-page-access.ts
│ │ │ └── notion-page-not-found.ts
│ │ ├── protocols/
│ │ │ ├── notion-api-content-response.ts
│ │ │ └── validation.ts
│ │ └── use-cases/
│ │ ├── http-post/
│ │ │ └── node-http-post-client.ts
│ │ ├── to-blocks/
│ │ │ ├── decoration-array-to-decorations.ts
│ │ │ ├── format-filter.ts
│ │ │ ├── notion-api-content-response-to-blocks.test.ts
│ │ │ ├── notion-api-content-response-to-blocks.ts
│ │ │ ├── prop-title-to-decorable-texts.ts
│ │ │ └── properties-parser.ts
│ │ ├── to-notion-api-content-responses/
│ │ │ ├── notion-api-page-fetcher.test.ts
│ │ │ ├── notion-api-page-fetcher.ts
│ │ │ └── services/
│ │ │ ├── index.ts
│ │ │ ├── notion-page-id-validation.service.ts
│ │ │ ├── page-chunk-validation.service.test.ts
│ │ │ ├── page-chunk-validation.service.ts
│ │ │ └── page-record-validation.service.ts
│ │ └── to-page-id/
│ │ ├── index.ts
│ │ ├── notion-url-to-page-id.test.ts
│ │ ├── notion-url-to-page-id.ts
│ │ └── services/
│ │ ├── id-normalizer.ts
│ │ ├── index.ts
│ │ └── url-validator.ts
│ ├── main/
│ │ ├── factories/
│ │ │ ├── blocks-to-html.factory.ts
│ │ │ ├── index.ts
│ │ │ ├── notion-api-page-fetcher.factory.ts
│ │ │ └── notion-url-to-page-id.factory.ts
│ │ ├── protocols/
│ │ │ └── notion-page.ts
│ │ └── use-cases/
│ │ └── notion-api-to-html/
│ │ ├── index.ts
│ │ ├── notion-page-to-html.test.ts
│ │ └── notion-page-to-html.ts
│ └── utils/
│ ├── base-64-converter.ts
│ ├── either.ts
│ ├── errors/
│ │ ├── forbidden-error.ts
│ │ ├── image-not-found-error.ts
│ │ └── index.ts
│ └── use-cases/
│ └── http-get/
│ └── node-http-get.ts
├── tsconfig.build.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
node_modules
dist
coverage
./data
requirements
.vscode
*.jpeg
================================================
FILE: .eslintrc.js
================================================
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
extends: [
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
rules: {},
};
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish
on:
push:
branches: [main]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-20.04
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Dependencies installation
run: npm install
- name: Test run
run: npm test
- name: Build
run: npm run build
- name: Publish
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Build test compose
run: |
make test
================================================
FILE: .gitignore
================================================
/coverage
/node_modules
.vscode
.idea
/dist
script.js
test.html
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:staged
================================================
FILE: .husky/pre-push
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run test:ci
================================================
FILE: .lintstagedrc.json
================================================
{
"*.ts": ["eslint 'src/**' --fix", "npm run test:staged"]
}
================================================
FILE: .npmignore
================================================
__tests__
coverage
node_modules
src
docs
jest.config.js
tsconfig.json
.eslintignore
.eslintrc.js
.gitignore
.huskyrc.json
.nvmrc
.lintstagedrc.json
.prettierrc.js
.tool-versions
.vscode
script.js
test.html
docker-compose*
Dockerfile*
Makefile
.husky
.github
================================================
FILE: .nvmrc
================================================
12.19.0
================================================
FILE: .prettierrc.js
================================================
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
};
================================================
FILE: Dockerfile.test
================================================
FROM node:14.17
WORKDIR /usr/src/notion-page-to-html
RUN npm -g i npm
COPY ./package*.json ./
RUN npm install
COPY . .
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Alexandre Nunes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# TEST
test_compose = docker-compose -f docker-compose.test.yml
.PRONY: test-build
test-build:
$(test_compose) build
.PRONY: test
test:
make test-build && $(test_compose) run notion-page-to-html-test && make test-down
.PRONY: test-down
test-down:
$(test_compose) down
================================================
FILE: README.md
================================================

# Notion Page To HTML
NodeJS tool to convert public notion pages to HTML.
Also available as public API:
[https://notion-page-to-html-api.vercel.app/](https://notion-page-to-html-api.vercel.app/)
## Supported features
Most of the native Notion blocks are currently supported:
- Headings
- Text With Decorations
- Quote
- Image
- YouTube Videos
- Code
- Math Equations
- To-do
- Checkbox
- Bulleted Lists
- Numbered Lists
- Toggle Lists
- Divider
- Callout
- Nested blocks
Embeds and tables are not supported yet.
## Why notion-page-to-html?
It's perfect as content manager system
- This tool can get any public page from Notion and convert it to html. This is perfect
for the ones who want to use Notion as CMS. Once it gets page content from Notion, it becomes completely independent (images are converted to base64 so you do not have to call Notion again to get content). You can convert a page and then make it private again.
It's fully customizable
- You can choose how you want to get page content. Do you want title, cover, and icon in html body? You can do that! Do you want they apart of html so you can choose where place it? You have it. Do want html without style? Without Equation and Code Highlighting scripts? Do you want body content only? You have those options too.
## Basic Usage
Install it in a NodeJS project using npm
```bash
npm install notion-page-to-html
```
Then, just import it and paste a public Notion page url
```jsx
const NotionPageToHtml = require('notion-page-to-html');
// using async/await
async function getPage() {
const { title, icon, cover, html } = await NotionPageToHtml.convert("https://www.notion.so/asnunes/Simple-Page-Text-4d64bbc0634d4758befa85c5a3a6c22f");
console.log(title, icon, cover, html);
}
getPage();
```
`cover` is a base64 string from original page cover image. `icon` can be an emoji or base64 image based on original page icon. `html` is a full html document by default. It has style, body, MathJax and PrismJS CDN scripts by default. You can pass some options to handle html content.
```jsx
NotionPageToHtml.convert(
'https://www.notion.so/asnunes/Simple-Page-Text-4d64bbc0634d4758befa85c5a3a6c22f',
options,
);
```
`options` is an object with the following keys
| Key | Default value | If true |
| ----------------------- | ------------- | ------------------------------------------------------ |
| `excludeCSS` | false | returns html without style tag |
| `excludeMetadata` | false | returns html without metatags |
| `excludeScripts` | false | returns html without script tags |
| `excludeHeaderFromBody` | false | returns html without title, cover and icon inside body |
| `excludeTitleFromHead` | false | returns html without title tag in head |
| `bodyContentOnly` | false | returns html body tag content only |
---
## Development and testing
1. Clone this application
2. Make sure you have node v14 or higher and then install all dependencies
````
npm i
````
Running tests:
````
npm test
````
Installing locally in another project:
````
npm run build
npm pack
````
Inside your project:
````
npm i /path/to/tar/gz
````
Docker approach for testing
1. Make sure you have Docker and Docker Compose installed and then run:
````
make test
````
## Contributing
We love your feedback! Feel free to:
- Report a bug
- Discuss the current state of the code
- Submit a fix
- Propose new features
- Become a maintainer
Just create a GitHub issue or a PR ;)
================================================
FILE: docker-compose.test.yml
================================================
version: '3.8'
services:
notion-page-to-html-test:
build:
context: .
dockerfile: Dockerfile.test
container_name: notion-page-to-html-test
logging:
driver: 'json-file'
options:
max-size: '10m'
max-file: '5'
command: |
npm test
================================================
FILE: jest.config.js
================================================
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleDirectories: ['node_modules'],
transform: {
'.+\\.ts$': 'ts-jest',
},
testMatch: ['<rootDir>/src/**/*.(test|spec).ts'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
collectCoverageFrom: ['src/**/*.ts', '!src/migrations/*.ts', '!src/server.ts', '!src/protocols/*.ts'],
coverageProvider: 'babel',
coverageDirectory: 'coverage',
restoreMocks: true,
};
================================================
FILE: package.json
================================================
{
"name": "notion-page-to-html",
"version": "1.2.0",
"description": "It converts public notion pages to html from url",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prebuild": "rm -rf ./dist",
"build": "tsc -p tsconfig.build.json",
"lint": "eslint '*/**/*.{js,ts}' --quiet --fix",
"lint:staged": "lint-staged",
"test": "jest --passWithNoTests --silent --noStackTrace --runInBand",
"test:watch": "npm test -- --watch",
"test:verbose": "jest --passWithNoTests --runInBand",
"test:staged": "npm test -- --findRelatedTests",
"test:ci": "npm test -- --coverage"
},
"repository": {
"type": "git",
"url": "git+https://github.com/asnunes/notion-page-to-html.git"
},
"keywords": [
"notion",
"page",
"html"
],
"author": "Alexandre Nunes",
"license": "MIT",
"bugs": {
"url": "https://github.com/asnunes/notion-page-to-html/issues"
},
"homepage": "https://github.com/asnunes/notion-page-to-html#readme",
"devDependencies": {
"@types/jest": "^27.0.2",
"@types/nock": "^11.1.0",
"@typescript-eslint/eslint-plugin": "^4.31.2",
"@typescript-eslint/parser": "^4.31.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"git-commit-msg-linter": "^3.2.8",
"husky": "^7.0.2",
"jest": "^27.2.2",
"lint-staged": "^11.1.2",
"nock": "^13.1.3",
"prettier": "^2.4.1",
"ts-jest": "^27.0.5",
"typescript": "^4.4.3"
}
}
================================================
FILE: src/__tests__/mocks/blocks.ts
================================================
import { Block, DecorableText, DecorationType, Decoration } from '../../data/protocols/blocks';
export const NO_TEXT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [],
},
];
export const SINGLE_TEXT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
];
export const SINGLE_TEXT_WITH_CHILDREN = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f41214426',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child',
decorations: [],
},
],
},
{
id: '80d0fc46-5511-4d1d-a4ec-8b1212114426',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child too',
decorations: [],
},
],
},
] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
];
export const SINGLE_TEXT_WITH_BOLD = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_ITALIC = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World',
decorations: [
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_BOLD_AND_ITALIC_SEPARATED = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [{ type: 'bold' }],
},
{
text: 'World',
decorations: [{ type: 'italic' }],
},
],
},
];
export const SINGLE_TEXT_WITH_UNDERLINE = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World',
decorations: [
{
type: 'underline' as DecorationType,
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_STRIKETHROUGH = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World',
decorations: [
{
type: 'strikethrough' as DecorationType,
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_CODE_DECORATION = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'myVar',
decorations: [
{
type: 'code' as DecorationType,
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_LINK = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World',
decorations: [
{
type: 'link' as DecorationType,
value: 'https://www.google.com',
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: { block_color: 'red_background' },
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World',
decorations: [
{
type: 'link' as DecorationType,
value: 'https://www.google.com',
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_EQUATION_DECORATION = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World ',
decorations: [],
},
{
text: '⁍',
decorations: [
{
type: 'equation' as DecorationType,
value: '2x',
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_COLOR = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello',
decorations: [
{
type: 'color' as DecorationType,
value: 'purple',
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_COLOR_BACKGROUND = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello',
decorations: [
{
type: 'color' as DecorationType,
value: 'yellow_background',
},
],
},
],
},
];
export const SINGLE_TEXT_WITH_BOLD_AND_ITALIC = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const TEXT_WITH_DECORATION = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const MULTILINE_TEXT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World\nIs everything alright?\nYes, Dude!',
decorations: [],
},
],
},
];
export const TEXT_WITH_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {
block_color: 'red_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a text with red background',
decorations: [],
},
],
},
];
export const TEXT_WITH_FORMAT_FOREGROUND = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {
block_color: 'purple',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a text with purple color',
decorations: [],
},
],
},
];
export const H1_TEXT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'header',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h1 title',
decorations: [],
},
],
},
];
export const H1_TEXT_WITH_DECORATIONS = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'header',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const H1_WITH_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'header',
properties: {},
format: {
block_color: 'green_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h1 with red background',
decorations: [],
},
],
},
];
export const H1_WITH_FORMAT_FOREGROUND = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'header',
properties: {},
format: {
block_color: 'yellow',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h1 with yellow color',
decorations: [],
},
],
},
];
export const H2_TEXT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_header',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h2 title',
decorations: [],
},
],
},
];
export const H2_TEXT_WITH_DECORATIONS = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_header',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const H2_WITH_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_header',
properties: {},
format: {
block_color: 'yellow_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h2 with yellow background',
decorations: [],
},
],
},
];
export const H2_WITH_FORMAT_FOREGROUND = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_header',
properties: {},
format: {
block_color: 'gray',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h2 with gray color',
decorations: [],
},
],
},
];
export const H3_TEXT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_sub_header',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h3 title',
decorations: [],
},
],
},
];
export const H3_TEXT_WITH_DECORATIONS = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_sub_header',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const H3_WITH_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_sub_header',
properties: {},
format: {
block_color: 'orange_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h3 with orange background',
decorations: [],
},
],
},
];
export const H3_WITH_FORMAT_FOREGROUND = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'sub_sub_header',
properties: {},
format: {
block_color: 'brown',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a h3 with brown color',
decorations: [],
},
],
},
];
export const UNORDERED_LIST_WITH_SINGLE_ITEM = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'bulleted_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test',
decorations: [],
},
],
},
];
export const UNORDERED_LIST_WITH_CHILDREN = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'bulleted_list',
properties: {},
format: {},
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f41214426',
type: 'bulleted_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child',
decorations: [],
},
],
},
{
id: '80d0fc46-5511-4d1d-a4ec-8b1212114426',
type: 'bulleted_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child too',
decorations: [],
},
],
},
] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
];
export const UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'bulleted_list',
properties: {},
format: {
block_color: 'brown_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a item with background',
decorations: [],
},
],
},
];
export const UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'bulleted_list',
properties: {},
format: {
block_color: 'orange',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a item with color',
decorations: [],
},
],
},
];
export const UNORDERED_LIST_WITH_TWO_ITEMS = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'bulleted_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test',
decorations: [],
},
],
},
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d752133',
type: 'bulleted_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test too',
decorations: [],
},
],
},
];
export const UNORDERED_LIST_WITH_DECORATED_ITEMS = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'bulleted_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const ORDERED_LIST_WITH_SINGLE_ITEM = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'numbered_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test',
decorations: [],
},
],
},
];
export const ORDERED_LIST_WITH_CHILDREN = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'numbered_list',
properties: {},
format: {},
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f41214426',
type: 'numbered_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child',
decorations: [],
},
],
},
{
id: '80d0fc46-5511-4d1d-a4ec-8b1212114426',
type: 'numbered_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child too',
decorations: [],
},
],
},
] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
];
export const ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'numbered_list',
properties: {},
format: {
block_color: 'gray_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a item with background',
decorations: [],
},
],
},
];
export const ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'numbered_list',
properties: {},
format: {
block_color: 'green',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a item with color',
decorations: [],
},
],
},
];
export const ORDERED_LIST_WITH_TWO_ITEMS = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'numbered_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test',
decorations: [],
},
],
},
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d752133',
type: 'numbered_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test too',
decorations: [],
},
],
},
];
export const ORDERED_LIST_WITH_DECORATED_ITEMS = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'numbered_list',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const TODO = [
{
id: 'd1e33c43-5079-4e66-961a-df032d38d532',
type: 'to_do',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test',
decorations: [],
},
],
},
];
export const TODO_WITH_CHILDREN = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'to_do',
properties: {},
format: {},
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f41214426',
type: 'to_do',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child',
decorations: [],
},
],
},
{
id: '80d0fc46-5511-4d1d-a4ec-8b1212114426',
type: 'to_do',
properties: { checked: 'Yes' },
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a child too',
decorations: [],
},
],
},
] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
];
export const TODO_WITH_FORMAT = [
{
id: 'd1e33c43-5079-4e66-961a-df032d38d532',
type: 'to_do',
properties: {},
format: {
block_color: 'blue_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a todo with style',
decorations: [],
},
],
},
];
export const TODO_WITH_FORMAT_FOREGROUND = [
{
id: 'd1e33c43-5079-4e66-961a-df032d38d532',
type: 'to_do',
properties: {},
format: {
block_color: 'blue',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a todo with style',
decorations: [],
},
],
},
];
export const CHECKED_TODO = [
{
id: 'd1e33c43-5079-4e66-961a-df032d38d532',
type: 'to_do',
properties: { checked: 'Yes' },
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test',
decorations: [],
},
],
},
];
export const UNCHECKED_AND_CHECKED_TODOS = [
{
id: 'd1e33c43-5079-4e66-961a-df032d2332',
type: 'to_do',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test',
decorations: [],
},
],
},
{
id: 'd1e33c43-5079-4e66-961a-df032d38d532',
type: 'to_do',
properties: { checked: 'Yes' },
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a test too',
decorations: [],
},
],
},
];
export const CODE = [
{
id: '479c7b34-6c22-4f2d-b947-8f47d02b48d6',
type: 'code',
properties: { language: 'JavaScript' },
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'function test() {\n\tvar isTesting = true;\n\treturn isTesting;\n}',
decorations: [],
},
],
},
];
export const CODE_WITH_DECORATION = [
{
id: '479c7b34-6c22-4f2d-b947-8f47d02b48d6',
type: 'code',
properties: { language: 'JavaScript' },
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'function test() {\n\tvar isTesting = true;\n\treturn ',
decorations: [],
},
{
text: 'isTesting',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ';\n}',
decorations: [],
},
],
},
];
export const QUOTE = [
{
id: 'e0a0cfa3-1f64-438b-ac79-95e5c7ad4565',
type: 'quote',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This a quote',
decorations: [] as Decoration[],
},
],
},
];
export const QUOTE_WITH_FORMAT = [
{
id: 'e0a0cfa3-1f64-438b-ac79-95e5c7ad4565',
type: 'quote',
properties: {},
format: {
block_color: 'purple_background',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This a quote with background',
decorations: [] as Decoration[],
},
],
},
];
export const QUOTE_WITH_FORMAT_FOREGROUND = [
{
id: 'e0a0cfa3-1f64-438b-ac79-95e5c7ad4565',
type: 'quote',
properties: {},
format: {
block_color: 'pink',
},
children: [] as Block[],
decorableTexts: [
{
text: 'This a quote with color',
decorations: [] as Decoration[],
},
],
},
];
export const QUOTE_WITH_DECORATION = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'quote',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
];
export const TEXT_BETWEEN_DIVIDER = [
{
id: 'e0a0cfa3-438b-ac79-95e5c7ad4565',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This a text',
decorations: [],
},
],
},
{
id: 'e0a0cfa3-1f64-438b-ac79-95e5c7ad4565',
type: 'divider',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [],
},
{
id: 'e0a0cfa3-438b-95e5c7ad4565',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This a text too',
decorations: [],
},
],
},
];
export const EMPTY_EQUATION = [
{
id: '9b01339a-9de6-4eb1-bd7a-4c6d537590c7',
type: 'equation',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [],
},
];
export const EQUATION = [
{
id: '9b01339a-9de6-4eb1-bd7a-4c6d537590c7',
type: 'equation',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: '\\int 2xdx = x^2 + C',
decorations: [],
},
],
},
];
export const NO_YOUTUBE_VIDEO = [
{
id: 'dcde43cb-7131-4687-8f22-c9789fa75f46',
type: 'video',
properties: {
source: 'https://www.example.com/watch?v=8G80nuEyDN4',
},
format: {},
children: [] as Block[],
decorableTexts: [],
},
];
export const YOUTUBE_VIDEO = [
{
id: 'dcde43cb-7131-4687-8f22-c9789fa75f46',
type: 'video',
properties: {
source: 'https://www.youtube.com/watch?v=xBFqxBfLJWc',
},
format: {},
children: [] as Block[],
decorableTexts: [],
},
];
export const TEXT_WITH_YOUTUBE_VIDEO = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: {},
format: {},
decorableTexts: [{ text: 'Simple Page Test', decorations: [] }],
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [] as Decoration[],
},
],
},
{
id: 'dcde43cb-7131-4687-8f22-c9789fa75f46',
type: 'video',
properties: {
source: 'https://www.youtube.com/watch?v=xBFqxBfLJWc',
},
format: {},
children: [] as Block[],
decorableTexts: [] as DecorableText[],
},
],
},
];
export const PAGE_WITH_YOUTUBE_VIDEO = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: {},
format: {},
decorableTexts: [{ text: 'Simple Page Test', decorations: [] }],
children: [
{
id: 'dcde43cb-7131-4687-8f22-c9789fa75f46',
type: 'video',
properties: {
source: 'https://www.youtube.com/watch?v=xBFqxBfLJWc',
},
format: {},
children: [] as Block[],
decorableTexts: [] as DecorableText[],
},
],
},
];
export const IMAGE = [
{
id: 'ec3b36fd-f77d-46b4-8592-5966488612b1',
type: 'image',
properties: {
source:
'https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bcedd078-56cd-4137-a28a-af16b5746874/767-50x50.jpg',
},
format: {},
children: [] as Block[],
decorableTexts: [],
},
];
export const IMAGE_WITH_CAPTION = [
{
id: 'ec3b36fd-f77d-46b4-8592-5966488612b1',
type: 'image',
properties: {
source:
'https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bcedd078-56cd-4137-a28a-af16b5746874/767-50x50.jpg',
caption: 'It is a caption',
},
format: {},
children: [] as Block[],
decorableTexts: [],
},
];
export const IMAGE_WITH_CUSTOM_SIZE = [
{
id: 'ec3b36fd-f77d-46b4-8592-5966488612b1',
type: 'image',
properties: {
source:
'https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bcedd078-56cd-4137-a28a-af16b5746874/767-50x50.jpg',
},
format: { block_width: 240 },
children: [] as Block[],
decorableTexts: [],
},
];
export const CALLOUT = [
{
id: '16431c64-3bf0-481f-a29f-d544780d84f3',
type: 'callout',
properties: { page_icon: '💡' },
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a callout',
decorations: [],
},
],
},
];
export const CALLOUT_WITH_IMAGE = [
{
id: '16431c64-3bf0-481f-a29f-d544780d84f3',
type: 'callout',
properties: { page_icon: 'https://example.com/image.png' },
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'This is a callout',
decorations: [],
},
],
},
];
export const CALLOUT_WITH_BACKGROUND = [
{
id: '16431c64-3bf0-481f-a29f-d544780d84f3',
type: 'callout',
properties: { page_icon: '💡' },
format: { block_color: 'red_background' },
children: [] as Block[],
decorableTexts: [
{
text: 'This is a callout',
decorations: [],
},
],
},
];
export const DETAILS_WITH_DECORATION = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: {},
format: {},
decorableTexts: [{ text: 'Simple Page Test', decorations: [] }],
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'toggle',
properties: {},
format: {},
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
],
decorableTexts: [
{
text: 'Hello ',
decorations: [],
},
{
text: 'World ',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
{
text: 'and',
decorations: [
{
type: 'bold' as DecorationType,
},
],
},
{
text: ' Sun',
decorations: [
{
type: 'bold' as DecorationType,
},
{
type: 'italic' as DecorationType,
},
],
},
],
},
],
},
];
export const DETAILS = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: {},
format: {},
decorableTexts: [{ text: 'Simple Page Test', decorations: [] }],
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'toggle',
properties: {},
format: {},
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
],
decorableTexts: [
{
text: 'This is a detail',
decorations: [] as Decoration[],
},
],
},
],
},
];
export const DETAILS_WITH_BG = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: {},
format: {},
decorableTexts: [{ text: 'Simple Page Test', decorations: [] }],
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'toggle',
properties: {},
format: {
block_color: 'red_background',
},
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
],
decorableTexts: [
{
text: 'This is a detail',
decorations: [] as Decoration[],
},
],
},
],
},
];
export const UNKNOWN = [
{
id: 'd1e33c43-5079-4e66-961a-df032d38d532',
type: 'headdfafdafader',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'What?!',
decorations: [],
},
],
},
];
export const PAGE_WITH_TITLE = {
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: {},
format: {},
decorableTexts: [{ text: 'Simple Page Title', decorations: [] }],
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
],
};
export const PAGE_WITHOUT_TITLE = {
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: {},
format: {},
decorableTexts: [],
children: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {},
format: {},
children: [] as Block[],
decorableTexts: [
{
text: 'Hello World',
decorations: [],
},
],
},
],
};
export const PAGE_WITH_TITLE_AND_COVER_IMAGE = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { page_cover: '/images/page-cover/solid_blue.png' },
format: { page_cover_position: 0.6 },
decorableTexts: [{ text: 'Page Title', decorations: [] }],
children: [],
},
];
export const PAGE_WITH_TITLE_AND_ICON = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { page_icon: '🤴' },
format: {},
decorableTexts: [{ text: 'Page Title', decorations: [] }],
children: [],
},
];
export const PAGE_WITH_TITLE_AND_COVER_IMAGE_NOT_FROM_NOTION = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { page_cover: 'https://www.example.com/some_image.png' },
format: { page_cover_position: 0.6 },
decorableTexts: [{ text: 'Page Title', decorations: [] }],
children: [],
},
];
export const PAGE_WITH_TITLE_AND_INVALID_COVER_IMAGE = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { page_cover: 'https://www.example.com/s.html' },
format: { page_cover_position: 0.6 },
decorableTexts: [{ text: 'Page Title', decorations: [] }],
children: [],
},
];
export const PAGE_WITH_TITLE_AND_EMOJI_ICON = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { page_icon: '🤴' },
format: {},
decorableTexts: [{ text: 'Page Title', decorations: [] }],
children: [],
},
];
export const PAGE_WITH_TITLE_AND_IMAGE_ICON = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { page_icon: 'https://www.example.com/some_image.png' },
format: {},
decorableTexts: [{ text: 'Page Title', decorations: [] }],
children: [],
},
];
================================================
FILE: src/__tests__/mocks/html.ts
================================================
import base64 from './img/base64';
const STYLE_TAG = `\
<style>
html {
-webkit-print-color-adjust: exact;
}
* {
box-sizing: border-box;
-webkit-print-color-adjust: exact;
}
html,
body {
margin: 0;
padding: 0;
font-family: system-ui, sans-serif;
color: #37352F;
}
body {
line-height: 1.5;
}
@media only screen {
body {
margin: 2em auto;
max-width: 900px;
color: rgb(55, 53, 47);
}
}
img {
max-width: 100%;
max-height: 70vh;
}
ol,
ul {
margin: 0;
margin-block-start: 0.6em;
margin-block-end: 0.6em;
}
li > ol:first-child,
li > ul:first-child {
margin-block-start: 0.6em;
}
ul > li {
list-style: disc;
}
ul.to-do-list {
text-indent: -1.7em;
}
ul.to-do-list > li {
list-style: none;
}
.to-do-children-checked {
text-decoration: line-through;
opacity: 0.375;
}
ul.toggle > li {
list-style: none;
}
ul {
padding-inline-start: 1.7em;
}
ul > li {
padding-left: 0.1em;
}
ol {
padding-inline-start: 1.6em;
}
ol > li {
padding-left: 0.2em;
}
.mono ol {
padding-inline-start: 2em;
}
.mono ol > li {
text-indent: -0.4em;
}
.toggle {
padding-inline-start: 0em;
list-style-type: none;
}
/* Indent toggle children */
.toggle > li > details {
padding-left: 1.7em;
}
.toggle > li > details > summary {
margin-left: -1.1em;
}
h1,
h2,
h3 {
letter-spacing: -0.01em;
line-height: 1.2;
font-weight: 600;
margin-bottom: 0;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
margin-top: 0;
margin-bottom: 0.75em;
}
.icon {
display: inline-block;
max-width: 1.2em;
max-height: 1.2em;
text-decoration: none;
vertical-align: text-bottom;
margin-right: 0.5em;
}
img.icon {
border-radius: 3px;
}
.page-cover-image {
display: block;
object-fit: cover;
width: 100%;
height: 30vh;
}
.page-header-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.page-header-icon-with-cover {
margin-top: -0.72em;
margin-left: 0.07em;
}
.page-header-icon img {
border-radius: 3px;
}
blockquote {
font-size: 1.25em;
margin: 1em 0;
padding-left: 1em;
border-left: 3px solid rgb(55, 53, 47);
}
h1 {
font-size: 1.875rem;
margin-top: 1.875rem;
}
h2 {
font-size: 1.5rem;
margin-top: 1.5rem;
}
h3 {
font-size: 1.25rem;
margin-top: 1.25rem;
}
.callout {
padding: 16px 16px 16px 12px;
display: flex;
width: 100%;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
align-items: center;
justify-content: center;
}
.callout-emoji {
height: 21.6px;
width: 21.6px;
font-size: 21.6px;
line-height: 1.1;
margin-left: 0px;
}
.callout p {
max-width: 100%;
width: 100%;
white-space: pre-wrap;
word-break: break-word;
margin-left: 10px;
}
.image {
border: none;
margin: 1.5em 0;
padding: 0;
border-radius: 0;
text-align: center;
}
figure {
margin: 1.25em 0;
page-break-inside: avoid;
}
figcaption {
opacity: 0.5;
font-size: 85%;
margin-top: 0.5em;
}
hr {
background: transparent;
display: block;
width: 100%;
height: 1px;
visibility: visible;
border: none;
border-bottom: 1px solid rgba(55, 53, 47, 0.09);
}
.checkbox {
display: inline-flex;
vertical-align: text-bottom;
width: 16px;
height: 16px;
background-size: 16px;
margin-left: 2px;
margin-right: 5px;
}
.checkbox-on {
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Crect%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%2358A9D7%22%2F%3E%0A%3Cpath%20d%3D%22M6.71429%2012.2852L14%204.9995L12.7143%203.71436L6.71429%209.71378L3.28571%206.2831L2%207.57092L6.71429%2012.2852Z%22%20fill%3D%22white%22%2F%3E%0A%3C%2Fsvg%3E");
}
.checkbox-off {
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Crect%20x%3D%220.75%22%20y%3D%220.75%22%20width%3D%2214.5%22%20height%3D%2214.5%22%20fill%3D%22white%22%20stroke%3D%22%2336352F%22%20stroke-width%3D%221.5%22%2F%3E%0A%3C%2Fsvg%3E");
}
</style>
`;
const HEADER = `\
<header>
<img class="page-cover-image" src="${base64}" style="object-position:center 40%">
<div class="page-header-icon page-header-icon-with-cover">
<span class="icon">🤴</span>
</div>
<h1 class="page-title">Simple Page Test</h1>
</header>
`;
const CONTENT_WITH_HEADER = `\
${HEADER}
<p>Hello World</p>
`;
const CONTENT_WITHOUT_HEADER = `\
<p>Hello World</p>
`;
export const FULL_DOCUMENT = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
${STYLE_TAG}
<title>Simple Page Test</title>
<link href="https://unpkg.com/prismjs@1.22.0/themes/prism.css" rel="stylesheet">
</head>
<body>
${CONTENT_WITH_HEADER}
<script src="https://unpkg.com/prismjs@1.22.0/components/prism-core.min.js"></script>
<script src="https://unpkg.com/prismjs@1.22.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
</script>
</body>
</html>
`;
export const DOCUMENT_WITHOUT_TITLE = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
${STYLE_TAG}
<link href="https://unpkg.com/prismjs@1.22.0/themes/prism.css" rel="stylesheet">
</head>
<body>
${CONTENT_WITH_HEADER}
<script src="https://unpkg.com/prismjs@1.22.0/components/prism-core.min.js"></script>
<script src="https://unpkg.com/prismjs@1.22.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
</script>
</body>
</html>
`;
export const DOCUMENT_WITHOUT_CSS = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Simple Page Test</title>
<link href="https://unpkg.com/prismjs@1.22.0/themes/prism.css" rel="stylesheet">
</head>
<body>
${CONTENT_WITH_HEADER}
<script src="https://unpkg.com/prismjs@1.22.0/components/prism-core.min.js"></script>
<script src="https://unpkg.com/prismjs@1.22.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
</script>
</body>
</html>
`;
export const DOCUMENT_METADATA = `
<!DOCTYPE html>
<html>
<head>
${STYLE_TAG}
<title>Simple Page Test</title>
<link href="https://unpkg.com/prismjs@1.22.0/themes/prism.css" rel="stylesheet">
</head>
<body>
${CONTENT_WITH_HEADER}
<script src="https://unpkg.com/prismjs@1.22.0/components/prism-core.min.js"></script>
<script src="https://unpkg.com/prismjs@1.22.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
</script>
</body>
</html>
`;
export const DOCUMENT_WITHOUT_SCRIPTS = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
${STYLE_TAG}
<title>Simple Page Test</title>
</head>
<body>
${CONTENT_WITH_HEADER}
</body>
</html>
`;
export const FULL_DOCUMENT_WITHOUT_HEADER_IN_BODY = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
${STYLE_TAG}
<title>Simple Page Test</title>
<link href="https://unpkg.com/prismjs@1.22.0/themes/prism.css" rel="stylesheet">
</head>
<body>
${CONTENT_WITHOUT_HEADER}
<script src="https://unpkg.com/prismjs@1.22.0/components/prism-core.min.js"></script>
<script src="https://unpkg.com/prismjs@1.22.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
</script>
</body>
</html>
`;
export const BODY_ONLY = CONTENT_WITHOUT_HEADER;
export const HEADER_WITH_TITLE_ONLY = `\
<header>
<h1 class="page-title">This is a title</h1>
</header>
`;
export const HEADER_WITH_TITLE_AND_COVER_IMAGE = `\
<header>
<img class="page-cover-image" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD" style="object-position:center 15%">
<h1 class="page-title">This is a title</h1>
</header>
`;
export const HEADER_WITH_TITLE_AND_COVER_IMAGE_WITHOUT_POSITION = `\
<header>
<img class="page-cover-image" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD" style="object-position:center 0%">
<h1 class="page-title">This is a title</h1>
</header>
`;
export const HEADER_WITH_TITLE_COVER_IMAGE_AND_IMAGE_ICON = `\
<header>
<img class="page-cover-image" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD" style="object-position:center 0%">
<div class="page-header-icon page-header-icon-with-cover">
<img class="icon" src="data:image/jpeg;base64,/4QDeRXhpZgAASUkqAAgAAAAGABIBAwA">
</div>
<h1 class="page-title">This is a title</h1>
</header>
`;
export const HEADER_WITH_TITLE_AND_IMAGE_ICON = `\
<header>
<div class="page-header-icon">
<img class="icon" src="data:image/jpeg;base64,/4QDeRXhpZgAASUkqAAgAAAAGABIBAwA">
</div>
<h1 class="page-title">This is a title</h1>
</header>
`;
export const HEADER_WITH_TITLE_AND_EMOJI_ICON = `\
<header>
<div class="page-header-icon">
<span class="icon">🤴</span>
</div>
<h1 class="page-title">This is a title</h1>
</header>
`;
================================================
FILE: src/__tests__/mocks/img/base64.ts
================================================
export default 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QDeRXhpZgAASUkqAAgAAAAGABIBAwABAAAAAQAAABoBBQABAAAAVgAAABsBBQABAAAAXgAAACgBAwABAAAAAgAAABMCAwABAAAAAQAAAGmHBAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAAABwAAkAcABAAAADAyMTABkQcABAAAAAECAwCGkgcAFgAAAMAAAAAAoAcABAAAADAxMDABoAMAAQAAAP//AAACoAQAAQAAADIAAAADoAQAAQAAADIAAAAAAAAAQVNDSUkAAABQaWNzdW0gSUQ6IDc2N//bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAkLicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/CABEIADIAMgMBIgACEQEDEQH/xAAaAAACAwEBAAAAAAAAAAAAAAAABAIDBQEG/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQIA/9oADAMBAAIQAxAAAAHGlKUujtef0ZMgiKuMiVWdaFVp1gcFl5kOj4nmqc1lvTuUjUvotSFQQDKzC5dpDbloDSAn/8QAIBAAAgICAwADAQAAAAAAAAAAAQIAAwQREhMxECFBIv/aAAgBAQABBQITUoUc87mk1Ll0+zB8VTMclz632Sp2PBB/LZNnJxudRIGCWHE6SlmCY9e3xaNMqqi6MpyF6e6qqdrWFNkgts1MRrTbsgiNOzQW2CwKbuFo+BB40/B7+//EABkRAAIDAQAAAAAAAAAAAAAAAAERABAgAv/aAAgBAwEBPwHAis9E04xn/8QAGxEAAgMAAwAAAAAAAAAAAAAAAAEQERICIGH/2gAIAQIBAT8BhRaPTSNIXFKKMdf/xAArEAACAAQCCAcBAAAAAAAAAAAAAQIRITEQMgMSIkFRgZGhIDBhYnFyscH/2gAIAQEABj8CwX8JLSOTEUqvB6knO2EzL2OeEyj3YO9hRatxHD5NrSLkbGk61El1GvbF+kH1W82YZsqJKrZlNaC/ArOCIz9saXLlyZm8j//EACEQAAMAAgIBBQEAAAAAAAAAAAABESExUWFBIHGBkbHw/9oACAEBAAE/IUFkKXb3K2SXGUa2PJHn2MbyF8ksV4MF42SjRVNQLVSSPWULgyrJYv0rYDUOHRXCGL0R+CO03ZMHWzv6bFswS/2SdJvkvFE5G06e4FMhPoH4VpTCJjM2eZMILvyOB4Hv5kH+Fo36ESKEqPwiGst8htBOkTwiaEHRSNOjGKpwXa36G0Ws9Gbt9+lq/IdxljSrB//aAAwDAQACAAMAAAAQ6lbzVmrzq95M89+9/8QAHREAAgEEAwAAAAAAAAAAAAAAAAERECAhMUFRYf/aAAgBAwEBPxCCMnFEb0MPG6YTSJOh4W//xAAZEQEBAQEBAQAAAAAAAAAAAAABABEgMVH/2gAIAQIBAT8Q2HBJNs/a9hwSJfbHP//EACIQAQADAAICAgIDAAAAAAAAAAEAESExQVFhcYGRwRDR8P/aAAgBAQABPxDB9zPbVv4lSFaqovqv97j5MIDp9vH1+4eaR4WPmFfA7CYIa89xTk4Vcvgq6cfEtQOi8mNCB7S+oxpYqnyVz09dRQSVzyMujQNyr0Z9Ex9dvSKxbLW14gsPRvfEqbZBfRF1rE7SpyXthIcgZFjQ9oKKY00aOkuRkCjc8qIKuIOwcS9BCgmHzKdSCxOH1DNo+map33zUMtES5Tl5+2IZWE/E9QV9tZQ+42SC8wH1KVAAe2Wy4itFTLsLeqj2e50L9cwBRowx/UXV54iCnDrzGXA6DSfcuEpxYv7h5VEaFj+YJbY7afaogQVDkkf8WXPLGjrHN4j7tHd9QmDl6n//2Q==';
================================================
FILE: src/__tests__/mocks/notion-api-responses.ts
================================================
export const SUCCESSFUL_PAGE_CHUCK = {
recordMap: {
block: {
'4d64bbc0-634d-4758-befa-85c5a3a6c22f': {
role: 'reader',
value: {
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
version: 31,
type: 'page',
properties: { title: [['Simple Page Test']] },
format: { page_cover: 'https://www.example.com/image.png', page_cover_position: 0.6, page_icon: '🤴' },
content: ['80d0fc46-5511-4d1d-a4ec-8b2f43d75226'],
permissions: [{ role: 'reader', type: 'public_permission', allow_duplicate: false }],
created_time: 1595516162445,
last_edited_time: 1595520360000,
parent_id: '8370825e-eb4c-483c-ace0-cc06e7dfc556',
parent_table: 'block',
alive: true,
created_by_table: 'notion_user',
created_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
last_edited_by_table: 'notion_user',
last_edited_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
'80d0fc46-5511-4d1d-a4ec-8b2f43d75226': {
role: 'reader',
value: {
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
version: 33,
type: 'text',
properties: { title: [['Hello World']] },
created_time: 1595516160000,
last_edited_time: 1595516160000,
parent_id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
parent_table: 'block',
alive: true,
created_by_table: 'notion_user',
created_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
last_edited_by_table: 'notion_user',
last_edited_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
},
notion_user: {
'408c862f-f07b-4036-b414-1ae5c5ce57b3': {
role: 'reader',
value: {
id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
version: 5,
email: 'user@example.com',
given_name: 'User',
family_name: 'Name',
onboarding_completed: true,
mobile_onboarding_completed: true,
},
},
},
space: {},
},
cursor: { stack: [] },
};
export const SUCCESSFUL_RECORDS = {
results: [
{
role: 'reader',
value: {
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
version: 31,
type: 'page',
properties: { title: [['Simple Page Test']] },
content: ['80d0fc46-5511-4d1d-a4ec-8b2f43d75226'],
permissions: [{ role: 'reader', type: 'public_permission', allow_duplicate: false }],
},
},
],
};
export const SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN = {
recordMap: {
block: {
'4d64bbc0-634d-4758-befa-85c5a3a6c22f': {
role: 'editor',
value: {
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
version: 262,
type: 'page',
properties: { title: [['Simple Page Text 2']] },
content: ['f8cf7a08-bf80-4f8b-a842-3db49df95e4d'],
permissions: [
{ role: 'editor', type: 'user_permission', user_id: 'user_id' },
{ role: 'reader', type: 'public_permission' },
],
created_time: 1595516162445,
last_edited_time: 1602527580000,
parent_id: '159177ec-0fb0-469e-a900-1a662b145a04',
parent_table: 'space',
alive: true,
created_by_table: 'notion_user',
created_by_id: 'user_id',
last_edited_by_table: 'notion_user',
last_edited_by_id: 'user_id',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
'f8cf7a08-bf80-4f8b-a842-3db49df95e4d': {
role: 'editor',
value: {
id: 'f8cf7a08-bf80-4f8b-a842-3db49df95e4d',
version: 49,
type: 'bulleted_list',
properties: { title: [['Estou testando']] },
content: ['0b73eab8-8c01-4140-ab4d-cd6a0886cd76'],
created_time: 1602511500000,
last_edited_time: 1602527580000,
parent_id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
parent_table: 'block',
alive: true,
created_by_table: 'notion_user',
created_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
last_edited_by_table: 'notion_user',
last_edited_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
'0b73eab8-8c01-4140-ab4d-cd6a0886cd76': {
role: 'editor',
value: {
id: '0b73eab8-8c01-4140-ab4d-cd6a0886cd76',
version: 12,
type: 'bulleted_list',
properties: { title: [['isso daqui']] },
content: ['6bebe374-1569-4836-9de5-847c91ecb3f8'],
created_time: 1602527580000,
last_edited_time: 1602527580000,
parent_id: 'f8cf7a08-bf80-4f8b-a842-3db49df95e4d',
parent_table: 'block',
alive: true,
created_by_table: 'notion_user',
created_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
last_edited_by_table: 'notion_user',
last_edited_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
'6bebe374-1569-4836-9de5-847c91ecb3f8': {
role: 'editor',
value: {
id: '6bebe374-1569-4836-9de5-847c91ecb3f8',
version: 32,
type: 'bulleted_list',
properties: { title: [['vamos ver se funciona']] },
created_time: 1602527580000,
last_edited_time: 1602527580000,
parent_id: '0b73eab8-8c01-4140-ab4d-cd6a0886cd76',
parent_table: 'block',
alive: true,
created_by_table: 'notion_user',
created_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
last_edited_by_table: 'notion_user',
last_edited_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
},
space: {
'159177ec-0fb0-469e-a900-1a662b145a04': {},
},
},
cursor: { stack: [] },
};
export const SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN_NOT_IN_CHUNK = {
recordMap: {
block: {
'4d64bbc0-634d-4758-befa-85c5a3a6c22f': {
role: 'editor',
value: {
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
version: 356,
type: 'page',
properties: { title: [['Simple Page Text 2']] },
content: ['9dfff855-7aed-4a99-8e60-2e1384399200'],
format: {},
permissions: [
{ role: 'editor', type: 'user_permission', user_id: 'user_id' },
{ role: 'reader', type: 'public_permission' },
],
created_time: 1595516162445,
last_edited_time: 1602862020000,
parent_id: '159177ec-0fb0-469e-a900-1a662b145a04',
parent_table: 'space',
alive: true,
created_by_table: 'notion_user',
created_by_id: 'user_id',
last_edited_by_table: 'notion_user',
last_edited_by_id: 'user_id',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
'9dfff855-7aed-4a99-8e60-2e1384399200': {
role: 'editor',
value: {
id: '9dfff855-7aed-4a99-8e60-2e1384399200',
version: 40,
type: 'toggle',
properties: { title: [['Isso é um detail']] },
content: ['12cfada5-6686-4561-9c4f-177b3be4b422'],
created_time: 1602689700000,
last_edited_time: 1602861180000,
parent_id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
parent_table: 'block',
alive: true,
created_by_table: 'notion_user',
created_by_id: 'use_id',
last_edited_by_table: 'notion_user',
last_edited_by_id: 'use_id',
shard_id: 285188,
space_id: '159177ec-0fb0-469e-a900-1a662b145a04',
},
},
},
space: {
'159177ec-0fb0-469e-a900-1a662b145a04': {
role: 'editor',
value: {
id: '159177ec-0fb0-469e-a900-1a662b145a04',
version: 173,
name: 'workspace_name',
domain: 'domais',
permissions: [{ role: 'editor', type: 'user_permission', user_id: 'user_id' }],
icon: 'https://lh3.googleusercontent.com/a-/AOh14GjqdgJy-51RgCHKPgIMrNaIX0y4QUAL6Mz9OFS4=s100',
beta_enabled: false,
pages: [
'b0e76ef7-ec48-4aeb-9877-3b7907f4335c',
'8cb60e50-7dec-4a8c-9032-e6c30a8ec642',
'c1665d8e-8d20-4b32-9a44-320428580441',
'98c36805-bd5f-448f-b199-7fbff24d9963',
'a8624141-f0bd-4e41-9f02-83bbc7fc2b4a',
'b2115542-dfaa-4599-8ba9-77d467a33f94',
'739c26a3-770d-44e7-b652-70b3a5ae0220',
'c215286e-fd31-4d74-9266-e832603a3e8e',
'5ca7e1ba-65f4-422d-a274-529f8f8ec664',
'4d64bbc0-634d-4758-befa-85c5a3a6c22f',
'f9466b44-33b3-4cb2-9083-b820ead2c4fd',
],
created_time: 1591708116643,
last_edited_time: 1602476700000,
created_by_table: 'notion_user',
created_by_id: 'user_id',
last_edited_by_table: 'notion_user',
last_edited_by_id: 'user_id',
shard_id: 285188,
plan_type: 'personal',
invite_link_code: '09cb83f87702b6ce2be323132a369b69e23085b3',
invite_link_enabled: true,
},
},
},
},
cursor: { stack: [] },
};
export const SUCCESSFUL_SYNC_RECORD_VALUE = {
recordMap: {
block: {
'12cfada5-6686-4561-9c4f-177b3be4b422': {
role: 'editor',
value: {
id: '12cfada5-6686-4561-9c4f-177b3be4b422',
version: 27,
type: 'text',
properties: { title: [['Lide com isso!']] },
created_time: 1602861180000,
last_edited_time: 1602861180000,
parent_id: '9dfff855-7aed-4a99-8e60-2e1384399200',
parent_table: 'block',
alive: true,
created_by_table: 'notion_user',
created_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
last_edited_by_table: 'notion_user',
last_edited_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
},
},
},
},
};
export const SUCCESSFUL_RECORDS_WITH_CHILDREN = {
results: [
{
role: 'reader',
value: {
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
version: 262,
type: 'page',
properties: { title: [['Simple Page Text 2']] },
content: ['f8cf7a08-bf80-4f8b-a842-3db49df95e4d'],
permissions: [
{ role: 'editor', type: 'user_permission', user_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3' },
{ role: 'reader', type: 'public_permission' },
],
created_time: 1595516162445,
last_edited_time: 1602527580000,
parent_id: '159177ec-0fb0-469e-a900-1a662b145a04',
parent_table: 'space',
alive: true,
created_by_table: 'notion_user',
created_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
last_edited_by_table: 'notion_user',
last_edited_by_id: '408c862f-f07b-4036-b414-1ae5c5ce57b3',
},
},
],
};
export const NO_PAGE_ACCESS_RECORDS = { results: [{ role: 'none' }] };
export const MISSING_CONTENT_RECORDS = {
results: [
{
role: 'reader',
value: {
id: '9a75a541-277f-4a64-80e7-5581f36672ba',
version: 22,
type: 'page',
permissions: [{ role: 'reader', type: 'public_permission' }],
created_time: 1595551283696,
last_edited_time: 1595551260000,
alive: true,
},
},
],
};
export const SINGLE_PAGE_WITH_COVER_IMAGE = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
format: { page_cover: '/images/page-cover/solid_blue.png', page_cover_position: 0.6 },
properties: {
title: [['Page Title']],
},
contents: [],
},
];
export const SINGLE_PAGE_WITH_ICON = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
format: { page_icon: '🤴' },
properties: {
title: [['Page Title']],
},
contents: [],
},
];
export const SINGLE_TEXT_AND_TITLE_NOTION_API_CONTENT_RESPONSE = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: { title: [['Hello World']] },
contents: [],
},
];
export const SINGLE_TEXT_WITH_BOLD = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {
title: [['Hello '], ['World', [['b']]]],
},
contents: [],
},
];
export const SINGLE_TEXT_WITH_BOLD_AND_ITALIC_TOGETHER = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {
title: [['Hello '], ['World', [['b'], ['i']]]],
},
contents: [],
},
];
export const SINGLE_TEXT_WITH_BOLD_AND_ITALIC = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {
title: [
['Hello ', [['b']]],
['World', [['i']]],
],
},
contents: [],
},
];
export const SINGLE_TEXT_WITH_COLOR = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {
title: [['Hello', [['h', 'purple']]]],
},
contents: [],
},
];
export const SINGLE_TEXT_WITH_EQUATION = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: {
title: [['Hello World '], ['⁍', [['e', '2x']]]],
},
contents: [],
},
];
export const SINGLE_TEXT_WITH_LINK = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: { title: [['Hello '], ['World', [['a', 'https://www.google.com']]]] },
contents: [],
},
];
export const SINGLE_TEXT_WITH_FORMAT = [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
format: { block_color: 'red_background' },
properties: { title: [['Hello '], ['World', [['a', 'https://www.google.com']]]] },
contents: [],
},
];
export const CALLOUT_WITH_PAGE_ICON = [
{
id: '16431c64-3bf0-481f-a29f-d544780d84f3',
type: 'callout',
properties: { title: [['This is a callout']] },
format: { page_icon: '💡' },
contents: [],
},
];
export const IMAGE_WITH_CUSTOM_SIZE = [
{
id: 'ec3b36fd-f77d-46b4-8592-5966488612b1',
type: 'image',
properties: {
source: [
[
'https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bcedd078-56cd-4137-a28a-af16b5746874/767-50x50.jpg',
],
],
},
format: {
block_width: 240,
block_height: 50,
display_source:
'https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bcedd078-56cd-4137-a28a-af16b5746874/767-50x50.jpg',
block_full_width: false,
block_page_width: false,
block_aspect_ratio: 1,
block_preserve_scale: true,
},
contents: [],
},
];
export const TEXT_NOTION_API_CONTENT_RESPONSE = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { title: [['Simple Page Test']] },
format: { page_cover: 'https://www.example.com/image.png', page_cover_position: 0.6, page_icon: '🤴' },
contents: [
{
id: '80d0fc46-5511-4d1d-a4ec-8b2f43d75226',
type: 'text',
properties: { title: [['Hello World']] },
contents: [],
},
],
},
];
export const VIDEO_NOTION_API_CONTENT_RESPONSE = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { title: [['Simple Page Test']] },
contents: [
{
id: 'dcde43cb-7131-4687-8f22-c9789fa75f46',
type: 'video',
properties: { source: [['https://www.youtube.com/watch?v=xBFqxBfLJWc']] },
contents: [],
},
],
},
];
export const LIST_WITH_CHILDREN_RESPONSE = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { title: [['Simple Page Text 2']] },
contents: [
{
id: 'f8cf7a08-bf80-4f8b-a842-3db49df95e4d',
type: 'bulleted_list',
properties: { title: [['Estou testando']] },
contents: [
{
id: '0b73eab8-8c01-4140-ab4d-cd6a0886cd76',
type: 'bulleted_list',
properties: { title: [['isso daqui']] },
contents: [
{
id: '6bebe374-1569-4836-9de5-847c91ecb3f8',
type: 'bulleted_list',
properties: { title: [['vamos ver se funciona']] },
contents: [],
},
],
},
],
},
],
},
];
export const DETAILS_RESPONSE = [
{
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
type: 'page',
properties: { title: [['Simple Page Text 2']] },
format: {},
contents: [
{
id: '9dfff855-7aed-4a99-8e60-2e1384399200',
type: 'toggle',
properties: { title: [['Isso é um detail']] },
contents: [
{
id: '12cfada5-6686-4561-9c4f-177b3be4b422',
type: 'text',
properties: { title: [['Lide com isso!']] },
contents: [],
},
],
},
],
},
];
================================================
FILE: src/data/helpers/block-to-inner-html.ts
================================================
import { Block, DecorableText } from '../protocols/blocks';
import { Decorator } from '../use-cases/blocks-to-html-converter/block-parsers/decorations/decorator';
import { replaceLineBreakByBrTag } from './replace-line-break-to-br-tag';
export const blockToInnerHtml = async (block: Block): Promise<string> => {
const decorator = new Decorator(block.decorableTexts || ([] as DecorableText[]));
const decoratedText = await decorator.decorate();
return Promise.resolve(replaceLineBreakByBrTag(decoratedText));
};
================================================
FILE: src/data/helpers/block-to-inner-text.ts
================================================
import { Block } from '../../data/protocols/blocks';
export const blockToInnerText = (block: Block): string => {
const decorableTexts = block.decorableTexts;
return decorableTexts ? decorableTexts.map((dt) => dt.text).join('') : '';
};
================================================
FILE: src/data/helpers/blocks-to-html.ts
================================================
import { Block } from '../protocols/blocks';
import { ListBlocksWrapper, BlockDispatcher, BlocksToHTML } from '../use-cases/blocks-to-html-converter';
export const blocksToHtml = async (blocks: Block[]): Promise<string> => {
const dispatcher = new BlockDispatcher();
const listWrapper = new ListBlocksWrapper();
const blocksToHtmlConverter = new BlocksToHTML(blocks, dispatcher, listWrapper);
const html = await blocksToHtmlConverter.convert();
return Promise.resolve(html);
};
export const indentBlocksToHtml = async (blocks: Block[]): Promise<string> => {
if (blocks.length === 0) return Promise.resolve('');
const html = await blocksToHtml(blocks);
return Promise.resolve(html);
};
================================================
FILE: src/data/helpers/color-to-hex.ts
================================================
export const backgroundColorToHex = (color: string): string => {
return backgroundColorsToHex[color] || '#FFFFFF';
};
export const foregroundColorToHex = (color: string): string => {
return foregroundColorTextToHEX[color] || '#37352F';
};
const foregroundColorTextToHEX: Record<string, string> = {
purple: '#6940A5',
yellow: '#E9AB01',
gray: '#9B9A97',
brown: '#64473A',
orange: '#D9730D',
green: '#0F7B6C',
blue: '#0B6E99',
pink: '#AD1A72',
red: '#E03E3E',
none: '#37352F',
};
const backgroundColorsToHex: Record<string, string> = {
gray_background: '#B4AEAE',
brown_background: '#E9E5E3',
orange_background: '#FAEBDD',
yellow_background: '#FBF3DB',
green_background: '#DDEDEA',
blue_background: '#DDEBF1',
purple_background: '#EAE4F2',
pink_background: '#F4DFEB',
red_background: '#FBE4E4',
};
================================================
FILE: src/data/helpers/replace-line-break-to-br-tag.ts
================================================
export const replaceLineBreakByBrTag = (str: string): string => str.replace(/[\r\n]/g, '</br>');
================================================
FILE: src/data/protocols/blocks/block.ts
================================================
import { DecorableText } from './decorable-text';
import { Format } from './format';
export type Block = {
id: string;
type: string;
children: Block[];
properties: Record<string, any>;
format: Format;
decorableTexts: DecorableText[];
};
================================================
FILE: src/data/protocols/blocks/decorable-text.ts
================================================
import { Decoration } from './decoration';
export type DecorableText = {
text: string;
decorations: Decoration[];
};
================================================
FILE: src/data/protocols/blocks/decoration.ts
================================================
export type Decoration = {
type: DecorationType;
value?: string;
};
export type DecorationType =
| 'plain'
| 'bold'
| 'italic'
| 'underline'
| 'strikethrough'
| 'link'
| 'code'
| 'color'
| 'equation';
================================================
FILE: src/data/protocols/blocks/format.ts
================================================
export type Format = {
block_color?: string;
block_width?: number;
page_icon?: string;
page_cover?: string;
page_cover_position?: number;
};
================================================
FILE: src/data/protocols/blocks/index.ts
================================================
export * from './block';
export * from './decorable-text';
export * from './decoration';
export * from './list-blocks-wrapper';
================================================
FILE: src/data/protocols/blocks/list-blocks-wrapper.ts
================================================
import { Block } from './block';
export interface ListBlocksWrapper {
wrapLists(blocks: Block[]): Block[];
}
================================================
FILE: src/data/protocols/html-options/html-options.ts
================================================
export type HtmlOptions = {
excludeTitleFromHead?: boolean;
excludeCSS?: boolean;
excludeMetadata?: boolean;
excludeScripts?: boolean;
excludeHeaderFromBody?: boolean;
bodyContentOnly?: boolean;
};
================================================
FILE: src/data/protocols/http-request/http-get-client.ts
================================================
import { HttpResponse } from './http-response';
export interface HttpGetClient {
get(url: string): Promise<HttpResponse>;
}
================================================
FILE: src/data/protocols/http-request/http-post-client.ts
================================================
import { HttpResponse } from './http-response';
export interface HttpPostClient {
post(url: string, body: Record<string, any>): Promise<HttpResponse>;
}
================================================
FILE: src/data/protocols/http-request/http-response.ts
================================================
export type HttpResponse = {
status: number;
data: Record<string, any> | string;
headers?: Record<string, string[]>;
};
================================================
FILE: src/data/protocols/http-request/index.ts
================================================
export * from './http-post-client';
export * from './http-get-client';
export * from './http-response';
================================================
FILE: src/data/protocols/page-props/image-cover.ts
================================================
export type ImageCover = {
base64: string;
position: number;
};
================================================
FILE: src/data/protocols/page-props/index.ts
================================================
export * from './page-props';
export * from './image-cover';
================================================
FILE: src/data/protocols/page-props/page-props.ts
================================================
export type PageProps = {
title: string;
coverImageSrc?: string;
coverImagePosition?: number;
icon?: string;
};
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-dispatcher.ts
================================================
import { Block } from '../../protocols/blocks';
import { ToHtml, ToHtmlClass } from '../../../domain/use-cases/to-html';
import * as blockParsers from './block-parsers';
export class BlockDispatcher {
dispatch(block: Block): ToHtml {
const ToHtmlConverter = fromBlockToHtmlConverter[block.type] || blockParsers.UnknownBlockToHtml;
return new ToHtmlConverter(block);
}
}
const fromBlockToHtmlConverter: Record<string, ToHtmlClass> = {
text: blockParsers.TextBlockToHtml,
header: blockParsers.HeaderBlockToHtml,
sub_header: blockParsers.SubHeaderBlockParser,
sub_sub_header: blockParsers.SubSubHeaderBlockParser,
to_do: blockParsers.ToDoBlockToHtml,
code: blockParsers.CodeBlockToHtml,
equation: blockParsers.EquationBlockToHtml,
quote: blockParsers.QuoteBlockToHtml,
divider: blockParsers.DividerBlockToHtml,
list: blockParsers.ListBlockToHtml,
video: blockParsers.YouTubeVideoBlockToHtml,
image: blockParsers.ImageBlockToHtml,
callout: blockParsers.CalloutBlockToHtml,
toggle: blockParsers.ToggleBlockToHtml,
page: blockParsers.PageBlockToHtml,
};
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/callout.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
import { Base64Converter } from '../../../../utils/base-64-converter';
export class CalloutBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
const iconHtml = await new IconToHtml(this._block.properties.page_icon, this._block.id).toHtml();
return Promise.resolve(`
<div class="callout"${style}>
${iconHtml}
<p>${await blockToInnerHtml(this._block)}</p>
</div>
`);
}
}
class IconToHtml {
private readonly _icon: string | undefined;
private readonly _id: string;
constructor(icon: string | undefined, id: string) {
this._icon = icon;
this._id = id;
}
async toHtml(): Promise<string> {
if (!this._icon) return `<div class="callout-emoji">💡</div>`;
if (!this._icon.startsWith('http')) return `<div class="callout-emoji">${this._icon}</div>`;
const url = `https://www.notion.so/image/${encodeURIComponent(this._icon)}?table=block&id=${this._id}`;
const imageSource = await Base64Converter.convert(url);
const caption = 'callout icon';
return `<div class="callout-image"><img src="${imageSource}" alt="${caption}"></div>`;
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/code.ts
================================================
import { Block } from '../../../../data/protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { blockToInnerText } from '../../../helpers/block-to-inner-text';
export class CodeBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const languageClass = this._language ? `class="language-${this._language}"` : '';
return Promise.resolve(
`<pre><code ${languageClass}>${blockToInnerText(this._block).replace(/(\s{4}|\t)/g, ' ')}</code></pre>`,
);
}
private get _language(): string {
return this._block.properties?.language?.toLowerCase().replace(/ /g, '');
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-dispatcher.ts
================================================
import { Decoration } from '../../../../../data/protocols/blocks/decoration';
import { ToHtml, ToHtmlClass } from '../../../../../domain/use-cases/to-html';
import * as DecorationParsers from './decoration-parsers';
export class DecoratorDispatcher {
dispatch(text: string, decoration: Decoration): ToHtml {
const ToHtmlConverter = fromDecorationTypeToParsers[decoration.type] || DecorationParsers.UnknownDecorationToHtml;
return new ToHtmlConverter(text, decoration);
}
}
const fromDecorationTypeToParsers: Record<string, ToHtmlClass> = {
bold: DecorationParsers.BoldDecorationToHtml,
italic: DecorationParsers.ItalicDecorationToHtml,
strikethrough: DecorationParsers.StrikeThroughDecorationToHtml,
code: DecorationParsers.CodeDecorationToHtml,
underline: DecorationParsers.UnderlineDecorationToHtml,
equation: DecorationParsers.EquationDecorationToHtml,
link: DecorationParsers.LinkDecorationToHtml,
color: DecorationParsers.ColorDecorationToHtml,
};
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/bold.ts
================================================
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class BoldDecorationToHtml implements ToHtml {
private readonly _text: string;
constructor(text: string) {
this._text = text;
}
async convert(): Promise<string> {
return Promise.resolve(`<strong>${this._text}</strong>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/code.ts
================================================
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class CodeDecorationToHtml implements ToHtml {
private readonly _text: string;
constructor(text: string) {
this._text = text;
}
async convert(): Promise<string> {
return Promise.resolve(`<code>${this._text}</code>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/color.test.ts
================================================
import { ColorDecorationToHtml } from './color';
import { Decoration } from '../../../../../protocols/blocks';
describe('#convert', () => {
describe('When color is given as foreground color', () => {
it('styles using css color property', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'purple' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toMatch('style="color:');
});
});
describe('When color is given as background color', () => {
it('styles using css background-color property', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'purple_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toMatch('style="background-color:');
});
it('do not preserves color value on style', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'purple_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).not.toMatch('purple_background');
});
});
describe('When purple color is given as foreground color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'purple' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #6940A5;">Text with color</span>');
});
});
describe('When yellow color is given as color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'yellow' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #E9AB01;">Text with color</span>');
});
});
describe('When gray color is given as foreground color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'gray' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #9B9A97;">Text with color</span>');
});
});
describe('When brown color is given as color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'brown' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #64473A;">Text with color</span>');
});
});
describe('When orange color is given as foreground color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'orange' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #D9730D;">Text with color</span>');
});
});
describe('When green color is given as foreground color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'green' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #0F7B6C;">Text with color</span>');
});
});
describe('When pink color is given as foreground color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'pink' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #AD1A72;">Text with color</span>');
});
});
describe('When red color is given as foreground color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'red' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #E03E3E;">Text with color</span>');
});
});
describe('When unknown color is given as foreground color', () => {
it('converts to equivalent default foreground color hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'refafad' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #37352F;">Text with color</span>');
});
});
describe('When no color value is given as foreground color', () => {
it('converts to equivalent default foreground color hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="color: #37352F;">Text with color</span>');
});
});
describe('When gray color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'gray_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #B4AEAE;">Text with color</span>');
});
});
describe('When brown color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'brown_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #E9E5E3;">Text with color</span>');
});
});
describe('When orange color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'orange_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #FAEBDD;">Text with color</span>');
});
});
describe('When yellow color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'yellow_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #FBF3DB;">Text with color</span>');
});
});
describe('When green color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'green_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #DDEDEA;">Text with color</span>');
});
});
describe('When blue color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'blue_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #DDEBF1;">Text with color</span>');
});
});
describe('When purple color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'purple_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #EAE4F2;">Text with color</span>');
});
});
describe('When pink color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'pink_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #F4DFEB;">Text with color</span>');
});
});
describe('When red color is given as background color', () => {
it('converts to equivalent hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'red_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #FBE4E4;">Text with color</span>');
});
});
describe('When unknown color is given as background color', () => {
it('converts to equivalent default background color hex code and apply style to html', async () => {
const text = 'Text with color';
const decoration: Decoration = { type: 'color', value: 'refafad_background' };
const result = await new ColorDecorationToHtml(text, decoration).convert();
expect(result).toBe('<span style="background-color: #FFFFFF;">Text with color</span>');
});
});
});
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/color.ts
================================================
import { Decoration } from '../../../../../../data/protocols/blocks';
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
import { foregroundColorToHex, backgroundColorToHex } from '../../../../../helpers/color-to-hex';
export class ColorDecorationToHtml implements ToHtml {
private readonly _text: string;
private readonly _decoration: Decoration;
constructor(text: string, decoration: Decoration) {
this._text = text;
this._decoration = decoration;
}
async convert(): Promise<string> {
return Promise.resolve(`<span style="${this._style}">${this._text}</span>`);
}
private _isBackground(): boolean {
return !!this._decoration.value?.includes('_background');
}
private get _style() {
const textColor = this._decoration.value || 'none';
if (this._isBackground()) return `background-color: ${backgroundColorToHex(textColor)};`;
return `color: ${foregroundColorToHex(textColor)};`;
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/equation.ts
================================================
import { Decoration } from '../../../../../../data/protocols/blocks';
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class EquationDecorationToHtml implements ToHtml {
private readonly _text: string;
private readonly _decoration: Decoration;
constructor(text: string, decoration: Decoration) {
this._text = text;
this._decoration = decoration;
}
async convert(): Promise<string> {
const equation = this._decoration.value;
return Promise.resolve(equation ? `$${equation}$` : '');
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/index.ts
================================================
export * from './bold';
export * from './code';
export * from './color';
export * from './equation';
export * from './italic';
export * from './link';
export * from './strikethrough';
export * from './underline';
export * from './unknown';
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/italic.ts
================================================
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class ItalicDecorationToHtml implements ToHtml {
private readonly _text: string;
constructor(text: string) {
this._text = text;
}
async convert(): Promise<string> {
return Promise.resolve(`<em>${this._text}</em>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/link.ts
================================================
import { Decoration } from '../../../../../../data/protocols/blocks';
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class LinkDecorationToHtml implements ToHtml {
private readonly _text: string;
private readonly _decoration: Decoration;
constructor(text: string, decoration: Decoration) {
this._text = text;
this._decoration = decoration;
}
async convert(): Promise<string> {
return Promise.resolve(`<a href="${this._link}" target="_blank">${this._text}</a>`);
}
private get _link(): string {
return this._decoration.value || '#';
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/strikethrough.ts
================================================
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class StrikeThroughDecorationToHtml implements ToHtml {
private readonly _text: string;
constructor(text: string) {
this._text = text;
}
async convert(): Promise<string> {
return Promise.resolve(`<del>${this._text}</del>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/underline.ts
================================================
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class UnderlineDecorationToHtml implements ToHtml {
private readonly _text: string;
constructor(text: string) {
this._text = text;
}
async convert(): Promise<string> {
return Promise.resolve(`<span style="text-decoration: underline;">${this._text}</span>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/unknown.ts
================================================
import { ToHtml } from '../../../../../../domain/use-cases/to-html';
export class UnknownDecorationToHtml implements ToHtml {
private readonly _text: string;
constructor(text: string) {
this._text = text;
}
async convert(): Promise<string> {
return Promise.resolve(this._text);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decorator.ts
================================================
import { DecorableText } from '../../../../../data/protocols/blocks/decorable-text';
import { DecoratorDispatcher } from './decoration-dispatcher';
export class Decorator {
private readonly _decorableTexts: DecorableText[];
constructor(decorableTexts: DecorableText[]) {
this._decorableTexts = decorableTexts;
}
async decorate(): Promise<string> {
const decorableTextsByDecorators = await Promise.all(
this._decorableTexts.map(await this._decorateByDecorableText.bind(this)),
);
return Promise.resolve(decorableTextsByDecorators.join(''));
}
async _decorateByDecorableText(decorableText: DecorableText): Promise<string> {
let html = decorableText.text;
for (const decoration of decorableText.decorations) {
const decorator = new DecoratorDispatcher().dispatch(html, decoration);
html = await decorator.convert();
}
return Promise.resolve(html);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/divider.ts
================================================
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
export class DividerBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
return Promise.resolve(`<hr>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/equation.ts
================================================
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { blockToInnerText } from '../../../helpers/block-to-inner-text';
export class EquationBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const { decorableTexts } = this._block;
if (decorableTexts.length === 0) return Promise.resolve('');
return Promise.resolve(decorableTexts ? `<div class="equation">$$${blockToInnerText(this._block)}$$</div>` : '');
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/header.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
export class HeaderBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
return Promise.resolve(`<h1${style}>${await blockToInnerHtml(this._block)}</h1>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/image.ts
================================================
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { Base64Converter } from '../../../../utils/base-64-converter';
import { FormatToStyle } from '../../format-to-style';
export class ImageBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
if (!this._rawSrc) return '';
const imageSource = await Base64Converter.convert(this._rawSrc);
const caption = this._caption;
const style = new FormatToStyle(this._block.format).toStyle();
return `
<figure class="image">
<img src="${imageSource}" alt="${caption}"${style}>
${caption !== '' ? `<figcaption>${caption}</figcaption>` : ''}
</figure>
`;
}
private get _rawSrc() {
const url = this._block.properties.source;
if (!url) return;
return `https://www.notion.so/image/${encodeURIComponent(url)}?table=block&id=${this._block.id}`;
}
private get _caption() {
return this._block.properties.caption || '';
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/index.ts
================================================
export * from './image';
export * from './list';
export * from './callout';
export * from './code';
export * from './divider';
export * from './equation';
export * from './header';
export * from './quote';
export * from './sub-header';
export * from './sub-sub-header';
export * from './text';
export * from './to-do';
export * from './toggle';
export * from './page';
export * from './unknown';
export * from './youtube-video';
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/list/index.ts
================================================
export * from './list';
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/list/list-item.ts
================================================
import { blockToInnerHtml } from '../../../../helpers/block-to-inner-html';
import { Block } from '../../../../protocols/blocks';
import { ToHtml } from '../../../../../domain/use-cases/to-html';
import { indentBlocksToHtml } from '../../../../helpers/blocks-to-html';
export class ListItemToHtml implements ToHtml {
private _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const childrenHtml = await indentBlocksToHtml(this._block.children);
return Promise.resolve(`<li>${await blockToInnerHtml(this._block)}${childrenHtml}</li>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/list/list.ts
================================================
import { Block } from '../../../../protocols/blocks';
import { ToHtml } from '../../../../../domain/use-cases/to-html';
import { ListItemToHtml } from './list-item';
import { FormatToStyle } from '../../../format-to-style';
export class ListBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const tag: string = fromTypeToTag[this._block.children[0].type] || fromTypeToTag.bulleted_list;
const style = new FormatToStyle(this._block.format).toStyle();
const innerHtml = await this._itemsHtml();
return Promise.resolve(`<${tag}${style}>\n${innerHtml}\n</${tag}>`);
}
private async _itemsHtml(): Promise<string> {
const items = await Promise.all(this._block.children.map(async (c) => new ListItemToHtml(c).convert()));
return Promise.resolve(items.join('\n'));
}
}
const fromTypeToTag: Record<string, string> = {
bulleted_list: 'ul',
numbered_list: 'ol',
};
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/page.ts
================================================
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { blocksToHtml } from '../../../helpers/blocks-to-html';
export class PageBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
return blocksToHtml(this._block.children);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/quote.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
export class QuoteBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
return Promise.resolve(`<blockquote${style}>${await blockToInnerHtml(this._block)}</blockquote>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/sub-header.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
export class SubHeaderBlockParser implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
return Promise.resolve(`<h2${style}>${await blockToInnerHtml(this._block)}</h2>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/sub-sub-header.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
export class SubSubHeaderBlockParser implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
return Promise.resolve(`<h3${style}>${await blockToInnerHtml(this._block)}</h3>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/text.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
import { indentBlocksToHtml } from '../../../helpers/blocks-to-html';
export class TextBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
const childrenHtml = await indentBlocksToHtml(this._block.children);
return Promise.resolve(`<p${style}>${await blockToInnerHtml(this._block)}${childrenHtml}</p>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/to-do.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
import { indentBlocksToHtml } from '../../../helpers/blocks-to-html';
export class ToDoBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
const childrenHtml = await indentBlocksToHtml(this._block.children);
return Promise.resolve(`\
<ul class="to-do-list"${style}>
<li>
<div class="checkbox checkbox-${this._isChecked() ? 'on' : 'off'}"></div>
<span class="to-do-children-${this._isChecked() ? 'checked' : 'unchecked'}">${await blockToInnerHtml(
this._block,
)}</span>${childrenHtml}
</li>
</ul>`);
}
private _isChecked(): boolean {
return !!this._block.properties.checked;
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/toggle.ts
================================================
import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
import { FormatToStyle } from '../../format-to-style';
import { indentBlocksToHtml } from '../../../helpers/blocks-to-html';
export class ToggleBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const style = new FormatToStyle(this._block.format).toStyle();
const childrenHtml = await indentBlocksToHtml(this._block.children);
return Promise.resolve(`
<details open=""${style}>
<summary>${await blockToInnerHtml(this._block)}</summary>
${childrenHtml}
</details>`);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/unknown.ts
================================================
import { ToHtml } from '../../../../domain/use-cases/to-html';
export class UnknownBlockToHtml implements ToHtml {
async convert(): Promise<string> {
return Promise.resolve('');
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/youtube-video.ts
================================================
import { Block } from '../../../protocols/blocks';
import { ToHtml } from '../../../../domain/use-cases/to-html';
export class YouTubeVideoBlockToHtml implements ToHtml {
private readonly _block: Block;
constructor(block: Block) {
this._block = block;
}
async convert(): Promise<string> {
const id = this._youtubeId;
if (!id) return '';
return `<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/${id}"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>`;
}
private get _youtubeId(): string | void {
const youtubeIdMatcher =
/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/gi;
return youtubeIdMatcher.exec(this._src)?.[1];
}
private get _src() {
return this._block.properties?.source;
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/blocks-to-html-converter.test.ts
================================================
import nock from 'nock';
import { resolve } from 'path';
import { Block } from '../../protocols/blocks';
import * as BlockMocks from '../../../__tests__/mocks/blocks';
import { BlocksToHTML, BlockDispatcher, ListBlocksWrapper } from './index';
import { ToHtml } from '../../../domain/use-cases/to-html';
import { Base64Converter } from '../../../utils/base-64-converter';
import base64Img from '../../../__tests__/mocks/img/base64';
describe('#convert', () => {
const makeSut = (blocks: Block[]): ToHtml => {
const blockDispatcher = new BlockDispatcher();
const listBlocksWrapper = new ListBlocksWrapper();
return new BlocksToHTML(blocks, blockDispatcher, listBlocksWrapper);
};
describe('When only a text block is given', () => {
describe('When empty text block is given', () => {
it('returns empty p tag', async () => {
const html = await makeSut(BlockMocks.NO_TEXT).convert();
expect(html).toBe('<p></p>');
});
});
describe('When single text block is given', () => {
it('returns html with p tag', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT).convert();
expect(html).toBe('<p>Hello World</p>');
});
});
describe('When single text block has children', () => {
it('returns html with p tag and children blocks inside', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_CHILDREN).convert();
expect(html.replace(/\s/g, '')).toBe(
`<p>Hello World
<p>This is a child</p>
<p>This is a child too</p>
</p>`.replace(/\s/g, ''),
);
});
});
describe('When single line text with bold part', () => {
it('returns html with single p paragraph with strong tag nested', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_BOLD).convert();
expect(html).toBe('<p>Hello <strong>World</strong></p>');
});
});
describe('When single line text with italic part', () => {
it('returns html with single p paragraph with strong tag nested', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_ITALIC).convert();
expect(html).toBe('<p>Hello <em>World</em></p>');
});
});
describe('When single line text with underline part', () => {
it('returns html with single p paragraph with span tag and underline style nested', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_UNDERLINE).convert();
expect(html).toBe('<p>Hello <span style="text-decoration: underline;">World</span></p>');
});
});
describe('When single line text with strikethrough part', () => {
it('returns html with single p paragraph with del tag inside', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_STRIKETHROUGH).convert();
expect(html).toBe('<p>Hello <del>World</del></p>');
});
});
describe('When single line text with code part', () => {
it('returns html with single p paragraph with code tag inside', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_CODE_DECORATION).convert();
expect(html).toBe('<p>Hello <code>myVar</code></p>');
});
});
describe('When single line text with link part', () => {
it('returns html with single p paragraph with a tag with given link', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_LINK).convert();
expect(html).toBe('<p>Hello <a href="https://www.google.com" target="_blank">World</a></p>');
});
});
describe('When single line text with inline equation part', () => {
it('returns html with single p paragraph equation wrapped inside $$', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_EQUATION_DECORATION).convert();
expect(html).toBe('<p>Hello World $2x$</p>');
});
});
describe('When single line text with color part', () => {
it('returns html with single p paragraph with span tag and color style inside', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_COLOR).convert();
expect(html).toBe('<p><span style="color: #6940A5;">Hello</span></p>');
});
});
describe('When single line text with color background part', () => {
it('returns html with single p paragraph with span tag and background color style inside', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_COLOR_BACKGROUND).convert();
expect(html).toBe('<p><span style="background-color: #FBF3DB;">Hello</span></p>');
});
});
describe('When single line text with bold and italic parts together', () => {
it('returns html with single p paragraph with strong and em tags nested', async () => {
const html = await makeSut(BlockMocks.SINGLE_TEXT_WITH_BOLD_AND_ITALIC).convert();
expect(html).toBe('<p>Hello <em><strong>World</strong></em></p>');
});
});
describe('When single line text with bold and italic parts apart', () => {
it('returns html with single p paragraph with strong and em tags nested', async () => {
const html = await makeSut(BlockMocks.TEXT_WITH_DECORATION).convert();
expect(html).toBe(
'<p>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></p>',
);
});
});
describe('When multiline text block is given', () => {
it('returns html with two p tags', async () => {
const html = await makeSut(BlockMocks.MULTILINE_TEXT).convert();
expect(html).toBe('<p>Hello World</br>Is everything alright?</br>Yes, Dude!</p>');
});
});
describe('When text block has background color', () => {
it('returns html p tag with style and background-color prop', async () => {
const html = await makeSut(BlockMocks.TEXT_WITH_FORMAT).convert();
expect(html).toBe('<p style="background-color: #FBE4E4; ">This is a text with red background</p>');
});
});
describe('When text block has foreground color', () => {
it('returns html p tag with style and color prop', async () => {
const html = await makeSut(BlockMocks.TEXT_WITH_FORMAT_FOREGROUND).convert();
expect(html).toBe('<p style="color: #6940A5; ">This is a text with purple color</p>');
});
});
});
describe('When only a h1 title block is given', () => {
describe('When single block is given', () => {
it('returns html with h1 tag', async () => {
const html = await makeSut(BlockMocks.H1_TEXT).convert();
expect(html).toBe('<h1>This is a h1 title</h1>');
});
});
describe('When single line header with decoration', () => {
it('returns html with single h1 with decoration tags inside', async () => {
const html = await makeSut(BlockMocks.H1_TEXT_WITH_DECORATIONS).convert();
expect(html).toBe(
'<h1>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></h1>',
);
});
});
describe('When header block has background color', () => {
it('returns html h1 tag with style and background-color prop', async () => {
const html = await makeSut(BlockMocks.H1_WITH_FORMAT).convert();
expect(html).toBe('<h1 style="background-color: #DDEDEA; ">This is a h1 with red background</h1>');
});
});
describe('When header block has foreground color', () => {
it('returns html h1 tag with style and color prop', async () => {
const html = await makeSut(BlockMocks.H1_WITH_FORMAT_FOREGROUND).convert();
expect(html).toBe('<h1 style="color: #E9AB01; ">This is a h1 with yellow color</h1>');
});
});
});
describe('When only a h2 title block is given', () => {
describe('When single block is given', () => {
it('returns html with h2 tag', async () => {
const html = await makeSut(BlockMocks.H2_TEXT).convert();
expect(html).toBe('<h2>This is a h2 title</h2>');
});
});
describe('When single line h2 with decoration', () => {
it('returns html with single h1 with decoration tags inside', async () => {
const html = await makeSut(BlockMocks.H2_TEXT_WITH_DECORATIONS).convert();
expect(html).toBe(
'<h2>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></h2>',
);
});
});
describe('When sub header block has background color', () => {
it('returns html h2 tag with style and background-color prop', async () => {
const html = await makeSut(BlockMocks.H2_WITH_FORMAT).convert();
expect(html).toBe('<h2 style="background-color: #FBF3DB; ">This is a h2 with yellow background</h2>');
});
});
describe('When sub header block has foreground color', () => {
it('returns html h2 tag with style and color prop', async () => {
const html = await makeSut(BlockMocks.H2_WITH_FORMAT_FOREGROUND).convert();
expect(html).toBe('<h2 style="color: #9B9A97; ">This is a h2 with gray color</h2>');
});
});
});
describe('When only a h3 title block is given', () => {
describe('When single block is given', () => {
it('returns html with h3 tag', async () => {
const html = await makeSut(BlockMocks.H3_TEXT).convert();
expect(html).toBe('<h3>This is a h3 title</h3>');
});
});
describe('When single line h3 with decoration', () => {
it('returns html with single h1 with decoration tags inside', async () => {
const html = await makeSut(BlockMocks.H3_TEXT_WITH_DECORATIONS).convert();
expect(html).toBe(
'<h3>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></h3>',
);
});
});
describe('When sub header block has background color', () => {
it('returns html h3 tag with style and background-color prop', async () => {
const html = await makeSut(BlockMocks.H3_WITH_FORMAT).convert();
expect(html).toBe('<h3 style="background-color: #FAEBDD; ">This is a h3 with orange background</h3>');
});
});
describe('When sub sub header block has foreground color', () => {
it('returns html h3 tag with style and color prop', async () => {
const html = await makeSut(BlockMocks.H3_WITH_FORMAT_FOREGROUND).convert();
expect(html).toBe('<h3 style="color: #64473A; ">This is a h3 with brown color</h3>');
});
});
});
describe('When only an unordered list block is given', () => {
describe('When single block is given', () => {
it('returns html with ul tag with li tag inside', async () => {
const html = await makeSut(BlockMocks.UNORDERED_LIST_WITH_SINGLE_ITEM).convert();
expect(html).toBe('<ul>\n<li>This is a test</li>\n</ul>');
});
});
describe('When block has children', () => {
it('returns html with ul and li tags and children blocks inside', async () => {
const html = await makeSut(BlockMocks.UNORDERED_LIST_WITH_CHILDREN).convert();
expect(html.replace(/\s/g, '')).toBe(
`<ul>
<li>Hello World
<ul>
<li>This is a child</li>
<li>This is a child too</li>
</ul>
</li>
</ul>`.replace(/\s/g, ''),
);
});
});
describe('When single block is given with background color', () => {
it('returns html with ul tag with li tag inside and background', async () => {
const html = await makeSut(BlockMocks.UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT).convert();
expect(html).toBe('<ul style="background-color: #E9E5E3; ">\n<li>This is a item with background</li>\n</ul>');
});
});
describe('When single block is given with foreground color', () => {
it('returns html with ul tag with li tag inside and foreground', async () => {
const html = await makeSut(BlockMocks.UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND).convert();
expect(html).toBe('<ul style="color: #D9730D; ">\n<li>This is a item with color</li>\n</ul>');
});
});
describe('When list block with two items is given', () => {
it('returns html with ul tag with li tag inside', async () => {
const html = await makeSut(BlockMocks.UNORDERED_LIST_WITH_TWO_ITEMS).convert();
expect(html).toBe('<ul>\n<li>This is a test</li>\n<li>This is a test too</li>\n</ul>');
});
});
describe('When single line unordered list with decoration', () => {
it('returns html with ul tag with li tag and decorations tags inside', async () => {
const html = await makeSut(BlockMocks.UNORDERED_LIST_WITH_DECORATED_ITEMS).convert();
expect(html).toBe(
'<ul>\n<li>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></li>\n</ul>',
);
});
});
});
describe('When only an ordered list block is given', () => {
describe('When single block is given', () => {
it('returns html with ol tag with li tag inside', async () => {
const html = await makeSut(BlockMocks.ORDERED_LIST_WITH_SINGLE_ITEM).convert();
expect(html).toBe('<ol>\n<li>This is a test</li>\n</ol>');
});
});
describe('When block has children', () => {
it('returns html with ul and li tags and children blocks inside', async () => {
const html = await makeSut(BlockMocks.ORDERED_LIST_WITH_CHILDREN).convert();
expect(html.replace(/\s/g, '')).toBe(
`<ol>
<li>Hello World
<ol>
<li>This is a child</li>
<li>This is a child too</li>
</ol>
</li>
</ol>`.replace(/\s/g, ''),
);
});
});
describe('When single block is given with background color', () => {
it('returns html with ol tag with li tag inside and background', async () => {
const html = await makeSut(BlockMocks.ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT).convert();
expect(html).toBe('<ol style="background-color: #B4AEAE; ">\n<li>This is a item with background</li>\n</ol>');
});
});
describe('When single block is given with foreground color', () => {
it('returns html with ol tag with li tag inside and foreground', async () => {
const html = await makeSut(BlockMocks.ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND).convert();
expect(html).toBe('<ol style="color: #0F7B6C; ">\n<li>This is a item with color</li>\n</ol>');
});
});
describe('When list block with two items is given', () => {
it('returns html with ol tag with li tag inside', async () => {
const html = await makeSut(BlockMocks.ORDERED_LIST_WITH_TWO_ITEMS).convert();
expect(html).toBe('<ol>\n<li>This is a test</li>\n<li>This is a test too</li>\n</ol>');
});
});
describe('When single line ordered list with decoration', () => {
it('returns html with ol tag with li tag and decorations tags inside', async () => {
const html = await makeSut(BlockMocks.ORDERED_LIST_WITH_DECORATED_ITEMS).convert();
expect(html).toBe(
'<ol>\n<li>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></li>\n</ol>',
);
});
});
});
describe('When only a to do list block is given', () => {
describe('When single unchecked block is given', () => {
it('returns html with a div and unchecked checkbox and label inside', async () => {
const html = await makeSut(BlockMocks.TODO).convert();
expect(html.replace(/\s/g, '')).toBe(
`\
<ul class="to-do-list">
<li>
<div class="checkbox checkbox-off"></div>
<span class="to-do-children-unchecked">This is a test</span>\
</li>\
</ul>\
`.replace(/\s/g, ''),
);
});
});
describe('When block has children', () => {
it('returns html with todo and children blocks inside', async () => {
const html = await makeSut(BlockMocks.TODO_WITH_CHILDREN).convert();
expect(html.replace(/\s/g, '')).toBe(
`\
<ul class="to-do-list">
<li>
<div class="checkbox checkbox-off"></div>
<span class="to-do-children-unchecked">Hello World</span>\
<ul class="to-do-list">
<li>
<div class="checkbox checkbox-off"></div>
<span class="to-do-children-unchecked">This is a child</span>\
</li>\
</ul>\
<ul class="to-do-list">
<li>
<div class="checkbox checkbox-on"></div>
<span class="to-do-children-checked">This is a child too</span>\
</li>\
</ul>\
</li>\
</ul>\
`.replace(/\s/g, ''),
);
});
});
describe('When single unchecked block with background color is given', () => {
it('returns html with a div and unchecked checkbox and label inside with style on div', async () => {
const html = await makeSut(BlockMocks.TODO_WITH_FORMAT).convert();
expect(html.replace(/\s/g, '')).toBe(
`\
<ul class="to-do-list" style="background-color: #DDEBF1; ">
<li>
<div class="checkbox checkbox-off"></div>
<span class="to-do-children-unchecked">This is a todo with style</span>\
</li>\
</ul>\
`.replace(/\s/g, ''),
);
});
});
describe('When single unchecked block with foreground color is given', () => {
it('returns html with a div and unchecked checkbox and label inside with style on div', async () => {
const html = await makeSut(BlockMocks.TODO_WITH_FORMAT_FOREGROUND).convert();
expect(html.replace(/\s/g, '')).toBe(
`\
<ul class="to-do-list" style="color: #0B6E99; ">
<li>
<div class="checkbox checkbox-off"></div>
<span class="to-do-children-unchecked">This is a todo with style</span>\
</li>\
</ul>\
`.replace(/\s/g, ''),
);
});
});
describe('When single checked block is given', () => {
it('returns html with a div and checked checkbox and label inside', async () => {
const html = await makeSut(BlockMocks.CHECKED_TODO).convert();
expect(html.replace(/\s/g, '')).toBe(
`\
<ul class="to-do-list">
<li>
<div class="checkbox checkbox-on"></div>
<span class="to-do-children-checked">This is a test</span>\
</li>\
</ul>\
`.replace(/\s/g, ''),
);
});
});
describe('When to-do block with two items is given', () => {
it('returns html with two divs and checkbox and label inside', async () => {
const html = await makeSut(BlockMocks.UNCHECKED_AND_CHECKED_TODOS).convert();
expect(html.replace(/\s/g, '')).toBe(
`\
<ul class="to-do-list">
<li>
<div class="checkbox checkbox-off"></div>
<span class="to-do-children-unchecked">This is a test</span>\
</li>\
</ul>\
<ul class="to-do-list">
<li>
<div class="checkbox checkbox-on"></div>
<span class="to-do-children-checked">This is a test too</span>\
</li>\
</ul>\
`.replace(/\s/g, ''),
);
});
});
});
describe('When single code block is given', () => {
describe('When there is no style on code block', () => {
it('returns html with pre tag and code tag inside', async () => {
const html = await makeSut(BlockMocks.CODE).convert();
expect(html).toBe(
`<pre><code class="language-javascript">function test() {\n var isTesting = true;\n return isTesting;\n}</code></pre>`,
);
});
});
describe('When there is style on code block', () => {
it('ignores styles and returns html with pre tag and code tag inside', async () => {
const html = await makeSut(BlockMocks.CODE_WITH_DECORATION).convert();
expect(html).toBe(
`<pre><code class="language-javascript">function test() {\n var isTesting = true;\n return isTesting;\n}</code></pre>`,
);
});
});
});
describe('When single quote block is given', () => {
describe('When there is no style on quote block', () => {
it('returns html with blockquote tag', async () => {
const html = await makeSut(BlockMocks.QUOTE).convert();
expect(html).toBe('<blockquote>This a quote</blockquote>');
});
});
describe('When there is style on quote block', () => {
it('returns html with blockquote tag and decorations inside', async () => {
const html = await makeSut(BlockMocks.QUOTE_WITH_DECORATION).convert();
expect(html).toBe(
`<blockquote>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></blockquote>`,
);
});
});
describe('When there is background color on quote', () => {
it('returns html with style with background color prop', async () => {
const html = await makeSut(BlockMocks.QUOTE_WITH_FORMAT).convert();
expect(html).toBe('<blockquote style="background-color: #EAE4F2; ">This a quote with background</blockquote>');
});
});
describe('When there is background color on quote', () => {
it('returns html with style with background color prop', async () => {
const html = await makeSut(BlockMocks.QUOTE_WITH_FORMAT_FOREGROUND).convert();
expect(html).toBe('<blockquote style="color: #AD1A72; ">This a quote with color</blockquote>');
});
});
});
describe('When divider block is given', () => {
it('returns html with hr tag', async () => {
const html = await makeSut(BlockMocks.TEXT_BETWEEN_DIVIDER).convert();
expect(html).toBe(`<p>This a text</p>\n<hr>\n<p>This a text too</p>`);
});
});
describe('When equation block is given', () => {
describe('When there is no equation content', () => {
it('returns empty string', async () => {
const html = await makeSut(BlockMocks.EMPTY_EQUATION).convert();
expect(html).toBe('');
});
});
describe('When there is no equation content', () => {
it('returns html with div tag and equation class with equation inside', async () => {
const html = await makeSut(BlockMocks.EQUATION).convert();
expect(html).toBe(`<div class="equation">$$\\int 2xdx = x^2 + C$$</div>`);
});
});
});
describe('When video block is given', () => {
describe('When it is not a youtube video', () => {
it('returns empty string', async () => {
const html = await makeSut(BlockMocks.NO_YOUTUBE_VIDEO).convert();
expect(html).toBe('');
});
});
describe('When it is a youtube video', () => {
it('returns html with iframe tag and embed id', async () => {
const html = await makeSut(BlockMocks.YOUTUBE_VIDEO).convert();
expect(html.replace(/\s/g, '')).toBe(
`<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/xBFqxBfLJWc"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>`.replace(/\s/g, ''),
);
});
});
});
describe('When image block is given', () => {
beforeEach(() => {
const imageSource =
'https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bcedd078-56cd-4137-a28a-af16b5746874/767-50x50.jpg';
const blockId = 'ec3b36fd-f77d-46b4-8592-5966488612b1';
nock('https://www.notion.so')
.get(`/image/${encodeURIComponent(imageSource)}?table=block&id=${blockId}`)
.replyWithFile(200, resolve('src/__tests__/mocks/img/baseImage.jpeg'), {
'content-type': 'image/jpeg',
});
});
describe('When image has no caption', () => {
it('returns html with img tag with src as base64', async () => {
const html = await makeSut(BlockMocks.IMAGE).convert();
expect(html.replace(/\s/g, '')).toBe(
`
<figure class="image">
<img src="${base64Img}" alt="" >
</figure>
`.replace(/\s/g, ''),
);
});
});
describe('When image has caption', () => {
it('returns html with img tag with src as base64 and alt attr with given caption', async () => {
const html = await makeSut(BlockMocks.IMAGE_WITH_CAPTION).convert();
expect(html.replace(/\s/g, '')).toBe(
`
<figure class="image">
<img src="${base64Img}" alt="It is a caption">
<figcaption>It is a caption</figcaption>
</figure>
`.replace(/\s/g, ''),
);
});
});
describe('When image has width', () => {
it('returns html with img tag with width in style', async () => {
const html = await makeSut(BlockMocks.IMAGE_WITH_CUSTOM_SIZE).convert();
expect(html.replace(/\s/g, '')).toBe(
`
<figure class="image">
<img src="${base64Img}" alt="" style="width: 240px; ">
</figure>
`.replace(/\s/g, ''),
);
});
});
describe('When detail block is given', () => {
describe('When there are no style on block', () => {
it('returns empty string', async () => {
const html = await makeSut(BlockMocks.DETAILS).convert();
expect(html.replace(/\s/g, '')).toBe(
`
<details open="">
<summary>This is a detail</summary>
<p>
Hello World
</p>
</details>
`.replace(/\s/g, ''),
);
});
});
describe('When there is style block', () => {
it('returns html with blockquote tag and decorations inside', async () => {
const html = await makeSut(BlockMocks.DETAILS_WITH_DECORATION).convert();
expect(html.replace(/\s/g, '')).toBe(
`
<details open="">
<summary>Hello <em><strong>World </strong></em><strong>and</strong><em><strong> Sun</strong></em></summary>
<p>
Hello World
</p>
</details>
`.replace(/\s/g, ''),
);
});
});
describe('When there is background color', () => {
it('returns html with background color for the intire block', async () => {
const html = await makeSut(BlockMocks.DETAILS_WITH_BG).convert();
expect(html.replace(/\s/g, '')).toBe(
`
<details open="" style="background-color: #FBE4E4; ">
<summary>This is a detail</summary>
<p>
Hello World
</p>
</details>
`.replace(/\s/g, ''),
);
});
});
});
describe('When image must have a table and block id attached to url', () => {
it('it should attach block id to it', async () => {
const base64ConverterSpy = jest.spyOn(Base64Converter, 'convert');
const source = BlockMocks.IMAGE_WITH_CAPTION[0].properties.source;
const id = BlockMocks.IMAGE_WITH_CAPTION[0].id;
await makeSut(BlockMocks.IMAGE_WITH_CAPTION).convert();
const expectedImageUrl = `https://www.notion.so/image/${encodeURIComponent(source)}?table=block&id=${id}`;
expect(base64ConverterSpy).toBeCalledWith(expectedImageUrl);
});
});
});
describe('When callout block is given', () => {
describe('with default background and emoji icon', () => {
it('converts to callout html', async () => {
const html = await makeSut(BlockMocks.CALLOUT).convert();
expect(html.replace(/\s/g, '')).toBe(
`<div class="callout">
<div class="callout-emoji">💡</div>
<p>This is a callout</p>
</div>`.replace(/\s/g, ''),
);
});
});
describe('with default background and image icon', () => {
beforeEach(() => {
const imageSource = 'https://example.com/image.png';
const blockId = '16431c64-3bf0-481f-a29f-d544780d84f3';
nock('https://www.notion.so')
.get(`/image/${encodeURIComponent(imageSource)}?table=block&id=${blockId}`)
.replyWithFile(200, resolve('src/__tests__/mocks/img/baseImage.jpeg'), {
'content-type': 'image/jpeg',
});
});
it('converts to callout html and image to base64', async () => {
const html = await makeSut(BlockMocks.CALLOUT_WITH_IMAGE).convert();
expect(html.replace(/\s/g, '')).toBe(
`<div class="callout">
<div class="callout-image"><img src="${base64Img}" alt="callout icon"></div>
<p>This is a callout</p>
</div>`.replace(/\s/g, ''),
);
});
it('it should attach block id to it', async () => {
const base64ConverterSpy = jest.spyOn(Base64Converter, 'convert');
const blocks = BlockMocks.CALLOUT_WITH_IMAGE;
const source = blocks[0].properties.page_icon;
const id = blocks[0].id;
await makeSut(blocks).convert();
const expectedImageUrl = `https://www.notion.so/image/${encodeURIComponent(source)}?table=block&id=${id}`;
expect(base64ConverterSpy).toBeCalledWith(expectedImageUrl);
});
});
describe('with given background and emoji icon', () => {
it('converts to callout html', async () => {
const html = await makeSut(BlockMocks.CALLOUT_WITH_BACKGROUND).convert();
expect(html.replace(/\s/g, '')).toBe(
`<div class="callout" style="background-color: #FBE4E4; ">
<div class="callout-emoji">💡</div>
<p>This is a callout</p>
</div>`.replace(/\s/g, ''),
);
});
});
});
describe('When unknown block is given', () => {
it('returns empty string', async () => {
const html = await makeSut(BlockMocks.UNKNOWN).convert();
expect(html).toBe('');
});
});
});
================================================
FILE: src/data/use-cases/blocks-to-html-converter/blocks-to-html-converter.ts
================================================
import { ToHtml } from '../../../domain/use-cases/to-html';
import { Block } from '../../protocols/blocks';
import { BlockDispatcher } from './block-dispatcher';
import { ListBlocksWrapper } from './list-blocks-wrapper';
export class BlocksToHTML implements ToHtml {
private _blocks: Block[];
private _dispatcher: BlockDispatcher;
private _listBlocksWrapper: ListBlocksWrapper;
constructor(blocks: Block[], dispatcher: BlockDispatcher, listBlocksWrapper: ListBlocksWrapper) {
this._dispatcher = dispatcher;
this._listBlocksWrapper = listBlocksWrapper;
this._blocks = this._wrapLists(blocks);
}
async convert(): Promise<string> {
const htmlPromises: Promise<string[]> = Promise.all(this._blocks.map(this._convertBlock.bind(this)));
const html = (await htmlPromises).join('\n');
return new Promise((resolve) => resolve(html));
}
private async _convertBlock(block: Block): Promise<string> {
const blockToHtmlConverter = this._dispatch(block);
const htmlBlock = await blockToHtmlConverter.convert();
return new Promise((resolve) => resolve(htmlBlock));
}
private _wrapLists(blocks: Block[]): Block[] {
return this._listBlocksWrapper.wrapLists(blocks);
}
private _dispatch(block: Block): ToHtml {
return this._dispatcher.dispatch(block);
}
}
================================================
FILE: src/data/use-cases/blocks-to-html-converter/index.ts
================================================
export * from './blocks-to-html-converter';
export * from './block-dispatcher';
export * from './list-blocks-wrapper';
================================================
FILE: src/data/use-cases/blocks-to-html-converter/list-blocks-wrapper.ts
================================================
import { Block } from '../../protocols/blocks';
export class ListBlocksWrapper {
wrapLists(blocks: Block[]): Block[] {
return blocks.reduce((blocks, b) => {
if (!this._isList(b)) return [...blocks, b];
if (this._isFirstItemOfAList(blocks, b)) return [...blocks, this._generateListBlock(b)];
const lastContent = blocks[blocks.length - 1];
lastContent.children.push(b);
return blocks;
}, [] as Block[]);
}
private _isList(block: Block): boolean {
return block && block.type.includes('list');
}
private _isFirstItemOfAList(blocks: Block[], currentBlock: Block): boolean {
const lastContent = blocks[blocks.length - 1];
return (
(!this._isList(lastContent) || (lastContent && lastContent.children[0].type !== currentBlock.type)) &&
this._isList(currentBlock)
);
}
private _generateListBlock(childBlock: Block): Block {
return {
id: `${childBlock.id}-parent`,
type: 'list',
properties: childBlock.properties,
format: childBlock.format,
children: [childBlock],
decorableTexts: [],
};
}
}
================================================
FILE: src/data/use-cases/format-to-style/format-to-style.ts
================================================
import { Format } from 'data/protocols/blocks/format';
import { foregroundColorToHex, backgroundColorToHex } from '../../helpers/color-to-hex';
export class FormatToStyle {
private readonly _format: Format;
constructor(format: Format) {
this._format = format;
}
toStyle(): string {
const styleProps = [];
const blockColor = this._format.block_color;
if (blockColor) styleProps.push(new BlockColorToProp(blockColor).toStyle());
const blockWidth = this._format.block_width;
if (blockWidth) styleProps.push(new BlockWidthToProp(blockWidth).toStyle());
if (styleProps.length === 0) return '';
return ` style="${styleProps.join('')}"`;
}
}
class BlockColorToProp {
private readonly _blockColor: string;
constructor(blockColor: string) {
this._blockColor = blockColor;
}
toStyle(): string {
if (this._isBackground()) return `background-color: ${backgroundColorToHex(this._blockColor)}; `;
return `color: ${foregroundColorToHex(this._blockColor)}; `;
}
private _isBackground(): boolean {
return !!this._blockColor?.includes('background');
}
}
class BlockWidthToProp {
private readonly _blockWidth: number;
constructor(blockWidth: number) {
this._blockWidth = blockWidth;
}
toStyle(): string {
return `width: ${this._blockWidth}px; `;
}
}
================================================
FILE: src/data/use-cases/format-to-style/index.ts
================================================
export * from './format-to-style';
================================================
FILE: src/data/use-cases/html-wrapper/header-from-template.test.ts
================================================
import { HeaderFromTemplate } from './header-from-template';
import * as html from '../../../__tests__/mocks/html';
describe('#toHeader', () => {
describe('when page has title only', () => {
it('returns html with header and h1', () => {
const pageProps = { title: 'This is a title' };
const result = new HeaderFromTemplate(pageProps).toHeader();
expect(result.replace(/\s/g, '')).toEqual(html.HEADER_WITH_TITLE_ONLY.replace(/\s/g, ''));
});
});
describe('when page has title and cover', () => {
describe('when coverImagePosition is given on pageProp', () => {
it('returns html with header and h1 and image position on image style', () => {
const pageProps = {
title: 'This is a title',
coverImageSrc: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD',
coverImagePosition: 15,
};
const result = new HeaderFromTemplate(pageProps).toHeader();
expect(result.replace(/\s/g, '')).toEqual(html.HEADER_WITH_TITLE_AND_COVER_IMAGE.replace(/\s/g, ''));
});
});
describe('when coverImagePosition is not given on pageProp', () => {
it('returns html with header and h1 and image position as 0% on image style', () => {
const pageProps = {
title: 'This is a title',
coverImageSrc: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD',
};
const result = new HeaderFromTemplate(pageProps).toHeader();
expect(result.replace(/\s/g, '')).toEqual(
html.HEADER_WITH_TITLE_AND_COVER_IMAGE_WITHOUT_POSITION.replace(/\s/g, ''),
);
});
});
});
describe('when page has title cover and icon', () => {
describe('when icon is an image', () => {
it('returns html with header with h1, image cover and image icon with page-header-icon-with-cover class', () => {
const pageProps = {
title: 'This is a title',
coverImageSrc: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD',
icon: 'data:image/jpeg;base64,/4QDeRXhpZgAASUkqAAgAAAAGABIBAwA',
};
const result = new HeaderFromTemplate(pageProps).toHeader();
expect(result.replace(/\s/g, '')).toEqual(html.HEADER_WITH_TITLE_COVER_IMAGE_AND_IMAGE_ICON.replace(/\s/g, ''));
});
});
});
describe('when page has title and icon', () => {
describe('when icon is an image', () => {
it('returns html with header with h1 and image icon', () => {
const pageProps = {
title: 'This is a title',
icon: 'data:image/jpeg;base64,/4QDeRXhpZgAASUkqAAgAAAAGABIBAwA',
};
const result = new HeaderFromTemplate(pageProps).toHeader();
expect(result.replace(/\s/g, '')).toEqual(html.HEADER_WITH_TITLE_AND_IMAGE_ICON.replace(/\s/g, ''));
});
});
describe('when icon is an emoji', () => {
it('retruns html with header with h1 and emoji in a div', () => {
const pageProps = {
title: 'This is a title',
icon: '🤴',
};
const result = new HeaderFromTemplate(pageProps).toHeader();
expect(result.replace(/\s/g, '')).toEqual(html.HEADER_WITH_TITLE_AND_EMOJI_ICON.replace(/\s/g, ''));
});
});
});
});
================================================
FILE: src/data/use-cases/html-wrapper/header-from-template.ts
================================================
import { PageProps } from '../../protocols/page-props/page-props';
export class HeaderFromTemplate {
private readonly _pageProps: PageProps;
constructor(pageProps: PageProps) {
this._pageProps = pageProps;
}
toHeader(): string {
return `\
<header>
${this._coverImageHtml}
${this._iconHtml}
${this._titleHtml}
</header>\
`;
}
private get _coverImageHtml(): string {
const { coverImageSrc, coverImagePosition } = this._pageProps;
return coverImageSrc
? `<img class="page-cover-image" src="${coverImageSrc}" style="object-position:center ${
coverImagePosition || 0
}%">`
: '';
}
private get _iconHtml(): string {
const { coverImageSrc, icon } = this._pageProps;
if (!icon) return '';
const imageCoverSrcClassName = coverImageSrc ? 'page-header-icon-with-cover' : '';
if (!icon.startsWith('data:image/'))
return `<div class="page-header-icon ${imageCoverSrcClassName}"><span class="icon">${icon}</span></div>`;
return `<div class="page-header-icon ${imageCoverSrcClassName}"><img class="icon" src="${icon}"></div>`;
}
private get _titleHtml(): string {
const { title } = this._pageProps;
return `<h1 class="page-title">${title}</h1>`;
}
}
================================================
FILE: src/data/use-cases/html-wrapper/options-html-wrapper.ts
================================================
import { PageProps } from 'data/protocols/page-props';
import { HtmlWrapper } from '../../../domain/use-cases/html-wrapper';
import { HtmlOptions } from '../../protocols/html-options/html-options';
import { HeaderFromTemplate } from './header-from-template';
import { SCRIPTS } from './scripts';
import { STYLE } from './styles';
export class OptionsHtmlWrapper implements HtmlWrapper {
private readonly _options: HtmlOptions;
constructor(options: HtmlOptions) {
this._options = options;
}
wrapHtml(pageProps: PageProps, html: string): string {
if (this._options.bodyContentOnly) return html;
const title = pageProps.title;
return `\
<!DOCTYPE html>
<html>
${this._headFromTemplate(title)}
<body>
${!this._options.excludeHeaderFromBody ? new HeaderFromTemplate(pageProps).toHeader() : ''}
${html}
${!this._options.excludeScripts ? SCRIPTS : ''}
</body>
</html>`;
}
private _headFromTemplate(title: string): string {
return `\
<head>
${!this._options.excludeMetadata ? '<meta charset="utf-8">' : ''}
${!this._options.excludeMetadata ? '<meta name="viewport" content="width=device-width, initial-scale=1">' : ''}
${!this._options.excludeCSS ? STYLE : ''}
${!this._options.excludeTitleFromHead ? `<title>${title}</title>` : ''}
${
!this._options.excludeScripts
? '<link href="https://unpkg.com/prismjs@1.22.0/themes/prism.css" rel="stylesheet">'
: ''
}
</head>`;
}
}
================================================
FILE: src/data/use-cases/html-wrapper/scripts.ts
================================================
export const SCRIPTS = `\
<script src="https://unpkg.com/prismjs@1.22.0/components/prism-core.min.js"></script>
<script src="https://unpkg.com/prismjs@1.22.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']]
}
};
</script>
<script id="MathJax-script" async
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js">
</script>\
`;
================================================
FILE: src/data/use-cases/html-wrapper/styles.ts
================================================
export const STYLE = `\
<style>
html {
-webkit-print-color-adjust: exact;
}
* {
box-sizing: border-box;
-webkit-print-color-adjust: exact;
}
html,
body {
margin: 0;
padding: 0;
font-family: system-ui, sans-serif;
color: #37352F;
}
body {
line-height: 1.5;
}
@media only screen {
body {
margin: 2em auto;
max-width: 900px;
color: rgb(55, 53, 47);
}
}
img {
max-width: 100%;
max-height: 70vh;
}
ol,
ul {
margin: 0;
margin-block-start: 0.6em;
margin-block-end: 0.6em;
}
li > ol:first-child,
li > ul:first-child {
margin-block-start: 0.6em;
}
ul > li {
list-style: disc;
}
ul.to-do-list {
text-indent: -1.7em;
}
ul.to-do-list > li {
list-style: none;
}
.to-do-children-checked {
text-decoration: line-through;
opacity: 0.375;
}
ul.toggle > li {
list-style: none;
}
ul {
padding-inline-start: 1.7em;
}
ul > li {
padding-left: 0.1em;
}
ol {
padding-inline-start: 1.6em;
}
ol > li {
padding-left: 0.2em;
}
.mono ol {
padding-inline-start: 2em;
}
.mono ol > li {
text-indent: -0.4em;
}
.toggle {
padding-inline-start: 0em;
list-style-type: none;
}
/* Indent toggle children */
.toggle > li > details {
padding-left: 1.7em;
}
.toggle > li > details > summary {
margin-left: -1.1em;
}
h1,
h2,
h3 {
letter-spacing: -0.01em;
line-height: 1.2;
font-weight: 600;
margin-bottom: 0;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
margin-top: 0;
margin-bottom: 0.75em;
}
.icon {
display: inline-block;
max-width: 1.2em;
max-height: 1.2em;
text-decoration: none;
vertical-align: text-bottom;
margin-right: 0.5em;
}
img.icon {
border-radius: 3px;
}
.page-cover-image {
display: block;
object-fit: cover;
width: 100%;
height: 30vh;
}
.page-header-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.page-header-icon-with-cover {
margin-top: -0.72em;
margin-left: 0.07em;
}
.page-header-icon img {
border-radius: 3px;
}
blockquote {
font-size: 1.25em;
margin: 1em 0;
padding-left: 1em;
border-left: 3px solid rgb(55, 53, 47);
}
h1 {
font-size: 1.875rem;
margin-top: 1.875rem;
}
h2 {
font-size: 1.5rem;
margin-top: 1.5rem;
}
h3 {
font-size: 1.25rem;
margin-top: 1.25rem;
}
.callout {
padding: 16px 16px 16px 12px;
display: flex;
width: 100%;
border-radius: 3px;
border-width: 1px;
border-style: solid;
border-color: transparent;
align-items: center;
justify-content: center;
}
.callout-emoji {
height: 21.6px;
width: 21.6px;
font-size: 21.6px;
line-height: 1.1;
margin-left: 0px;
}
.callout p {
max-width: 100%;
width: 100%;
white-space: pre-wrap;
word-break: break-word;
margin-left: 10px;
}
.image {
border: none;
margin: 1.5em 0;
padding: 0;
border-radius: 0;
text-align: center;
}
figure {
margin: 1.25em 0;
page-break-inside: avoid;
}
figcaption {
opacity: 0.5;
font-size: 85%;
margin-top: 0.5em;
}
hr {
background: transparent;
display: block;
width: 100%;
height: 1px;
visibility: visible;
border: none;
border-bottom: 1px solid rgba(55, 53, 47, 0.09);
}
.checkbox {
display: inline-flex;
vertical-align: text-bottom;
width: 16px;
height: 16px;
background-size: 16px;
margin-left: 2px;
margin-right: 5px;
}
.checkbox-on {
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Crect%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22%2358A9D7%22%2F%3E%0A%3Cpath%20d%3D%22M6.71429%2012.2852L14%204.9995L12.7143%203.71436L6.71429%209.71378L3.28571%206.2831L2%207.57092L6.71429%2012.2852Z%22%20fill%3D%22white%22%2F%3E%0A%3C%2Fsvg%3E");
}
.checkbox-off {
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Crect%20x%3D%220.75%22%20y%3D%220.75%22%20width%3D%2214.5%22%20height%3D%2214.5%22%20fill%3D%22white%22%20stroke%3D%22%2336352F%22%20stroke-width%3D%221.5%22%2F%3E%0A%3C%2Fsvg%3E");
}
</style>`;
================================================
FILE: src/data/use-cases/page-block-to-page-props/index.ts
================================================
export * from './page-block-to-page-props';
================================================
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-cover-image-block.ts
================================================
import { Block } from '../../protocols/blocks';
import { Base64Converter } from '../../../utils/base-64-converter';
import { ImageCover } from '../../protocols/page-props';
export class PageBlockToCoverImageSource {
private readonly _pageBlock: Block;
constructor(pageBlock: Block) {
this._pageBlock = pageBlock;
}
async toImageCover(): Promise<ImageCover | null> {
const pageCover = this._pageBlock.properties.page_cover;
if (!pageCover || !this._isImageURL(pageCover)) return Promise.resolve(null);
let head = '';
if (pageCover.startsWith('/')) head = 'https://www.notion.so';
const base64 = await Base64Converter.convert(this.getImageAuthenticatedSrc(head + pageCover));
const position = this._pageCoverPositionToPositionCenter(this._pageBlock.format.page_cover_position || 0.6);
return { base64, position };
}
private _isImageURL(url: string): boolean {
return /(?:([^:\/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*\.(?:jpg|gif|png|jpeg))(?:\?([^#]*))?(?:#(.*))?/gi.test(url);
}
private getImageAuthenticatedSrc(src: string): string {
return `https://www.notion.so/image/${encodeURIComponent(src)}?table=block&id=${this._pageBlock.id}`;
}
private _pageCoverPositionToPositionCenter(coverPosition: number): number {
return (1 - coverPosition) * 100;
}
}
================================================
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-icon.ts
================================================
import { Block } from '../../protocols/blocks';
import { Base64Converter } from '../../../utils/base-64-converter';
export class PageBlockToIcon {
private readonly _pageBlock: Block;
constructor(pageBlock: Block) {
this._pageBlock = pageBlock;
}
async toIcon(): Promise<string | null> {
const icon = this._pageBlock.properties.page_icon;
if (!icon) return Promise.resolve(null);
if (!icon.startsWith('http')) return icon;
const url = `https://www.notion.so/image/${encodeURIComponent(icon)}?table=block&id=${this._pageBlock.id}`;
return Base64Converter.convert(url);
}
}
================================================
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-page-props.test.ts
================================================
import nock from 'nock';
import { resolve } from 'path';
import { PageBlockToPageProps } from './index';
import { Base64Converter } from '../../../utils/base-64-converter';
import * as Blocks from '../../../__tests__/mocks/blocks';
import base64Img from '../../../__tests__/mocks/img/base64';
describe('#toPageProps', () => {
describe('when page was title only', () => {
it('returns page prop with title only and correct value', async () => {
const pageBlockToPageProps = new PageBlockToPageProps(Blocks.PAGE_WITH_TITLE);
const result = await pageBlockToPageProps.toPageProps();
expect(result).toEqual({ title: 'Simple Page Title' });
});
});
describe('when page was no title', () => {
it('returns page prop with title setted as an empty string', async () => {
const pageBlockToPageProps = new PageBlockToPageProps(Blocks.PAGE_WITHOUT_TITLE);
const result = await pageBlockToPageProps.toPageProps();
expect(result).toEqual({ title: '' });
});
});
describe('when page has title and cover image', () => {
describe('when image is from notion', () => {
it('returns base64 image in coverImageSrc prop', async () => {
nock('https://www.notion.so')
.get('/image/https%3A%2F%2Fwww.notion.so%2Fimages%2Fpage-cover%2Fsolid_blue.png')
.query({
table: 'block',
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
})
.replyWithFile(200, resolve('src/__tests__/mocks/img/baseImage.jpeg'), {
'content-type': 'image/jpeg',
});
const pageBlockToPageProps = new PageBlockToPageProps(Blocks.PAGE_WITH_TITLE_AND_COVER_IMAGE[0]);
const result = await pageBlockToPageProps.toPageProps();
expect(result).toEqual({ title: 'Page Title', coverImageSrc: base64Img, coverImagePosition: 40 });
});
});
describe('when image is not from notion', () => {
it('returns base64 image in coverImageSrc prop', async () => {
nock('https://www.notion.so')
.get('/image/https%3A%2F%2Fwww.example.com%2Fsome_image.png')
.query({
table: 'block',
id: '4d64bbc0-634d-4758-befa-85c5a3a6c22f',
})
.replyWithFile(200, resolve('src/__tests__/mocks/img/baseImage.jpeg'), {
'content-type': 'image/jpeg',
});
const pageBlockToPageProps = new PageBlockToPageProps(
Blocks.PAGE_WITH_TITLE_AND_COVER_IMAGE_NOT_FROM_NOTION[0],
);
const result = await pageBlockToPageProps.toPageProps();
expect(result).toEqual({ title: 'Page Title', coverImageSrc: base64Img, coverImagePosition: 40 });
});
});
describe('when image url is not valid', () => {
it('returns base64 image in coverImageSrc prop', async () => {
const pageBlockToPageProps = new PageBlockToPageProps(Blocks.PAGE_WITH_TITLE_AND_INVALID_COVER_IMAGE[0]);
const result = await pageBlockToPageProps.toPageProps();
expect(result).toEqual({ title: 'Page Title' });
});
});
});
describe('when page has title and icon', () => {
describe('when icon is an utf-8 emoji', () => {
it('returns emoji in page prop', async () => {
const pageBlockToPageProps = new PageBlockToPageProps(Blocks.PAGE_WITH_TITLE_AND_EMOJI_ICON[0]);
const result = await pageBlockToPageProps.toPageProps();
expect(result).toEqual({ title: 'Page Title', icon: '🤴' });
});
});
describe('when icon is an image url', () => {
const block = Blocks.PAGE_WITH_TITLE_AND_IMAGE_ICON[0];
const imageSource = block.properties.page_icon;
beforeEach(() => {
nock('https://www.notion.so')
.get(`/image/${encodeURIComponent(imageSource)}?table=block&id=${block.id}`)
.replyWithFile(200, resolve('src/__tests__/mocks/img/baseImage.jpeg'), {
'content-type': 'image/jpeg',
});
});
it('returns image as base64 in page prop', async () => {
const pageBlockToPageProps = new PageBlockToPageProps(block);
const result = await pageBlockToPageProps.toPageProps();
expect(result).toEqual({ title: 'Page Title', icon: base64Img });
});
it('attaches block id to image url on base64 convertion', async () => {
const base64ConverterSpy = jest.spyOn(Base64Converter, 'convert');
const pageBlockToPageProps = new PageBlockToPageProps(block);
await pageBlockToPageProps.toPageProps();
const expectedImageUrl = `https://www.notion.so/image/${encodeURIComponent(imageSource)}?table=block&id=${
block.id
}`;
expect(base64ConverterSpy).toBeCalledWith(expectedImageUrl);
});
});
});
});
================================================
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-page-props.ts
================================================
import { Block } from '../../protocols/blocks';
import { PageProps } from '../../protocols/page-props';
import { PageBlockToTitle } from './page-block-to-title';
import { PageBlockToCoverImageSource } from './page-block-to-cover-image-block';
import { PageBlockToIcon } from './page-block-to-icon';
export class PageBlockToPageProps {
private readonly _pageBlock: Block;
constructor(pageBlock: Block) {
this._pageBlock = pageBlock;
}
async toPageProps(): Promise<PageProps> {
const title = new PageBlockToTitle(this._pageBlock).toTitle();
const coverImage = await new PageBlockToCoverImageSource(this._pageBlock).toImageCover();
const icon = await new PageBlockToIcon(this._pageBlock).toIcon();
return Promise.resolve({
title,
...(coverImage && { coverImageSrc: coverImage.base64, coverImagePosition: coverImage.position }),
...(icon && { icon }),
});
}
}
================================================
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-title.ts
================================================
import { Block } from '../../protocols/blocks';
export class PageBlockToTitle {
private readonly _pageBlock: Block;
constructor(pageBlock: Block) {
this._pageBlock = pageBlock;
}
toTitle(): string {
return this._pageBlock.decorableTexts[0]?.text || '';
}
}
================================================
FILE: src/domain/use-cases/html-wrapper.ts
================================================
import { PageProps } from '../../data/protocols/page-props';
export interface HtmlWrapper {
wrapHtml(pageProps: PageProps, html: string): string;
}
================================================
FILE: src/domain/use-cases/to-html.ts
================================================
export interface ToHtml {
convert(): Promise<string>;
}
export interface ToHtmlClass {
new (...args: any): ToHtml;
}
================================================
FILE: src/index.ts
================================================
import { NotionPageToHtml } from './main/use-cases/notion-api-to-html';
export default NotionPageToHtml;
module.exports = NotionPageToHtml;
================================================
FILE: src/infra/errors/index.ts
================================================
export * from './missing-content';
export * from './missing-page-id';
export * from './notion-page-access';
export * from './invalid-page-url';
export * from './notion-page-not-found';
================================================
FILE: src/infra/errors/invalid-page-url.ts
================================================
export class InvalidPageUrlError extends Error {
constructor(url: string) {
super(`Url "${url}" is not a valid notion page.`);
this.name = 'InvalidPageUrlError';
}
}
================================================
FILE: src/infra/errors/missing-content.ts
================================================
export class MissingContentError extends Error {
constructor(pageId: string) {
super(`Can not find content on page ${pageId}. Is it empty?`);
this.name = 'MissingContentError';
}
}
================================================
FILE: src/infra/errors/missing-page-id.ts
================================================
export class MissingPageIdError extends Error {
constructor() {
super('PageId is Missing');
this.name = 'MissingPageIdError';
}
}
================================================
FILE: src/infra/errors/notion-page-access.ts
================================================
export class NotionPageAccessError extends Error {
constructor(pageId: string) {
super(`Can not read Notion Page of id ${pageId}. Is it open for public reading?`);
this.name = 'NotionPageAccessError';
}
}
================================================
FILE: src/infra/errors/notion-page-not-found.ts
================================================
export class NotionPageNotFound extends Error {
constructor(pageId: string) {
super(
`Can not find Notion Page of id ${pageId}. Is the url correct? It is the original page or a redirect page (not supported)?`,
);
this.name = 'NotionPageNotFound';
}
}
================================================
FILE: src/infra/protocols/notion-api-content-response.ts
================================================
export type NotionApiContentResponse = {
id: string;
type: string;
properties: Record<string, any>;
format?: Record<string, any>;
contents: NotionApiContentResponse[];
};
================================================
FILE: src/infra/protocols/validation.ts
================================================
export interface Validation<Args extends Array<unknown> = []> {
validate(...args: Args): Error | null;
}
================================================
FILE: src/infra/use-cases/http-post/node-http-post-client.ts
================================================
import { HttpPostClient, HttpResponse } from '../../../data/protocols/http-request';
import https, { RequestOptions } from 'https';
import { URL } from 'url';
export class NodeHttpPostClient implements HttpPostClient {
async post(url: string, body: Record<string, any>): Promise<HttpResponse> {
const urlHandler = new URL(url);
const stringifiedBody = JSON.stringify(body);
const options: RequestOptions = {
hostname: urlHandler.hostname,
path: urlHandler.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': stringifiedBody.length,
},
};
let status = 504;
const requestAsPromised: Promise<HttpResponse> = new Promise((resolve, reject) => {
const req = https
.request(options, (res) => {
status = res.statusCode || 504;
const chunks = new Array<Uint8Array>();
res.on('data', (chunk) => {
chunks.push(chunk);
});
res.on('end', () => {
const result = Buffer.concat(chunks).toString('utf8');
resolve({ status, data: JSON.parse(result) });
});
})
.on('error', (err) => reject(err.message));
req.write(stringifiedBody);
req.end();
});
return requestAsPromised;
}
}
================================================
FILE: src/infra/use-cases/to-blocks/decoration-array-to-decorations.ts
================================================
import { Decoration, DecorationType } from '../../../data/protocols/blocks';
export class DecorationArrayToDecorations {
private readonly _decorationsArray: Array<any>;
constructor(decorationsArray: Array<any>) {
this._decorationsArray = decorationsArray;
}
toDecorations(): Decoration[] {
if (!this._decorationsArray) return [] as Decoration[];
return this._decorationsArray.map((decorations) => {
const [type, value] = decorations;
return {
type: fromDecorationArrayTypeToDecorationType[type] || 'plain',
...(value && { value }),
};
});
}
}
const fromDecorationArrayTypeToDecorationType: Record<string, DecorationType> = {
b: 'bold',
i: 'italic',
_: 'underline',
s: 'strikethrough',
c: 'code',
a: 'link',
e: 'equation',
h: 'color',
};
================================================
FILE: src/infra/use-cases/to-blocks/format-filter.ts
================================================
export class FormatFilter {
private readonly _format: Record<string, any>;
constructor(format: Record<string, any> | undefined) {
this._format = format || {};
}
filter(): Record<string, any> {
const presentAcceptableKeys = Object.keys(this._format).filter((k) => ACCEPTABLE_KEYS.includes(k));
return presentAcceptableKeys.reduce<Record<string, any>>((filteredFormat, key) => {
return {
...filteredFormat,
[key]: this._format[key],
};
}, {} as Record<string, any>);
}
}
const ACCEPTABLE_KEYS: string[] = ['block_color', 'page_cover_position', 'block_width'];
================================================
FILE: src/infra/use-cases/to-blocks/notion-api-content-response-to-blocks.test.ts
================================================
import * as NotionApiMocks from '../../../__tests__/mocks/notion-api-responses';
import * as BlockMocks from '../../../__tests__/mocks/blocks';
import { NotionApiContentResponsesToBlocks } from './notion-api-content-response-to-blocks';
describe('#toBlocks', () => {
describe('when page with title and single text content is given', () => {
it('converts to one single text block with given content', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_AND_TITLE_NOTION_API_CONTENT_RESPONSE;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT);
});
});
describe('when page with text content with bold content is given', () => {
it('converts to one block with two decorations', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_WITH_BOLD;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT_WITH_BOLD);
});
});
describe('when page with text content with bold and italic content is given', () => {
describe('when they are together', () => {
it('converts to one block with two decorations', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_WITH_BOLD_AND_ITALIC_TOGETHER;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT_WITH_BOLD_AND_ITALIC);
});
});
describe('when they are not together', () => {
it('converts to one block with two decorations', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_WITH_BOLD_AND_ITALIC;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT_WITH_BOLD_AND_ITALIC_SEPARATED);
});
});
});
describe('when page with color text content is given', () => {
it('converts to one block with decoration with value', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_WITH_COLOR;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT_WITH_COLOR);
});
});
describe('when page with equation text content is given', () => {
it('converts to one block with decoration with value', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_WITH_EQUATION;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT_WITH_EQUATION_DECORATION);
});
});
describe('when page with link text content is given', () => {
it('converts to one block with decoration with value', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_WITH_LINK;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT_WITH_LINK);
});
});
describe('when page with format is given', () => {
it('passes format prop to block', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_TEXT_WITH_FORMAT;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.SINGLE_TEXT_WITH_FORMAT);
});
});
describe('when page with custom image size is given', () => {
it('passes block_width to format', () => {
const notionApiContentResponses = NotionApiMocks.IMAGE_WITH_CUSTOM_SIZE;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.IMAGE_WITH_CUSTOM_SIZE);
});
});
describe('when page with page_icon in format is given', () => {
it('passes format prop to properties', () => {
const notionApiContentResponses = NotionApiMocks.CALLOUT_WITH_PAGE_ICON;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.CALLOUT);
});
});
describe('when page with youtube link', () => {
it('converts to one block with decoration with value', () => {
const notionApiContentResponses = NotionApiMocks.VIDEO_NOTION_API_CONTENT_RESPONSE;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.PAGE_WITH_YOUTUBE_VIDEO);
});
});
describe('when page with page cover and page cover position is given', () => {
it('converts to page block with page_cover and page_conver_position in format prop', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_PAGE_WITH_COVER_IMAGE;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.PAGE_WITH_TITLE_AND_COVER_IMAGE);
});
});
describe('when page with page icon is given', () => {
it('converts to page block with page_icon in format prop', () => {
const notionApiContentResponses = NotionApiMocks.SINGLE_PAGE_WITH_ICON;
const notionApiContentResponsesToBlocks = new NotionApiContentResponsesToBlocks(notionApiContentResponses);
const result = notionApiContentResponsesToBlocks.toBlocks();
expect(result).toEqual(BlockMocks.PAGE_WITH_TITLE_AND_ICON);
});
});
});
================================================
FILE: src/infra/use-cases/to-blocks/notion-api-content-response-to-blocks.ts
================================================
import { Block } from '../../../data/protocols/blocks';
import { NotionApiContentResponse } from '../../protocols/notion-api-content-response';
import { PropTitleToDecorableTexts } from '../to-blocks/prop-title-to-decorable-texts';
import { FormatFilter } from './format-filter';
import { PropertiesParser } from './properties-parser';
export class NotionApiContentResponsesToBlocks {
private readonly _notionApiContentResponses: NotionApiContentResponse[];
constructor(notionApiContentResponses: NotionApiContentResponse[]) {
this._notionApiContentResponses = notionApiContentResponses;
}
toBlocks(): Block[] {
if (!this._notionApiContentResponses) return [];
return this._notionApiContentResponses.map((nacr) => ({
id: nacr.id,
type: nacr.type,
format: new FormatFilter(nacr.format).filter(),
properties: new PropertiesParser(nacr.format, nacr.properties).parse(),
children: new NotionApiContentResponsesToBlocks(nacr.contents).toBlocks(),
decorableTexts: new PropTitleToDecorableTexts(nacr.properties?.title).toDecorableTexts(),
}));
}
}
================================================
FILE: src/infra/use-cases/to-blocks/prop-title-to-decorable-texts.ts
================================================
import { DecorableText } from '../../../data/protocols/blocks';
import { DecorationArrayToDecorations } from './decoration-array-to-decorations';
export class PropTitleToDecorableTexts {
private readonly _title: any[] | undefined;
constructor(title: any[] | undefined) {
this._title = title;
}
toDecorableTexts(): DecorableText[] {
if (!this._title) return [] as DecorableText[];
return this._title.map((richText: any[]) => {
const text = richText[0].toString();
const decorationsArray = richText[1];
return {
text,
decorations: new DecorationArrayToDecorations(decorationsArray).toDecorations(),
};
});
}
}
================================================
FILE: src/infra/use-cases/to-blocks/properties-parser.ts
================================================
export class PropertiesParser {
private readonly _format: Record<string, any>;
private readonly _properties: Record<string, any>;
constructor(format: Record<string, any> | undefined, properties: Record<string, any> | undefined) {
this._format = format || {};
this._properties = properties || {};
}
parse(): Record<string, any> {
const avaliableKeys = Object.keys({ ...this._format, ...this._properties }).filter((k) =>
KEYS_TO_PRESERVE.includes(k),
);
return avaliableKeys.reduce<Record<string, any>>(
(format, key) => ({
...format,
[key]: this._properties[key]?.[0]?.[0] || this._format[key],
}),
{},
);
}
}
const KEYS_TO_PRESERVE = ['source', 'caption', 'language', 'checked', 'page_icon', 'page_cover'];
================================================
FILE: src/infra/use-cases/to-notion-api-content-responses/notion-api-page-fetcher.test.ts
================================================
import nock from 'nock';
import { NotionApiPageFetcher } from './notion-api-page-fetcher';
import { NotionPageIdValidator, PageRecordValidator, PageChunkValidator } from './services';
import { NodeHttpPostClient } from '../http-post/node-http-post-client';
import { MissingContentError, MissingPageIdError, NotionPageAccessError } from '../../errors';
import * as NotionApiMocks from '../../../__tests__/mocks/notion-api-responses';
describe('#getNotionPageContents', () => {
afterEach(() => {
nock.cleanAll();
});
const makeSut = (notionPageId: string): NotionApiPageFetcher => {
const httpPostClient = new NodeHttpPostClient();
const notionPageIdValidator = new NotionPageIdValidator();
const pageRecordValidator = new PageRecordValidator();
const pageChunkValidator = new PageChunkValidator();
return new NotionApiPageFetcher(
notionPageId,
httpPostClient,
notionPageIdValidator,
pageRecordValidator,
pageChunkValidator,
);
};
describe('when notion page id is valid and page is public', () => {
it('returns NotionApiContentResponse object with page content when page is valid', async () => {
nock('https://www.notion.so').post('/api/v3/loadPageChunk').reply(200, NotionApiMocks.SUCCESSFUL_PAGE_CHUCK);
nock('https://www.notion.so').post('/api/v3/getRecordValues').reply(200, NotionApiMocks.SUCCESSFUL_RECORDS);
const notionPageId = '4d64bbc0-634d-4758-befa-85c5a3a6c22f';
const apiInterface = makeSut(notionPageId);
const response = await apiInterface.getNotionPageContents();
expect(response).toEqual(NotionApiMocks.TEXT_NOTION_API_CONTENT_RESPONSE);
});
it('passes its children when it is available', async () => {
nock('https://www.notion.so')
.post('/api/v3/loadPageChunk')
.reply(200, NotionApiMocks.SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN);
nock('https://www.notion.so')
.post('/api/v3/getRecordValues')
.reply(200, NotionApiMocks.SUCCESSFUL_RECORDS_WITH_CHILDREN);
const notionPageId = '4d64bbc0-634d-4758-befa-85c5a3a6c22f';
const apiInterface = makeSut(notionPageId);
const response = await apiInterface.getNotionPageContents();
expect(response).toEqual(NotionApiMocks.LIST_WITH_CHILDREN_RESPONSE);
});
describe('when children is not available on page chunk but it is available by request', () => {
it('get out block from new request and passes in content', async () => {
nock('https://www.notion.so')
.post('/api/v3/loadPageChunk')
.reply(200, NotionApiMocks.SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN_NOT_IN_CHUNK);
nock('https://www.notion.so')
.post('/api/v3/syncRecordValues')
.reply(200, NotionApiMocks.SUCCESSFUL_SYNC_RECORD_VALUE);
nock('https://www.notion.so')
.post('/api/v3/getRecordValues')
.reply(200, NotionApiMocks.SUCCESSFUL_RECORDS_WITH_CHILDREN);
const notionPageId = '4d64bbc0-634d-4758-befa-85c5a3a6c22f';
const apiInterface = makeSut(notionPageId);
const response = await apiInterface.getNotionPageContents();
expect(response).toEqual(NotionApiMocks.DETAILS_RESPONSE);
});
});
});
describe('when notion page id is missing', () => {
it('throws MissingPageIdError', async () => {
const response = () => makeSut('').getNotionPageContents();
await expect(response).toThrow(new MissingPageIdError());
});
});
describe('when notion page is not open for public reading', () => {
it('throws NotionPageAccessError', async () => {
nock('https://www.notion.so').post('/api/v3/getRecordValues').reply(200, NotionApiMocks.NO_PAGE_ACCESS
gitextract_8y8r4zl3/ ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── workflows/ │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .lintstagedrc.json ├── .npmignore ├── .nvmrc ├── .prettierrc.js ├── Dockerfile.test ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.test.yml ├── jest.config.js ├── package.json ├── src/ │ ├── __tests__/ │ │ └── mocks/ │ │ ├── blocks.ts │ │ ├── html.ts │ │ ├── img/ │ │ │ └── base64.ts │ │ └── notion-api-responses.ts │ ├── data/ │ │ ├── helpers/ │ │ │ ├── block-to-inner-html.ts │ │ │ ├── block-to-inner-text.ts │ │ │ ├── blocks-to-html.ts │ │ │ ├── color-to-hex.ts │ │ │ └── replace-line-break-to-br-tag.ts │ │ ├── protocols/ │ │ │ ├── blocks/ │ │ │ │ ├── block.ts │ │ │ │ ├── decorable-text.ts │ │ │ │ ├── decoration.ts │ │ │ │ ├── format.ts │ │ │ │ ├── index.ts │ │ │ │ └── list-blocks-wrapper.ts │ │ │ ├── html-options/ │ │ │ │ └── html-options.ts │ │ │ ├── http-request/ │ │ │ │ ├── http-get-client.ts │ │ │ │ ├── http-post-client.ts │ │ │ │ ├── http-response.ts │ │ │ │ └── index.ts │ │ │ └── page-props/ │ │ │ ├── image-cover.ts │ │ │ ├── index.ts │ │ │ └── page-props.ts │ │ └── use-cases/ │ │ ├── blocks-to-html-converter/ │ │ │ ├── block-dispatcher.ts │ │ │ ├── block-parsers/ │ │ │ │ ├── callout.ts │ │ │ │ ├── code.ts │ │ │ │ ├── decorations/ │ │ │ │ │ ├── decoration-dispatcher.ts │ │ │ │ │ ├── decoration-parsers/ │ │ │ │ │ │ ├── bold.ts │ │ │ │ │ │ ├── code.ts │ │ │ │ │ │ ├── color.test.ts │ │ │ │ │ │ ├── color.ts │ │ │ │ │ │ ├── equation.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── italic.ts │ │ │ │ │ │ ├── link.ts │ │ │ │ │ │ ├── strikethrough.ts │ │ │ │ │ │ ├── underline.ts │ │ │ │ │ │ └── unknown.ts │ │ │ │ │ └── decorator.ts │ │ │ │ ├── divider.ts │ │ │ │ ├── equation.ts │ │ │ │ ├── header.ts │ │ │ │ ├── image.ts │ │ │ │ ├── index.ts │ │ │ │ ├── list/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── list-item.ts │ │ │ │ │ └── list.ts │ │ │ │ ├── page.ts │ │ │ │ ├── quote.ts │ │ │ │ ├── sub-header.ts │ │ │ │ ├── sub-sub-header.ts │ │ │ │ ├── text.ts │ │ │ │ ├── to-do.ts │ │ │ │ ├── toggle.ts │ │ │ │ ├── unknown.ts │ │ │ │ └── youtube-video.ts │ │ │ ├── blocks-to-html-converter.test.ts │ │ │ ├── blocks-to-html-converter.ts │ │ │ ├── index.ts │ │ │ └── list-blocks-wrapper.ts │ │ ├── format-to-style/ │ │ │ ├── format-to-style.ts │ │ │ └── index.ts │ │ ├── html-wrapper/ │ │ │ ├── header-from-template.test.ts │ │ │ ├── header-from-template.ts │ │ │ ├── options-html-wrapper.ts │ │ │ ├── scripts.ts │ │ │ └── styles.ts │ │ └── page-block-to-page-props/ │ │ ├── index.ts │ │ ├── page-block-to-cover-image-block.ts │ │ ├── page-block-to-icon.ts │ │ ├── page-block-to-page-props.test.ts │ │ ├── page-block-to-page-props.ts │ │ └── page-block-to-title.ts │ ├── domain/ │ │ └── use-cases/ │ │ ├── html-wrapper.ts │ │ └── to-html.ts │ ├── index.ts │ ├── infra/ │ │ ├── errors/ │ │ │ ├── index.ts │ │ │ ├── invalid-page-url.ts │ │ │ ├── missing-content.ts │ │ │ ├── missing-page-id.ts │ │ │ ├── notion-page-access.ts │ │ │ └── notion-page-not-found.ts │ │ ├── protocols/ │ │ │ ├── notion-api-content-response.ts │ │ │ └── validation.ts │ │ └── use-cases/ │ │ ├── http-post/ │ │ │ └── node-http-post-client.ts │ │ ├── to-blocks/ │ │ │ ├── decoration-array-to-decorations.ts │ │ │ ├── format-filter.ts │ │ │ ├── notion-api-content-response-to-blocks.test.ts │ │ │ ├── notion-api-content-response-to-blocks.ts │ │ │ ├── prop-title-to-decorable-texts.ts │ │ │ └── properties-parser.ts │ │ ├── to-notion-api-content-responses/ │ │ │ ├── notion-api-page-fetcher.test.ts │ │ │ ├── notion-api-page-fetcher.ts │ │ │ └── services/ │ │ │ ├── index.ts │ │ │ ├── notion-page-id-validation.service.ts │ │ │ ├── page-chunk-validation.service.test.ts │ │ │ ├── page-chunk-validation.service.ts │ │ │ └── page-record-validation.service.ts │ │ └── to-page-id/ │ │ ├── index.ts │ │ ├── notion-url-to-page-id.test.ts │ │ ├── notion-url-to-page-id.ts │ │ └── services/ │ │ ├── id-normalizer.ts │ │ ├── index.ts │ │ └── url-validator.ts │ ├── main/ │ │ ├── factories/ │ │ │ ├── blocks-to-html.factory.ts │ │ │ ├── index.ts │ │ │ ├── notion-api-page-fetcher.factory.ts │ │ │ └── notion-url-to-page-id.factory.ts │ │ ├── protocols/ │ │ │ └── notion-page.ts │ │ └── use-cases/ │ │ └── notion-api-to-html/ │ │ ├── index.ts │ │ ├── notion-page-to-html.test.ts │ │ └── notion-page-to-html.ts │ └── utils/ │ ├── base-64-converter.ts │ ├── either.ts │ ├── errors/ │ │ ├── forbidden-error.ts │ │ ├── image-not-found-error.ts │ │ └── index.ts │ └── use-cases/ │ └── http-get/ │ └── node-http-get.ts ├── tsconfig.build.json └── tsconfig.json
SYMBOL INDEX (362 symbols across 83 files)
FILE: src/__tests__/mocks/blocks.ts
constant NO_TEXT (line 3) | const NO_TEXT = [
constant SINGLE_TEXT (line 14) | const SINGLE_TEXT = [
constant SINGLE_TEXT_WITH_CHILDREN (line 30) | const SINGLE_TEXT_WITH_CHILDREN = [
constant SINGLE_TEXT_WITH_BOLD (line 73) | const SINGLE_TEXT_WITH_BOLD = [
constant SINGLE_TEXT_WITH_ITALIC (line 97) | const SINGLE_TEXT_WITH_ITALIC = [
constant SINGLE_TEXT_WITH_BOLD_AND_ITALIC_SEPARATED (line 121) | const SINGLE_TEXT_WITH_BOLD_AND_ITALIC_SEPARATED = [
constant SINGLE_TEXT_WITH_UNDERLINE (line 141) | const SINGLE_TEXT_WITH_UNDERLINE = [
constant SINGLE_TEXT_WITH_STRIKETHROUGH (line 165) | const SINGLE_TEXT_WITH_STRIKETHROUGH = [
constant SINGLE_TEXT_WITH_CODE_DECORATION (line 189) | const SINGLE_TEXT_WITH_CODE_DECORATION = [
constant SINGLE_TEXT_WITH_LINK (line 213) | const SINGLE_TEXT_WITH_LINK = [
constant SINGLE_TEXT_WITH_FORMAT (line 238) | const SINGLE_TEXT_WITH_FORMAT = [
constant SINGLE_TEXT_WITH_EQUATION_DECORATION (line 263) | const SINGLE_TEXT_WITH_EQUATION_DECORATION = [
constant SINGLE_TEXT_WITH_COLOR (line 288) | const SINGLE_TEXT_WITH_COLOR = [
constant SINGLE_TEXT_WITH_COLOR_BACKGROUND (line 309) | const SINGLE_TEXT_WITH_COLOR_BACKGROUND = [
constant SINGLE_TEXT_WITH_BOLD_AND_ITALIC (line 330) | const SINGLE_TEXT_WITH_BOLD_AND_ITALIC = [
constant TEXT_WITH_DECORATION (line 357) | const TEXT_WITH_DECORATION = [
constant MULTILINE_TEXT (line 403) | const MULTILINE_TEXT = [
constant TEXT_WITH_FORMAT (line 419) | const TEXT_WITH_FORMAT = [
constant TEXT_WITH_FORMAT_FOREGROUND (line 437) | const TEXT_WITH_FORMAT_FOREGROUND = [
constant H1_TEXT (line 455) | const H1_TEXT = [
constant H1_TEXT_WITH_DECORATIONS (line 471) | const H1_TEXT_WITH_DECORATIONS = [
constant H1_WITH_FORMAT (line 517) | const H1_WITH_FORMAT = [
constant H1_WITH_FORMAT_FOREGROUND (line 535) | const H1_WITH_FORMAT_FOREGROUND = [
constant H2_TEXT (line 553) | const H2_TEXT = [
constant H2_TEXT_WITH_DECORATIONS (line 569) | const H2_TEXT_WITH_DECORATIONS = [
constant H2_WITH_FORMAT (line 615) | const H2_WITH_FORMAT = [
constant H2_WITH_FORMAT_FOREGROUND (line 633) | const H2_WITH_FORMAT_FOREGROUND = [
constant H3_TEXT (line 651) | const H3_TEXT = [
constant H3_TEXT_WITH_DECORATIONS (line 667) | const H3_TEXT_WITH_DECORATIONS = [
constant H3_WITH_FORMAT (line 713) | const H3_WITH_FORMAT = [
constant H3_WITH_FORMAT_FOREGROUND (line 731) | const H3_WITH_FORMAT_FOREGROUND = [
constant UNORDERED_LIST_WITH_SINGLE_ITEM (line 749) | const UNORDERED_LIST_WITH_SINGLE_ITEM = [
constant UNORDERED_LIST_WITH_CHILDREN (line 765) | const UNORDERED_LIST_WITH_CHILDREN = [
constant UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT (line 808) | const UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT = [
constant UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND (line 826) | const UNORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND = [
constant UNORDERED_LIST_WITH_TWO_ITEMS (line 844) | const UNORDERED_LIST_WITH_TWO_ITEMS = [
constant UNORDERED_LIST_WITH_DECORATED_ITEMS (line 873) | const UNORDERED_LIST_WITH_DECORATED_ITEMS = [
constant ORDERED_LIST_WITH_SINGLE_ITEM (line 919) | const ORDERED_LIST_WITH_SINGLE_ITEM = [
constant ORDERED_LIST_WITH_CHILDREN (line 935) | const ORDERED_LIST_WITH_CHILDREN = [
constant ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT (line 978) | const ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT = [
constant ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND (line 996) | const ORDERED_LIST_WITH_SINGLE_ITEM_AND_FORMAT_FOREGROUND = [
constant ORDERED_LIST_WITH_TWO_ITEMS (line 1014) | const ORDERED_LIST_WITH_TWO_ITEMS = [
constant ORDERED_LIST_WITH_DECORATED_ITEMS (line 1043) | const ORDERED_LIST_WITH_DECORATED_ITEMS = [
constant TODO (line 1089) | const TODO = [
constant TODO_WITH_CHILDREN (line 1105) | const TODO_WITH_CHILDREN = [
constant TODO_WITH_FORMAT (line 1148) | const TODO_WITH_FORMAT = [
constant TODO_WITH_FORMAT_FOREGROUND (line 1166) | const TODO_WITH_FORMAT_FOREGROUND = [
constant CHECKED_TODO (line 1184) | const CHECKED_TODO = [
constant UNCHECKED_AND_CHECKED_TODOS (line 1200) | const UNCHECKED_AND_CHECKED_TODOS = [
constant CODE (line 1229) | const CODE = [
constant CODE_WITH_DECORATION (line 1245) | const CODE_WITH_DECORATION = [
constant QUOTE (line 1273) | const QUOTE = [
constant QUOTE_WITH_FORMAT (line 1289) | const QUOTE_WITH_FORMAT = [
constant QUOTE_WITH_FORMAT_FOREGROUND (line 1307) | const QUOTE_WITH_FORMAT_FOREGROUND = [
constant QUOTE_WITH_DECORATION (line 1325) | const QUOTE_WITH_DECORATION = [
constant TEXT_BETWEEN_DIVIDER (line 1371) | const TEXT_BETWEEN_DIVIDER = [
constant EMPTY_EQUATION (line 1408) | const EMPTY_EQUATION = [
constant EQUATION (line 1419) | const EQUATION = [
constant NO_YOUTUBE_VIDEO (line 1435) | const NO_YOUTUBE_VIDEO = [
constant YOUTUBE_VIDEO (line 1448) | const YOUTUBE_VIDEO = [
constant TEXT_WITH_YOUTUBE_VIDEO (line 1461) | const TEXT_WITH_YOUTUBE_VIDEO = [
constant PAGE_WITH_YOUTUBE_VIDEO (line 1496) | const PAGE_WITH_YOUTUBE_VIDEO = [
constant IMAGE (line 1518) | const IMAGE = [
constant IMAGE_WITH_CAPTION (line 1532) | const IMAGE_WITH_CAPTION = [
constant IMAGE_WITH_CUSTOM_SIZE (line 1547) | const IMAGE_WITH_CUSTOM_SIZE = [
constant CALLOUT (line 1561) | const CALLOUT = [
constant CALLOUT_WITH_IMAGE (line 1577) | const CALLOUT_WITH_IMAGE = [
constant CALLOUT_WITH_BACKGROUND (line 1593) | const CALLOUT_WITH_BACKGROUND = [
constant DETAILS_WITH_DECORATION (line 1609) | const DETAILS_WITH_DECORATION = [
constant DETAILS (line 1678) | const DETAILS = [
constant DETAILS_WITH_BG (line 1717) | const DETAILS_WITH_BG = [
constant UNKNOWN (line 1758) | const UNKNOWN = [
constant PAGE_WITH_TITLE (line 1774) | const PAGE_WITH_TITLE = {
constant PAGE_WITHOUT_TITLE (line 1797) | const PAGE_WITHOUT_TITLE = {
constant PAGE_WITH_TITLE_AND_COVER_IMAGE (line 1820) | const PAGE_WITH_TITLE_AND_COVER_IMAGE = [
constant PAGE_WITH_TITLE_AND_ICON (line 1831) | const PAGE_WITH_TITLE_AND_ICON = [
constant PAGE_WITH_TITLE_AND_COVER_IMAGE_NOT_FROM_NOTION (line 1842) | const PAGE_WITH_TITLE_AND_COVER_IMAGE_NOT_FROM_NOTION = [
constant PAGE_WITH_TITLE_AND_INVALID_COVER_IMAGE (line 1853) | const PAGE_WITH_TITLE_AND_INVALID_COVER_IMAGE = [
constant PAGE_WITH_TITLE_AND_EMOJI_ICON (line 1864) | const PAGE_WITH_TITLE_AND_EMOJI_ICON = [
constant PAGE_WITH_TITLE_AND_IMAGE_ICON (line 1875) | const PAGE_WITH_TITLE_AND_IMAGE_ICON = [
FILE: src/__tests__/mocks/html.ts
constant STYLE_TAG (line 3) | const STYLE_TAG = `\
constant HEADER (line 258) | const HEADER = `\
constant CONTENT_WITH_HEADER (line 268) | const CONTENT_WITH_HEADER = `\
constant CONTENT_WITHOUT_HEADER (line 273) | const CONTENT_WITHOUT_HEADER = `\
constant FULL_DOCUMENT (line 277) | const FULL_DOCUMENT = `
constant DOCUMENT_WITHOUT_TITLE (line 305) | const DOCUMENT_WITHOUT_TITLE = `
constant DOCUMENT_WITHOUT_CSS (line 332) | const DOCUMENT_WITHOUT_CSS = `
constant DOCUMENT_METADATA (line 359) | const DOCUMENT_METADATA = `
constant DOCUMENT_WITHOUT_SCRIPTS (line 385) | const DOCUMENT_WITHOUT_SCRIPTS = `
constant FULL_DOCUMENT_WITHOUT_HEADER_IN_BODY (line 400) | const FULL_DOCUMENT_WITHOUT_HEADER_IN_BODY = `
constant BODY_ONLY (line 428) | const BODY_ONLY = CONTENT_WITHOUT_HEADER;
constant HEADER_WITH_TITLE_ONLY (line 430) | const HEADER_WITH_TITLE_ONLY = `\
constant HEADER_WITH_TITLE_AND_COVER_IMAGE (line 436) | const HEADER_WITH_TITLE_AND_COVER_IMAGE = `\
constant HEADER_WITH_TITLE_AND_COVER_IMAGE_WITHOUT_POSITION (line 443) | const HEADER_WITH_TITLE_AND_COVER_IMAGE_WITHOUT_POSITION = `\
constant HEADER_WITH_TITLE_COVER_IMAGE_AND_IMAGE_ICON (line 450) | const HEADER_WITH_TITLE_COVER_IMAGE_AND_IMAGE_ICON = `\
constant HEADER_WITH_TITLE_AND_IMAGE_ICON (line 460) | const HEADER_WITH_TITLE_AND_IMAGE_ICON = `\
constant HEADER_WITH_TITLE_AND_EMOJI_ICON (line 469) | const HEADER_WITH_TITLE_AND_EMOJI_ICON = `\
FILE: src/__tests__/mocks/notion-api-responses.ts
constant SUCCESSFUL_PAGE_CHUCK (line 1) | const SUCCESSFUL_PAGE_CHUCK = {
constant SUCCESSFUL_RECORDS (line 67) | const SUCCESSFUL_RECORDS = {
constant SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN (line 83) | const SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN = {
constant SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN_NOT_IN_CHUNK (line 181) | const SUCCESSFUL_PAGE_CHUCK_WITH_CHILDREN_NOT_IN_CHUNK = {
constant SUCCESSFUL_SYNC_RECORD_VALUE (line 273) | const SUCCESSFUL_SYNC_RECORD_VALUE = {
constant SUCCESSFUL_RECORDS_WITH_CHILDREN (line 298) | const SUCCESSFUL_RECORDS_WITH_CHILDREN = {
constant NO_PAGE_ACCESS_RECORDS (line 326) | const NO_PAGE_ACCESS_RECORDS = { results: [{ role: 'none' }] };
constant MISSING_CONTENT_RECORDS (line 328) | const MISSING_CONTENT_RECORDS = {
constant SINGLE_PAGE_WITH_COVER_IMAGE (line 345) | const SINGLE_PAGE_WITH_COVER_IMAGE = [
constant SINGLE_PAGE_WITH_ICON (line 357) | const SINGLE_PAGE_WITH_ICON = [
constant SINGLE_TEXT_AND_TITLE_NOTION_API_CONTENT_RESPONSE (line 369) | const SINGLE_TEXT_AND_TITLE_NOTION_API_CONTENT_RESPONSE = [
constant SINGLE_TEXT_WITH_BOLD (line 378) | const SINGLE_TEXT_WITH_BOLD = [
constant SINGLE_TEXT_WITH_BOLD_AND_ITALIC_TOGETHER (line 389) | const SINGLE_TEXT_WITH_BOLD_AND_ITALIC_TOGETHER = [
constant SINGLE_TEXT_WITH_BOLD_AND_ITALIC (line 400) | const SINGLE_TEXT_WITH_BOLD_AND_ITALIC = [
constant SINGLE_TEXT_WITH_COLOR (line 414) | const SINGLE_TEXT_WITH_COLOR = [
constant SINGLE_TEXT_WITH_EQUATION (line 425) | const SINGLE_TEXT_WITH_EQUATION = [
constant SINGLE_TEXT_WITH_LINK (line 436) | const SINGLE_TEXT_WITH_LINK = [
constant SINGLE_TEXT_WITH_FORMAT (line 445) | const SINGLE_TEXT_WITH_FORMAT = [
constant CALLOUT_WITH_PAGE_ICON (line 455) | const CALLOUT_WITH_PAGE_ICON = [
constant IMAGE_WITH_CUSTOM_SIZE (line 465) | const IMAGE_WITH_CUSTOM_SIZE = [
constant TEXT_NOTION_API_CONTENT_RESPONSE (line 490) | const TEXT_NOTION_API_CONTENT_RESPONSE = [
constant VIDEO_NOTION_API_CONTENT_RESPONSE (line 507) | const VIDEO_NOTION_API_CONTENT_RESPONSE = [
constant LIST_WITH_CHILDREN_RESPONSE (line 523) | const LIST_WITH_CHILDREN_RESPONSE = [
constant DETAILS_RESPONSE (line 553) | const DETAILS_RESPONSE = [
FILE: src/data/protocols/blocks/block.ts
type Block (line 4) | type Block = {
FILE: src/data/protocols/blocks/decorable-text.ts
type DecorableText (line 3) | type DecorableText = {
FILE: src/data/protocols/blocks/decoration.ts
type Decoration (line 1) | type Decoration = {
type DecorationType (line 6) | type DecorationType =
FILE: src/data/protocols/blocks/format.ts
type Format (line 1) | type Format = {
FILE: src/data/protocols/blocks/list-blocks-wrapper.ts
type ListBlocksWrapper (line 3) | interface ListBlocksWrapper {
FILE: src/data/protocols/html-options/html-options.ts
type HtmlOptions (line 1) | type HtmlOptions = {
FILE: src/data/protocols/http-request/http-get-client.ts
type HttpGetClient (line 3) | interface HttpGetClient {
FILE: src/data/protocols/http-request/http-post-client.ts
type HttpPostClient (line 3) | interface HttpPostClient {
FILE: src/data/protocols/http-request/http-response.ts
type HttpResponse (line 1) | type HttpResponse = {
FILE: src/data/protocols/page-props/image-cover.ts
type ImageCover (line 1) | type ImageCover = {
FILE: src/data/protocols/page-props/page-props.ts
type PageProps (line 1) | type PageProps = {
FILE: src/data/use-cases/blocks-to-html-converter/block-dispatcher.ts
class BlockDispatcher (line 5) | class BlockDispatcher {
method dispatch (line 6) | dispatch(block: Block): ToHtml {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/callout.ts
class CalloutBlockToHtml (line 7) | class CalloutBlockToHtml implements ToHtml {
method constructor (line 10) | constructor(block: Block) {
method convert (line 14) | async convert(): Promise<string> {
class IconToHtml (line 27) | class IconToHtml {
method constructor (line 31) | constructor(icon: string | undefined, id: string) {
method toHtml (line 36) | async toHtml(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/code.ts
class CodeBlockToHtml (line 5) | class CodeBlockToHtml implements ToHtml {
method constructor (line 8) | constructor(block: Block) {
method convert (line 12) | async convert(): Promise<string> {
method _language (line 20) | private get _language(): string {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-dispatcher.ts
class DecoratorDispatcher (line 5) | class DecoratorDispatcher {
method dispatch (line 6) | dispatch(text: string, decoration: Decoration): ToHtml {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/bold.ts
class BoldDecorationToHtml (line 3) | class BoldDecorationToHtml implements ToHtml {
method constructor (line 6) | constructor(text: string) {
method convert (line 10) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/code.ts
class CodeDecorationToHtml (line 3) | class CodeDecorationToHtml implements ToHtml {
method constructor (line 6) | constructor(text: string) {
method convert (line 10) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/color.ts
class ColorDecorationToHtml (line 5) | class ColorDecorationToHtml implements ToHtml {
method constructor (line 9) | constructor(text: string, decoration: Decoration) {
method convert (line 14) | async convert(): Promise<string> {
method _isBackground (line 18) | private _isBackground(): boolean {
method _style (line 22) | private get _style() {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/equation.ts
class EquationDecorationToHtml (line 4) | class EquationDecorationToHtml implements ToHtml {
method constructor (line 8) | constructor(text: string, decoration: Decoration) {
method convert (line 13) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/italic.ts
class ItalicDecorationToHtml (line 3) | class ItalicDecorationToHtml implements ToHtml {
method constructor (line 6) | constructor(text: string) {
method convert (line 10) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/link.ts
class LinkDecorationToHtml (line 4) | class LinkDecorationToHtml implements ToHtml {
method constructor (line 8) | constructor(text: string, decoration: Decoration) {
method convert (line 13) | async convert(): Promise<string> {
method _link (line 17) | private get _link(): string {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/strikethrough.ts
class StrikeThroughDecorationToHtml (line 3) | class StrikeThroughDecorationToHtml implements ToHtml {
method constructor (line 6) | constructor(text: string) {
method convert (line 10) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/underline.ts
class UnderlineDecorationToHtml (line 2) | class UnderlineDecorationToHtml implements ToHtml {
method constructor (line 5) | constructor(text: string) {
method convert (line 9) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/unknown.ts
class UnknownDecorationToHtml (line 3) | class UnknownDecorationToHtml implements ToHtml {
method constructor (line 6) | constructor(text: string) {
method convert (line 10) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decorator.ts
class Decorator (line 4) | class Decorator {
method constructor (line 7) | constructor(decorableTexts: DecorableText[]) {
method decorate (line 11) | async decorate(): Promise<string> {
method _decorateByDecorableText (line 18) | async _decorateByDecorableText(decorableText: DecorableText): Promise<...
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/divider.ts
class DividerBlockToHtml (line 4) | class DividerBlockToHtml implements ToHtml {
method constructor (line 7) | constructor(block: Block) {
method convert (line 11) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/equation.ts
class EquationBlockToHtml (line 5) | class EquationBlockToHtml implements ToHtml {
method constructor (line 8) | constructor(block: Block) {
method convert (line 12) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/header.ts
class HeaderBlockToHtml (line 6) | class HeaderBlockToHtml implements ToHtml {
method constructor (line 9) | constructor(block: Block) {
method convert (line 13) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/image.ts
class ImageBlockToHtml (line 6) | class ImageBlockToHtml implements ToHtml {
method constructor (line 9) | constructor(block: Block) {
method convert (line 13) | async convert(): Promise<string> {
method _rawSrc (line 28) | private get _rawSrc() {
method _caption (line 35) | private get _caption() {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/list/list-item.ts
class ListItemToHtml (line 6) | class ListItemToHtml implements ToHtml {
method constructor (line 9) | constructor(block: Block) {
method convert (line 13) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/list/list.ts
class ListBlockToHtml (line 6) | class ListBlockToHtml implements ToHtml {
method constructor (line 9) | constructor(block: Block) {
method convert (line 13) | async convert(): Promise<string> {
method _itemsHtml (line 22) | private async _itemsHtml(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/page.ts
class PageBlockToHtml (line 5) | class PageBlockToHtml implements ToHtml {
method constructor (line 8) | constructor(block: Block) {
method convert (line 12) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/quote.ts
class QuoteBlockToHtml (line 6) | class QuoteBlockToHtml implements ToHtml {
method constructor (line 9) | constructor(block: Block) {
method convert (line 13) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/sub-header.ts
class SubHeaderBlockParser (line 6) | class SubHeaderBlockParser implements ToHtml {
method constructor (line 9) | constructor(block: Block) {
method convert (line 13) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/sub-sub-header.ts
class SubSubHeaderBlockParser (line 6) | class SubSubHeaderBlockParser implements ToHtml {
method constructor (line 9) | constructor(block: Block) {
method convert (line 13) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/text.ts
class TextBlockToHtml (line 7) | class TextBlockToHtml implements ToHtml {
method constructor (line 10) | constructor(block: Block) {
method convert (line 14) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/to-do.ts
class ToDoBlockToHtml (line 7) | class ToDoBlockToHtml implements ToHtml {
method constructor (line 10) | constructor(block: Block) {
method convert (line 14) | async convert(): Promise<string> {
method _isChecked (line 29) | private _isChecked(): boolean {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/toggle.ts
class ToggleBlockToHtml (line 7) | class ToggleBlockToHtml implements ToHtml {
method constructor (line 10) | constructor(block: Block) {
method convert (line 14) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/unknown.ts
class UnknownBlockToHtml (line 3) | class UnknownBlockToHtml implements ToHtml {
method convert (line 4) | async convert(): Promise<string> {
FILE: src/data/use-cases/blocks-to-html-converter/block-parsers/youtube-video.ts
class YouTubeVideoBlockToHtml (line 4) | class YouTubeVideoBlockToHtml implements ToHtml {
method constructor (line 7) | constructor(block: Block) {
method convert (line 11) | async convert(): Promise<string> {
method _youtubeId (line 24) | private get _youtubeId(): string | void {
method _src (line 30) | private get _src() {
FILE: src/data/use-cases/blocks-to-html-converter/blocks-to-html-converter.ts
class BlocksToHTML (line 6) | class BlocksToHTML implements ToHtml {
method constructor (line 11) | constructor(blocks: Block[], dispatcher: BlockDispatcher, listBlocksWr...
method convert (line 17) | async convert(): Promise<string> {
method _convertBlock (line 23) | private async _convertBlock(block: Block): Promise<string> {
method _wrapLists (line 29) | private _wrapLists(blocks: Block[]): Block[] {
method _dispatch (line 33) | private _dispatch(block: Block): ToHtml {
FILE: src/data/use-cases/blocks-to-html-converter/list-blocks-wrapper.ts
class ListBlocksWrapper (line 3) | class ListBlocksWrapper {
method wrapLists (line 4) | wrapLists(blocks: Block[]): Block[] {
method _isList (line 16) | private _isList(block: Block): boolean {
method _isFirstItemOfAList (line 20) | private _isFirstItemOfAList(blocks: Block[], currentBlock: Block): boo...
method _generateListBlock (line 29) | private _generateListBlock(childBlock: Block): Block {
FILE: src/data/use-cases/format-to-style/format-to-style.ts
class FormatToStyle (line 4) | class FormatToStyle {
method constructor (line 7) | constructor(format: Format) {
method toStyle (line 11) | toStyle(): string {
class BlockColorToProp (line 25) | class BlockColorToProp {
method constructor (line 28) | constructor(blockColor: string) {
method toStyle (line 32) | toStyle(): string {
method _isBackground (line 37) | private _isBackground(): boolean {
class BlockWidthToProp (line 42) | class BlockWidthToProp {
method constructor (line 45) | constructor(blockWidth: number) {
method toStyle (line 49) | toStyle(): string {
FILE: src/data/use-cases/html-wrapper/header-from-template.ts
class HeaderFromTemplate (line 3) | class HeaderFromTemplate {
method constructor (line 6) | constructor(pageProps: PageProps) {
method toHeader (line 10) | toHeader(): string {
method _coverImageHtml (line 20) | private get _coverImageHtml(): string {
method _iconHtml (line 30) | private get _iconHtml(): string {
method _titleHtml (line 41) | private get _titleHtml(): string {
FILE: src/data/use-cases/html-wrapper/options-html-wrapper.ts
class OptionsHtmlWrapper (line 8) | class OptionsHtmlWrapper implements HtmlWrapper {
method constructor (line 11) | constructor(options: HtmlOptions) {
method wrapHtml (line 15) | wrapHtml(pageProps: PageProps, html: string): string {
method _headFromTemplate (line 32) | private _headFromTemplate(title: string): string {
FILE: src/data/use-cases/html-wrapper/scripts.ts
constant SCRIPTS (line 1) | const SCRIPTS = `\
FILE: src/data/use-cases/html-wrapper/styles.ts
constant STYLE (line 1) | const STYLE = `\
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-cover-image-block.ts
class PageBlockToCoverImageSource (line 5) | class PageBlockToCoverImageSource {
method constructor (line 8) | constructor(pageBlock: Block) {
method toImageCover (line 12) | async toImageCover(): Promise<ImageCover | null> {
method _isImageURL (line 25) | private _isImageURL(url: string): boolean {
method getImageAuthenticatedSrc (line 29) | private getImageAuthenticatedSrc(src: string): string {
method _pageCoverPositionToPositionCenter (line 33) | private _pageCoverPositionToPositionCenter(coverPosition: number): num...
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-icon.ts
class PageBlockToIcon (line 4) | class PageBlockToIcon {
method constructor (line 7) | constructor(pageBlock: Block) {
method toIcon (line 11) | async toIcon(): Promise<string | null> {
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-page-props.ts
class PageBlockToPageProps (line 7) | class PageBlockToPageProps {
method constructor (line 10) | constructor(pageBlock: Block) {
method toPageProps (line 14) | async toPageProps(): Promise<PageProps> {
FILE: src/data/use-cases/page-block-to-page-props/page-block-to-title.ts
class PageBlockToTitle (line 3) | class PageBlockToTitle {
method constructor (line 6) | constructor(pageBlock: Block) {
method toTitle (line 10) | toTitle(): string {
FILE: src/domain/use-cases/html-wrapper.ts
type HtmlWrapper (line 3) | interface HtmlWrapper {
FILE: src/domain/use-cases/to-html.ts
type ToHtml (line 1) | interface ToHtml {
type ToHtmlClass (line 5) | interface ToHtmlClass {
FILE: src/infra/errors/invalid-page-url.ts
class InvalidPageUrlError (line 1) | class InvalidPageUrlError extends Error {
method constructor (line 2) | constructor(url: string) {
FILE: src/infra/errors/missing-content.ts
class MissingContentError (line 1) | class MissingContentError extends Error {
method constructor (line 2) | constructor(pageId: string) {
FILE: src/infra/errors/missing-page-id.ts
class MissingPageIdError (line 1) | class MissingPageIdError extends Error {
method constructor (line 2) | constructor() {
FILE: src/infra/errors/notion-page-access.ts
class NotionPageAccessError (line 1) | class NotionPageAccessError extends Error {
method constructor (line 2) | constructor(pageId: string) {
FILE: src/infra/errors/notion-page-not-found.ts
class NotionPageNotFound (line 1) | class NotionPageNotFound extends Error {
method constructor (line 2) | constructor(pageId: string) {
FILE: src/infra/protocols/notion-api-content-response.ts
type NotionApiContentResponse (line 1) | type NotionApiContentResponse = {
FILE: src/infra/protocols/validation.ts
type Validation (line 1) | interface Validation<Args extends Array<unknown> = []> {
FILE: src/infra/use-cases/http-post/node-http-post-client.ts
class NodeHttpPostClient (line 5) | class NodeHttpPostClient implements HttpPostClient {
method post (line 6) | async post(url: string, body: Record<string, any>): Promise<HttpRespon...
FILE: src/infra/use-cases/to-blocks/decoration-array-to-decorations.ts
class DecorationArrayToDecorations (line 3) | class DecorationArrayToDecorations {
method constructor (line 6) | constructor(decorationsArray: Array<any>) {
method toDecorations (line 10) | toDecorations(): Decoration[] {
FILE: src/infra/use-cases/to-blocks/format-filter.ts
class FormatFilter (line 1) | class FormatFilter {
method constructor (line 4) | constructor(format: Record<string, any> | undefined) {
method filter (line 8) | filter(): Record<string, any> {
constant ACCEPTABLE_KEYS (line 19) | const ACCEPTABLE_KEYS: string[] = ['block_color', 'page_cover_position',...
FILE: src/infra/use-cases/to-blocks/notion-api-content-response-to-blocks.ts
class NotionApiContentResponsesToBlocks (line 7) | class NotionApiContentResponsesToBlocks {
method constructor (line 10) | constructor(notionApiContentResponses: NotionApiContentResponse[]) {
method toBlocks (line 14) | toBlocks(): Block[] {
FILE: src/infra/use-cases/to-blocks/prop-title-to-decorable-texts.ts
class PropTitleToDecorableTexts (line 4) | class PropTitleToDecorableTexts {
method constructor (line 7) | constructor(title: any[] | undefined) {
method toDecorableTexts (line 11) | toDecorableTexts(): DecorableText[] {
FILE: src/infra/use-cases/to-blocks/properties-parser.ts
class PropertiesParser (line 1) | class PropertiesParser {
method constructor (line 5) | constructor(format: Record<string, any> | undefined, properties: Recor...
method parse (line 10) | parse(): Record<string, any> {
constant KEYS_TO_PRESERVE (line 25) | const KEYS_TO_PRESERVE = ['source', 'caption', 'language', 'checked', 'p...
FILE: src/infra/use-cases/to-notion-api-content-responses/notion-api-page-fetcher.ts
constant NOTION_API_PATH (line 5) | const NOTION_API_PATH = 'https://www.notion.so/api/v3/';
class NotionApiPageFetcher (line 7) | class NotionApiPageFetcher {
method constructor (line 8) | constructor(
method getNotionPageContents (line 19) | async getNotionPageContents(): Promise<NotionApiContentResponse[]> {
method mapContentsIdToContent (line 35) | private async mapContentsIdToContent(
method contentsNotInChunk (line 47) | private async contentsNotInChunk(
method contentsInChunk (line 76) | private async contentsInChunk(
method fetchRecordValues (line 100) | private async fetchRecordValues(): Promise<HttpResponse> {
method fetchPageChunk (line 111) | private fetchPageChunk(): Promise<HttpResponse> {
method fetchRecordValuesByContentIds (line 123) | private fetchRecordValuesByContentIds(contentIds: string[]): Promise<H...
FILE: src/infra/use-cases/to-notion-api-content-responses/services/notion-page-id-validation.service.ts
class NotionPageIdValidator (line 4) | class NotionPageIdValidator implements Validation<[string]> {
method validate (line 5) | validate(notionPageId: string): Error | null {
FILE: src/infra/use-cases/to-notion-api-content-responses/services/page-chunk-validation.service.ts
class PageChunkValidator (line 4) | class PageChunkValidator implements Validation<[string, number]> {
method validate (line 5) | validate(notionPageId: string, pageChunkStatus: number): Error | null {
FILE: src/infra/use-cases/to-notion-api-content-responses/services/page-record-validation.service.ts
class PageRecordValidator (line 6) | class PageRecordValidator implements Validation<[string, HttpResponse]> {
method validate (line 7) | validate(notionPageId: string, pageRecord: HttpResponse): Error | null {
FILE: src/infra/use-cases/to-page-id/notion-url-to-page-id.ts
class NotionUrlToPageId (line 3) | class NotionUrlToPageId {
method constructor (line 4) | constructor(
method toPageId (line 10) | toPageId(): string {
method ununormalizedPageId (line 17) | private get ununormalizedPageId(): string {
FILE: src/infra/use-cases/to-page-id/services/id-normalizer.ts
class IdNormalizer (line 1) | class IdNormalizer {
method normalizeId (line 2) | normalizeId(id: string): string {
FILE: src/infra/use-cases/to-page-id/services/url-validator.ts
class UrlValidator (line 4) | class UrlValidator implements Validation<[string]> {
method validate (line 5) | validate(url: string): Error | null {
method isNotionPargeUrl (line 10) | private isNotionPargeUrl(url: string): boolean {
FILE: src/main/protocols/notion-page.ts
type NotionPage (line 1) | type NotionPage = {
FILE: src/main/use-cases/notion-api-to-html/notion-page-to-html.ts
class NotionPageToHtml (line 12) | class NotionPageToHtml {
method convert (line 34) | static async convert(pageURL: string, htmlOptions: HtmlOptions = {}): ...
FILE: src/utils/base-64-converter.ts
class Base64Converter (line 3) | class Base64Converter {
method constructor (line 6) | constructor(imageURL: string) {
method convert (line 10) | static async convert(imageURL: string): Promise<string> {
method _convert (line 14) | async _convert(): Promise<string> {
FILE: src/utils/either.ts
type Either (line 1) | type Either<S, F> = Success<S, F> | Failure<S, F>;
class Success (line 3) | class Success<S, F> {
method constructor (line 4) | constructor(readonly value: S) {}
method isSuccess (line 6) | isSuccess(): this is Success<S, F> {
method isFailure (line 10) | isFailure(): this is Failure<S, F> {
class Failure (line 15) | class Failure<S, F> {
method constructor (line 16) | constructor(readonly value: F) {}
method isSuccess (line 18) | isSuccess(): this is Success<S, F> {
method isFailure (line 22) | isFailure(): this is Failure<S, F> {
function sendSuccess (line 27) | function sendSuccess<S, F>(value: S): Either<S, F> {
function sendFailure (line 31) | function sendFailure<S, F>(value: F): Either<S, F> {
FILE: src/utils/errors/forbidden-error.ts
class ForbiddenError (line 1) | class ForbiddenError extends Error {
method constructor (line 2) | constructor(message: string) {
FILE: src/utils/errors/image-not-found-error.ts
class ImageNotFoundError (line 1) | class ImageNotFoundError extends Error {
method constructor (line 2) | constructor(path: string) {
FILE: src/utils/use-cases/http-get/node-http-get.ts
class NodeHttpGetClient (line 5) | class NodeHttpGetClient implements HttpGetClient {
method get (line 6) | async get(url: string): Promise<HttpResponse> {
Condensed preview — 138 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (227K chars).
[
{
"path": ".eslintignore",
"chars": 61,
"preview": "node_modules\ndist\ncoverage\n./data\nrequirements\n.vscode\n*.jpeg"
},
{
"path": ".eslintrc.js",
"chars": 384,
"preview": "module.exports = {\n parser: '@typescript-eslint/parser',\n parserOptions: {\n ecmaVersion: 2020,\n sourceType: 'mod"
},
{
"path": ".github/workflows/publish.yml",
"chars": 550,
"preview": "name: Publish\non:\n push:\n branches: [main]\n workflow_dispatch:\njobs:\n publish:\n runs-on: ubuntu-20.04\n strat"
},
{
"path": ".github/workflows/test.yml",
"chars": 206,
"preview": "name: Test\non:\n pull_request:\n branches: [main]\njobs:\n build:\n runs-on: ubuntu-20.04\n steps:\n - uses: ac"
},
{
"path": ".gitignore",
"chars": 63,
"preview": "/coverage\n/node_modules\n.vscode\n.idea\n/dist\nscript.js\ntest.html"
},
{
"path": ".husky/pre-commit",
"chars": 62,
"preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm run lint:staged\n"
},
{
"path": ".husky/pre-push",
"chars": 58,
"preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpm run test:ci\n"
},
{
"path": ".lintstagedrc.json",
"chars": 63,
"preview": "{\n \"*.ts\": [\"eslint 'src/**' --fix\", \"npm run test:staged\"]\n}\n"
},
{
"path": ".npmignore",
"chars": 257,
"preview": "__tests__\ncoverage\nnode_modules\nsrc\ndocs\njest.config.js\ntsconfig.json\n.eslintignore\n.eslintrc.js\n.gitignore\n.huskyrc.jso"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "12.19.0\n"
},
{
"path": ".prettierrc.js",
"chars": 115,
"preview": "module.exports = {\n semi: true,\n trailingComma: 'all',\n singleQuote: true,\n printWidth: 120,\n tabWidth: 2,\n};\n"
},
{
"path": "Dockerfile.test",
"chars": 118,
"preview": "FROM node:14.17\nWORKDIR /usr/src/notion-page-to-html\nRUN npm -g i npm\nCOPY ./package*.json ./\nRUN npm install\nCOPY . ."
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2020 Alexandre Nunes\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "Makefile",
"chars": 273,
"preview": "# TEST\ntest_compose = docker-compose -f docker-compose.test.yml\n\n.PRONY: test-build\ntest-build:\n\t$(test_compose) build\n\n"
},
{
"path": "README.md",
"chars": 3740,
"preview": "\n\n# Notion Page To HTML\n\nNodeJS tool to convert public notion pages to HTML.\n\nAlso availab"
},
{
"path": "docker-compose.test.yml",
"chars": 290,
"preview": "version: '3.8'\nservices:\n notion-page-to-html-test:\n build:\n context: .\n dockerfile: Dockerfile.test\n c"
},
{
"path": "jest.config.js",
"chars": 458,
"preview": "module.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n moduleDirectories: ['node_modules'],\n transform: {"
},
{
"path": "package.json",
"chars": 1512,
"preview": "{\n \"name\": \"notion-page-to-html\",\n \"version\": \"1.2.0\",\n \"description\": \"It converts public notion pages to html from "
},
{
"path": "src/__tests__/mocks/blocks.ts",
"chars": 37824,
"preview": "import { Block, DecorableText, DecorationType, Decoration } from '../../data/protocols/blocks';\n\nexport const NO_TEXT = "
},
{
"path": "src/__tests__/mocks/html.ts",
"chars": 10699,
"preview": "import base64 from './img/base64';\n\nconst STYLE_TAG = `\\\n<style>\n html {\n -webkit-print-color-adjust: exact;\n }\n *"
},
{
"path": "src/__tests__/mocks/img/base64.ts",
"chars": 2030,
"preview": "export default 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QDeRXhpZgAASUkqAAgAAAAGABIBAwABAAAAAQAAABoBBQABAAAAV"
},
{
"path": "src/__tests__/mocks/notion-api-responses.ts",
"chars": 17485,
"preview": "export const SUCCESSFUL_PAGE_CHUCK = {\n recordMap: {\n block: {\n '4d64bbc0-634d-4758-befa-85c5a3a6c22f': {\n "
},
{
"path": "src/data/helpers/block-to-inner-html.ts",
"chars": 518,
"preview": "import { Block, DecorableText } from '../protocols/blocks';\nimport { Decorator } from '../use-cases/blocks-to-html-conve"
},
{
"path": "src/data/helpers/block-to-inner-text.ts",
"chars": 241,
"preview": "import { Block } from '../../data/protocols/blocks';\n\nexport const blockToInnerText = (block: Block): string => {\n cons"
},
{
"path": "src/data/helpers/blocks-to-html.ts",
"chars": 704,
"preview": "import { Block } from '../protocols/blocks';\nimport { ListBlocksWrapper, BlockDispatcher, BlocksToHTML } from '../use-ca"
},
{
"path": "src/data/helpers/color-to-hex.ts",
"chars": 841,
"preview": "export const backgroundColorToHex = (color: string): string => {\n return backgroundColorsToHex[color] || '#FFFFFF';\n};\n"
},
{
"path": "src/data/helpers/replace-line-break-to-br-tag.ts",
"chars": 97,
"preview": "export const replaceLineBreakByBrTag = (str: string): string => str.replace(/[\\r\\n]/g, '</br>');\n"
},
{
"path": "src/data/protocols/blocks/block.ts",
"chars": 250,
"preview": "import { DecorableText } from './decorable-text';\nimport { Format } from './format';\n\nexport type Block = {\n id: string"
},
{
"path": "src/data/protocols/blocks/decorable-text.ts",
"chars": 122,
"preview": "import { Decoration } from './decoration';\n\nexport type DecorableText = {\n text: string;\n decorations: Decoration[];\n}"
},
{
"path": "src/data/protocols/blocks/decoration.ts",
"chars": 224,
"preview": "export type Decoration = {\n type: DecorationType;\n value?: string;\n};\n\nexport type DecorationType =\n | 'plain'\n | 'b"
},
{
"path": "src/data/protocols/blocks/format.ts",
"chars": 151,
"preview": "export type Format = {\n block_color?: string;\n block_width?: number;\n page_icon?: string;\n page_cover?: string;\n pa"
},
{
"path": "src/data/protocols/blocks/index.ts",
"chars": 128,
"preview": "export * from './block';\nexport * from './decorable-text';\nexport * from './decoration';\nexport * from './list-blocks-wr"
},
{
"path": "src/data/protocols/blocks/list-blocks-wrapper.ts",
"chars": 112,
"preview": "import { Block } from './block';\n\nexport interface ListBlocksWrapper {\n wrapLists(blocks: Block[]): Block[];\n}\n"
},
{
"path": "src/data/protocols/html-options/html-options.ts",
"chars": 210,
"preview": "export type HtmlOptions = {\n excludeTitleFromHead?: boolean;\n excludeCSS?: boolean;\n excludeMetadata?: boolean;\n exc"
},
{
"path": "src/data/protocols/http-request/http-get-client.ts",
"chars": 127,
"preview": "import { HttpResponse } from './http-response';\n\nexport interface HttpGetClient {\n get(url: string): Promise<HttpRespon"
},
{
"path": "src/data/protocols/http-request/http-post-client.ts",
"chars": 156,
"preview": "import { HttpResponse } from './http-response';\n\nexport interface HttpPostClient {\n post(url: string, body: Record<stri"
},
{
"path": "src/data/protocols/http-request/http-response.ts",
"chars": 126,
"preview": "export type HttpResponse = {\n status: number;\n data: Record<string, any> | string;\n headers?: Record<string, string[]"
},
{
"path": "src/data/protocols/http-request/index.ts",
"chars": 104,
"preview": "export * from './http-post-client';\nexport * from './http-get-client';\nexport * from './http-response';\n"
},
{
"path": "src/data/protocols/page-props/image-cover.ts",
"chars": 68,
"preview": "export type ImageCover = {\n base64: string;\n position: number;\n};\n"
},
{
"path": "src/data/protocols/page-props/index.ts",
"chars": 61,
"preview": "export * from './page-props';\nexport * from './image-cover';\n"
},
{
"path": "src/data/protocols/page-props/page-props.ts",
"chars": 120,
"preview": "export type PageProps = {\n title: string;\n coverImageSrc?: string;\n coverImagePosition?: number;\n icon?: string;\n};\n"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-dispatcher.ts",
"chars": 1092,
"preview": "import { Block } from '../../protocols/blocks';\nimport { ToHtml, ToHtmlClass } from '../../../domain/use-cases/to-html';"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/callout.ts",
"chars": 1507,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/code.ts",
"chars": 736,
"preview": "import { Block } from '../../../../data/protocols/blocks';\nimport { ToHtml } from '../../../../domain/use-cases/to-html'"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-dispatcher.ts",
"chars": 983,
"preview": "import { Decoration } from '../../../../../data/protocols/blocks/decoration';\nimport { ToHtml, ToHtmlClass } from '../.."
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/bold.ts",
"chars": 322,
"preview": "import { ToHtml } from '../../../../../../domain/use-cases/to-html';\n\nexport class BoldDecorationToHtml implements ToHtm"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/code.ts",
"chars": 318,
"preview": "import { ToHtml } from '../../../../../../domain/use-cases/to-html';\n\nexport class CodeDecorationToHtml implements ToHtm"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/color.test.ts",
"chars": 10321,
"preview": "import { ColorDecorationToHtml } from './color';\nimport { Decoration } from '../../../../../protocols/blocks';\n\ndescribe"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/color.ts",
"chars": 955,
"preview": "import { Decoration } from '../../../../../../data/protocols/blocks';\nimport { ToHtml } from '../../../../../../domain/u"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/equation.ts",
"chars": 543,
"preview": "import { Decoration } from '../../../../../../data/protocols/blocks';\nimport { ToHtml } from '../../../../../../domain/u"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/index.ts",
"chars": 240,
"preview": "export * from './bold';\nexport * from './code';\nexport * from './color';\nexport * from './equation';\nexport * from './it"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/italic.ts",
"chars": 316,
"preview": "import { ToHtml } from '../../../../../../domain/use-cases/to-html';\n\nexport class ItalicDecorationToHtml implements ToH"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/link.ts",
"chars": 601,
"preview": "import { Decoration } from '../../../../../../data/protocols/blocks';\nimport { ToHtml } from '../../../../../../domain/u"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/strikethrough.ts",
"chars": 325,
"preview": "import { ToHtml } from '../../../../../../domain/use-cases/to-html';\n\nexport class StrikeThroughDecorationToHtml impleme"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/underline.ts",
"chars": 358,
"preview": "import { ToHtml } from '../../../../../../domain/use-cases/to-html';\nexport class UnderlineDecorationToHtml implements T"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decoration-parsers/unknown.ts",
"chars": 303,
"preview": "import { ToHtml } from '../../../../../../domain/use-cases/to-html';\n\nexport class UnknownDecorationToHtml implements To"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/decorations/decorator.ts",
"chars": 918,
"preview": "import { DecorableText } from '../../../../../data/protocols/blocks/decorable-text';\nimport { DecoratorDispatcher } from"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/divider.ts",
"chars": 341,
"preview": "import { Block } from '../../../protocols/blocks';\nimport { ToHtml } from '../../../../domain/use-cases/to-html';\n\nexpor"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/equation.ts",
"chars": 607,
"preview": "import { Block } from '../../../protocols/blocks';\nimport { ToHtml } from '../../../../domain/use-cases/to-html';\nimport"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/header.ts",
"chars": 586,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/image.ts",
"chars": 1088,
"preview": "import { Block } from '../../../protocols/blocks';\nimport { ToHtml } from '../../../../domain/use-cases/to-html';\nimport"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/index.ts",
"chars": 429,
"preview": "export * from './image';\nexport * from './list';\nexport * from './callout';\nexport * from './code';\nexport * from './div"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/list/index.ts",
"chars": 24,
"preview": "export * from './list';\n"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/list/list-item.ts",
"chars": 615,
"preview": "import { blockToInnerHtml } from '../../../../helpers/block-to-inner-html';\nimport { Block } from '../../../../protocols"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/list/list.ts",
"chars": 1007,
"preview": "import { Block } from '../../../../protocols/blocks';\nimport { ToHtml } from '../../../../../domain/use-cases/to-html';\n"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/page.ts",
"chars": 413,
"preview": "import { Block } from '../../../protocols/blocks';\nimport { ToHtml } from '../../../../domain/use-cases/to-html';\nimport"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/quote.ts",
"chars": 601,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/sub-header.ts",
"chars": 589,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/sub-sub-header.ts",
"chars": 592,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/text.ts",
"chars": 741,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/to-do.ts",
"chars": 1031,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/toggle.ts",
"chars": 786,
"preview": "import { blockToInnerHtml } from '../../../helpers/block-to-inner-html';\nimport { Block } from '../../../protocols/block"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/unknown.ts",
"chars": 191,
"preview": "import { ToHtml } from '../../../../domain/use-cases/to-html';\n\nexport class UnknownBlockToHtml implements ToHtml {\n as"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/block-parsers/youtube-video.ts",
"chars": 922,
"preview": "import { Block } from '../../../protocols/blocks';\nimport { ToHtml } from '../../../../domain/use-cases/to-html';\n\nexpor"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/blocks-to-html-converter.test.ts",
"chars": 30550,
"preview": "import nock from 'nock';\nimport { resolve } from 'path';\n\nimport { Block } from '../../protocols/blocks';\nimport * as Bl"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/blocks-to-html-converter.ts",
"chars": 1313,
"preview": "import { ToHtml } from '../../../domain/use-cases/to-html';\nimport { Block } from '../../protocols/blocks';\nimport { Blo"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/index.ts",
"chars": 119,
"preview": "export * from './blocks-to-html-converter';\nexport * from './block-dispatcher';\nexport * from './list-blocks-wrapper';\n"
},
{
"path": "src/data/use-cases/blocks-to-html-converter/list-blocks-wrapper.ts",
"chars": 1115,
"preview": "import { Block } from '../../protocols/blocks';\n\nexport class ListBlocksWrapper {\n wrapLists(blocks: Block[]): Block[] "
},
{
"path": "src/data/use-cases/format-to-style/format-to-style.ts",
"chars": 1334,
"preview": "import { Format } from 'data/protocols/blocks/format';\nimport { foregroundColorToHex, backgroundColorToHex } from '../.."
},
{
"path": "src/data/use-cases/format-to-style/index.ts",
"chars": 35,
"preview": "export * from './format-to-style';\n"
},
{
"path": "src/data/use-cases/html-wrapper/header-from-template.test.ts",
"chars": 3242,
"preview": "import { HeaderFromTemplate } from './header-from-template';\nimport * as html from '../../../__tests__/mocks/html';\n\ndes"
},
{
"path": "src/data/use-cases/html-wrapper/header-from-template.ts",
"chars": 1251,
"preview": "import { PageProps } from '../../protocols/page-props/page-props';\n\nexport class HeaderFromTemplate {\n private readonly"
},
{
"path": "src/data/use-cases/html-wrapper/options-html-wrapper.ts",
"chars": 1414,
"preview": "import { PageProps } from 'data/protocols/page-props';\nimport { HtmlWrapper } from '../../../domain/use-cases/html-wrapp"
},
{
"path": "src/data/use-cases/html-wrapper/scripts.ts",
"chars": 401,
"preview": "export const SCRIPTS = `\\\n<script src=\"https://unpkg.com/prismjs@1.22.0/components/prism-core.min.js\"></script>\n<script "
},
{
"path": "src/data/use-cases/html-wrapper/styles.ts",
"chars": 4191,
"preview": "export const STYLE = `\\\n<style>\nhtml {\n -webkit-print-color-adjust: exact;\n}\n\n* {\n box-sizing: border-box;\n -webkit-p"
},
{
"path": "src/data/use-cases/page-block-to-page-props/index.ts",
"chars": 44,
"preview": "export * from './page-block-to-page-props';\n"
},
{
"path": "src/data/use-cases/page-block-to-page-props/page-block-to-cover-image-block.ts",
"chars": 1321,
"preview": "import { Block } from '../../protocols/blocks';\nimport { Base64Converter } from '../../../utils/base-64-converter';\nimpo"
},
{
"path": "src/data/use-cases/page-block-to-page-props/page-block-to-icon.ts",
"chars": 609,
"preview": "import { Block } from '../../protocols/blocks';\nimport { Base64Converter } from '../../../utils/base-64-converter';\n\nexp"
},
{
"path": "src/data/use-cases/page-block-to-page-props/page-block-to-page-props.test.ts",
"chars": 4770,
"preview": "import nock from 'nock';\nimport { resolve } from 'path';\nimport { PageBlockToPageProps } from './index';\nimport { Base64"
},
{
"path": "src/data/use-cases/page-block-to-page-props/page-block-to-page-props.ts",
"chars": 912,
"preview": "import { Block } from '../../protocols/blocks';\nimport { PageProps } from '../../protocols/page-props';\nimport { PageBlo"
},
{
"path": "src/data/use-cases/page-block-to-page-props/page-block-to-title.ts",
"chars": 278,
"preview": "import { Block } from '../../protocols/blocks';\n\nexport class PageBlockToTitle {\n private readonly _pageBlock: Block;\n\n"
},
{
"path": "src/domain/use-cases/html-wrapper.ts",
"chars": 151,
"preview": "import { PageProps } from '../../data/protocols/page-props';\n\nexport interface HtmlWrapper {\n wrapHtml(pageProps: PageP"
},
{
"path": "src/domain/use-cases/to-html.ts",
"chars": 122,
"preview": "export interface ToHtml {\n convert(): Promise<string>;\n}\n\nexport interface ToHtmlClass {\n new (...args: any): ToHtml;\n"
},
{
"path": "src/index.ts",
"chars": 141,
"preview": "import { NotionPageToHtml } from './main/use-cases/notion-api-to-html';\n\nexport default NotionPageToHtml;\nmodule.exports"
},
{
"path": "src/infra/errors/index.ts",
"chars": 185,
"preview": "export * from './missing-content';\nexport * from './missing-page-id';\nexport * from './notion-page-access';\nexport * fro"
},
{
"path": "src/infra/errors/invalid-page-url.ts",
"chars": 178,
"preview": "export class InvalidPageUrlError extends Error {\n constructor(url: string) {\n super(`Url \"${url}\" is not a valid not"
},
{
"path": "src/infra/errors/missing-content.ts",
"chars": 193,
"preview": "export class MissingContentError extends Error {\n constructor(pageId: string) {\n super(`Can not find content on page"
},
{
"path": "src/infra/errors/missing-page-id.ts",
"chars": 142,
"preview": "export class MissingPageIdError extends Error {\n constructor() {\n super('PageId is Missing');\n this.name = 'Missi"
},
{
"path": "src/infra/errors/notion-page-access.ts",
"chars": 217,
"preview": "export class NotionPageAccessError extends Error {\n constructor(pageId: string) {\n super(`Can not read Notion Page o"
},
{
"path": "src/infra/errors/notion-page-not-found.ts",
"chars": 273,
"preview": "export class NotionPageNotFound extends Error {\n constructor(pageId: string) {\n super(\n `Can not find Notion Pa"
},
{
"path": "src/infra/protocols/notion-api-content-response.ts",
"chars": 181,
"preview": "export type NotionApiContentResponse = {\n id: string;\n type: string;\n properties: Record<string, any>;\n format?: Rec"
},
{
"path": "src/infra/protocols/validation.ts",
"chars": 107,
"preview": "export interface Validation<Args extends Array<unknown> = []> {\n validate(...args: Args): Error | null;\n}\n"
},
{
"path": "src/infra/use-cases/http-post/node-http-post-client.ts",
"chars": 1327,
"preview": "import { HttpPostClient, HttpResponse } from '../../../data/protocols/http-request';\nimport https, { RequestOptions } fr"
},
{
"path": "src/infra/use-cases/to-blocks/decoration-array-to-decorations.ts",
"chars": 819,
"preview": "import { Decoration, DecorationType } from '../../../data/protocols/blocks';\n\nexport class DecorationArrayToDecorations "
},
{
"path": "src/infra/use-cases/to-blocks/format-filter.ts",
"chars": 615,
"preview": "export class FormatFilter {\n private readonly _format: Record<string, any>;\n\n constructor(format: Record<string, any> "
},
{
"path": "src/infra/use-cases/to-blocks/notion-api-content-response-to-blocks.test.ts",
"chars": 6594,
"preview": "import * as NotionApiMocks from '../../../__tests__/mocks/notion-api-responses';\nimport * as BlockMocks from '../../../_"
},
{
"path": "src/infra/use-cases/to-blocks/notion-api-content-response-to-blocks.ts",
"chars": 1107,
"preview": "import { Block } from '../../../data/protocols/blocks';\nimport { NotionApiContentResponse } from '../../protocols/notion"
},
{
"path": "src/infra/use-cases/to-blocks/prop-title-to-decorable-texts.ts",
"chars": 679,
"preview": "import { DecorableText } from '../../../data/protocols/blocks';\nimport { DecorationArrayToDecorations } from './decorati"
},
{
"path": "src/infra/use-cases/to-blocks/properties-parser.ts",
"chars": 787,
"preview": "export class PropertiesParser {\n private readonly _format: Record<string, any>;\n private readonly _properties: Record<"
},
{
"path": "src/infra/use-cases/to-notion-api-content-responses/notion-api-page-fetcher.test.ts",
"chars": 4538,
"preview": "import nock from 'nock';\nimport { NotionApiPageFetcher } from './notion-api-page-fetcher';\nimport { NotionPageIdValidato"
},
{
"path": "src/infra/use-cases/to-notion-api-content-responses/notion-api-page-fetcher.ts",
"chars": 4736,
"preview": "import { HttpPostClient, HttpResponse } from '../../../data/protocols/http-request';\nimport { NotionApiContentResponse }"
},
{
"path": "src/infra/use-cases/to-notion-api-content-responses/services/index.ts",
"chars": 152,
"preview": "export * from './page-record-validation.service';\nexport * from './notion-page-id-validation.service';\nexport * from './"
},
{
"path": "src/infra/use-cases/to-notion-api-content-responses/services/notion-page-id-validation.service.ts",
"chars": 334,
"preview": "import { Validation } from '../../../protocols/validation';\nimport { MissingPageIdError } from '../../../errors';\n\nexpor"
},
{
"path": "src/infra/use-cases/to-notion-api-content-responses/services/page-chunk-validation.service.test.ts",
"chars": 914,
"preview": "import { PageChunkValidator } from './index';\n\ndescribe('PageChunkValidator', () => {\n const makeSut = () => new PageCh"
},
{
"path": "src/infra/use-cases/to-notion-api-content-responses/services/page-chunk-validation.service.ts",
"chars": 520,
"preview": "import { NotionPageAccessError, NotionPageNotFound } from '../../../../infra/errors';\nimport { Validation } from '../../"
},
{
"path": "src/infra/use-cases/to-notion-api-content-responses/services/page-record-validation.service.ts",
"chars": 667,
"preview": "import { Validation } from '../../../protocols/validation';\n\nimport { NotionPageAccessError, MissingContentError } from "
},
{
"path": "src/infra/use-cases/to-page-id/index.ts",
"chars": 41,
"preview": "export * from './notion-url-to-page-id';\n"
},
{
"path": "src/infra/use-cases/to-page-id/notion-url-to-page-id.test.ts",
"chars": 1491,
"preview": "import { NotionUrlToPageId } from './index';\nimport { InvalidPageUrlError } from '../../errors';\nimport { UrlValidator, "
},
{
"path": "src/infra/use-cases/to-page-id/notion-url-to-page-id.ts",
"chars": 632,
"preview": "import { IdNormalizer, UrlValidator } from './services';\n\nexport class NotionUrlToPageId {\n constructor(\n private re"
},
{
"path": "src/infra/use-cases/to-page-id/services/id-normalizer.ts",
"chars": 272,
"preview": "export class IdNormalizer {\n normalizeId(id: string): string {\n const isItAlreadyNormalized = id.length === 36;\n "
},
{
"path": "src/infra/use-cases/to-page-id/services/index.ts",
"chars": 66,
"preview": "export * from './id-normalizer';\nexport * from './url-validator';\n"
},
{
"path": "src/infra/use-cases/to-page-id/services/url-validator.ts",
"chars": 474,
"preview": "import { Validation } from '../../../protocols/validation';\nimport { InvalidPageUrlError } from '../../../errors';\n\nexpo"
},
{
"path": "src/main/factories/blocks-to-html.factory.ts",
"chars": 403,
"preview": "import { Block } from '../../data/protocols/blocks';\nimport { BlocksToHTML, BlockDispatcher, ListBlocksWrapper } from '."
},
{
"path": "src/main/factories/index.ts",
"chars": 142,
"preview": "export * from './notion-url-to-page-id.factory';\nexport * from './notion-api-page-fetcher.factory';\nexport * from './blo"
},
{
"path": "src/main/factories/notion-api-page-fetcher.factory.ts",
"chars": 841,
"preview": "import { NotionApiPageFetcher } from '../../infra/use-cases/to-notion-api-content-responses/notion-api-page-fetcher';\nim"
},
{
"path": "src/main/factories/notion-url-to-page-id.factory.ts",
"chars": 390,
"preview": "import { NotionUrlToPageId } from '../../infra/use-cases/to-page-id';\nimport { IdNormalizer, UrlValidator } from '../../"
},
{
"path": "src/main/protocols/notion-page.ts",
"chars": 99,
"preview": "export type NotionPage = {\n html: string;\n title?: string;\n icon?: string;\n cover?: string;\n};\n"
},
{
"path": "src/main/use-cases/notion-api-to-html/index.ts",
"chars": 84,
"preview": "export * from './notion-page-to-html';\nexport * from '../../protocols/notion-page';\n"
},
{
"path": "src/main/use-cases/notion-api-to-html/notion-page-to-html.test.ts",
"chars": 5883,
"preview": "import nock from 'nock';\nimport { resolve } from 'path';\nimport { NotionPageToHtml } from './index';\nimport { InvalidPag"
},
{
"path": "src/main/use-cases/notion-api-to-html/notion-page-to-html.ts",
"chars": 3161,
"preview": "import { PageBlockToPageProps } from '../../../data/use-cases/page-block-to-page-props';\nimport { HtmlOptions } from '.."
},
{
"path": "src/utils/base-64-converter.ts",
"chars": 527,
"preview": "import { NodeHttpGetClient } from './use-cases/http-get/node-http-get';\n\nexport class Base64Converter {\n private readon"
},
{
"path": "src/utils/either.ts",
"chars": 636,
"preview": "export type Either<S, F> = Success<S, F> | Failure<S, F>;\n\nexport class Success<S, F> {\n constructor(readonly value: S)"
},
{
"path": "src/utils/errors/forbidden-error.ts",
"chars": 137,
"preview": "export class ForbiddenError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'Forbid"
},
{
"path": "src/utils/errors/image-not-found-error.ts",
"chars": 177,
"preview": "export class ImageNotFoundError extends Error {\n constructor(path: string) {\n super(`Image on path ${path} could not"
},
{
"path": "src/utils/errors/index.ts",
"chars": 76,
"preview": "export * from './forbidden-error';\nexport * from './image-not-found-error';\n"
},
{
"path": "src/utils/use-cases/http-get/node-http-get.ts",
"chars": 1352,
"preview": "import { HttpGetClient, HttpResponse } from '../../../data/protocols/http-request';\nimport https from 'https';\nimport { "
},
{
"path": "tsconfig.build.json",
"chars": 131,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"exclude\": [\"node_modules\", \"**/*.d.ts\", \"**/*.spec.ts\", \"**/*.test.ts\", \"**/__tests"
},
{
"path": "tsconfig.json",
"chars": 386,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \"src\",\n \"outDir\": \"dist\",\n \"sourceMap\": true,\n \"declaration\": true,\n "
}
]
About this extraction
This page contains the full source code of the asnunes/notion-page-to-html GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 138 files (203.2 KB), approximately 61.5k tokens, and a symbol index with 362 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.