├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── favicon-32x32.png ├── src ├── App.tsx ├── assets │ ├── copy-icon.png │ ├── loader.svg │ └── react.svg ├── components │ ├── Accordion.tsx │ ├── Button.tsx │ ├── FileUpload.tsx │ ├── Hint.tsx │ ├── MultiChoice.tsx │ ├── NumberInput.tsx │ ├── Pagination.tsx │ ├── RadioInput.tsx │ ├── Tabs.tsx │ ├── TextArea.tsx │ └── TextInput.tsx ├── css │ ├── blink.module.css │ ├── fadein.module.css │ └── index.css ├── data │ └── glyphs.json ├── global │ └── types.ts ├── hooks │ ├── useApp.ts │ ├── useFileUpload.ts │ ├── useForm.ts │ ├── useImportExport.ts │ ├── useLocalStorage.ts │ └── useResults.ts ├── layout │ ├── Footer.tsx │ ├── Header.tsx │ └── Layout.tsx ├── main.tsx ├── utils │ ├── createBookGenerator.ts │ ├── createStringWrapper.ts │ └── worker.ts ├── views │ ├── form │ │ ├── Form.tsx │ │ ├── FormInput.tsx │ │ ├── FormSettings.tsx │ │ └── FormSettingsAdvanced.tsx │ ├── tabs │ │ ├── Output.tsx │ │ ├── Results.tsx │ │ └── UnsupportedCharacters.tsx │ └── top │ │ └── ImportExport.tsx └── vite-env.d.ts ├── tailwind.config.js ├── text2book.png ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | root: true, 5 | env: { browser: true, es2020: true }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 10 | 'plugin:react-hooks/recommended', 11 | ], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | sourceType: 'module', 16 | project: true, 17 | tsconfigRootDir: __dirname, 18 | }, 19 | plugins: ['react-refresh'], 20 | rules: { 21 | 'react-refresh/only-export-components': [ 22 | 'warn', 23 | { allowConstantExport: true }, 24 | ], 25 | '@typescript-eslint/no-non-null-assertion': 'off', 26 | semi: 2, 27 | 'no-console': 2, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 90, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "jsxSingleQuote": true, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TheWilley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Text2Book 4 |
5 | Text2Book 6 |
7 |

8 | 9 |

A tool to convert text to Minecraft books

10 | 11 |

12 | GitHub release (with filter) 13 | license MIT 14 | Github Issues 15 |

16 | 17 |

18 | Introduction • 19 | Features • 20 | Usage • 21 | License 22 |

23 | 24 | ## Introduction 25 | 26 | Text2Book is a web application that allows users to convert text into Minecraft books by generating commands for command 27 | blocks or text sections which can be manually copied over. This tool is designed to simplify the process of creating 28 | in-game books with custom content for Minecraft players and mapmakers. Instead of manually entering lengthy commands or 29 | taking a wild guess how much text to copy over on each page, Text2Book automates the process, making it quicker and more 30 | user-friendly. 31 | 32 | ## Features 33 | 34 | - **Text and File Input**: Enter the desired text content directly into the application or upload a text file. 35 | - **Command and Text Generation**: Generate the necessary commands or text section to create the Minecraft books with the desired content. 36 | - **File and Text output**: Save generated content as a file or display directly within the app in a list. 37 | - **Export and Import**: Save your progress and import it later to continue working on your Minecraft book. 38 | - **Minecraft Version Selection**: ~~Choose between Java and Bedrock versions to generate the appropriate commands.~~ (Only Java works for now — Bedrock is under development, see [#21](https://github.com/TheWilley/Text2Book/issues/21)) 39 | - **Incredible Speed**: Generates entire books in seconds by using the very same algorithm implemented in Minecraft (_all Harry Potter books combined in 3 seconds!_). 40 | - **Copy to Clipboard**: Easily copy the generated commands or text lines to your clipboard for quick in-game implementation. 41 | 42 | ## Usage 43 | 44 | Simply go to the [official webpage](https://thewilley.github.io/Text2Book/) to get started, or run the app yourself by following these steps: 45 | 46 | ```bash 47 | # Clone this repository 48 | $ git clone https://github.com/thewilley/Text2Book.git 49 | 50 | # Go into the repository 51 | $ cd Text2Book 52 | 53 | # Install dependencies 54 | $ npm install 55 | 56 | # Build app 57 | $ npm run build 58 | 59 | # If you want to start the app 60 | $ npm run preview 61 | 62 | # If you want to develop the app 63 | $ npm run dev 64 | ``` 65 | 66 | ## License 67 | 68 | Text2Book is licensed under the [MIT License](LICENSE), which allows you to use, modify, and distribute the code for 69 | personal and commercial purposes. However, it comes with no warranties or guarantees. If you use Text2Book, please 70 | provide attribution to the original repository and authors. 71 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Text2Book 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text2book", 3 | "homepage": "https://thewilley.github.io/Text2Book/", 4 | "author": "TheWilley", 5 | "private": true, 6 | "version": "0.2.1", 7 | "type": "module", 8 | "scripts": { 9 | "dev": "vite", 10 | "build": "tsc && vite build", 11 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 12 | "preview": "vite preview", 13 | "test": "node test/test.js", 14 | "predeploy": "npm run build", 15 | "deploy": "gh-pages -d dist", 16 | "prettier": "prettier src --write" 17 | }, 18 | "dependencies": { 19 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 20 | "@fortawesome/free-brands-svg-icons": "^6.7.2", 21 | "@fortawesome/free-regular-svg-icons": "^6.7.2", 22 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 23 | "@fortawesome/react-fontawesome": "^0.2.2", 24 | "classnames": "^2.5.1", 25 | "file-saver": "^2.0.5", 26 | "gh-pages": "^5.0.0", 27 | "github-pages": "^3.0.2", 28 | "glob": "^10.3.10", 29 | "minimist": "^1.2.8", 30 | "nbt": "^0.8.1", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "use-local-storage": "^3.0.0" 34 | }, 35 | "devDependencies": { 36 | "@types/file-saver": "^2.0.7", 37 | "@types/react": "^18.2.14", 38 | "@types/react-dom": "^18.2.6", 39 | "@typescript-eslint/eslint-plugin": "^5.61.0", 40 | "@typescript-eslint/parser": "^5.61.0", 41 | "@vitejs/plugin-react": "^4.0.1", 42 | "autoprefixer": "^10.4.14", 43 | "eslint": "^8.44.0", 44 | "eslint-plugin-react-hooks": "^4.6.0", 45 | "eslint-plugin-react-refresh": "^0.4.1", 46 | "postcss": "^8.4.26", 47 | "prettier": "^3.2.4", 48 | "tailwindcss": "^3.3.3", 49 | "typescript": "^5.0.2", 50 | "vite": "^4.4.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWilley/Text2Book/7dc3b60c4050143edc99ac5e1012f51e6431ae9b/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import Layout from './layout/Layout.tsx'; 2 | import useApp from './hooks/useApp.ts'; 3 | import Form from './views/form/Form.tsx'; 4 | import Output from './views/tabs/Output.tsx'; 5 | import ImportExport from './views/top/ImportExport.tsx'; 6 | 7 | function App() { 8 | const { 9 | results, 10 | loading, 11 | fadeinProps, 12 | timeToGenerate, 13 | outputFormat, 14 | setFadeIn, 15 | showResults, 16 | setOutputFormat, 17 | } = useApp(); 18 | 19 | return ( 20 | 21 | 22 |
28 | 35 | 36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /src/assets/copy-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheWilley/Text2Book/7dc3b60c4050143edc99ac5e1012f51e6431ae9b/src/assets/copy-icon.png -------------------------------------------------------------------------------- /src/assets/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from 'react'; 2 | 3 | function Accordion(props: { 4 | children: React.ReactNode | React.ReactNode[]; 5 | id: string; 6 | label: ReactNode; 7 | }) { 8 | const [open, setOpen] = useState(false); 9 | 10 | return ( 11 |
12 | setOpen(!open)} 19 | readOnly 20 | /> 21 | 27 | {open && props.children} 28 |
29 | ); 30 | } 31 | 32 | export default Accordion; 33 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | function Button(props: { id: string; label: ReactNode; onclick: () => void }) { 4 | return ( 5 |
6 | 14 | 20 |
21 | ); 22 | } 23 | 24 | export default Button; 25 | -------------------------------------------------------------------------------- /src/components/FileUpload.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import useFileUpload from '../hooks/useFileUpload.ts'; 3 | 4 | function FileUpload(props: { 5 | callback: (text: string) => void; 6 | useFileUpload: boolean; 7 | label: ReactNode; 8 | }) { 9 | const { fileName, handleFileChange } = useFileUpload(props.callback); 10 | 11 | return ( 12 | <> 13 | 16 |
17 | 25 |
26 | {fileName || 'Choose a file'} 27 | 33 |
34 |
35 | 36 | ); 37 | } 38 | 39 | export default FileUpload; 40 | -------------------------------------------------------------------------------- /src/components/Hint.tsx: -------------------------------------------------------------------------------- 1 | import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import React from 'react'; 4 | 5 | type Props = { 6 | text: string; 7 | position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; 8 | padding?: number; 9 | children?: React.ReactNode; 10 | }; 11 | 12 | function Hint(props: Props) { 13 | // Set default position to 'top-left' if not provided 14 | const position = props.position || 'top-left'; 15 | const padding = 0 + (props.padding || 0); 16 | 17 | const hintStyle = { 18 | position: 'absolute' as const, 19 | ...(position === 'top-left' && { top: padding, left: padding }), 20 | ...(position === 'top-right' && { top: padding, right: padding }), 21 | ...(position === 'bottom-left' && { bottom: padding, left: padding }), 22 | ...(position === 'bottom-right' && { bottom: padding, right: padding }), 23 | }; 24 | 25 | // Ensure the parent container has relative positioning 26 | return ( 27 |
28 | {props.children} 29 |
30 |
31 |
32 | 33 |
34 | 35 | {props.text} 36 | 37 |
38 |
39 |
40 | ); 41 | } 42 | 43 | export default Hint; 44 | -------------------------------------------------------------------------------- /src/components/MultiChoice.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | function MultiChoice(props: { 4 | name: string; 5 | items: { id: string; label: ReactNode; checked: boolean; callback: () => void }[]; 6 | }) { 7 | return ( 8 |
13 | {props.items.map((item) => ( 14 |
15 | 23 | 29 |
30 | ))} 31 |
32 | ); 33 | } 34 | 35 | export default MultiChoice; 36 | -------------------------------------------------------------------------------- /src/components/NumberInput.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | function NumberInput(props: { 4 | label: ReactNode; 5 | id: string; 6 | placeholder: string; 7 | value: number; 8 | max: number; 9 | min: number; 10 | setter: (value: number) => void; 11 | required?: boolean; 12 | }) { 13 | return ( 14 | <> 15 | 18 | props.setter(parseInt(e.target.value))} 27 | required={props.required} 28 | /> 29 | 30 | ); 31 | } 32 | 33 | export default NumberInput; 34 | -------------------------------------------------------------------------------- /src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | onPrevious: () => void; 3 | onNext: () => void; 4 | previousDisabled: boolean; 5 | nextDisabled: boolean; 6 | visible: boolean; 7 | page: number; 8 | }; 9 | 10 | function Pagination(props: Props) { 11 | return ( 12 |
13 | 20 | {props.page} 21 | 28 |
29 | ); 30 | } 31 | 32 | export default Pagination; 33 | -------------------------------------------------------------------------------- /src/components/RadioInput.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | function RadioInput(props: { 4 | id: string; 5 | label: ReactNode; 6 | name?: string; 7 | checked: boolean; 8 | callback: () => void; 9 | }) { 10 | return ( 11 |
12 | 20 | 26 |
27 | ); 28 | } 29 | 30 | export default RadioInput; 31 | -------------------------------------------------------------------------------- /src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useState } from 'react'; 2 | 3 | interface TabItem { 4 | id: string; 5 | label: ReactNode; 6 | element: ReactNode; 7 | } 8 | 9 | function Tabs(props: { name: string; items: TabItem[]; disabled: boolean }) { 10 | // State to track the currently active tab 11 | const [activeId, setActiveId] = useState(props.items[0]?.id); 12 | 13 | return ( 14 |
21 | {/* Tab Buttons */} 22 |
23 | {props.items.map((item) => ( 24 |
25 | {/* Hidden Radio Button */} 26 | setActiveId(item.id)} 33 | disabled={props.disabled} 34 | /> 35 | {/* Label acting as the tab button */} 36 | 42 |
43 | ))} 44 |
45 | 46 | {/* Active Tab Content */} 47 |
48 | {props.items.map((item) => ( 49 |
55 | {item.element} 56 |
57 | ))} 58 |
59 |
60 | ); 61 | } 62 | 63 | export default Tabs; 64 | -------------------------------------------------------------------------------- /src/components/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | function TextArea(props: { 4 | label: ReactNode; 5 | id: string; 6 | placeholder: string; 7 | value: string; 8 | setter: (value: string) => void; 9 | required?: boolean; 10 | }) { 11 | return ( 12 | <> 13 | 16 |