├── .github └── pull_request_template.md ├── .gitignore ├── .husky └── pre-commit ├── .mise.toml ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING-CN.md ├── CONTRIBUTING-KR.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README-KR.md ├── README.md ├── package-lock.json ├── package.json ├── packages ├── core │ ├── .gitignore │ ├── CONTRIBUTING-KR.md │ ├── CONTRIBUTING.md │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── custom.d.ts │ │ └── lib │ │ │ ├── components │ │ │ ├── bulleted-list-item.tsx │ │ │ ├── callout.tsx │ │ │ ├── code │ │ │ │ ├── index.tsx │ │ │ │ └── support-language │ │ │ │ │ ├── dart.ts │ │ │ │ │ ├── elixir.ts │ │ │ │ │ ├── go.ts │ │ │ │ │ ├── java.ts │ │ │ │ │ ├── kotlin.ts │ │ │ │ │ ├── markdown.ts │ │ │ │ │ ├── python.ts │ │ │ │ │ ├── sql.ts │ │ │ │ │ └── typescript.ts │ │ │ ├── column-list.tsx │ │ │ ├── column.tsx │ │ │ ├── divider.tsx │ │ │ ├── equation.tsx │ │ │ ├── fallback.tsx │ │ │ ├── headings.tsx │ │ │ ├── image │ │ │ │ ├── assets │ │ │ │ │ ├── arrow_back.svg │ │ │ │ │ ├── arrow_forward.svg │ │ │ │ │ ├── close.svg │ │ │ │ │ ├── download.svg │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── minus.svg │ │ │ │ │ └── plus.svg │ │ │ │ ├── hooks │ │ │ │ │ └── image-viewer │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── use-cursor-visibility.tsx │ │ │ │ │ │ ├── use-image-navigation.tsx │ │ │ │ │ │ ├── use-image-scale.tsx │ │ │ │ │ │ └── use-prevent-scroll.tsx │ │ │ │ ├── image-viewer-tools-tooltip.tsx │ │ │ │ ├── image-viewer-tools.tsx │ │ │ │ ├── image-viewer.tsx │ │ │ │ ├── image.tsx │ │ │ │ ├── index.ts │ │ │ │ └── lib │ │ │ │ │ ├── download-image-file.ts │ │ │ │ │ ├── get-cursor-style.ts │ │ │ │ │ ├── get-gap.ts │ │ │ │ │ ├── get-image-url-or-null.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── scale-round.ts │ │ │ ├── index.ts │ │ │ ├── internal │ │ │ │ ├── icon.tsx │ │ │ │ └── rich-text.tsx │ │ │ ├── numbered-list-item.tsx │ │ │ ├── paragraph.tsx │ │ │ ├── quote.tsx │ │ │ ├── table-row.tsx │ │ │ ├── table.tsx │ │ │ ├── todo.tsx │ │ │ ├── toggle.tsx │ │ │ └── video.tsx │ │ │ ├── index.css │ │ │ ├── index.ts │ │ │ ├── notion.tsx │ │ │ ├── types.ts │ │ │ └── utils.ts │ ├── tsconfig.app.json │ ├── tsconfig.app.tsbuildinfo │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── tsconfig.node.tsbuildinfo │ └── vite.config.ts └── story │ ├── .gitignore │ ├── .storybook │ ├── main.ts │ └── preview.ts │ ├── eslint.config.js │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── lib │ │ └── Notion.tsx │ └── stories │ │ ├── bulleted-list-item │ │ ├── bulleted-list-item.json │ │ └── bulleted-list-item.stories.tsx │ │ ├── callout │ │ ├── callout.json │ │ └── callout.stories.ts │ │ ├── code │ │ ├── code.json │ │ ├── code.stories.tsx │ │ ├── getCodeExampleJson.ts │ │ └── support-code-examples │ │ │ ├── dart.ts │ │ │ ├── elixir.ts │ │ │ ├── go.ts │ │ │ ├── index.ts │ │ │ ├── java.ts │ │ │ ├── javascript.ts │ │ │ ├── kotlin.ts │ │ │ ├── markdown.ts │ │ │ ├── python.ts │ │ │ ├── sql.ts │ │ │ └── typescript.ts │ │ ├── column │ │ ├── column.json │ │ └── column.stories.tsx │ │ ├── divider │ │ ├── divider.json │ │ └── divider.stories.tsx │ │ ├── equation │ │ ├── equation.json │ │ └── equation.stories.tsx │ │ ├── fallback │ │ ├── fallback.json │ │ └── fallback.stories.tsx │ │ ├── headings │ │ ├── headings.json │ │ └── headings.stories.tsx │ │ ├── image │ │ ├── image.json │ │ └── image.stories.tsx │ │ ├── numbered-list-item │ │ ├── numbered-list-item.json │ │ └── numbered-list-item.stories.tsx │ │ ├── paragraph │ │ ├── paragraph.json │ │ └── paragraph.stories.tsx │ │ ├── quote │ │ ├── quote.json │ │ └── quote.stories.tsx │ │ ├── table │ │ ├── table.json │ │ └── table.stories.tsx │ │ ├── todo │ │ ├── todo.json │ │ └── todo.stories.tsx │ │ ├── toggle │ │ ├── toggle.json │ │ └── toggle.stories.tsx │ │ └── video │ │ ├── video.json │ │ └── video.stories.tsx │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── previewjs.png /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description of Changes 2 | 3 | Here is reference: #25 4 | 5 | --- 6 | 7 | ## support 'custom your created block' block 8 | 9 | - Please add **the block you created** to replace the 'crate' block. 10 | 11 | - ex) add react-katex dependency to support equation block 12 | -> **add custom solution for rendering 'your custom block'** 13 | 14 | ## Review point 15 | 16 | - ex) Instead of adding the react-katex dependency, should I create a custom solution specifically for this project? 17 | -> **Should we integrate an existing solution or use a custom implementation for 'your custom block'?** 18 | 19 | ## To reproduce 20 | 21 | - npm run story:start 22 | - click 'your created block' on the Storybook left side panel 23 | 24 | ## Screenshot 25 | 26 | - A screenshot of your created block being rendered in Storybook. 27 | 28 | ## Review Guide 29 | 30 | Reviews are conducted based on priority levels, such as p0, p1, p2, p3, p4, and p5. 31 | p0 ~ p2: If the author decides not to reflect a review for p0 to p2, it signals that a proper discussion with the reviewer is 32 | necessary. It is expected that the review will be resolved either through incorporating the feedback or through further discussion. 33 | p3: indicates that the reviewer has identified a significant issue, but either lacks a clear solution or the comment lacks sufficient context. Further explanation or additional discussion on the reviewer's concerns is needed. 34 | p4, p5: p4 and p5 suggest low priority, and if the author does not deem them important, these comments can be disregarded. 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env 4 | dist/ 5 | build/ 6 | *.log 7 | .cache -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | # .mise.toml 2 | # https://mise.jdx.dev/getting-started.html 3 | 4 | # About mise: 5 | # mise is a polyglot development environment manager. It helps you manage and switch between 6 | # different versions of programming languages, tools, and other development dependencies. 7 | # mise ensures consistency across different development environments and simplifies 8 | # the setup process for new contributors. 9 | 10 | [tools] 11 | # The 'tools' section specifies the tools required for your project. 12 | # By defining versions for each tool, you ensure a consistent development environment. 13 | 14 | # Specifies the Node.js version. 15 | # This setting means the project will use Node.js version 20.x. 16 | node = "20" 17 | 18 | # You can add other tools here as well. For example: 19 | # python = "3.9" 20 | # ruby = "3.1" 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode 4 | "esbenp.prettier-vscode", 5 | // https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint 6 | "dbaeumer.vscode-eslint", 7 | // https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss 8 | "bradlc.vscode-tailwindcss", 9 | // https://marketplace.visualstudio.com/items?itemName=zenclabs.previewjs 10 | "zenclabs.previewjs", 11 | // https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml 12 | // for .mise.toml file, reference https://mise.jdx.dev/configuration.html 13 | "tamasfe.even-better-toml" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // The following settings ensure that the root .prettierrc configuration 3 | // is applied consistently across all sub-packages in the project. 4 | // This was set up to address an issue where the VSCode extension wasn't automatically formatting on save for sub-packages. 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.formatOnSave": true, 7 | "prettier.configPath": ".prettierrc", 8 | // This setting prevents the specified words from being flagged as spelling errors. 9 | // it's expected that more words will be added to this list as needed in the future. 10 | "cSpell.words": ["typia"] 11 | } 12 | -------------------------------------------------------------------------------- /CONTRIBUTING-CN.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## 目录 4 | 5 | 1. 介绍 6 | - 1.1 项目概述 7 | - 1.2 我们项目的独特之处 8 | - 1.3 项目结构 9 | 2. 贡献指南 10 | - 2.1 贡献流程 11 | - 2.2 PR 审批流程 12 | - 2.3 维护者与贡献者的角色 13 | - 2.4 发生问题(Issue) 和 PR 管理 14 | 3. 开发环境及指南 15 | - 3.1 开发环境设置 16 | - 3.2 PreviewJS 设置方法 17 | - 3.3 组件开发指南 18 | 4. 项目路线图 19 | 5. 项目结构说明 20 | 21 | ## 1. 介绍 22 | 23 | 感谢您对 react-notion-custom 项目的关注! 24 | 25 | 我们的目标是将 Notion 强大的内容管理功能与 React 灵活的 UI 实现能力相结合,让开发者可以轻松构建基于 Notion 的自定义博客或网站。 26 | 27 | ```jsx 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | ### 我们项目的独特之处 38 | 39 | react-notion-custom 通过以下特点与其他类似的库区别开来: 40 | 41 | 1. **使用 Notion 官方 API**:我们直接利用 Notion 官方 API,从而支持更稳定、最新的功能。 42 | 43 | 2. **最大化的自定义自由度**:为开发者提供最大程度的自由,能够根据需要修改组件和更改样式。 44 | 45 | 3. **高质量的 Notion 组件**:我们提供的高质量组件几乎与实际的 Notion 一致,让用户轻松实现熟悉且专业的 UI。 46 | 47 | 4. **性能优化**:通过高效的渲染和数据管理,能够快速加载和显示大规模的 Notion 页面。 48 | 49 | 凭借这些特点,react-notion-custom 将成为开发者结合 Notion 强大功能与 React 灵活性的最佳工具。 50 | 51 | ### 项目结构 52 | 53 | ``` 54 | react-notion-custom/ 55 | ├── packages/ 56 | │ ├── react-notion-custom/ 57 | │ ├── notion-dump/ 58 | │ ├── docs/ 59 | │ └── story/ 60 | ├── README.md 61 | └── CONTRIBUTING.md 62 | ``` 63 | 64 | - `react-notion-custom`: 核心库,使用 Notion API 将 Notion 页面渲染为 React 组件。 65 | - `notion-dump`: CLI 工具,用于提取 Notion 页面的数据并保存为 JSON 文件。 66 | - `docs`: 包含项目文档和示例的网站。 67 | - `story`: 使用 Storybook 进行 UI 组件测试和文档编写。 68 | 69 | ### 初始设置 70 | 71 | 要开始项目,在根目录运行以下命令: 72 | 73 | ``` 74 | npm install 75 | ``` 76 | 77 | ## 2. 贡献指南 78 | 79 | ### 2.1 贡献流程 80 | 81 | 1. Fork 项目。 82 | 2. Clone 到本地(local):`git clone 83 | 3. 安装依赖:`npm install` 84 | 4. 创建新分支:`git checkout -b feature/your-feature-name` 85 | - 分支名称以 `feature/`、`fix/`、`docs/` 等开始,并添加简短说明。 86 | 5. 提交更改:`git commit -m "Add some feature"` 87 | 6. 推(Push) 到 Fork 的仓库:`git push origin feature/your-feature-name` 88 | 7. 创建 Pull Request (拉取请求)。 89 | 90 | 如果在PR (拉取请求)标题中添加 `[DRAFT]`,可以请求中期审查或代码审阅。 91 | 92 | ### 2.2 PR 审批流程 93 | 94 | 所有 Pull Request (拉取请求) 需要由项目维护者 “Moon-DaeSeung” 批准。 95 | PR 请使用英语撰写。 96 | 97 | ### 2.3 维护者与贡献者的角色 98 | 99 | - 维护者:负责决定项目方向并进行代码审查。 100 | - 贡献者:可以参与修复错误、提出和实现新功能、改进文档等。 101 | 102 | ### 2.4 发生问题(Issue) 和 PR 管理 103 | 104 | - 维护者和贡献者可以自由创建和解决问题。 105 | - 维护者需要按功能分支来请求 PR。 106 | - 禁止直接向推(Push) main 分支。 107 | 108 | ## 3. 开发环境及指南 109 | 110 | ### 3.1 开发环境设置 111 | 112 | 1. 确保已安装 Node.js 和 npm。 113 | 2. Clone 项目后,在根目录运行 `npm install` 安装所有依赖。 114 | 115 | ### 3.2 PreviewJS 设置方法 116 | 117 | PreviewJS 能够在编辑器内直接预览 Storybook,开发体验更为便利。 118 | 119 | 1. 在 VSCode 中安装 [PreviewJS](https://previewjs.com/) 扩展程序。 120 | 2. 打开项目,并在每个 Story 代码中选择 “Open Preview”。 121 | 122 | ![previewjs](./previewjs.png) 123 | 124 | ### 3.3 组件开发指南 125 | 126 | 开发 react-notion-custom 组件时,请遵循以下指南: 127 | 128 | 1. 使用 Storybook 进行开发: 129 | 130 | - 为每个组件编写 Story,并通过 Storybook 测试组件的不同状态和 props。 131 | - 可以通过在根目录运行 `npm run story:start` 来启动 Storybook。 132 | 133 | 2. 使用 PreviewJS: 134 | 135 | - 在 VSCode 中使用 PreviewJS 可以在编辑器中直接预览 Storybook,开发效率更高。 136 | 137 | 3. 组件命名与结构: 138 | 139 | - 组件名称请使用 PascalCase。 140 | - 组件文件名和文件夹名使用 kebab-case。这意味着所有字母都是小写,单词之间用连字符(-)分隔。例如:`text-block.tsx` 141 | 142 | ## 4. 项目路线图 143 | 144 | ### 1. 开发并发布 react-notion-custom 库 145 | 146 | ### 2. 开发并发布 notion-dump CLI 工具 147 | 148 | ### 3. 开发 docs 项目 149 | 150 | - 编写项目介绍页面 151 | - 包含使用 NextJS 编写博客的示例 152 | - 编写详细的使用指南和 API 文档 153 | - 运营项目博客 154 | - 定期更新和共享 changelog 155 | 156 | 每个阶段的进展情况和详细内容可以在 问题跟踪系统(Issue Tracker) 中查看。 157 | 158 | ## 5. 项目结构说明 159 | 160 | ### react-notion-custom 和 notion-dump 的角色 161 | 162 | 1. **notion-dump**: 163 | 164 | - 使用 Notion API 获取指定 Notion 页面的内容。 165 | - 将获取的数据转换为 JSON 格式并保存到本地(local)。 166 | - 通过此过程,能够轻松分析和使用 Notion 页面的结构和内容。 167 | 168 | 2. **react-notion-custom**: 169 | - 接受 notion-dump 生成的 JSON 数据作为输入。 170 | - 解析 JSON 数据并转换为对应的 React 组件。 171 | - 提供与 Notion 各个块类型(文本、图片、表格等)对应的 React 组件。 172 | - 用户可以自定义这些组件,添加所需的样式和功能。 173 | 174 | ### 流程图示 175 | 176 | 以下是通过 react-notion-custom 渲染 Notion 页面的过程的 ASCII 图示: 177 | 178 | ``` 179 | +-------------+ +-------------+ +-------------------+ 180 | | Notion | | notion-dump | | react-notion-custom | 181 | | Page | --> | (CLI) | --> | (Library) | 182 | +-------------+ +-------------+ +-------------------+ 183 | | | | 184 | | | | 185 | v v v 186 | +---------+ +-----------+ +----------------+ 187 | | Content | | JSON | | React Components | 188 | | • Text | | { | | | 189 | | • Image | -----> | "blocks"| -----> | | 190 | | • Table | | ... | | | 191 | | • ... | | } | | ... | 192 | +---------+ +-----------+ +----------------+ 193 | | 194 | | 195 | v 196 | +--------------+ 197 | | Rendered Page | 198 | | (Customizable)| 199 | +--------------+ 200 | 201 | ``` 202 | 203 | 通过此过程,用户可以直接将 Notion 页面的内容导入并在 React 应用中进行渲染,同时根据需求自定义样式和功能。 204 | 205 | --- 206 | 207 | 各子项目的贡献指南: 208 | 209 | - [react-notion-custom 贡献方法](https://www.notion.so/packages/react-notion-custom/CONTRIBUTING-KR.md) 210 | - React 组件开发、Notion API 集成、性能优化等 211 | - [notion-dump 贡献方法](https://www.notion.so/packages/notion-dump/CONTRIBUTING-KR.md) 212 | - CLI 工具开发、Notion 数据提取与转换逻辑改进等 213 | - [文档贡献方法](https://www.notion.so/packages/docs/CONTRIBUTING-KR.md) 214 | - 文档编写、示例代码改进、多语言支持等 215 | 216 | 详细的贡献方法请参考相应的链接。 217 | -------------------------------------------------------------------------------- /CONTRIBUTING-KR.md: -------------------------------------------------------------------------------- 1 | # 기여 가이드 2 | 3 | ## 목차 4 | 5 | 1. 소개 6 | - 1.1 프로젝트 개요 7 | - 1.2 우리 프로젝트의 특별한 점 8 | - 1.3 프로젝트 구조 9 | 2. 기여 가이드라인 10 | - 2.1 기여 프로세스 11 | - 2.2 PR 승인 프로세스 12 | - 2.3 메인테이너와 기여자의 역할 13 | - 2.4 이슈와 PR 관리 14 | 3. 개발 환경 및 가이드라인 15 | - 3.1 개발 환경 설정 16 | - 3.2 PreviewJS 설정 17 | - 3.3 컴포넌트 개발 가이드라인 18 | 4. 프로젝트 로드맵 19 | 5. 프로젝트 구조 설명 20 | 21 | ## 1. 소개 22 | 23 | NotionPresso 프로젝트에 관심을 가져주셔서 감사합니다! 24 | 25 | 우리의 목표는 Notion의 강력한 콘텐츠 관리 기능과 React의 유연한 UI 구현을 결합하여, 개발자들이 Notion을 기반으로 커스텀 블로그나 웹사이트를 쉽게 만들 수 있도록 하는 것입니다. 26 | 27 | ```jsx 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | ### 우리 프로젝트의 특별한 점 38 | 39 | NotionPresso는 다음과 같은 특징으로 다른 유사 라이브러리들과 차별화됩니다: 40 | 41 | 1. **Notion 공식 API 사용**: Notion의 공식 API를 직접 활용하여 더 안정적이고 최신 기능을 지원할 수 있습니다. 42 | 43 | 2. **최대한의 커스터마이징 자유도**: 개발자들에게 최대한의 자유를 제공합니다. 원하는 대로 컴포넌트를 수정하고 스타일을 변경할 수 있습니다. 44 | 45 | 3. **고수준의 Notion 컴포넌트**: 실제 Notion과 거의 동일하게 보이는 고품질 컴포넌트를 제공합니다. 이를 통해 사용자들은 친숙하고 전문적인 UI를 쉽게 구현할 수 있습니다. 46 | 47 | 4. **성능 최적화**: 효율적인 렌더링과 데이터 관리를 통해 큰 Notion 페이지도 빠르게 로드하고 표시할 수 있습니다. 48 | 49 | 이러한 특징들을 통해 NotionPresso는 Notion의 파워와 React의 유연성을 동시에 제공하는 최고의 도구가 되고자 합니다. 50 | 51 | ### 프로젝트 구조 52 | 53 | ``` 54 | notionpresso/ 55 | ├── packages/ 56 | │ ├── core/ # @notionpresso/react 패키지 57 | │ └── story/ # 컴포넌트 스토리 58 | ├── README.md 59 | └── CONTRIBUTING.md 60 | ``` 61 | 62 | - `core`: Notion API를 사용하여 Notion 페이지를 React 컴포넌트로 렌더링하는 핵심 라이브러리 63 | - `story`: Storybook을 사용한 UI 컴포넌트 테스트 및 문서화 64 | 65 | ### 초기 설정 66 | 67 | 프로젝트를 시작하려면 루트 디렉토리에서 다음 명령어를 실행하세요: 68 | 69 | ``` 70 | npm install 71 | ``` 72 | 73 | ## 2. 기여 가이드라인 74 | 75 | ### 2.1 기여 프로세스 76 | 77 | 1. 프로젝트를 Fork 합니다. 78 | 2. 로컬에 클론합니다: `git clone https://github.com/notionpresso/react.git` 79 | 3. 의존성을 설치합니다: `npm install` 80 | 4. 새 브랜치를 만듭니다: `git checkout -b feature/your-feature-name` 81 | - 브랜치 이름은 `feature/`, `fix/`, `docs/` 등으로 시작하고 간단한 설명이 뒤따릅니다. 82 | 5. 변경사항을 커밋합니다: `git commit -m "Add some feature"` 83 | 6. Fork한 저장소에 푸시합니다: `git push origin feature/your-feature-name` 84 | 7. Pull Request를 생성합니다. 85 | 86 | 작업 중 중간 리뷰나 피드백을 요청하고 싶다면 PR 제목에 `[DRAFT]`를 붙일 수 있습니다. 87 | 88 | ### 2.2 PR 승인 프로세스 89 | 90 | 모든 Pull Request는 프로젝트 메인테이너인 문대승의 승인이 필요합니다. 91 | PR은 모두 영어로 작성해주세요. 92 | 93 | ### 2.3 메인테이너와 기여자의 역할 94 | 95 | - 메인테이너: 프로젝트 방향을 결정하고 코드 리뷰를 담당합니다. 96 | - 기여자: 버그 수정, 새로운 기능 제안 및 구현, 문서 개선 등에 참여할 수 있습니다. 97 | 98 | ### 2.4 이슈와 PR 관리 99 | 100 | - 메인테이너와 기여자 모두 자유롭게 이슈를 생성하고 해결할 수 있습니다. 101 | - 메인테이너는 PR 요청 시 기능별로 별도의 브랜치를 생성해야 합니다. 102 | - main 브랜치로의 직접 푸시는 금지됩니다. 103 | 104 | ## 3. 개발 환경 및 가이드라인 105 | 106 | ### 3.1 개발 환경 설정 107 | 108 | 1. mise 설치: 109 | 110 | - mise는 모든 기여자가 동일한 Node.js 버전을 사용하도록 보장하는 다중 언어 개발 환경 관리자입니다. 111 | - https://mise.jdx.dev/getting-started.html 의 지침에 따라 mise를 설치하세요. 112 | 113 | 2. 프로젝트 클론 및 mise 설정: 114 | 115 | - 프로젝트를 클론한 후 프로젝트 루트 디렉토리로 이동합니다. 116 | - `mise install`을 실행하여 지정된 Node.js 버전을 설치합니다. 117 | 118 | 3. Node.js 설치 확인: 119 | 120 | - `node -v`를 실행하여 올바른 Node.js 버전이 설치되었는지 확인합니다. 121 | - 버전은 프로젝트 루트의 `.mise.toml` 파일에 지정된 것과 일치해야 합니다. 122 | 123 | 4. 프로젝트 의존성 설치: 124 | - 루트 디렉토리에서 `npm install`을 실행하여 모든 의존성을 설치합니다. 125 | 126 | 참고: mise를 사용하면 시스템에 설치된 다른 Node.js 버전에 관계없이 이 프로젝트에 지정된 정확한 Node.js 버전을 사용할 수 있습니다. 127 | 128 | ### 3.2 PreviewJS 설정 129 | 130 | PreviewJS를 사용하면 에디터에서 직접 Storybook을 볼 수 있어서 개발에 매우 편리합니다. 131 | 132 | 1. VSCode에 [PreviewJS](https://previewjs.com/) 확장을 설치합니다. 133 | 2. 프로젝트를 열고 각 스토리 코드에서 "Open Preview"를 선택합니다. 134 | 135 | ### 3.3 컴포넌트 개발 가이드라인 136 | 137 | NotionPresso 컴포넌트를 개발할 때는 다음 가이드라인을 따라주세요: 138 | 139 | 1. Storybook 개발: 140 | 141 | - 각 컴포넌트에 대한 스토리를 작성하고 Storybook을 통해 다양한 상태와 props를 테스트합니다. 142 | - 루트 디렉토리에서 `npm run story:start`로 Storybook을 실행할 수 있습니다. 143 | 144 | 2. PreviewJS 사용: 145 | 146 | - VSCode에서 PreviewJS를 사용하면 에디터에서 직접 Storybook을 보면서 개발할 수 있어 매우 효율적입니다. 147 | 148 | 3. 컴포넌트 이름 및 구조: 149 | - 컴포넌트 이름은 PascalCase로 작성합니다. 150 | - 컴포넌트 파일 이름과 폴더 이름은 kebab-case를 사용합니다. 즉, 모든 단어를 소문자로 쓰고 하이픈(-)으로 구분합니다. 예) `text-block.tsx` 151 | 152 | ## 4. 프로젝트 로드맵 153 | 154 | ### 1. @notionpresso/react 라이브러리 개발 및 배포 155 | 156 | ### 2. @notionpresso/cli 도구 개발 및 배포 157 | 158 | ### 3. docs 프로젝트 개발 159 | 160 | - 프로젝트 소개 페이지 작성 161 | - NextJS를 사용한 블로그 작성 예제 포함 162 | - 상세 사용 가이드 및 API 문서 작성 163 | - 프로젝트 블로그 운영 164 | - 정기적인 업데이트와 변경 로그 공유 165 | 166 | 각 단계의 진행 상황과 세부 내용은 이슈 트래커에서 확인할 수 있습니다. 167 | 168 | ## 5. 프로젝트 구조 설명 169 | 170 | ### @notionpresso/react와 @notionpresso/cli의 역할 171 | 172 | 1. **@notionpresso/cli**: 173 | 174 | - Notion API를 사용하여 특정 Notion 페이지의 내용을 가져옵니다. 175 | - 가져온 데이터를 JSON 형식으로 변환하여 로컬에 저장합니다. 176 | - `npx npresso` 명령어를 통해 사용할 수 있습니다. 177 | - 이 과정을 통해 Notion 페이지의 구조와 내용을 쉽게 분석하고 활용할 수 있습니다. 178 | 179 | 2. **@notionpresso/react**: 180 | - @notionpresso/cli가 생성한 JSON 데이터를 입력으로 받습니다. 181 | - JSON 데이터를 분석하여 해당하는 React 컴포넌트로 변환합니다. 182 | - 각 Notion 블록 타입(텍스트, 이미지, 테이블 등)에 대응하는 React 컴포넌트를 제공합니다. 183 | - 사용자는 이 컴포넌트들을 커스터마이징하여 원하는 스타일과 기능을 추가할 수 있습니다. 184 | 185 | ### 프로세스 다이어그램 186 | 187 | 다음 ASCII 아트는 NotionPresso를 통해 Notion 페이지를 렌더링하는 과정을 나타냅니다: 188 | 189 | ``` 190 | +-------------+ +----------------+ +------------------+ 191 | | Notion | | @notionpresso/ | | @notionpresso/ | 192 | | Page | --> | cli | --> | react | 193 | +-------------+ +----------------+ +------------------+ 194 | | | | 195 | | | | 196 | v v v 197 | +---------+ +-----------+ +----------------+ 198 | | Content | | JSON | | React Components | 199 | | • Text | | { | | | 200 | | • Image | -----> | "blocks"| -----> | | 201 | | • Table | | ... | | | 202 | | • ... | | } | | ... | 203 | +---------+ +-----------+ +----------------+ 204 | | 205 | | 206 | v 207 | +--------------+ 208 | | Rendered Page | 209 | | (Customizable)| 210 | +--------------+ 211 | ``` 212 | 213 | 이 과정을 통해 사용자는 Notion 페이지 내용을 그대로 가져와서 React 애플리케이션에서 렌더링할 수 있으며, 필요에 따라 스타일과 기능을 커스터마이징할 수 있습니다. 214 | 215 | --- 216 | 217 | 각 하위 프로젝트별 기여 가이드: 218 | 219 | - [@notionpresso/react 기여 방법](./packages/core/CONTRIBUTING.md) 220 | - React 컴포넌트 개발, Notion API 통합, 성능 최적화 등 221 | 222 | 각 프로젝트의 자세한 기여 방법은 해당 링크를 참조해주세요. 223 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 [meursyphus](https://github.com/meursyphus) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-KR.md: -------------------------------------------------------------------------------- 1 | # NotionPresso: 노션으로 나만의 블로그 만들기 2 | 3 | [English Version](./README.md) 4 | 5 | ## 🚀 노션 + Next.js로 나만의 웹사이트를 만들어보세요! 6 | 7 | NotionPresso를 사용하면 노션의 강력한 콘텐츠 관리 기능과 Next.js의 현대적인 웹 기능을 결합하여 완벽하게 커스터마이징 가능한 웹사이트나 블로그를 만들 수 있습니다. 복잡한 CMS나 데이터베이스 설정에 시간을 낭비하지 마세요. 노션에서 콘텐츠를 관리하고 NotionPresso로 멋진 웹사이트를 만들어보세요! 8 | 9 | ### 이 프로젝트는 두 가지 핵심 도구로 구성되어 있습니다: 10 | 11 | 1. **@notionpresso/cli**: 노션 페이지의 콘텐츠를 추출하여 JSON 파일로 변환합니다. 12 | 2. **@notionpresso/react**: 추출된 노션 콘텐츠를 React 컴포넌트로 렌더링합니다. 13 | 14 | ## 🌟 주요 기능 15 | 16 | - **노션 공식 API 사용**: 안정적이고 최신 기능을 항상 지원합니다. 17 | - **완벽한 커스터마이징**: 모든 컴포넌트를 원하는 대로 수정하고 스타일링할 수 있습니다. 18 | - **고품질 노션 컴포넌트**: 실제 노션과 동일하게 보이는 컴포넌트를 제공합니다. 19 | - **최적화된 성능**: 대용량 노션 페이지도 빠르게 로드하고 렌더링합니다. 20 | - **쉬운 콘텐츠 관리**: 노션에서 직접 콘텐츠를 관리하고 실시간으로 웹사이트에 반영하세요. 21 | 22 | ## 🚀 시작하기: 나만의 노션 블로그 만들기 23 | 24 | ### 1. 노션 페이지 준비하기 25 | 26 | 1. 노션에서 새 페이지를 만들고 콘텐츠를 작성하세요. 27 | 2. 페이지를 공개로 설정하고 공유 링크를 복사하세요. 28 | 29 | ### 2. 노션 페이지 ID 얻기 30 | 31 | 1. 노션에서 콘텐츠 페이지로 이동하세요. 32 | 2. 우측 상단의 '공유' 버튼을 클릭하고 'Share to web' 옵션을 활성화하여 페이지를 공개로 설정하세요. 33 | 3. 링크를 복사하세요. URL은 다음과 같은 형식일 것입니다: 34 | ``` 35 | https://www.notion.so/your-page-title-1234567890abcdef12345678 36 | ``` 37 | 4. URL의 마지막 부분(예: `1234567890abcdef12345678`)이 페이지 ID입니다. 38 | 39 | ### 3. 노션 통합 토큰 얻기 40 | 41 | 1. [노션 개발자 포털](https://www.notion.so/my-integrations)로 이동하세요. 42 | 2. '새로운 통합 생성'을 클릭하여 새 통합을 만드세요. 43 | 3. 통합 이름과 권한을 설정하고 '제출'을 클릭하세요. 44 | 4. 생성된 **Internal Integration Token**을 복사하여 저장하세요. 45 | 46 | ### 4. @notionpresso/cli로 콘텐츠 추출하기 47 | 48 | ```bash 49 | npm install -g @notionpresso/cli 50 | npresso --page YOUR_PAGE_URL --auth YOUR_INTEGRATION_TOKEN 51 | ``` 52 | 53 | ### 5. Next.js 프로젝트 템플릿으로 시작하기 54 | 55 | ```bash 56 | git clone https://github.com/notionpresso/nextjs-blog-template.git my-blog 57 | cd my-blog 58 | npm install 59 | ``` 60 | 61 | ### 6. @notionpresso/react로 페이지 렌더링하기 62 | 63 | ```jsx 64 | import { Notion } from "@notionpresso/react"; 65 | import notionData from "./notion-data.json"; 66 | 67 | function HomePage() { 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | export default HomePage; 80 | ``` 81 | 82 | ### 7. 배포하기 83 | 84 | Next.js 앱을 Cloudflare Pages나 Vercel에 배포하고 여러분의 노션 블로그를 세상과 공유하세요! 85 | 86 | ## 📚 상세 사용법 87 | 88 | 더 자세한 사용 방법과 고급 커스터마이징 옵션은 [문서](https://notionpresso.com)를 참조해 주세요. 89 | 90 | ## 🛠 설치 91 | 92 | ```bash 93 | npm install @notionpresso/react @notionpresso/cli 94 | ``` 95 | 96 | ## 🗺 로드맵 97 | 98 | 1. 다양한 노션 블록 타입 지원 확대 99 | 2. 코드 스플리팅과 데이터 캐싱을 포함한 성능 최적화 100 | 3. SEO 최적화 도구 통합 101 | 4. 다국어 지원 강화 102 | 103 | 자세한 개발 계획은 [CONTRIBUTING.md](./CONTRIBUTING.md)를 참조해 주세요. 104 | 105 | ## 🤝 기여하기 106 | 107 | NotionPresso는 여러분의 기여를 환영합니다! 버그 리포트, 기능 제안, 코드 기여 등 어떤 형태로든 자유롭게 참여해 주세요. 자세한 내용은 [CONTRIBUTING.md](./CONTRIBUTING.md)를 확인해 주세요. 108 | 109 | ## 📄 라이선스 110 | 111 | 이 프로젝트는 MIT 라이선스 하에 배포됩니다. 자세한 내용은 [LICENSE](./LICENSE) 파일을 참조하세요. 112 | 113 | ## 📮 연락처 114 | 115 | 질문이나 피드백이 있으시다면 [GitHub 이슈](https://github.com/notionpresso/react/issues)를 생성하거나 [이메일](mailto:helper.notionpresso@gmail.com)로 연락해 주세요. 116 | 117 | --- 118 | 119 | NotionPresso로 노션 콘텐츠를 활용한 멋진 웹사이트를 만들어보세요! 🎉 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NotionPresso: Create Your Custom Blog with Notion 2 | 3 | [한국어 버전(Korean Version)](./README-KR.md) 4 | 5 | ## 🚀 Build Your Own Website with Notion + Next.js! 6 | 7 | With NotionPresso, you can combine Notion's powerful content management capabilities with Next.js's modern web features to create a fully customizable website or blog. Stop wasting time on complex CMS or database setups. Manage your content in Notion and create a stunning website with NotionPresso! 8 | 9 | ### This project consists of two core tools: 10 | 11 | 1. **@notionpresso/cli**: Extracts content from Notion pages and converts it into JSON files. 12 | 2. **@notionpresso/react**: Renders the extracted Notion content as React components. 13 | 14 | ## 🌟 Key Features 15 | 16 | - **Uses Notion's Official API**: Always supports stable and latest features. 17 | - **Perfect Customization**: Modify and style all components as you wish. 18 | - **High-Quality Notion Components**: Provides components that look exactly like real Notion. 19 | - **Optimized Performance**: Quickly loads and renders even large Notion pages. 20 | - **Easy Content Management**: Manage content directly in Notion and reflect it on your website in real-time. 21 | 22 | ## 🚀 Getting Started: Create Your Own Notion Blog 23 | 24 | ### 1. Prepare Your Notion Page 25 | 26 | 1. Create a new page in Notion and write your content. 27 | 2. Set the page to public and copy the share link. 28 | 29 | ### 2. Get Your Notion Page ID 30 | 31 | 1. Go to your content page in Notion. 32 | 2. Click the 'Share' button in the top right corner and enable the 'Share to web' option to make the page public. 33 | 3. Copy the link. The URL will be in this format: 34 | ``` 35 | https://www.notion.so/your-page-title-1234567890abcdef12345678 36 | ``` 37 | 4. The last part of the URL (e.g., `1234567890abcdef12345678`) is your page ID. 38 | 39 | ### 3. Get Your Notion Integration Token 40 | 41 | 1. Go to the [Notion developer portal](https://www.notion.so/my-integrations). 42 | 2. Click 'New integration' to create a new integration. 43 | 3. Set the integration name and permissions, then click 'Submit'. 44 | 4. Copy and save the **Internal Integration Token** that's generated. 45 | 46 | ### 4. Extract Content with @notionpresso/cli 47 | 48 | ```bash 49 | npm install -g @notionpresso/cli 50 | npresso --page YOUR_PAGE_URL --auth YOUR_INTEGRATION_TOKEN 51 | ``` 52 | 53 | ### 5. Set Up Next.js Project with Template 54 | 55 | ```bash 56 | git clone https://github.com/notionpresso/nextjs-blog-template.git my-blog 57 | cd my-blog 58 | npm install 59 | ``` 60 | 61 | ### 6. Render Page with @notionpresso/react 62 | 63 | ```jsx 64 | import { Notion } from "@notionpresso/react"; 65 | import notionData from "./notion-data.json"; 66 | 67 | function HomePage() { 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | export default HomePage; 80 | ``` 81 | 82 | ### 7. Deploy 83 | 84 | Deploy your Next.js app to Cloudflare Pages or Vercel and share your Notion blog with the world! 85 | 86 | ## 📚 Detailed Usage 87 | 88 | For more detailed usage instructions and advanced customization options, please visit our [documentation](https://notionpresso.com). 89 | 90 | ## 🛠 Installation 91 | 92 | ```bash 93 | npm install @notionpresso/react @notionpresso/cli 94 | ``` 95 | 96 | ## 🗺 Roadmap 97 | 98 | 1. Expand support for various Notion block types 99 | 2. Performance optimization including code splitting and data caching 100 | 3. Integrate SEO optimization tools 101 | 4. Enhance multilingual support 102 | 103 | For detailed development plans, please refer to [CONTRIBUTING.md](./CONTRIBUTING.md). 104 | 105 | ## 🤝 Contributing 106 | 107 | NotionPresso welcomes your contributions! Whether it's bug reports, feature suggestions, or code contributions, please feel free to participate in any form. For more details, please check [CONTRIBUTING.md](./CONTRIBUTING.md). 108 | 109 | ## 📄 License 110 | 111 | This project is distributed under the MIT License. For more details, please refer to the [LICENSE](./LICENSE) file. 112 | 113 | ## 📮 Contact 114 | 115 | If you have any questions or feedback, please create a [GitHub issue](https://github.com/notionpresso/react/issues) or contact us via [email](mailto:helper.notionpresso@gmail.com). 116 | 117 | --- 118 | 119 | Create a fantastic website with your Notion content using NotionPresso! 🎉 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notionpresso/react", 3 | "version": "0.0.1", 4 | "description": "this is a react library for notionpresso", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "story:build": "npm run build-storybook --workspace=story", 9 | "story:start": "npm run storybook --workspace=story", 10 | "library:build": "npm run build --workspace=@notionpresso/react", 11 | "start": "npm run story:start", 12 | "build": "npm run library:build", 13 | "prepare": "husky install", 14 | "format": "prettier --write ." 15 | }, 16 | "author": "meuryphus", 17 | "license": "MIT", 18 | "workspaces": [ 19 | "packages/*" 20 | ], 21 | "devDependencies": { 22 | "husky": "^8.0.0", 23 | "lint-staged": "^15.2.10", 24 | "prettier": "^3.3.3" 25 | }, 26 | "lint-staged": { 27 | "*.{js,jsx,ts,tsx,md,mdx,css}": [ 28 | "prettier --write --ignore-unknown" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/core/CONTRIBUTING-KR.md: -------------------------------------------------------------------------------- 1 | # 기여 가이드 2 | 3 | ## 목차 4 | 5 | 1. 소개 6 | - 1.1 프로젝트 개요 7 | - 1.2 우리 프로젝트의 특별한 점 8 | - 1.3 프로젝트 구조 9 | 2. 기여 가이드라인 10 | - 2.1 기여 프로세스 11 | - 2.2 PR 승인 프로세스 12 | - 2.3 메인테이너와 기여자의 역할 13 | - 2.4 이슈와 PR 관리 14 | 3. 개발 환경 및 가이드라인 15 | - 3.1 개발 환경 설정 16 | - 3.2 PreviewJS 설정 17 | - 3.3 컴포넌트 개발 가이드라인 18 | 4. 프로젝트 로드맵 19 | 5. 프로젝트 구조 설명 20 | 21 | ## 1. 소개 22 | 23 | NotionPresso 프로젝트에 관심을 가져주셔서 감사합니다! 24 | 25 | 우리의 목표는 Notion의 강력한 콘텐츠 관리 기능과 React의 유연한 UI 구현을 결합하여, 개발자들이 Notion을 기반으로 커스텀 블로그나 웹사이트를 쉽게 만들 수 있도록 하는 것입니다. 26 | 27 | ```jsx 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | ### 우리 프로젝트의 특별한 점 38 | 39 | NotionPresso는 다음과 같은 특징으로 다른 유사 라이브러리들과 차별화됩니다: 40 | 41 | 1. **Notion 공식 API 사용**: Notion의 공식 API를 직접 활용하여 더 안정적이고 최신 기능을 지원할 수 있습니다. 42 | 43 | 2. **최대한의 커스터마이징 자유도**: 개발자들에게 최대한의 자유를 제공합니다. 원하는 대로 컴포넌트를 수정하고 스타일을 변경할 수 있습니다. 44 | 45 | 3. **고수준의 Notion 컴포넌트**: 실제 Notion과 거의 동일하게 보이는 고품질 컴포넌트를 제공합니다. 이를 통해 사용자들은 친숙하고 전문적인 UI를 쉽게 구현할 수 있습니다. 46 | 47 | 4. **성능 최적화**: 효율적인 렌더링과 데이터 관리를 통해 큰 Notion 페이지도 빠르게 로드하고 표시할 수 있습니다. 48 | 49 | 이러한 특징들을 통해 NotionPresso는 Notion의 파워와 React의 유연성을 동시에 제공하는 최고의 도구가 되고자 합니다. 50 | 51 | ### 프로젝트 구조 52 | 53 | ``` 54 | notionpresso/ 55 | ├── packages/ 56 | │ ├── core/ # @notionpresso/react 패키지 57 | │ └── story/ # 컴포넌트 스토리 58 | ├── README.md 59 | └── CONTRIBUTING.md 60 | ``` 61 | 62 | - `core`: Notion API를 사용하여 Notion 페이지를 React 컴포넌트로 렌더링하는 핵심 라이브러리 63 | - `story`: Storybook을 사용한 UI 컴포넌트 테스트 및 문서화 64 | 65 | ### 초기 설정 66 | 67 | 프로젝트를 시작하려면 루트 디렉토리에서 다음 명령어를 실행하세요: 68 | 69 | ``` 70 | npm install 71 | ``` 72 | 73 | ## 2. 기여 가이드라인 74 | 75 | ### 2.1 기여 프로세스 76 | 77 | 1. 프로젝트를 Fork 합니다. 78 | 2. 로컬에 클론합니다: `git clone https://github.com/notionpresso/react.git` 79 | 3. 의존성을 설치합니다: `npm install` 80 | 4. 새 브랜치를 만듭니다: `git checkout -b feature/your-feature-name` 81 | - 브랜치 이름은 `feature/`, `fix/`, `docs/` 등으로 시작하고 간단한 설명이 뒤따릅니다. 82 | 5. 변경사항을 커밋합니다: `git commit -m "Add some feature"` 83 | 6. Fork한 저장소에 푸시합니다: `git push origin feature/your-feature-name` 84 | 7. Pull Request를 생성합니다. 85 | 86 | 작업 중 중간 리뷰나 피드백을 요청하고 싶다면 PR 제목에 `[DRAFT]`를 붙일 수 있습니다. 87 | 88 | ### 2.2 PR 승인 프로세스 89 | 90 | 모든 Pull Request는 프로젝트 메인테이너인 문대승의 승인이 필요합니다. 91 | PR은 모두 영어로 작성해주세요. 92 | 93 | ### 2.3 메인테이너와 기여자의 역할 94 | 95 | - 메인테이너: 프로젝트 방향을 결정하고 코드 리뷰를 담당합니다. 96 | - 기여자: 버그 수정, 새로운 기능 제안 및 구현, 문서 개선 등에 참여할 수 있습니다. 97 | 98 | ### 2.4 이슈와 PR 관리 99 | 100 | - 메인테이너와 기여자 모두 자유롭게 이슈를 생성하고 해결할 수 있습니다. 101 | - 메인테이너는 PR 요청 시 기능별로 별도의 브랜치를 생성해야 합니다. 102 | - main 브랜치로의 직접 푸시는 금지됩니다. 103 | 104 | ## 3. 개발 환경 및 가이드라인 105 | 106 | ### 3.1 개발 환경 설정 107 | 108 | 1. mise 설치: 109 | 110 | - mise는 모든 기여자가 동일한 Node.js 버전을 사용하도록 보장하는 다중 언어 개발 환경 관리자입니다. 111 | - https://mise.jdx.dev/getting-started.html 의 지침에 따라 mise를 설치하세요. 112 | 113 | 2. 프로젝트 클론 및 mise 설정: 114 | 115 | - 프로젝트를 클론한 후 프로젝트 루트 디렉토리로 이동합니다. 116 | - `mise install`을 실행하여 지정된 Node.js 버전을 설치합니다. 117 | 118 | 3. Node.js 설치 확인: 119 | 120 | - `node -v`를 실행하여 올바른 Node.js 버전이 설치되었는지 확인합니다. 121 | - 버전은 프로젝트 루트의 `.mise.toml` 파일에 지정된 것과 일치해야 합니다. 122 | 123 | 4. 프로젝트 의존성 설치: 124 | - 루트 디렉토리에서 `npm install`을 실행하여 모든 의존성을 설치합니다. 125 | 126 | 참고: mise를 사용하면 시스템에 설치된 다른 Node.js 버전에 관계없이 이 프로젝트에 지정된 정확한 Node.js 버전을 사용할 수 있습니다. 127 | 128 | ### 3.2 PreviewJS 설정 129 | 130 | PreviewJS를 사용하면 에디터에서 직접 Storybook을 볼 수 있어서 개발에 매우 편리합니다. 131 | 132 | 1. VSCode에 [PreviewJS](https://previewjs.com/) 확장을 설치합니다. 133 | 2. 프로젝트를 열고 각 스토리 코드에서 "Open Preview"를 선택합니다. 134 | 135 | ### 3.3 컴포넌트 개발 가이드라인 136 | 137 | NotionPresso 컴포넌트를 개발할 때는 다음 가이드라인을 따라주세요: 138 | 139 | 1. Storybook 개발: 140 | 141 | - 각 컴포넌트에 대한 스토리를 작성하고 Storybook을 통해 다양한 상태와 props를 테스트합니다. 142 | - 루트 디렉토리에서 `npm run story:start`로 Storybook을 실행할 수 있습니다. 143 | 144 | 2. PreviewJS 사용: 145 | 146 | - VSCode에서 PreviewJS를 사용하면 에디터에서 직접 Storybook을 보면서 개발할 수 있어 매우 효율적입니다. 147 | 148 | 3. 컴포넌트 이름 및 구조: 149 | - 컴포넌트 이름은 PascalCase로 작성합니다. 150 | - 컴포넌트 파일 이름과 폴더 이름은 kebab-case를 사용합니다. 즉, 모든 단어를 소문자로 쓰고 하이픈(-)으로 구분합니다. 예) `text-block.tsx` 151 | 152 | ## 4. 프로젝트 로드맵 153 | 154 | ### 1. @notionpresso/react 라이브러리 개발 및 배포 155 | 156 | ### 2. @notionpresso/cli 도구 개발 및 배포 157 | 158 | ### 3. docs 프로젝트 개발 159 | 160 | - 프로젝트 소개 페이지 작성 161 | - NextJS를 사용한 블로그 작성 예제 포함 162 | - 상세 사용 가이드 및 API 문서 작성 163 | - 프로젝트 블로그 운영 164 | - 정기적인 업데이트와 변경 로그 공유 165 | 166 | 각 단계의 진행 상황과 세부 내용은 이슈 트래커에서 확인할 수 있습니다. 167 | 168 | ## 5. 프로젝트 구조 설명 169 | 170 | ### @notionpresso/react와 @notionpresso/cli의 역할 171 | 172 | 1. **@notionpresso/cli**: 173 | 174 | - Notion API를 사용하여 특정 Notion 페이지의 내용을 가져옵니다. 175 | - 가져온 데이터를 JSON 형식으로 변환하여 로컬에 저장합니다. 176 | - `npx npresso` 명령어를 통해 사용할 수 있습니다. 177 | - 이 과정을 통해 Notion 페이지의 구조와 내용을 쉽게 분석하고 활용할 수 있습니다. 178 | 179 | 2. **@notionpresso/react**: 180 | - @notionpresso/cli가 생성한 JSON 데이터를 입력으로 받습니다. 181 | - JSON 데이터를 분석하여 해당하는 React 컴포넌트로 변환합니다. 182 | - 각 Notion 블록 타입(텍스트, 이미지, 테이블 등)에 대응하는 React 컴포넌트를 제공합니다. 183 | - 사용자는 이 컴포넌트들을 커스터마이징하여 원하는 스타일과 기능을 추가할 수 있습니다. 184 | 185 | ### 프로세스 다이어그램 186 | 187 | 다음 ASCII 아트는 NotionPresso를 통해 Notion 페이지를 렌더링하는 과정을 나타냅니다: 188 | 189 | ``` 190 | +-------------+ +----------------+ +------------------+ 191 | | Notion | | @notionpresso/ | | @notionpresso/ | 192 | | Page | --> | cli | --> | react | 193 | +-------------+ +----------------+ +------------------+ 194 | | | | 195 | | | | 196 | v v v 197 | +---------+ +-----------+ +----------------+ 198 | | Content | | JSON | | React Components | 199 | | • Text | | { | | | 200 | | • Image | -----> | "blocks"| -----> | | 201 | | • Table | | ... | | | 202 | | • ... | | } | | ... | 203 | +---------+ +-----------+ +----------------+ 204 | | 205 | | 206 | v 207 | +--------------+ 208 | | Rendered Page | 209 | | (Customizable)| 210 | +--------------+ 211 | ``` 212 | 213 | 이 과정을 통해 사용자는 Notion 페이지 내용을 그대로 가져와서 React 애플리케이션에서 렌더링할 수 있으며, 필요에 따라 스타일과 기능을 커스터마이징할 수 있습니다. 214 | 215 | --- 216 | 217 | 각 하위 프로젝트별 기여 가이드: 218 | 219 | - [@notionpresso/react 기여 방법](./packages/core/CONTRIBUTING.md) 220 | - React 컴포넌트 개발, Notion API 통합, 성능 최적화 등 221 | 222 | 각 프로젝트의 자세한 기여 방법은 해당 링크를 참조해주세요. 223 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # NotionPresso: Create Your Custom Blog with Notion 2 | 3 | [한국어 버전(Korean Version)](./README-KR.md) 4 | 5 | ## 🚀 Build Your Own Website with Notion + Next.js! 6 | 7 | With NotionPresso, you can combine Notion's powerful content management capabilities with Next.js's modern web features to create a fully customizable website or blog. Stop wasting time on complex CMS or database setups. Manage your content in Notion and create a stunning website with NotionPresso! 8 | 9 | ### This project consists of two core tools: 10 | 11 | 1. **@notionpresso/cli**: Extracts content from Notion pages and converts it into JSON files. 12 | 2. **@notionpresso/react**: Renders the extracted Notion content as React components. 13 | 14 | ## 🌟 Key Features 15 | 16 | - **Uses Notion's Official API**: Always supports stable and latest features. 17 | - **Perfect Customization**: Modify and style all components as you wish. 18 | - **High-Quality Notion Components**: Provides components that look exactly like real Notion. 19 | - **Optimized Performance**: Quickly loads and renders even large Notion pages. 20 | - **Easy Content Management**: Manage content directly in Notion and reflect it on your website in real-time. 21 | 22 | ## 🚀 Getting Started: Create Your Own Notion Blog 23 | 24 | ### 1. Prepare Your Notion Page 25 | 26 | 1. Create a new page in Notion and write your content. 27 | 2. Set the page to public and copy the share link. 28 | 29 | ### 2. Get Your Notion Page ID 30 | 31 | 1. Go to your content page in Notion. 32 | 2. Click the 'Share' button in the top right corner and enable the 'Share to web' option to make the page public. 33 | 3. Copy the link. The URL will be in this format: 34 | ``` 35 | https://www.notion.so/your-page-title-1234567890abcdef12345678 36 | ``` 37 | 4. The last part of the URL (e.g., `1234567890abcdef12345678`) is your page ID. 38 | 39 | ### 3. Get Your Notion Integration Token 40 | 41 | 1. Go to the [Notion developer portal](https://www.notion.so/my-integrations). 42 | 2. Click 'New integration' to create a new integration. 43 | 3. Set the integration name and permissions, then click 'Submit'. 44 | 4. Copy and save the **Internal Integration Token** that's generated. 45 | 46 | ### 4. Extract Content with @notionpresso/cli 47 | 48 | ```bash 49 | npm install -g @notionpresso/cli 50 | npresso --page YOUR_PAGE_URL --auth YOUR_INTEGRATION_TOKEN 51 | ``` 52 | 53 | ### 5. Set Up Next.js Project with Template 54 | 55 | ```bash 56 | git clone https://github.com/notionpresso/nextjs-blog-template.git my-blog 57 | cd my-blog 58 | npm install 59 | ``` 60 | 61 | ### 6. Render Page with @notionpresso/react 62 | 63 | ```jsx 64 | import { Notion } from "@notionpresso/react"; 65 | import notionData from "./notion-data.json"; 66 | 67 | function HomePage() { 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | export default HomePage; 80 | ``` 81 | 82 | ### 7. Deploy 83 | 84 | Deploy your Next.js app to Cloudflare Pages or Vercel and share your Notion blog with the world! 85 | 86 | ## 📚 Detailed Usage 87 | 88 | For more detailed usage instructions and advanced customization options, please visit our [documentation](https://notionpresso.com). 89 | 90 | ## 🛠 Installation 91 | 92 | ```bash 93 | npm install @notionpresso/react @notionpresso/cli 94 | ``` 95 | 96 | ## 🗺 Roadmap 97 | 98 | 1. Expand support for various Notion block types 99 | 2. Performance optimization including code splitting and data caching 100 | 3. Integrate SEO optimization tools 101 | 4. Enhance multilingual support 102 | 103 | For detailed development plans, please refer to [CONTRIBUTING.md](./CONTRIBUTING.md). 104 | 105 | ## 🤝 Contributing 106 | 107 | NotionPresso welcomes your contributions! Whether it's bug reports, feature suggestions, or code contributions, please feel free to participate in any form. For more details, please check [CONTRIBUTING.md](./CONTRIBUTING.md). 108 | 109 | ## 📄 License 110 | 111 | This project is distributed under the MIT License. For more details, please refer to the [LICENSE](./LICENSE) file. 112 | 113 | ## 📮 Contact 114 | 115 | If you have any questions or feedback, please create a [GitHub issue](https://github.com/notionpresso/react/issues) or contact us via [email](mailto:helper.notionpresso@gmail.com). 116 | 117 | --- 118 | 119 | Create a fantastic website with your Notion content using NotionPresso! 🎉 120 | -------------------------------------------------------------------------------- /packages/core/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | { ignores: ["dist"] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ["**/*.{ts,tsx}"], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | "react-hooks": reactHooks, 18 | "react-refresh": reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | "react-refresh/only-export-components": [ 23 | "warn", 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /packages/core/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notionpresso/react", 3 | "version": "0.0.6", 4 | "private": false, 5 | "homepage": "https://notionpresso.com", 6 | "description": "This is wellmade React components for Notion Opensource", 7 | "author": "notionpresso", 8 | "license": "MIT", 9 | "keywords": [ 10 | "notion", 11 | "notion-react", 12 | "notion-opensource", 13 | "notion-react-x", 14 | "notion-react-renderer" 15 | ], 16 | "type": "module", 17 | "main": "dist/notionpresso.js", 18 | "types": "dist/index.d.ts", 19 | "files": [ 20 | "dist" 21 | ], 22 | "sideEffects": [ 23 | "**/*.css" 24 | ], 25 | "scripts": { 26 | "dev": "vite", 27 | "build": "tsc && vite build", 28 | "prepublishOnly": "npm run build" 29 | }, 30 | "dependencies": { 31 | "@matejmazur/react-katex": "^3.1.3", 32 | "framer-motion": "^11.9.0", 33 | "clipboard-copy": "^4.0.1", 34 | "he": "^1.2.0", 35 | "katex": "^0.16.11", 36 | "prismjs": "^1.29.0", 37 | "romans": "^1.1.2", 38 | "react-helmet": "^6.1.0" 39 | }, 40 | "devDependencies": { 41 | "@cozy-blog/notion-client": "^0.0.22", 42 | "@types/katex": "^0.16.7", 43 | "@types/node": "^18.0.0", 44 | "@types/prismjs": "^1.26.4", 45 | "@types/react": "^18.0.0", 46 | "@types/he": "^1.2.3", 47 | "@types/react-dom": "^18.0.0", 48 | "@types/react-helmet": "^6.1.11", 49 | "@types/romans": "^2.0.2", 50 | "@vitejs/plugin-react": "^3.0.0", 51 | "react": "^18.2.0", 52 | "react-dom": "^18.2.0", 53 | "typescript": "^4.9.0", 54 | "vite": "^4.0.0", 55 | "vite-plugin-dts": "^2.0.0", 56 | "vite-plugin-lib-inject-css": "^1.3.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/core/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/bulleted-list-item.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { BulletedListItemArgs } from "../types"; 3 | import { bulletedListItemMarker, getColorCss } from "../utils"; 4 | import RichText from "./internal/rich-text"; 5 | 6 | type BulletedListItemProps = { 7 | children?: React.ReactNode; 8 | } & BulletedListItemArgs; 9 | 10 | const BulletedListItem: React.FC = ({ 11 | children, 12 | ...props 13 | }) => { 14 | const { 15 | bulleted_list_item: { rich_text: texts, color }, 16 | } = props; 17 | const { marker, format } = bulletedListItemMarker.getMarker(props); 18 | 19 | return ( 20 |
    24 |
  • 25 |
    26 | 30 | {marker} 31 | 32 |

    33 | 34 |

    35 |
    36 | {children} 37 |
  • 38 |
39 | ); 40 | }; 41 | 42 | export default BulletedListItem; 43 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/callout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import type { CalloutArgs } from "../types"; 5 | import { getColorCss } from "../utils"; 6 | import RichText from "./internal/rich-text"; 7 | import Icon from "./internal/icon"; 8 | 9 | type CalloutProps = { 10 | children?: React.ReactNode; 11 | } & CalloutArgs; 12 | 13 | const Callout: React.FC = ({ children, ...props }) => { 14 | const { callout } = props; 15 | 16 | return ( 17 |
20 |
21 | 24 |
25 | 26 |
27 |
28 | {children} 29 |
30 | ); 31 | }; 32 | 33 | export default Callout; 34 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import copyToClipboard from "clipboard-copy"; 3 | import RichText from "../internal/rich-text"; 4 | import Prisim from "prismjs"; 5 | import "prismjs/themes/prism.css"; 6 | import loadDart from "./support-language/dart"; 7 | import loadElixir from "./support-language/elixir"; 8 | import loadGo from "./support-language/go"; 9 | import loadJava from "./support-language/java"; 10 | import loadKotlin from "./support-language/kotlin"; 11 | import loadMarkdown from "./support-language/markdown"; 12 | import loadPython from "./support-language/python"; 13 | import loadSql from "./support-language/sql"; 14 | import loadTypescript from "./support-language/typescript"; 15 | import type { CodeArgs } from "../../types"; 16 | 17 | [ 18 | loadDart, 19 | loadTypescript, 20 | loadElixir, 21 | loadGo, 22 | loadJava, 23 | loadKotlin, 24 | loadMarkdown, 25 | loadPython, 26 | loadSql, 27 | ].forEach((load) => load(Prisim)); 28 | 29 | const Code = ({ ...props }: CodeArgs) => { 30 | const { 31 | code: { caption, rich_text: texts, language }, 32 | } = props; 33 | const [showTooltip, setShowTooltip] = useState(false); 34 | const content = texts.map(({ plain_text }) => plain_text).join(""); 35 | 36 | useEffect(() => { 37 | let timer: NodeJS.Timeout; 38 | if (showTooltip) { 39 | timer = setTimeout(() => { 40 | setShowTooltip(false); 41 | }, 2000); 42 | } 43 | return () => clearTimeout(timer); 44 | }, [showTooltip]); 45 | 46 | const handleCopy = () => { 47 | copyToClipboard(content); 48 | setShowTooltip(true); 49 | }; 50 | 51 | return ( 52 |
53 |
54 |
55 |
56 | {language.replace(/^[a-z]/, (char) => char.toUpperCase())} 57 |
58 | 71 |
72 | 73 |
82 | 83 |
84 | {caption.length !== 0 && ( 85 |
86 | 87 |
88 | )} 89 |
93 |
{"Copied"}
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default Code; 100 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/dart.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | let loaded = false; 3 | 4 | export default function load(Prism: any) { 5 | if (loaded) return; 6 | _load(Prism); 7 | loaded = true; 8 | } 9 | 10 | function _load(Prism: any) { 11 | const keywords = [ 12 | /\b(?:async|sync|yield)\*/, 13 | /\b(?:abstract|assert|async|await|break|case|catch|class|const|continue|covariant|default|deferred|do|dynamic|else|enum|export|extends|extension|external|factory|final|finally|for|get|hide|if|implements|import|in|interface|library|mixin|new|null|on|operator|part|rethrow|return|set|show|static|super|switch|sync|this|throw|try|typedef|var|void|while|with|yield)\b/, 14 | ]; 15 | 16 | // Handles named imports, such as http.Client 17 | const packagePrefix = /(^|[^\w.])(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/ 18 | .source; 19 | 20 | // based on the dart naming conventions 21 | const className = { 22 | pattern: RegExp(packagePrefix + /[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source), 23 | lookbehind: true, 24 | inside: { 25 | namespace: { 26 | pattern: /^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/, 27 | inside: { 28 | punctuation: /\./, 29 | }, 30 | }, 31 | }, 32 | }; 33 | 34 | Prism.languages.dart = Prism.languages.extend("clike", { 35 | "class-name": [ 36 | className, 37 | { 38 | // variables and parameters 39 | // this to support class names (or generic parameters) which do not contain a lower case letter (also works for methods) 40 | pattern: RegExp(packagePrefix + /[A-Z]\w*(?=\s+\w+\s*[;,=()])/.source), 41 | lookbehind: true, 42 | inside: className.inside, 43 | }, 44 | ], 45 | keyword: keywords, 46 | operator: 47 | /\bis!|\b(?:as|is)\b|\+\+|--|&&|\|\||<<=?|>>=?|~(?:\/=?)?|[+\-*/%&^|=!<>]=?|\?/, 48 | }); 49 | 50 | Prism.languages.insertBefore("dart", "string", { 51 | "string-literal": { 52 | pattern: 53 | /r?(?:("""|''')[\s\S]*?\1|(["'])(?:\\.|(?!\2)[^\\\r\n])*\2(?!\2))/, 54 | greedy: true, 55 | inside: { 56 | interpolation: { 57 | pattern: /((?:^|[^\\])(?:\\{2})*)\$(?:\w+|\{(?:[^{}]|\{[^{}]*\})*\})/, 58 | lookbehind: true, 59 | inside: { 60 | punctuation: /^\$\{?|\}$/, 61 | expression: { 62 | pattern: /[\s\S]+/, 63 | inside: Prism.languages.dart, 64 | }, 65 | }, 66 | }, 67 | string: /[\s\S]+/, 68 | }, 69 | }, 70 | string: undefined, 71 | }); 72 | 73 | Prism.languages.insertBefore("dart", "class-name", { 74 | metadata: { 75 | pattern: /@\w+/, 76 | alias: "function", 77 | }, 78 | }); 79 | 80 | Prism.languages.insertBefore("dart", "class-name", { 81 | generics: { 82 | pattern: 83 | /<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<(?:[\w\s,.&?]|<[\w\s,.&?]*>)*>)*>)*>/, 84 | inside: { 85 | "class-name": className, 86 | keyword: keywords, 87 | punctuation: /[<>(),.:]/, 88 | operator: /[?&|]/, 89 | }, 90 | }, 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/elixir.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | let loaded = false; 4 | 5 | export default function load(Prism: any) { 6 | if (loaded) return; 7 | _load(Prism); 8 | loaded = true; 9 | } 10 | 11 | function _load(Prism: any) { 12 | Prism.languages.elixir = { 13 | doc: { 14 | pattern: 15 | /@(?:doc|moduledoc)\s+(?:("""|''')[\s\S]*?\1|("|')(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2)/, 16 | inside: { 17 | attribute: /^@\w+/, 18 | string: /['"][\s\S]+/, 19 | }, 20 | }, 21 | comment: { 22 | pattern: /#.*/, 23 | greedy: true, 24 | }, 25 | // ~r"""foo""" (multi-line), ~r'''foo''' (multi-line), ~r/foo/, ~r|foo|, ~r"foo", ~r'foo', ~r(foo), ~r[foo], ~r{foo}, ~r 26 | regex: { 27 | pattern: 28 | /~[rR](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|[^\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[uismxfr]*/, 29 | greedy: true, 30 | }, 31 | string: [ 32 | { 33 | // ~s"""foo""" (multi-line), ~s'''foo''' (multi-line), ~s/foo/, ~s|foo|, ~s"foo", ~s'foo', ~s(foo), ~s[foo], ~s{foo} (with interpolation care), ~s 34 | pattern: 35 | /~[cCsSwW](?:("""|''')(?:\\[\s\S]|(?!\1)[^\\])+\1|([/|"'])(?:\\.|(?!\2)[^\\\r\n])+\2|\((?:\\.|[^\\)\r\n])+\)|\[(?:\\.|[^\\\]\r\n])+\]|\{(?:\\.|#\{[^}]+\}|#(?!\{)|[^#\\}\r\n])+\}|<(?:\\.|[^\\>\r\n])+>)[csa]?/, 36 | greedy: true, 37 | inside: { 38 | // See interpolation below 39 | }, 40 | }, 41 | { 42 | pattern: /("""|''')[\s\S]*?\1/, 43 | greedy: true, 44 | inside: { 45 | // See interpolation below 46 | }, 47 | }, 48 | { 49 | // Multi-line strings are allowed 50 | pattern: /("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, 51 | greedy: true, 52 | inside: { 53 | // See interpolation below 54 | }, 55 | }, 56 | ], 57 | atom: { 58 | // Look-behind prevents bad highlighting of the :: operator 59 | pattern: /(^|[^:]):\w+/, 60 | lookbehind: true, 61 | alias: "symbol", 62 | }, 63 | module: { 64 | pattern: /\b[A-Z]\w*\b/, 65 | alias: "class-name", 66 | }, 67 | // Look-ahead prevents bad highlighting of the :: operator 68 | "attr-name": /\b\w+\??:(?!:)/, 69 | argument: { 70 | // Look-behind prevents bad highlighting of the && operator 71 | pattern: /(^|[^&])&\d+/, 72 | lookbehind: true, 73 | alias: "variable", 74 | }, 75 | attribute: { 76 | pattern: /@\w+/, 77 | alias: "variable", 78 | }, 79 | function: /\b[_a-zA-Z]\w*[?!]?(?:(?=\s*(?:\.\s*)?\()|(?=\/\d))/, 80 | number: /\b(?:0[box][a-f\d_]+|\d[\d_]*)(?:\.[\d_]+)?(?:e[+-]?[\d_]+)?\b/i, 81 | keyword: 82 | /\b(?:after|alias|and|case|catch|cond|def(?:callback|delegate|exception|impl|macro|module|n|np|p|protocol|struct)?|do|else|end|fn|for|if|import|not|or|quote|raise|require|rescue|try|unless|unquote|use|when)\b/, 83 | boolean: /\b(?:false|nil|true)\b/, 84 | operator: [ 85 | /\bin\b|&&?|\|[|>]?|\\\\|::|\.\.\.?|\+\+?|-[->]?|<[-=>]|>=|!==?|\B!|=(?:==?|[>~])?|[*\/^]/, 86 | { 87 | // We don't want to match << 88 | pattern: /([^<])<(?!<)/, 89 | lookbehind: true, 90 | }, 91 | { 92 | // We don't want to match >> 93 | pattern: /([^>])>(?!>)/, 94 | lookbehind: true, 95 | }, 96 | ], 97 | punctuation: /<<|>>|[.,%[\]{}()]/, 98 | }; 99 | 100 | Prism.languages.elixir.string.forEach(function (o: any) { 101 | o.inside = { 102 | interpolation: { 103 | pattern: /#\{[^}]+\}/, 104 | inside: { 105 | delimiter: { 106 | pattern: /^#\{|\}$/, 107 | alias: "punctuation", 108 | }, 109 | rest: Prism.languages.elixir, 110 | }, 111 | }, 112 | }; 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/go.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | let loaded = false; 4 | 5 | export default function load(Prism: any) { 6 | if (loaded) return; 7 | _load(Prism); 8 | loaded = true; 9 | } 10 | 11 | function _load(Prism: any) { 12 | Prism.languages.go = Prism.languages.extend("clike", { 13 | string: { 14 | pattern: /(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/, 15 | lookbehind: true, 16 | greedy: true, 17 | }, 18 | keyword: 19 | /\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|const)\b/, 20 | boolean: /\b(?:_|false|iota|nil|true)\b/, 21 | number: [ 22 | // binary and octal integers 23 | /\b0(?:b[01_]+|o[0-7_]+)i?\b/i, 24 | // hexadecimal integers and floats 25 | /\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i, 26 | // decimal integers and floats 27 | /(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i, 28 | ], 29 | operator: 30 | /[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./, 31 | builtin: 32 | /\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/, 33 | }); 34 | 35 | Prism.languages.insertBefore("go", "string", { 36 | char: { 37 | pattern: /'(?:\\.|[^'\\\r\n]){0,10}'/, 38 | greedy: true, 39 | }, 40 | }); 41 | 42 | delete Prism.languages.go["class-name"]; 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/java.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | let loaded = false; 4 | 5 | export default function load(Prism: any) { 6 | if (loaded) return; 7 | _load(Prism); 8 | loaded = true; 9 | } 10 | 11 | function _load(Prism: any) { 12 | const keywords = 13 | /\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record(?!\s*[(){}[\]<>=%~.:,;?+\-*/&|^])|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|const|void|volatile|while|with|yield)\b/; 14 | 15 | // full package (optional) + parent classes (optional) 16 | const classNamePrefix = /(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source; 17 | 18 | // based on the java naming conventions 19 | const className = { 20 | pattern: RegExp( 21 | /(^|[^\w.])/.source + 22 | classNamePrefix + 23 | /[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source, 24 | ), 25 | lookbehind: true, 26 | inside: { 27 | namespace: { 28 | pattern: /^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/, 29 | inside: { 30 | punctuation: /\./, 31 | }, 32 | }, 33 | punctuation: /\./, 34 | }, 35 | }; 36 | 37 | Prism.languages.java = Prism.languages.extend("clike", { 38 | string: { 39 | pattern: /(^|[^\\])"(?:\\.|[^"\\\r\n])*"/, 40 | lookbehind: true, 41 | greedy: true, 42 | }, 43 | "class-name": [ 44 | className, 45 | { 46 | // constiables, parameters, and constructor references 47 | // this to support class names (or generic parameters) which do not contain a lower case letter (also works for methods) 48 | pattern: RegExp( 49 | /(^|[^\w.])/.source + 50 | classNamePrefix + 51 | /[A-Z]\w*(?=\s+\w+\s*[;,=()]|\s*(?:\[[\s,]*\]\s*)?::\s*new\b)/ 52 | .source, 53 | ), 54 | lookbehind: true, 55 | inside: className.inside, 56 | }, 57 | { 58 | // class names based on keyword 59 | // this to support class names (or generic parameters) which do not contain a lower case letter (also works for methods) 60 | pattern: RegExp( 61 | /(\b(?:class|enum|extends|implements|instanceof|interface|new|record|throws)\s+)/ 62 | .source + 63 | classNamePrefix + 64 | /[A-Z]\w*\b/.source, 65 | ), 66 | lookbehind: true, 67 | inside: className.inside, 68 | }, 69 | ], 70 | keyword: keywords, 71 | function: [ 72 | Prism.languages.clike.function, 73 | { 74 | pattern: /(::\s*)[a-z_]\w*/, 75 | lookbehind: true, 76 | }, 77 | ], 78 | number: 79 | /\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i, 80 | operator: { 81 | pattern: 82 | /(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m, 83 | lookbehind: true, 84 | }, 85 | constant: /\b[A-Z][A-Z_\d]+\b/, 86 | }); 87 | 88 | Prism.languages.insertBefore("java", "string", { 89 | "triple-quoted-string": { 90 | // http://openjdk.java.net/jeps/355#Description 91 | pattern: /"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/, 92 | greedy: true, 93 | alias: "string", 94 | }, 95 | char: { 96 | pattern: /'(?:\\.|[^'\\\r\n]){1,6}'/, 97 | greedy: true, 98 | }, 99 | }); 100 | 101 | Prism.languages.insertBefore("java", "class-name", { 102 | annotation: { 103 | pattern: /(^|[^.])@\w+(?:\s*\.\s*\w+)*/, 104 | lookbehind: true, 105 | alias: "punctuation", 106 | }, 107 | generics: { 108 | pattern: 109 | /<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/, 110 | inside: { 111 | "class-name": className, 112 | keyword: keywords, 113 | punctuation: /[<>(),.:]/, 114 | operator: /[?&|]/, 115 | }, 116 | }, 117 | import: [ 118 | { 119 | pattern: RegExp( 120 | /(\bimport\s+)/.source + 121 | classNamePrefix + 122 | /(?:[A-Z]\w*|\*)(?=\s*;)/.source, 123 | ), 124 | lookbehind: true, 125 | inside: { 126 | namespace: className.inside.namespace, 127 | punctuation: /\./, 128 | operator: /\*/, 129 | "class-name": /\w+/, 130 | }, 131 | }, 132 | { 133 | pattern: RegExp( 134 | /(\bimport\s+static\s+)/.source + 135 | classNamePrefix + 136 | /(?:\w+|\*)(?=\s*;)/.source, 137 | ), 138 | lookbehind: true, 139 | alias: "static", 140 | inside: { 141 | namespace: className.inside.namespace, 142 | static: /\b\w+$/, 143 | punctuation: /\./, 144 | operator: /\*/, 145 | "class-name": /\w+/, 146 | }, 147 | }, 148 | ], 149 | namespace: { 150 | pattern: RegExp( 151 | /(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?!)[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace( 152 | //g, 153 | function () { 154 | return keywords.source; 155 | }, 156 | ), 157 | ), 158 | lookbehind: true, 159 | inside: { 160 | punctuation: /\./, 161 | }, 162 | }, 163 | }); 164 | } 165 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/kotlin.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | let loaded = false; 4 | 5 | export default function load(Prism: any) { 6 | if (loaded) return; 7 | _load(Prism); 8 | loaded = true; 9 | } 10 | 11 | function _load(Prism: any) { 12 | Prism.languages.kotlin = Prism.languages.extend("clike", { 13 | keyword: { 14 | // The lookbehind prevents wrong highlighting of e.g. kotlin.properties.get 15 | pattern: 16 | /(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|const|constarg|when|where|while)\b/, 17 | lookbehind: true, 18 | }, 19 | function: [ 20 | { 21 | pattern: /(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/, 22 | greedy: true, 23 | }, 24 | { 25 | pattern: /(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/, 26 | lookbehind: true, 27 | greedy: true, 28 | }, 29 | ], 30 | number: 31 | /\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/, 32 | operator: 33 | /\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/, 34 | }); 35 | 36 | delete Prism.languages.kotlin["class-name"]; 37 | 38 | const interpolationInside = { 39 | "interpolation-punctuation": { 40 | pattern: /^\$\{?|\}$/, 41 | alias: "punctuation", 42 | }, 43 | expression: { 44 | pattern: /[\s\S]+/, 45 | inside: Prism.languages.kotlin, 46 | }, 47 | }; 48 | 49 | Prism.languages.insertBefore("kotlin", "string", { 50 | // https://kotlinlang.org/spec/expressions.html#string-interpolation-expressions 51 | "string-literal": [ 52 | { 53 | pattern: /"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/, 54 | alias: "multiline", 55 | inside: { 56 | interpolation: { 57 | pattern: /\$(?:[a-z_]\w*|\{[^{}]*\})/i, 58 | inside: interpolationInside, 59 | }, 60 | string: /[\s\S]+/, 61 | }, 62 | }, 63 | { 64 | pattern: /"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/, 65 | alias: "singleline", 66 | inside: { 67 | interpolation: { 68 | pattern: /((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i, 69 | lookbehind: true, 70 | inside: interpolationInside, 71 | }, 72 | string: /[\s\S]+/, 73 | }, 74 | }, 75 | ], 76 | char: { 77 | // https://kotlinlang.org/spec/expressions.html#character-literals 78 | pattern: /'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/, 79 | greedy: true, 80 | }, 81 | }); 82 | 83 | delete Prism.languages.kotlin["string"]; 84 | 85 | Prism.languages.insertBefore("kotlin", "keyword", { 86 | annotation: { 87 | pattern: /\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/, 88 | alias: "builtin", 89 | }, 90 | }); 91 | 92 | Prism.languages.insertBefore("kotlin", "function", { 93 | label: { 94 | pattern: /\b\w+@|@\w+\b/, 95 | alias: "symbol", 96 | }, 97 | }); 98 | 99 | Prism.languages.kt = Prism.languages.kotlin; 100 | Prism.languages.kts = Prism.languages.kotlin; 101 | } 102 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/python.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | let loaded = false; 3 | 4 | export default function load(Prism: any) { 5 | if (loaded) return; 6 | _load(Prism); 7 | loaded = true; 8 | } 9 | 10 | function _load(Prism: any) { 11 | Prism.languages.python = { 12 | comment: { 13 | pattern: /(^|[^\\])#.*/, 14 | lookbehind: true, 15 | greedy: true, 16 | }, 17 | "string-interpolation": { 18 | pattern: 19 | /(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i, 20 | greedy: true, 21 | inside: { 22 | interpolation: { 23 | // "{" "}" 24 | pattern: 25 | /((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/, 26 | lookbehind: true, 27 | inside: { 28 | "format-spec": { 29 | pattern: /(:)[^:(){}]+(?=\}$)/, 30 | lookbehind: true, 31 | }, 32 | "conversion-option": { 33 | pattern: /![sra](?=[:}]$)/, 34 | alias: "punctuation", 35 | }, 36 | rest: null, 37 | }, 38 | }, 39 | string: /[\s\S]+/, 40 | }, 41 | }, 42 | "triple-quoted-string": { 43 | pattern: /(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i, 44 | greedy: true, 45 | alias: "string", 46 | }, 47 | string: { 48 | pattern: /(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i, 49 | greedy: true, 50 | }, 51 | function: { 52 | pattern: /((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g, 53 | lookbehind: true, 54 | }, 55 | "class-name": { 56 | pattern: /(\bclass\s+)\w+/i, 57 | lookbehind: true, 58 | }, 59 | decorator: { 60 | pattern: /(^[\t ]*)@\w+(?:\.\w+)*/m, 61 | lookbehind: true, 62 | alias: ["annotation", "punctuation"], 63 | inside: { 64 | punctuation: /\./, 65 | }, 66 | }, 67 | keyword: 68 | /\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/, 69 | builtin: 70 | /\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/, 71 | boolean: /\b(?:False|None|True)\b/, 72 | number: 73 | /\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i, 74 | operator: /[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/, 75 | punctuation: /[{}[\];(),.:]/, 76 | }; 77 | 78 | Prism.languages.python["string-interpolation"].inside[ 79 | "interpolation" 80 | ].inside.rest = Prism.languages.python; 81 | 82 | Prism.languages.py = Prism.languages.python; 83 | } 84 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/sql.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | let loaded = false; 4 | 5 | export default function load(Prism: any) { 6 | if (loaded) return; 7 | _load(Prism); 8 | loaded = true; 9 | } 10 | 11 | function _load(Prism: any) { 12 | Prism.languages.sql = { 13 | comment: { 14 | pattern: /(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/, 15 | lookbehind: true, 16 | }, 17 | constiable: [ 18 | { 19 | pattern: /@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/, 20 | greedy: true, 21 | }, 22 | /@[\w.$]+/, 23 | ], 24 | string: { 25 | pattern: /(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/, 26 | greedy: true, 27 | lookbehind: true, 28 | }, 29 | identifier: { 30 | pattern: /(^|[^@\\])`(?:\\[\s\S]|[^`\\]|``)*`/, 31 | greedy: true, 32 | lookbehind: true, 33 | inside: { 34 | punctuation: /^`|`$/, 35 | }, 36 | }, 37 | function: 38 | /\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i, // Should we highlight user defined functions too? 39 | keyword: 40 | /\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|const(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i, 41 | boolean: /\b(?:FALSE|NULL|TRUE)\b/i, 42 | number: /\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i, 43 | operator: 44 | /[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i, 45 | punctuation: /[;[\]()`,.]/, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/code/support-language/typescript.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | let loaded = false; 4 | 5 | export default function load(Prism: any) { 6 | if (loaded) return; 7 | _load(Prism); 8 | loaded = true; 9 | } 10 | 11 | function _load(Prism: any) { 12 | Prism.languages.typescript = Prism.languages.extend("javascript", { 13 | "class-name": { 14 | pattern: 15 | /(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/, 16 | lookbehind: true, 17 | greedy: true, 18 | inside: null, // see below 19 | }, 20 | builtin: 21 | /\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/, 22 | }); 23 | 24 | // The keywords TypeScript adds to JavaScript 25 | Prism.languages.typescript.keyword.push( 26 | /\b(?:abstract|declare|is|keyof|readonly|require)\b/, 27 | // keywords that have to be followed by an identifier 28 | /\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/, 29 | // This is for `import type *, {}` 30 | /\btype\b(?=\s*(?:[\{*]|$))/, 31 | ); 32 | 33 | // doesn't work with TS because TS is too complex 34 | delete Prism.languages.typescript["parameter"]; 35 | delete Prism.languages.typescript["literal-property"]; 36 | 37 | // a version of typescript specifically for highlighting types 38 | const typeInside = Prism.languages.extend("typescript", {}); 39 | delete typeInside["class-name"]; 40 | 41 | Prism.languages.typescript["class-name"].inside = typeInside; 42 | 43 | Prism.languages.insertBefore("typescript", "function", { 44 | decorator: { 45 | pattern: /@[$\w\xA0-\uFFFF]+/, 46 | inside: { 47 | at: { 48 | pattern: /^@/, 49 | alias: "operator", 50 | }, 51 | function: /^[\s\S]+/, 52 | }, 53 | }, 54 | "generic-function": { 55 | // e.g. foo( ... 56 | pattern: 57 | /#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/, 58 | greedy: true, 59 | inside: { 60 | function: /^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/, 61 | generic: { 62 | pattern: /<[\s\S]+/, // everything after the first < 63 | alias: "class-name", 64 | inside: typeInside, 65 | }, 66 | }, 67 | }, 68 | }); 69 | 70 | Prism.languages.ts = Prism.languages.typescript; 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/column-list.tsx: -------------------------------------------------------------------------------- 1 | import { ColumnListArgs } from "../types"; 2 | 3 | const ColumnList: React.FC<{ children: React.ReactNode } & ColumnListArgs> = ({ 4 | children, 5 | ...props 6 | }) => { 7 | const columnCount = props.blocks?.length ?? 0; 8 | 9 | return ( 10 |
14 | {children} 15 |
16 | ); 17 | }; 18 | 19 | export default ColumnList; 20 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/column.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Column: React.FC<{ children: React.ReactNode }> = ({ children }) => { 4 | return ( 5 | <> 6 |
{children}
7 |
8 | 9 | ); 10 | }; 11 | 12 | export default Column; 13 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/divider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { DividerArgs } from "../types"; 3 | 4 | type DividerProps = DividerArgs; 5 | 6 | const Divider: React.FC = () => { 7 | return ( 8 |
9 |
10 |
11 | ); 12 | }; 13 | 14 | export default Divider; 15 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/equation.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @link react-katex https://github.com/MatejBransky/react-katex?tab=readme-ov-file 3 | */ 4 | import "katex/dist/katex.min.css"; 5 | import TeX from "@matejmazur/react-katex"; 6 | 7 | import { EquationArgs } from "../types"; 8 | 9 | const Equation = ({ equation: { expression } }: EquationArgs) => { 10 | return ( 11 | 12 | {expression} 13 | 14 | ); 15 | }; 16 | 17 | export default Equation; 18 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/fallback.tsx: -------------------------------------------------------------------------------- 1 | export type FallbackProps = { 2 | type: string; 3 | id: string; 4 | [key: string]: any; 5 | }; 6 | 7 | export default function Fallback({ type, id, ...props }: FallbackProps) { 8 | if (process.env.NODE_ENV === "development") { 9 | return ( 10 |
15 |
16 | {type} Blocks are 17 | not yet supported. 18 |
19 |
20 | ); 21 | } 22 | return null; 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/headings.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useCallback, useMemo, useState } from "react"; 4 | import type { HeadingsArgs, HeadingConfig } from "../types"; 5 | import { getColorCss } from "../utils"; 6 | import RichText from "./internal/rich-text"; 7 | 8 | type HeadingsProps = { 9 | children?: React.ReactNode; 10 | } & HeadingsArgs; 11 | 12 | const Headings: React.FC = ({ children, type, ...props }) => { 13 | const { 14 | [type]: { color, rich_text: texts, is_toggleable }, 15 | } = props; 16 | 17 | const [open, setOpen] = useState(false); 18 | 19 | const { headingTag: HeadingTag, headingClassName } = 20 | useMemo(() => { 21 | switch (type) { 22 | case "heading_2": 23 | return { headingTag: "h2", headingClassName: "notion-h2" }; 24 | case "heading_3": 25 | return { headingTag: "h3", headingClassName: "notion-h3" }; 26 | default: 27 | return { headingTag: "h1", headingClassName: "notion-h1" }; 28 | } 29 | }, [type]); 30 | 31 | // Generate id to make it convenient to write TableOfContents 32 | const id = useMemo( 33 | () => texts.map(({ plain_text }) => plain_text).join(""), 34 | [texts], 35 | ); 36 | 37 | const toggleOpen = useCallback(() => setOpen((prevOpen) => !prevOpen), []); 38 | 39 | return ( 40 |
44 | {is_toggleable ? ( 45 | <> 46 |
47 | 56 | 59 | 60 | 61 |
62 | {children} 63 | 64 | ) : ( 65 | 66 | 67 | 68 | )} 69 |
70 | ); 71 | }; 72 | 73 | export default Headings; 74 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/assets/arrow_back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/assets/arrow_forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/assets/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/assets/index.ts: -------------------------------------------------------------------------------- 1 | import ArrowBack from "./arrow_back.svg"; 2 | import ArrowForward from "./arrow_forward.svg"; 3 | import Close from "./close.svg"; 4 | import Download from "./download.svg"; 5 | import Minus from "./minus.svg"; 6 | import Plus from "./plus.svg"; 7 | 8 | export default { 9 | ArrowBack, 10 | ArrowForward, 11 | Close, 12 | Download, 13 | Minus, 14 | Plus, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/assets/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/hooks/image-viewer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./use-cursor-visibility"; 2 | export * from "./use-image-navigation"; 3 | export * from "./use-image-scale"; 4 | export * from "./use-prevent-scroll"; 5 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/hooks/image-viewer/use-cursor-visibility.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useCallback } from "react"; 2 | 3 | export const useCursorVisibility = () => { 4 | const [isCursorVisible, setIsCursorVisible] = useState(true); 5 | const cursorTimeOutRef = useRef(); 6 | 7 | const handleMoveMouse = useCallback(() => { 8 | setIsCursorVisible(true); 9 | 10 | clearTimeout(cursorTimeOutRef.current); 11 | 12 | if (cursorTimeOutRef.current) { 13 | clearTimeout(cursorTimeOutRef.current); 14 | } 15 | 16 | cursorTimeOutRef.current = setTimeout(() => { 17 | setIsCursorVisible(false); 18 | }, 2000); 19 | }, []); 20 | 21 | return { 22 | isCursorVisible, 23 | handleMoveMouse, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/hooks/image-viewer/use-image-navigation.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | 3 | export const useImageNavigation = ( 4 | currentImageIndex: number, 5 | setCurrentImageIndex: React.Dispatch>, 6 | urlsLength: number, 7 | ) => { 8 | const toNextImage = useCallback(() => { 9 | setCurrentImageIndex((previousIndex) => { 10 | if (previousIndex < urlsLength - 1) { 11 | return previousIndex + 1; 12 | } 13 | return previousIndex; 14 | }); 15 | }, [urlsLength, setCurrentImageIndex]); 16 | 17 | const toPreviousImage = useCallback(() => { 18 | setCurrentImageIndex((previousIndex) => { 19 | if (previousIndex > 0) { 20 | return previousIndex - 1; 21 | } 22 | return previousIndex; 23 | }); 24 | }, [setCurrentImageIndex]); 25 | 26 | const hasNext = useMemo( 27 | () => currentImageIndex < urlsLength - 1, 28 | [currentImageIndex, urlsLength], 29 | ); 30 | const hasPrevious = useMemo(() => currentImageIndex > 0, [currentImageIndex]); 31 | 32 | return { 33 | toNextImage, 34 | toPreviousImage, 35 | hasNext, 36 | hasPrevious, 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/hooks/image-viewer/use-image-scale.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useCallback, useEffect } from "react"; 2 | 3 | import { scaleRound } from "../../lib"; 4 | 5 | export const useImageScale = () => { 6 | const [scale, setScale] = useState(1); 7 | const [displayScale, setDisplayScale] = useState(100); 8 | 9 | const [scaleOriginX, setScaleOriginX] = useState(0.5); 10 | const [scaleOriginY, setScaleOriginY] = useState(0.5); 11 | 12 | const [isScaleFocus, setIsScaleFocus] = useState(false); 13 | 14 | const imageRef = useRef(null); 15 | const scaleInputRef = useRef(null); 16 | 17 | useEffect(() => { 18 | if (isScaleFocus) { 19 | imageRef.current?.focus(); 20 | scaleInputRef.current?.select(); 21 | } 22 | }, [isScaleFocus]); 23 | 24 | const handleScaleFocus = useCallback(() => { 25 | setIsScaleFocus(true); 26 | }, []); 27 | 28 | const handleScaleBlur = useCallback(() => { 29 | const displayScaleValue = scaleRound(displayScale); 30 | setIsScaleFocus(false); 31 | setScale(displayScaleValue / 100); 32 | setDisplayScale(displayScaleValue); 33 | }, [displayScale]); 34 | 35 | const handleScaleEnter = useCallback( 36 | (event: React.KeyboardEvent) => { 37 | if (event.key === "Enter") { 38 | setScaleOriginX(0.5); 39 | setScaleOriginY(0.5); 40 | 41 | let displayScaleValue = displayScale; 42 | 43 | if (displayScale <= 50 || displayScale >= 200) { 44 | displayScaleValue = scaleRound(displayScale); 45 | } 46 | 47 | const newScale = displayScaleValue / 100; 48 | setScale(newScale); 49 | setDisplayScale(displayScaleValue); 50 | setIsScaleFocus(false); 51 | } 52 | }, 53 | [displayScale], 54 | ); 55 | 56 | const handleScaleChange = useCallback( 57 | (event: React.ChangeEvent) => { 58 | const { value } = event.target; 59 | 60 | setDisplayScale(+value); 61 | 62 | if (!isScaleFocus) { 63 | const newScale = Math.max(50, Math.min(200, +displayScale)) / 100; 64 | setScale(newScale); 65 | } 66 | }, 67 | [displayScale, isScaleFocus], 68 | ); 69 | 70 | const handleZoomInOut = useCallback( 71 | (event: React.MouseEvent) => { 72 | if (!imageRef.current) { 73 | return; 74 | } 75 | 76 | if (scale === 0.5) { 77 | setScale(1); 78 | setDisplayScale(100); 79 | setScaleOriginX(0.5); 80 | setScaleOriginY(0.5); 81 | return; 82 | } 83 | 84 | if (scale === 1.5) { 85 | setScale(1); 86 | setDisplayScale(100); 87 | return; 88 | } 89 | 90 | const { width, height, top, left } = 91 | imageRef.current.getBoundingClientRect(); 92 | 93 | const currentMouseX = (event.clientX - left) / width; 94 | const currentMouseY = (event.clientY - top) / height; 95 | 96 | const newScale = scale === 1 ? 1.5 : 1; 97 | setScale(newScale); 98 | setDisplayScale(newScale * 100); 99 | 100 | setScaleOriginX(currentMouseX); 101 | setScaleOriginY(currentMouseY); 102 | 103 | imageRef.current.style.transition = "transform 0.3s ease"; 104 | }, 105 | [scale], 106 | ); 107 | 108 | const handleScaleUp = useCallback(() => { 109 | if (scale === 1) { 110 | setScaleOriginX(0.5); 111 | setScaleOriginY(0.5); 112 | } 113 | 114 | setScale((previousScale) => Math.min(previousScale + 0.5, 2)); 115 | setDisplayScale((previousDisplayScale) => { 116 | return Math.min(+previousDisplayScale + 50, 200); 117 | }); 118 | }, [scale]); 119 | 120 | const handleScaleDown = useCallback(() => { 121 | if (scale === 1) { 122 | setScaleOriginX(0.5); 123 | setScaleOriginY(0.5); 124 | } 125 | 126 | setScale((previousScale) => Math.max(previousScale - 0.5, 0.5)); 127 | setDisplayScale((previousDisplayScale) => { 128 | return Math.max(+previousDisplayScale - 50, 50); 129 | }); 130 | }, [scale]); 131 | 132 | return { 133 | imageRef, 134 | scaleInputRef, 135 | isScaleFocus, 136 | setIsScaleFocus, 137 | handleScaleBlur, 138 | handleScaleFocus, 139 | handleScaleChange, 140 | handleScaleEnter, 141 | scale, 142 | displayScale, 143 | setScale, 144 | setDisplayScale, 145 | scaleOriginX, 146 | scaleOriginY, 147 | handleZoomInOut, 148 | handleScaleUp, 149 | handleScaleDown, 150 | }; 151 | }; 152 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/hooks/image-viewer/use-prevent-scroll.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | import { getGapStyles, getGapWidth } from "../../lib"; 4 | 5 | export const usePreventScroll = (isOpened: boolean) => { 6 | useEffect(() => { 7 | const styleElement = document.createElement("style"); 8 | 9 | if (isOpened) { 10 | document.body.setAttribute("data-scroll-locked", "true"); 11 | const gap = getGapWidth(); 12 | 13 | const scrollLockedStyles = getGapStyles(gap); 14 | styleElement.textContent = scrollLockedStyles; 15 | document.head.appendChild(styleElement); 16 | } 17 | 18 | return () => { 19 | document.body.removeAttribute("data-scroll-locked"); 20 | if (styleElement.parentNode) { 21 | styleElement.parentNode.removeChild(styleElement); 22 | } 23 | }; 24 | }, [isOpened]); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/image-viewer-tools-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { motion, AnimatePresence } from "framer-motion"; 3 | 4 | type ImageViewerToolsToolTipProps = { 5 | content: string; 6 | children: React.ReactNode; 7 | className?: string; 8 | hint: string; 9 | disabled?: boolean; 10 | }; 11 | 12 | const ImageViewerToolsToolTip: React.FC = ({ 13 | children, 14 | className, 15 | content, 16 | hint, 17 | disabled = false, 18 | }) => { 19 | const [isVisible, setIsVisible] = useState(false); 20 | 21 | return ( 22 |
setIsVisible(true)} 24 | onMouseLeave={() => setIsVisible(false)} 25 | className="notion-image-viewer-tooltip-container" 26 | > 27 | {children} 28 | {!disabled && ( 29 | 30 | {isVisible && ( 31 | 41 |

{content}

42 | 43 |

{hint}

44 |
45 | )} 46 |
47 | )} 48 |
49 | ); 50 | }; 51 | 52 | export default ImageViewerToolsToolTip; 53 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/image-viewer-tools.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { motion } from "framer-motion"; 4 | import { handleDownload } from "./lib"; 5 | 6 | import Icon from "./assets"; 7 | import Tooltip from "./image-viewer-tools-tooltip"; 8 | 9 | type ImageViewerToolsProps = { 10 | url: string; 11 | currentImageIndex: number; 12 | imageLength: number; 13 | scaleInputRef: React.MutableRefObject; 14 | setIsOpened: React.Dispatch>; 15 | hasPrevious: boolean; 16 | hasNext: boolean; 17 | toPreviousImage: () => void; 18 | toNextImage: () => void; 19 | scale: number; 20 | displayScale: number; 21 | onScaleUp: () => void; 22 | onScaleDown: () => void; 23 | isScaleFocus: boolean; 24 | setIsScaleFocus: (focused: boolean) => void; 25 | onScaleFocus: () => void; 26 | onScaleBlur: () => void; 27 | onScaleChange: (e: React.ChangeEvent) => void; 28 | onScaleEnter: (e: React.KeyboardEvent) => void; 29 | }; 30 | 31 | const ImageViewerTools: React.FC = ({ 32 | url, 33 | currentImageIndex, 34 | imageLength, 35 | scaleInputRef, 36 | hasPrevious, 37 | hasNext, 38 | setIsOpened, 39 | toPreviousImage, 40 | toNextImage, 41 | displayScale, 42 | onScaleUp, 43 | onScaleDown, 44 | isScaleFocus, 45 | setIsScaleFocus, 46 | onScaleFocus, 47 | onScaleBlur, 48 | onScaleChange, 49 | onScaleEnter, 50 | }) => { 51 | return ( 52 | 58 |
59 | 64 | 72 | 73 | 74 | 79 | 87 | 88 |
89 | 90 |
91 | 92 | 95 | 96 | 97 | {isScaleFocus ? ( 98 |
103 | 113 | % 114 |
115 | ) : ( 116 | 122 | )} 123 | 124 | 127 | 128 |
129 | 130 | 137 | 138 | 139 | 146 | 147 |
148 | ); 149 | }; 150 | 151 | export default ImageViewerTools; 152 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/image/image-viewer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | 4 | import { 5 | useCursorVisibility, 6 | useImageNavigation, 7 | useImageScale, 8 | usePreventScroll, 9 | } from "./hooks/image-viewer"; 10 | 11 | import { getCursorStyle } from "./lib"; 12 | 13 | import ImageViewerTools from "./image-viewer-tools"; 14 | 15 | type ImageViewerProps = { 16 | children: React.ReactNode; 17 | urls: string[]; 18 | url: string; 19 | currentImageIndex: number; 20 | setCurrentImageIndex: React.Dispatch>; 21 | }; 22 | 23 | const ImageViewer: React.FC = ({ 24 | url, 25 | urls, 26 | children, 27 | currentImageIndex, 28 | setCurrentImageIndex, 29 | }) => { 30 | const [isOpened, setIsOpened] = useState(false); 31 | 32 | const { toNextImage, toPreviousImage, hasNext, hasPrevious } = 33 | useImageNavigation(currentImageIndex, setCurrentImageIndex, urls.length); 34 | 35 | const { 36 | imageRef, 37 | scaleInputRef, 38 | isScaleFocus, 39 | setIsScaleFocus, 40 | handleScaleBlur, 41 | handleScaleFocus, 42 | handleScaleEnter, 43 | handleScaleChange, 44 | scale, 45 | displayScale, 46 | setScale, 47 | setDisplayScale, 48 | scaleOriginX, 49 | scaleOriginY, 50 | handleZoomInOut, 51 | handleScaleUp, 52 | handleScaleDown, 53 | } = useImageScale(); 54 | 55 | const { isCursorVisible, handleMoveMouse } = useCursorVisibility(); 56 | 57 | useEffect(() => { 58 | if (currentImageIndex || isOpened) { 59 | setScale(1); 60 | setDisplayScale(100); 61 | } 62 | }, [isOpened, currentImageIndex, setScale, setDisplayScale]); 63 | 64 | useEffect(() => { 65 | if (!isOpened) { 66 | return; 67 | } 68 | 69 | imageRef.current?.focus(); 70 | 71 | const handleKeyDown = (e: KeyboardEvent) => { 72 | const keyDownEvents: { [key: string]: () => void } = { 73 | Escape: () => setIsOpened(false), 74 | "+": handleScaleUp, 75 | "=": handleScaleUp, 76 | "-": handleScaleDown, 77 | ArrowLeft: toPreviousImage, 78 | ArrowRight: toNextImage, 79 | }; 80 | const action = keyDownEvents[e.key]; 81 | 82 | if (action) { 83 | action(); 84 | } 85 | }; 86 | 87 | window.addEventListener("keydown", handleKeyDown); 88 | return () => window.removeEventListener("keydown", handleKeyDown); 89 | }, [ 90 | imageRef, 91 | isOpened, 92 | handleScaleUp, 93 | handleScaleDown, 94 | toNextImage, 95 | toPreviousImage, 96 | ]); 97 | 98 | const handleImageClick = useCallback( 99 | (clickedUrl: string) => { 100 | const index = urls.findIndex((imgUrl) => imgUrl === clickedUrl); 101 | if (index !== -1) { 102 | setCurrentImageIndex(index); 103 | setIsOpened(true); 104 | } 105 | }, 106 | [urls, setCurrentImageIndex, setIsOpened], 107 | ); 108 | 109 | usePreventScroll(isOpened); 110 | 111 | return ( 112 | <> 113 | 120 | 121 | 122 | {isOpened && ( 123 | 133 | 55 |

56 | 57 |

58 |
59 | 60 | {otherChildren} 61 | 62 | ); 63 | }; 64 | 65 | type DefaultToggleIconProps = { 66 | open: boolean; 67 | }; 68 | 69 | const DefaultToggleIcon = ({ open }: DefaultToggleIconProps) => { 70 | return ( 71 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | const ToggleIcon: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( 80 | <>{children} 81 | ); 82 | 83 | Toggle.Icon = ToggleIcon; 84 | 85 | export default Toggle; 86 | -------------------------------------------------------------------------------- /packages/core/src/lib/components/video.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RichText from "./internal/rich-text"; 3 | import { VideoArgs } from "../types"; 4 | import { getYoutubeId } from "../utils"; 5 | 6 | type VideoProps = VideoArgs; 7 | const Video: React.FC = ({ ...props }) => { 8 | const { 9 | video: { type, file, external, caption }, 10 | } = props; 11 | 12 | const renderVideoContent = () => { 13 | if (type === "file" && file != null) { 14 | return