├── .all-contributorsrc ├── .eslintignore ├── .eslintrc ├── .fiddly.config.json ├── .gitignore ├── .netlify └── state.json ├── .prettierrc ├── .travis.yml ├── Deployment.md ├── LICENSE.md ├── Readme.md ├── __tests__ ├── cli-integration.test.js └── test-readme │ ├── dark │ ├── .fiddly.config.json │ └── readme.md │ ├── duplicate-images │ ├── .fiddly.config.json │ ├── Readme.md │ └── logo.png │ ├── logo │ ├── .fiddly.config.json │ ├── Readme.md │ └── logo.png │ ├── meta-tags │ ├── .fiddly.config.json │ └── Readme.md │ ├── noHeader │ ├── .fiddly.config.json │ └── Readme.md │ ├── package-json │ ├── package.json │ └── readme.md │ ├── path-prefix │ ├── .fiddly.config.json │ ├── logo.png │ ├── one.md │ └── readme.md │ ├── several-files │ ├── .fiddly.config.json │ ├── one.md │ └── readme.md │ └── spectrum │ └── readme.md ├── bin └── fiddly.js ├── logo.ico ├── logo.png ├── media ├── netifly1.png ├── netifly2.png └── surge.png ├── now.json ├── oranda.json ├── package.json ├── src ├── cli.js ├── commands │ ├── css │ │ ├── _dark.scss │ │ ├── _gh-corner.scss │ │ ├── _main.scss │ │ ├── _tacit.scss │ │ ├── night-owl.scss │ │ ├── prism-theme.scss │ │ └── style.scss │ ├── deploy.js │ ├── fiddly.js │ ├── lint.js │ └── watch.js └── utils │ ├── DEFAULT_FILENAMES.js │ ├── fiddlyImports.js │ ├── githubCorner.js │ ├── head.js │ ├── header.js │ └── remoteStyles.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "fiddly", 3 | "projectOwner": "SaraVieira", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "Readme.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "contributors": [ 12 | { 13 | "login": "SaraVieira", 14 | "name": "Sara Vieira", 15 | "avatar_url": "https://avatars0.githubusercontent.com/u/1051509?v=4", 16 | "profile": "http://iamsaravieira.com", 17 | "contributions": [ 18 | "code", 19 | "design", 20 | "ideas" 21 | ] 22 | }, 23 | { 24 | "login": "BrunoScheufler", 25 | "name": "Bruno Scheufler", 26 | "avatar_url": "https://avatars2.githubusercontent.com/u/4772980?v=4", 27 | "profile": "https://brunoscheufler.com", 28 | "contributions": [ 29 | "code" 30 | ] 31 | }, 32 | { 33 | "login": "siddharthkp", 34 | "name": "Siddharth Kshetrapal", 35 | "avatar_url": "https://avatars0.githubusercontent.com/u/1863771?v=4", 36 | "profile": "https://sid.studio", 37 | "contributions": [ 38 | "code" 39 | ] 40 | }, 41 | { 42 | "login": "jamonholmgren", 43 | "name": "Jamon Holmgren", 44 | "avatar_url": "https://avatars3.githubusercontent.com/u/1479215?v=4", 45 | "profile": "https://jamonholmgren.com", 46 | "contributions": [ 47 | "code" 48 | ] 49 | }, 50 | { 51 | "login": "timothyis", 52 | "name": "Timothy", 53 | "avatar_url": "https://avatars0.githubusercontent.com/u/1695613?v=4", 54 | "profile": "http://timothy.is", 55 | "contributions": [ 56 | "code" 57 | ] 58 | }, 59 | { 60 | "login": "cherniavskii", 61 | "name": "Andrew Cherniavskii", 62 | "avatar_url": "https://avatars2.githubusercontent.com/u/13808724?v=4", 63 | "profile": "https://github.com/cherniavskii", 64 | "contributions": [ 65 | "code" 66 | ] 67 | }, 68 | { 69 | "login": "TimKolberger", 70 | "name": "timkolberger", 71 | "avatar_url": "https://avatars2.githubusercontent.com/u/16899513?v=4", 72 | "profile": "https://github.com/TimKolberger", 73 | "contributions": [ 74 | "code" 75 | ] 76 | } 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "globals": { 4 | "test": true, 5 | "expect": true 6 | }, 7 | "rules": { 8 | "space-before-function-paren": 0, 9 | "indent": 0, 10 | "comma-dangle": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.fiddly.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logo": "./logo.png", 3 | "favicon": "./logo.ico", 4 | "darkTheme": true, 5 | "additionalFiles": ["./Deployment.md"] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | coverage 5 | .nyc_output 6 | yarn-error.lock 7 | public 8 | __tests__/testoutput 9 | package.lock 10 | .eslintcache 11 | 12 | .vercel -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "39f332ee-1d24-4e06-a545-77ee14eed816" 3 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | yarn: true 6 | node_js: 7 | - 8 8 | - 9 9 | -------------------------------------------------------------------------------- /Deployment.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 |
4 | 5 | - [Netlify](#netlify) 6 | - [Now](#now) 7 | - [Surge](#surge) 8 | - [GitHub Pages](#githubpages) 9 | - [GitLab Pages](#gitlabpages) 10 | 11 | ## Netlify 12 | 13 | Head over to https://netlify.com and click on new site from git: 14 | 15 | ![New site from git](./media/netifly1.png) 16 | 17 | It will ask you to choose a provider and choose the one you are using. After that it will ask you for what repo it is and after you selected it just select these options on the next screen: 18 | 19 | ![Deploy Static](./media/netifly2.png) 20 | 21 | _The public folder may be a different one depending on the dist folder you selected_ 22 | 23 | ## Now 24 | 25 | To use Now, first create a file called `now.json` and put the following options in it: 26 | 27 | ```json 28 | { 29 | "version": 2, 30 | "builds": [ 31 | { 32 | "src": "package.json", 33 | "use": "@now/static-build", 34 | "config": { "distDir": "public" } 35 | } 36 | ] 37 | } 38 | ``` 39 | 40 | _The distDir folder may be a different one depending on the dist folder you selected_ 41 | 42 | For Now to build fiddly when deploying, to ensure a fresh build for each deployment, add the following script to your `package.json` scripts list to let Now know to use fiddly to build your readme as the index. 43 | 44 | ```json 45 | { 46 | "now-build": "fiddly" 47 | } 48 | ``` 49 | 50 | For more info you can check the [Now docs](https://zeit.co/docs/) 51 | 52 | ## Surge 53 | 54 | To deploy with Surge you need a http://surge.sh account and then to install the surge cli or like me run with npx. 55 | It will ask you to login and after that it wants to know the project path. In here you put the dist folder. 56 | After that pick and domain and done 🎉 57 | 58 | ![Surge](./media/surge.png) 59 | 60 | ## GitHub Pages 61 | 62 | For GitHub pages there is a command: 63 | 64 | ```bash 65 | fiddly deploy 66 | ``` 67 | 68 | This command will build your project if no version exists and deploy it to `gh-pages` branch on github. 69 | It uses [gh-pages](https://github.com/tschaub/gh-pages#options) under the hood so any options you pass there you can also pass in your `deployment` option in `.fiddly.config.json`. 70 | ✨ 71 | 72 | ## Gitlab Pages 73 | 74 | To deploy to Gitlab Pages add a new job with the title _pages_ in your `.gitlab-ci.yml` configuration file. 75 | Gitlab Pages hosts your static files at `https://.gitlab.io/`. 76 | You can use the `PATH_PREFIX` environment variable to configure your fiddly build. 77 | 🎉 78 | 79 | ```yaml 80 | pages: 81 | image: node 82 | script: 83 | - PATH_PREFIX="/$CI_PROJECT_NAME" npx fiddly 84 | artifacts: 85 | paths: 86 | - public 87 | only: 88 | - master 89 | ``` 90 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 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 | # Fiddly 2 | 3 | Create beautiful and simple HTML pages from your Readme.md files 4 | 5 | - 🛠 No config 6 | - 👩‍💻 Code Highlighting 7 | - 💯 Emoji Support 8 | - ✨ Creates Static files (only JS is prism) 9 | - 🏳️‍🌈 Pretty Pages 10 | - 🦄 Customizable 11 | - 🖼 Image minification 12 | - 🧠 Custom Meta Tags 13 | - 🇳🇱 [CodeSandbox](https://codesandbox.io) and iframe Support 14 | 15 | ```bash 16 | yarn add fiddly --dev 17 | ``` 18 | 19 | ```bash 20 | npm install fiddly --save-dev 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```json 26 | { 27 | ... 28 | "scripts": { 29 | "build:demo": "fiddly", 30 | .... 31 | } 32 | ``` 33 | 34 | Deploy automatically to netlify 🎉 35 | 36 | [This Readme on Netlify](https://fiddly.netlify.com/) 37 | 38 | [This Readme with white theme](https://5c2678b67b891f18dc5a2a42--fiddly.netlify.com/) 39 | 40 | ## Usage with npx 41 | 42 | 43 | 44 | If you just want a quick fancy HTML page from the Readme but don't care about running this in continuous deployment you can also use `npx` to run it as a one time thing. 45 | 46 | 47 | 48 | ```bash 49 | npx fiddly 50 | ``` 51 | 52 | By running this in the root folder you will also get a public folder 53 | 54 | ## Options 55 | 56 | Options are placed in a `.fiddly.config.json` or as a `fiddly` key in `package.json`. 57 | It can contain the following options: 58 | 59 | 60 | 61 | | Option | Default | Description | 62 | | --------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | 63 | | file | Readme.md, readme.md, or README.md | Your Readme.md name | 64 | | name | name in package.json | The project name that is in the title and the header | 65 | | logo | '' | The project logo that is in the header | 66 | | shareCard | '' | URL to social media preview image for meta tags (recommended size: 1200x628, URL cannot be relative) | 67 | | description | description in package.json | The project description for meta tags | 68 | | homepage | null | The project homepage for meta tags | 69 | | noHeader | false | Show no header and just the markdown content | 70 | | darkTheme | false | Dark theme ofc 🎉 | 71 | | favicon | '' | Favicon url or local path | 72 | | dist | public | To what folder to render your HTML | 73 | | styles | {} | Styles to apply to the page. Object or path to css/scss file | 74 | | additionalFiles | [] | Any other pages to create. It expects an array of paths of markdown files | 75 | | repo | null | Link to point the github corner | 76 | | pathPrefix | Environment var PATH_PREFIX or '/' | Host your fiddly files at e.g. /my-fiddly-project | 77 | | meta | [] | Any extra meta tags you would like | 78 | | remoteStyles | [] | Array of any remote styles you want to include (eg: Google Fonts) | 79 | | remoteScripts | [] | Array of any remote scripts you want to include (eg: Google Analytics) | 80 | | deployment | {} | Deployment options for github pages. Accepts all options [here](https://github.com/tschaub/gh-pages#options) | 81 | 82 | 83 | 84 | ### Example of styles 85 | 86 | For styles you can either use a style object like so and that will override the 87 | default styles applied. Like so: 88 | 89 | ```json 90 | { 91 | "styles": { 92 | "h1": { 93 | "color": "blue", 94 | "backgroundColor": "red" 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | Another option is to give the path to a local css or scss file. 101 | In this case you need to override any specificity issues. 102 | You can by using the `#fiddly` id. 103 | Example: 104 | 105 | ```css 106 | body { 107 | background: #fff; 108 | } 109 | 110 | #fiddly { 111 | h1 { 112 | text-transform: uppercase; 113 | } 114 | } 115 | ``` 116 | 117 | ## Meta Tags 118 | 119 | To create any meta tags it uses an array system like so: 120 | 121 | ```json 122 | "meta": [ 123 | { "name": "description", "content": "A cool page" }, 124 | { "property": "robots", "content": "robots.txt" } 125 | ] 126 | ``` 127 | 128 | This will create the following HTML: 129 | 130 | ```html 131 | 132 | 133 | ``` 134 | 135 | The first key on the object can have any name and will be applied as presented, the second one must have the name of content and will work as presented above. 136 | 137 | ## Images 138 | 139 | Any images linked in your markdown that are local will be minified and copied to your dist folder. 140 | If some image is not found it will be ignored. 141 | 142 | ## GitHub Corner 143 | 144 | The GitHub corner comes from either the `repo` option in your `.fiddly.config.json` 145 | or from the repository url in your `package.json`. 146 | If none is present it will not be shown. 147 | 148 | ## Lint 149 | 150 | Fiddly also exports a command to let you lint all the markdown files you specified. 151 | 152 | You can run this by using the `lint` command 153 | 154 | ```json 155 | "lint:md" : "fiddly lint" 156 | ``` 157 | 158 | ## Deploy 159 | 160 | Fiddly also exports a command to let you deploy your new site to GitHub pages 161 | 162 | You can run this by using the `deploy` command 163 | 164 | ```json 165 | "deploy" : "fiddly deploy" 166 | ``` 167 | 168 | Options for this can be passed in a `deployment` key in your config file. 169 | All options can be found here: [https://github.com/tschaub/gh-pages#options](https://github.com/tschaub/gh-pages#options) 170 | 171 | ## Acknowledgements 172 | 173 | - Base styles from [medium.css](https://github.com/lucagez/medium.css) 174 | - Logo from [OpenMoji](http://www.openmoji.org/library.html?search=beautiful&emoji=2728) 175 | 176 | ## Contributors 177 | 178 | 179 | 180 | 181 | | [
Sara Vieira](http://iamsaravieira.com)
[💻](https://github.com/SaraVieira/fiddly/commits?author=SaraVieira "Code") [🎨](#design-SaraVieira "Design") [🤔](#ideas-SaraVieira "Ideas, Planning, & Feedback") | [
Bruno Scheufler](https://brunoscheufler.com)
[💻](https://github.com/SaraVieira/fiddly/commits?author=BrunoScheufler "Code") | [
Siddharth Kshetrapal](https://sid.studio)
[💻](https://github.com/SaraVieira/fiddly/commits?author=siddharthkp "Code") | [
Jamon Holmgren](https://jamonholmgren.com)
[💻](https://github.com/SaraVieira/fiddly/commits?author=jamonholmgren "Code") | [
Timothy](http://timothy.is)
[💻](https://github.com/SaraVieira/fiddly/commits?author=timothyis "Code") | [
Andrew Cherniavskii](https://github.com/cherniavskii)
[💻](https://github.com/SaraVieira/fiddly/commits?author=cherniavskii "Code") | [
timkolberger](https://github.com/TimKolberger)
[💻](https://github.com/SaraVieira/fiddly/commits?author=TimKolberger "Code") | 182 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | 183 | 184 | 185 | 186 | 187 | 188 | 189 | ## License 190 | 191 | MIT - see [LICENSE](https://github.com/SaraVieira/fiddly/blob/master/LICENSE.md) 192 | -------------------------------------------------------------------------------- /__tests__/cli-integration.test.js: -------------------------------------------------------------------------------- 1 | const { system, filesystem } = require('gluegun') 2 | 3 | const src = filesystem.path(__dirname, '..') 4 | const success = 'Generated your static files at public/' 5 | 6 | const cli = async (cmd) => 7 | system.run('node ' + filesystem.path(src, 'bin', 'fiddly') + ` ${cmd}`) 8 | 9 | test('generates html', async () => { 10 | const output = await cli() 11 | 12 | expect(output).toContain(success) 13 | expect(filesystem.exists('public/index.html')).toBeTruthy() 14 | 15 | filesystem.remove('public') 16 | }) 17 | 18 | test('generates css', async () => { 19 | const output = await cli() 20 | expect(output).toContain(success) 21 | 22 | expect(filesystem.exists('public/style.css')).toBeTruthy() 23 | 24 | filesystem.remove('public') 25 | }) 26 | 27 | test('generates dark', async () => { 28 | const prevDir = process.cwd() 29 | 30 | process.chdir('./__tests__/test-readme/dark/') 31 | 32 | const output = await cli() 33 | 34 | expect(output).toContain(success) 35 | const html = filesystem.read('public/index.html') 36 | 37 | expect(html).toContain('
') 38 | 39 | filesystem.remove('public') 40 | process.chdir(prevDir) 41 | }) 42 | 43 | test('reads config from package.json', async () => { 44 | const prevDir = process.cwd() 45 | 46 | process.chdir('./__tests__/test-readme/package-json') 47 | 48 | const output = await cli() 49 | 50 | expect(output).toContain(success.replace('public', 'testoutput')) 51 | const css = filesystem.read('testoutput/style.css') 52 | 53 | expect(css).toContain('font-size:18em') 54 | 55 | filesystem.remove('testoutput') 56 | process.chdir(prevDir) 57 | }) 58 | 59 | test('generates several files', async () => { 60 | const prevDir = process.cwd() 61 | 62 | process.chdir('./__tests__/test-readme/several-files') 63 | 64 | const output = await cli() 65 | 66 | expect(output).toContain(success) 67 | 68 | expect(filesystem.exists('public/index.html')).toBeTruthy() 69 | expect(filesystem.exists('public/one.html')).toBeTruthy() 70 | 71 | filesystem.remove('public') 72 | process.chdir(prevDir) 73 | }) 74 | 75 | test('spectrum test', async () => { 76 | const prevDir = process.cwd() 77 | 78 | process.chdir('./__tests__/test-readme/spectrum') 79 | 80 | const output = await cli() 81 | 82 | expect(output).toContain(success) 83 | const html = filesystem.read('public/index.html') 84 | 85 | expect(html).toContain('Simple, powerful online communities') 86 | 87 | expect(filesystem.exists('public/index.html')).toBeTruthy() 88 | 89 | filesystem.remove('public') 90 | process.chdir(prevDir) 91 | }) 92 | 93 | test('noHeader test', async () => { 94 | const prevDir = process.cwd() 95 | 96 | process.chdir('./__tests__/test-readme/noHeader') 97 | 98 | const output = await cli() 99 | 100 | expect(output).toContain(success) 101 | 102 | const html = filesystem.read('public/index.html') 103 | 104 | expect(html).not.toContain(' { 113 | const prevDir = process.cwd() 114 | 115 | process.chdir('./__tests__/test-readme/logo') 116 | 117 | const output = await cli() 118 | 119 | expect(output).toContain(success) 120 | 121 | expect(filesystem.exists('public/logo.png')).toBeTruthy() 122 | 123 | filesystem.remove('public') 124 | process.chdir(prevDir) 125 | }) 126 | 127 | test('Image test', async () => { 128 | const prevDir = process.cwd() 129 | 130 | process.chdir('./__tests__/test-readme/duplicate-images') 131 | 132 | const output = await cli() 133 | 134 | expect(output).toContain(success) 135 | 136 | expect(filesystem.exists('public/logo.png')).toBeTruthy() 137 | 138 | filesystem.remove('public') 139 | process.chdir(prevDir) 140 | }) 141 | 142 | test('Prefixes logo and additional file paths', async () => { 143 | const prevDir = process.cwd() 144 | 145 | process.chdir('./__tests__/test-readme/path-prefix') 146 | 147 | const output = await cli() 148 | 149 | expect(output).toContain(success) 150 | expect(filesystem.exists('public/index.html')).toBeTruthy() 151 | const html = filesystem.read('public/index.html') 152 | 153 | expect(html).toContain('href="/fiddly-rocks/one.html"') 154 | expect(html).toContain('src="/fiddly-rocks/logo.png"') 155 | 156 | filesystem.remove('public') 157 | process.chdir(prevDir) 158 | }) 159 | 160 | test('Adds meta tags', async () => { 161 | const prevDir = process.cwd() 162 | 163 | process.chdir('./__tests__/test-readme/meta-tags') 164 | 165 | const output = await cli() 166 | 167 | expect(output).toContain(success) 168 | expect(filesystem.exists('public/index.html')).toBeTruthy() 169 | const html = filesystem.read('public/index.html') 170 | 171 | expect(html).toContain( 172 | // prettier-ignore 173 | // eslint-disable-next-line no-useless-escape 174 | '' 175 | ) 176 | 177 | // prettier-ignore 178 | // eslint-disable-next-line no-useless-escape 179 | expect(html).toContain('') 180 | 181 | filesystem.remove('public') 182 | process.chdir(prevDir) 183 | }) 184 | -------------------------------------------------------------------------------- /__tests__/test-readme/dark/.fiddly.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "darkTheme": true 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/dark/readme.md: -------------------------------------------------------------------------------- 1 | # Just a test 2 | 3 | Testing a thing. 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/duplicate-images/.fiddly.config.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /__tests__/test-readme/duplicate-images/Readme.md: -------------------------------------------------------------------------------- 1 | # sup 2 | 3 | ![test](./logo.png) 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/duplicate-images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/__tests__/test-readme/duplicate-images/logo.png -------------------------------------------------------------------------------- /__tests__/test-readme/logo/.fiddly.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logo": "./logo.png" 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/logo/Readme.md: -------------------------------------------------------------------------------- 1 | ## Test 2 | -------------------------------------------------------------------------------- /__tests__/test-readme/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/__tests__/test-readme/logo/logo.png -------------------------------------------------------------------------------- /__tests__/test-readme/meta-tags/.fiddly.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": [ 3 | { "name": "description", "content": "Helmet application" }, 4 | { "property": "robots", "content": "robots.txt" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/test-readme/meta-tags/Readme.md: -------------------------------------------------------------------------------- 1 | ## Test 2 | -------------------------------------------------------------------------------- /__tests__/test-readme/noHeader/.fiddly.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "noHeader": true 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/noHeader/Readme.md: -------------------------------------------------------------------------------- 1 | ## Test 2 | -------------------------------------------------------------------------------- /__tests__/test-readme/package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testpkg", 3 | "fiddly": { 4 | "dist": "testoutput", 5 | "styles": { 6 | "h1": { 7 | "font-size": "18em" 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /__tests__/test-readme/package-json/readme.md: -------------------------------------------------------------------------------- 1 | # Just a test 2 | 3 | Testing a thing. 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/path-prefix/.fiddly.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalFiles": ["./one.md"], 3 | "pathPrefix": "/fiddly-rocks", 4 | "logo": "logo.png" 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/test-readme/path-prefix/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/__tests__/test-readme/path-prefix/logo.png -------------------------------------------------------------------------------- /__tests__/test-readme/path-prefix/one.md: -------------------------------------------------------------------------------- 1 | # Test 2 | -------------------------------------------------------------------------------- /__tests__/test-readme/path-prefix/readme.md: -------------------------------------------------------------------------------- 1 | # Just a test 2 | 3 | Testing a thing. 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/several-files/.fiddly.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalFiles": ["./one.md"] 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/several-files/one.md: -------------------------------------------------------------------------------- 1 | # Test 2 | -------------------------------------------------------------------------------- /__tests__/test-readme/several-files/readme.md: -------------------------------------------------------------------------------- 1 | # Just a test 2 | 3 | Testing a thing. 4 | -------------------------------------------------------------------------------- /__tests__/test-readme/spectrum/readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Spectrum](./public/img/media.png)](https://spectrum.chat) 4 | 5 | ### Simple, powerful online communities. 6 | 7 |
8 | 9 | This is the main monorepo codebase of [Spectrum](https://spectrum.chat). Every single line of code that's not packaged into a reusable library is in this repository. 10 | 11 | ## What is Spectrum? 12 | 13 | ### Vision 14 | 15 | It is difficult to grow, manage and measure the impact of online communities. Community owners need modern, chat-based communities but are running into scaling issues when their community grows beyond a few hundred members. It becomes hard to keep track of who's who, know what conversations are happening, and ensure that the community is staying healthy and productive. 16 | 17 | Spectrum aims to be the best platform to build any kind of community online by combining the best of web 2.0 forums and real-time chat apps. With best-in-class moderation tooling, a single platform for all your communities, threaded conversations by default, community health monitoring (and much more to come), we think that we will be able to help more people start and grow the best online communities. 18 | 19 | > "[Spectrum] will take the place that Reddit used to have a long time ago for communities (especially tech) to freely share ideas and interact. Except realtime and trolling-free." 20 | > 21 | > \- [Guillermo Rauch (@rauchg)](https://twitter.com/rauchg/status/930946768841228288) 22 | 23 | ### Status 24 | 25 | Spectrum has been under full-time development since March, 2017. See [the roadmap](https://github.com/withspectrum/spectrum/projects/19) for up-to-date information about our current areas of focus. 26 | 27 |
28 | 29 |
30 | 31 | ## Docs 32 | 33 | - [Contributing](#contributing) 34 | - [Ground Rules](#ground-rules) 35 | - [Codebase](#codebase) 36 | - [Technologies](#technologies) 37 | - [Folder Structure](#folder-structure) 38 | - [Code Style](#code-style) 39 | - [First time setup](#first-time-setup) 40 | - [Running the app locally](#running-the-app-locally) 41 | - [Roadmap](https://github.com/withspectrum/spectrum/projects/19) 42 | - [Technical](docs/) 43 | - [Testing](docs/testing/intro.md) 44 | - [Background Jobs](docs/workers/background-jobs.md) 45 | - [Deployment](docs/deployments.md) 46 | - [API](docs/backend/api/) 47 | - [Fragments](docs/backend/api/fragments.md) 48 | - [Pagination](docs/backend/api/pagination.md) 49 | - [Testing](docs/backend/api/testing.md) 50 | - [Tips and Tricks](docs/backend/api/tips-and-tricks.md) 51 | 52 | ## Contributing 53 | 54 | **We heartily welcome any and all contributions that match [our product roadmap](https://github.com/withspectrum/spectrum/projects/19) and engineering standards!** 55 | 56 | That being said, this codebase isn't your typical open source project because it's not a library or package with a limited scope—it's our entire product. 57 | 58 | ### Ground Rules 59 | 60 | #### Contributions and discussion guidelines 61 | 62 | All conversations and communities on Spectrum agree to our underlying [code of conduct](https://github.com/withspectrum/code-of-conduct). This code of conduct also applies to all conversations that happen within our contributor community here on GitHub. We expect discussions in issues and pull requests to stay positive, productive, and respectful. Remember: there are real people on the other side of that screen! 63 | 64 | #### Reporting a bug or discussing a feature idea 65 | 66 | If you found a technical bug on Spectrum or have ideas for features we should implement, the issue tracker is the best place to share your ideas. Make sure to follow the issue template and you should be golden! ([click here to open a new issue](https://github.com/withspectrum/spectrum/issues/new)) 67 | 68 | #### Fixing a bug or implementing a new feature 69 | 70 | If you find a bug on Spectrum and open a PR that fixes it we'll review it as soon as possible to ensure it matches our engineering standards. If you want to implement a new feature, open an issue first to discuss what it'd look like and to ensure it fits in [our roadmap](https://github.com/withspectrum/spectrum/projects/19) and plans for the app. 71 | 72 | If you want to contribute but are unsure to start, we have [a "good first issue" label](https://github.com/withspectrum/spectrum/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which is applied to newcomer-friendly issues. Take a look at [the full list of good first issues](https://github.com/withspectrum/spectrum/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick something you like! There is also [an "open" channel in the Spectrum community on Spectrum](https://spectrum.chat/spectrum/open) (how meta), if you run into troubles while trying to contribute that is the best place to talk to us. 73 | 74 | Want to fix a bug or implement an agreed-upon feature? Great, jump to the [local setup instructions](#first-time-setup)! 75 | 76 |
77 | 78 |
79 | 80 | ### Codebase 81 | 82 | #### Technologies 83 | 84 | With the ground rules out of the way, let's talk about the coarse architecture of this mono repo: 85 | 86 | - **Full-stack JavaScript**: We use Node.js to power our servers, and React to power our frontend apps. Almost all of the code you'll touch in this codebase will be JavaScript. 87 | - **Background Jobs**: We leverage background jobs (powered by [`bull`](https://github.com/OptimalBits/bull) and Redis) a lot. These jobs are handled by a handful of small worker servers, each with its own purpose. 88 | 89 | Here is a list of all the big technologies we use: 90 | 91 | - **RethinkDB**: Data storage 92 | - **Redis**: Background jobs and caching 93 | - **GraphQL**: API, powered by the entire Apollo toolchain 94 | - **Flowtype**: Type-safe JavaScript 95 | - **PassportJS**: Authentication 96 | - **React**: Frontend React app 97 | - **DraftJS**: WYSIWYG writing experience on the web 98 | 99 | #### Folder structure 100 | 101 | ```sh 102 | spectrum/ 103 | ├── api # API server 104 | ├── athena # Worker server (notifications and general processing) 105 | ├── chronos # Worker server (cron jobs) 106 | ├── desktop # desktop apps (build with electron) 107 | ├── docs 108 | ├── email-templates 109 | ├── hermes # Worker server (email sending) 110 | ├── hyperion # Server rendering server 111 | ├── mercury # Worker server (reputation) 112 | ├── public # Public files used on the frontend 113 | ├── shared # Shared JavaScript code 114 | ├── src # Frontend SPA 115 | └── vulcan # Worker server (search indexing; syncing with Algolia) 116 | ``` 117 | 118 |
119 | Click to learn about the worker naming scheme 120 | 121 | #### Naming Scheme 122 | 123 | As you can see we follow a loose naming scheme based on ancient Greek, Roman, and philosophical figures that are somewhat related to what our servers do: 124 | 125 | - Hyperion: (/haɪˈpɪəriən/) is one of the twelve Titan children of Gaia and Uranus. 126 | - Athena (/əˈθiːnə/) is the goddess of wisdom, craft, and war. 127 | - Hermes (/ˈhɜːrmiːz/) is the messenger god, moving between the worlds of the mortal and the divine. 128 | - Chronos (/ˈkroʊnɒs/) is the personification of Time in pre-Socratic philosophy 129 | - Mercury (/ˈmɜːrkjʊri/) is the patron god of financial gain, commerce, eloquence (and thus poetry), messages/communication (including divination), travelers, boundaries, luck, trickery and thieves 130 | 131 |
132 | 133 | #### Code Style 134 | 135 | We run Prettier on-commit, which means you can write code in whatever style you want and it will be automatically formatted according to the common style when you run `git commit`. We also have ESLint setup, although we've disabled all stylistic rules since Prettier takes care of those. 136 | 137 | ##### Rules 138 | 139 | - **All new `.js` files must be flow typed**: Since we only introduced Flowtype after we finished building the first version of Spectrum, we enforce in CI that all new files added to the codebase are typed. (if you've never used Flowtype before that's totally fine, just write your code in plain JS and let us know in the PR body, we can take care of it for you) 140 | - **No `console.log`s in any file**: We use the `debug` module across the codebase to log debugging information in development only. Never commit a file that contains a `console.log` as CI will fail your build. The only exceptions are errors, which you can log, but you have to use `console.error` to be explicit about it 141 | 142 |
143 | 144 |
145 | 146 | ### First time setup 147 | 148 | The first step to running Spectrum locally is downloading the code by cloning the repository: 149 | 150 | ```sh 151 | git clone git@github.com:withspectrum/spectrum.git 152 | ``` 153 | 154 | If you get `Permission denied` error using `ssh` refer [here](https://help.github.com/articles/error-permission-denied-publickey/) 155 | or use `https` link as a fallback. 156 | 157 | ```sh 158 | git clone https://github.com/withspectrum/spectrum.git 159 | ``` 160 | 161 | #### Installation 162 | 163 | Spectrum has four big installation steps: 164 | 165 | 1. **Install RethinkDB**: See [the RethinkDB documentation](https://rethinkdb.com/docs/install/) for instructions on installing it with your OS. 166 | 2. **Install Redis**: See [the Redis documentation](https://redis.io/download) for instructions on installing it with your OS. 167 | 3. **Install yarn**: We use [yarn](https://yarnpkg.com) to handle our JavaScript dependencies. (plain `npm` doesn't work due to our monorepo setup) See [the yarn documentation](https://yarnpkg.com/en/docs/install) for instructions on installing it. 168 | 169 | Once you have RethinkDB, Redis and yarn installed locally its time to install the JavaScript dependencies. Because it's pretty tedious to install the dependencies for each worker individually we've created a script that goes through and runs `yarn install` for every worker for you: (this takes a couple minutes, so dive into the [technical docs](./docs) in the meantime) 170 | 171 | ```sh 172 | node shared/install-dependencies.js 173 | ``` 174 | 175 | You've now finished installing everything! Let's migrate the database and you'll be ready to go :100: 176 | 177 | #### Migrating the database 178 | 179 | When you first download the code and want to run it locally you have to migrate the database and seed it with test data. First, start rethinkdb in its own terminal tab: 180 | 181 | ```sh 182 | rethinkdb 183 | ``` 184 | 185 | Then, in a new tab, run these commands: 186 | 187 | ```sh 188 | yarn run db:migrate 189 | yarn run db:seed 190 | # ⚠️ To empty the database (e.g. if there's faulty data) run yarn run db:drop 191 | ``` 192 | 193 | There's a shortcut for dropping, migrating and seeding the database too: 194 | 195 | ```sh 196 | yarn run db:reset 197 | ``` 198 | 199 | The `testing` database used in end to end tests is managed separately. It is built, migrated, and seeded when you run: 200 | 201 | ```sh 202 | yarn run start:api:test 203 | ``` 204 | 205 | To drop the `testing` database, go to http://localhost:8080/#tables while `rethinkdb` is running, and click Delete Database on the appropriate database. 206 | 207 | #### Getting the secrets 208 | 209 | While the app will run without any secrets set up, you won't be able to sign in locally. To get that set up, copy the provided example secrets file to the real location: 210 | 211 | ``` 212 | cp now-secrets.example.json now-secrets.json 213 | ``` 214 | 215 | > Note: If you're an employee at Spectrum we've got a more complete list of secrets that also lets you upload images etc. in 1Password, search for "now-secrets.json" to find it. 216 | 217 | Now you're ready to run the app locally and sign into your local instance! 218 | 219 | ### Running the app locally 220 | 221 | #### Background services 222 | 223 | Whenever you want to run Spectrum locally you have to have RethinkDB and Redis running in the background. First start rethinkdb like we did to migrate the database: 224 | 225 | ```sh 226 | rethinkdb 227 | ``` 228 | 229 | Then (without closing the rethinkdb tab!) open another tab and start Redis: 230 | 231 | ```sh 232 | redis-server 233 | ``` 234 | 235 | #### Start the servers 236 | 237 | Depending on what you're trying to work on you'll need to start different servers. Generally, all servers run in development mode by doing `yarn run dev:`, e.g. `yarn run dev:hermes` to start the email worker. 238 | 239 | No matter what you're trying to do though, you'll want to have the API running, so start that in a background tab: 240 | 241 | ``` 242 | yarn run dev:api 243 | ``` 244 | 245 | #### Develop the web UI 246 | 247 | To develop the frontend and web UI run 248 | 249 | ``` 250 | yarn run dev:web 251 | ``` 252 | 253 | #### Develop the desktop app 254 | 255 | To develop the desktop app you have to have the dev web server running in the background (`yarn run dev:web`) and then, in another terminal tab, run: 256 | 257 | ``` 258 | yarn run dev:desktop 259 | ``` 260 | 261 | > Note: If something didn't work or you ran into troubles please submit PRs to improve this doc and keep it up to date! 262 | 263 |
264 |
265 | 266 |
267 | 268 | ## License 269 | 270 | BSD 3-Clause, see the [LICENSE](./LICENSE) file. 271 | -------------------------------------------------------------------------------- /bin/fiddly.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // run the CLI with the current process arguments 4 | require('../src/cli').run(process.argv) 5 | -------------------------------------------------------------------------------- /logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/logo.ico -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/logo.png -------------------------------------------------------------------------------- /media/netifly1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/media/netifly1.png -------------------------------------------------------------------------------- /media/netifly2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/media/netifly2.png -------------------------------------------------------------------------------- /media/surge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraVieira/fiddly/293a096c6f781bb28965425e64ed67a6ddd09ecf/media/surge.png -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "alias": "fiddly", 4 | "public": true, 5 | "builds": [ 6 | { "src": "package.json", "use": "@now/static-build", "config": { "distDir": "public" } } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /oranda.json: -------------------------------------------------------------------------------- 1 | {"name":"fiddly","repository":"https://github.com/SaraVieira/fiddly","description":"Create beautiful and simple HTML pages from your Readme.md files","homepage":"https://fiddly.netlify.com","changelog":false,"theme":"dark"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fiddly", 3 | "description": "Create beautiful and simple HTML pages from your Readme.md files", 4 | "version": "0.10.0", 5 | "bin": { 6 | "fiddly": "bin/fiddly.js" 7 | }, 8 | "scripts": { 9 | "format": "prettier --write \"src/**/*.js\"", 10 | "test": "jest", 11 | "posttest": "npm run format", 12 | "watch": "jest --watch", 13 | "coverage": "jest --coverage", 14 | "build": "./bin/fiddly.js", 15 | "prepublish": "npm run test", 16 | "contributors:add": "all-contributors add", 17 | "contributors:generate": "all-contributors generate", 18 | "now-build": "npm run build" 19 | }, 20 | "files": [ 21 | "LICENSE", 22 | "Readme.md", 23 | "bin", 24 | "src" 25 | ], 26 | "keywords": [ 27 | "generation", 28 | "markdown", 29 | "static", 30 | "sites", 31 | "fiddly" 32 | ], 33 | "author": { 34 | "name": "Sara Vieira", 35 | "email": "hey@iamsaravieira.com", 36 | "url": "https://iamsaravieira.com/" 37 | }, 38 | "engines": { 39 | "node": ">=8.0.0" 40 | }, 41 | "license": "MIT", 42 | "dependencies": { 43 | "axios": "^0.21.1", 44 | "clean-css": "^5.1.5", 45 | "create-html": "^4.1.0", 46 | "gh-pages": "^3.2.3", 47 | "gluegun": "4.7.0", 48 | "imagemin": "^8.0.1", 49 | "imagemin-jpegtran": "^7.0.0", 50 | "imagemin-pngquant": "^9.0.2", 51 | "markdown-it": "^12.2.0", 52 | "markdown-it-anchor": "8.3.0", 53 | "markdown-it-checkbox": "^1.1.0", 54 | "markdown-it-emoji": "^2.0.0", 55 | "markdown-it-for-inline": "^0.1.1", 56 | "markdown-it-github-headings": "^2.0.0", 57 | "markdown-it-inline-comments": "^1.0.1", 58 | "markdown-it-prism": "^2.2.0", 59 | "markdownlint": "^0.24.0", 60 | "node-sass": "^6.0.1", 61 | "normalize.css": "^8.0.1", 62 | "snarkdown": "^2.0.0", 63 | "to-css": "^1.2.1" 64 | }, 65 | "devDependencies": { 66 | "all-contributors-cli": "^6.20.0", 67 | "eslint": "^7.32.0", 68 | "eslint-config-standard": "^16.0.3", 69 | "eslint-plugin-import": "^2.24.2", 70 | "eslint-plugin-node": "^11.1.0", 71 | "eslint-plugin-promise": "^5.1.0", 72 | "jest": "^27.1.1", 73 | "prettier": "^2.4.0" 74 | }, 75 | "jest": { 76 | "testEnvironment": "node" 77 | }, 78 | "homepage": "https://github.com/SaraVieira/fiddly", 79 | "bugs": { 80 | "url": "https://github.com/SaraVieira/fiddly/issues", 81 | "email": "hey@iamsaravieira.com" 82 | }, 83 | "repository": { 84 | "type": "git", 85 | "url": "https://github.com/SaraVieira/fiddly" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | const { build } = require('gluegun') 2 | 3 | /** 4 | * Create the cli and kick it off 5 | */ 6 | async function run(argv) { 7 | // create a CLI runtime 8 | const cli = build() 9 | .brand('fiddly') 10 | .src(__dirname) 11 | .help() // provides default for help, h, --help, -h 12 | .version() // provides default for version, v, --version, -v 13 | .create() 14 | 15 | // and run it 16 | const toolbox = await cli.run(argv) 17 | 18 | // send it back (for testing, mostly) 19 | return toolbox 20 | } 21 | 22 | module.exports = { run } 23 | -------------------------------------------------------------------------------- /src/commands/css/_dark.scss: -------------------------------------------------------------------------------- 1 | /* Dark Mode */ 2 | 3 | .body.dark { 4 | color: #f5f5f5; 5 | background: #1a1919; 6 | 7 | pre code { 8 | background: transparent; 9 | color: rgba(0, 0, 0, 0.84); 10 | } 11 | 12 | code { 13 | color: rgba(255, 255, 255, 0.85); 14 | background: rgba(0, 0, 0, 0.84); 15 | } 16 | 17 | .anchor svg path { 18 | fill: white; 19 | } 20 | 21 | pre:not([class]) { 22 | background: #f5f2f0; 23 | padding: 1em; 24 | margin: 0.5em 0; 25 | overflow: auto; 26 | } 27 | 28 | h1, 29 | h2, 30 | h3, 31 | h5, 32 | h4, 33 | h6, 34 | p, 35 | a { 36 | color: #f5f5f5; 37 | background: #1a1919; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/css/_gh-corner.scss: -------------------------------------------------------------------------------- 1 | /* GITHUB CORNER */ 2 | 3 | .github-corner:hover .octo-arm { 4 | animation: octocat-wave 560ms ease-in-out 5 | } 6 | 7 | @keyframes octocat-wave { 8 | 0%, 100% { 9 | transform: rotate(0) 10 | } 11 | 20%, 60% { 12 | transform: rotate(-25deg) 13 | } 14 | 40%, 80% { 15 | transform: rotate(10deg) 16 | } 17 | } 18 | 19 | @media (max-width:500px) { 20 | .github-corner:hover .octo-arm { 21 | animation: none 22 | } 23 | .github-corner .octo-arm { 24 | animation: octocat-wave 560ms ease-in-out 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/css/_main.scss: -------------------------------------------------------------------------------- 1 | /* GENERAL */ 2 | 3 | html, 4 | body, 5 | .body, 6 | #fiddly { 7 | width: 100%; 8 | min-height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | body { 14 | margin-top: -20px; 15 | font-family: 'Lato', sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | 19 | .body { 20 | padding-top: 40px; 21 | padding-bottom: 60px; 22 | } 23 | 24 | .container { 25 | width: 1100px; 26 | max-width: 80%; 27 | margin: auto; 28 | } 29 | } 30 | 31 | iframe { 32 | margin: 20px 0; 33 | } 34 | 35 | /* CODE */ 36 | 37 | code { 38 | font-size: 18px; 39 | background: rgba(0, 0, 0, 0.05); 40 | border-radius: 2px; 41 | padding: 3px 5px; 42 | color: rgba(0, 0, 0, 0.84); 43 | line-height: 1.5; 44 | display: initial; 45 | } 46 | 47 | /* TEXT */ 48 | 49 | p, 50 | i, 51 | a, 52 | li { 53 | margin-top: 21px; 54 | font-family: 'Lato', sans-serif; 55 | font-size: 21px; 56 | letter-spacing: -0.03px; 57 | line-height: 1.58; 58 | } 59 | 60 | li { 61 | margin-top: 10px; 62 | } 63 | 64 | a { 65 | text-decoration: underline; 66 | } 67 | 68 | blockquote { 69 | font-family: 'Playfair Display', serif; 70 | font-size: 30px; 71 | font-style: italic; 72 | letter-spacing: -0.36px; 73 | line-height: 44.4px; 74 | overflow-wrap: break-word; 75 | margin: 55px 0 33px 0; 76 | color: rgba(0, 0, 0, 0.68); 77 | padding: 0 0 0 50px; 78 | } 79 | 80 | mark, 81 | .highlighted { 82 | background: #7dffb3; 83 | } 84 | 85 | ::selection { 86 | background-color: lavender; 87 | } 88 | 89 | /* HEADER */ 90 | 91 | header { 92 | display: flex; 93 | align-items: center; 94 | justify-content: center; 95 | flex-direction: column; 96 | padding: 0; 97 | margin-bottom: 20px; 98 | 99 | .logo { 100 | max-width: 300px; 101 | margin: 20px 0; 102 | } 103 | 104 | h1 { 105 | text-transform: capitalize; 106 | } 107 | } 108 | 109 | /* HEADINGS */ 110 | 111 | h1 { 112 | font-size: 48px; 113 | text-align: left; 114 | margin-bottom: 8px; 115 | } 116 | 117 | h2 { 118 | font-size: 26px; 119 | font-weight: 700; 120 | padding: 0; 121 | text-align: left; 122 | line-height: 34.5px; 123 | letter-spacing: -0.45px; 124 | } 125 | 126 | h1, 127 | h2, 128 | p, 129 | i, 130 | a { 131 | color: rgba(0, 0, 0, 0.84); 132 | text-rendering: optimizeLegibility; 133 | } 134 | 135 | h1, 136 | h2, 137 | h3 { 138 | display: flex; 139 | align-items: center; 140 | 141 | > a { 142 | margin-top: 0; 143 | } 144 | } 145 | 146 | h1, 147 | h2, 148 | h3, 149 | h4, 150 | h5, 151 | h6 { 152 | font-weight: bold; 153 | font-family: 'Playfair Display', serif; 154 | } 155 | 156 | .anchor { 157 | padding-right: 10px; 158 | margin: 0; 159 | } 160 | -------------------------------------------------------------------------------- /src/commands/css/_tacit.scss: -------------------------------------------------------------------------------- 1 | input, 2 | textarea, 3 | select, 4 | button, 5 | option, 6 | html, 7 | body { 8 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | font-size: 18px; 10 | font-stretch: normal; 11 | font-style: normal; 12 | font-weight: 400; 13 | line-height: 29.7px; 14 | } 15 | 16 | input, 17 | textarea, 18 | select, 19 | button, 20 | option, 21 | html, 22 | body { 23 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | font-size: 18px; 25 | font-stretch: normal; 26 | font-style: normal; 27 | font-weight: 400; 28 | line-height: 29.7px; 29 | } 30 | 31 | th { 32 | font-weight: 600; 33 | } 34 | 35 | td, 36 | th { 37 | border-bottom: 1.08px solid #595959; 38 | overflow: auto; 39 | -webkit-overflow-scrolling: touch; 40 | padding: 14px 10px; 41 | text-align: left; 42 | vertical-align: top; 43 | } 44 | 45 | thead th { 46 | border-bottom-width: 2.16px; 47 | padding-bottom: 6.3px; 48 | } 49 | 50 | table { 51 | display: block; 52 | overflow-x: auto; 53 | -webkit-overflow-scrolling: touch; 54 | } 55 | 56 | input, 57 | textarea, 58 | select, 59 | button, 60 | option { 61 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 62 | font-size: 18px; 63 | font-stretch: normal; 64 | font-style: normal; 65 | font-weight: 400; 66 | line-height: 29.7px; 67 | } 68 | 69 | fieldset { 70 | display: flex; 71 | flex-direction: row; 72 | flex-wrap: wrap; 73 | } 74 | 75 | fieldset legend { 76 | margin: 18px 0; 77 | } 78 | 79 | input, 80 | textarea, 81 | select, 82 | button { 83 | border-radius: 3.6px; 84 | display: inline-block; 85 | padding: 9.9px; 86 | } 87 | 88 | input + label, 89 | input + input[type='checkbox'], 90 | input + input[type='radio'], 91 | textarea + label, 92 | textarea + input[type='checkbox'], 93 | textarea + input[type='radio'], 94 | select + label, 95 | select + input[type='checkbox'], 96 | select + input[type='radio'], 97 | button + label, 98 | button + input[type='checkbox'], 99 | button + input[type='radio'] { 100 | page-break-before: always; 101 | } 102 | 103 | input, 104 | select, 105 | label { 106 | margin-right: 3.6px; 107 | } 108 | 109 | textarea { 110 | min-height: 90px; 111 | min-width: 360px; 112 | } 113 | 114 | label { 115 | display: inline-block; 116 | margin-bottom: 12.6px; 117 | } 118 | 119 | label + * { 120 | page-break-before: always; 121 | } 122 | 123 | label > input { 124 | margin-bottom: 0; 125 | } 126 | 127 | input[type='submit'], 128 | input[type='reset'], 129 | button { 130 | background: #f2f2f2; 131 | color: #191919; 132 | cursor: pointer; 133 | display: inline; 134 | margin-bottom: 18px; 135 | margin-right: 7.2px; 136 | padding: 6.525px 23.4px; 137 | text-align: center; 138 | } 139 | 140 | input[type='submit']:hover, 141 | input[type='reset']:hover, 142 | button:hover { 143 | background: #d9d9d9; 144 | color: #000; 145 | } 146 | 147 | input[type='submit'][disabled], 148 | input[type='reset'][disabled], 149 | button[disabled] { 150 | background: #e6e5e5; 151 | color: #403f3f; 152 | cursor: not-allowed; 153 | } 154 | 155 | input[type='submit'], 156 | button[type='submit'] { 157 | background: #275a90; 158 | color: #fff; 159 | } 160 | 161 | input[type='submit']:hover, 162 | button[type='submit']:hover { 163 | background: #173454; 164 | color: #bfbfbf; 165 | } 166 | 167 | input, 168 | select, 169 | textarea { 170 | margin-bottom: 18px; 171 | } 172 | 173 | input[type='text'], 174 | input[type='password'], 175 | input[type='email'], 176 | input[type='url'], 177 | input[type='phone'], 178 | input[type='tel'], 179 | input[type='number'], 180 | input[type='datetime'], 181 | input[type='date'], 182 | input[type='month'], 183 | input[type='week'], 184 | input[type='color'], 185 | input[type='time'], 186 | input[type='search'], 187 | input[type='range'], 188 | input[type='file'], 189 | input[type='datetime-local'], 190 | select, 191 | textarea { 192 | border: 1px solid #595959; 193 | padding: 5.4px 6.3px; 194 | } 195 | 196 | input[type='checkbox'], 197 | input[type='radio'] { 198 | flex-grow: 0; 199 | height: 29.7px; 200 | margin-left: 0; 201 | margin-right: 9px; 202 | vertical-align: middle; 203 | } 204 | 205 | input[type='checkbox'] + label, 206 | input[type='radio'] + label { 207 | page-break-before: avoid; 208 | } 209 | 210 | select[multiple] { 211 | min-width: 270px; 212 | } 213 | 214 | input, 215 | textarea, 216 | select, 217 | button, 218 | option, 219 | html, 220 | body { 221 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 222 | font-size: 18px; 223 | font-stretch: normal; 224 | font-style: normal; 225 | font-weight: 400; 226 | line-height: 29.7px; 227 | } 228 | 229 | pre, 230 | code, 231 | kbd, 232 | samp, 233 | var, 234 | output { 235 | font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; 236 | font-size: 14.4px; 237 | } 238 | 239 | pre { 240 | line-height: 25.2px; 241 | overflow: auto; 242 | -webkit-overflow-scrolling: touch; 243 | padding-left: 18px; 244 | } 245 | 246 | pre code { 247 | background: none; 248 | border: 0; 249 | line-height: 29.7px; 250 | padding: 0; 251 | } 252 | 253 | code, 254 | kbd { 255 | background: #daf1e0; 256 | border-radius: 3.6px; 257 | color: #2a6f3b; 258 | display: inline-block; 259 | line-height: 18px; 260 | padding: 3.6px 6.3px 2.7px; 261 | } 262 | 263 | kbd { 264 | background: #2a6f3b; 265 | color: #fff; 266 | } 267 | 268 | mark { 269 | background: #ffc; 270 | padding: 0 3.6px; 271 | } 272 | 273 | input, 274 | textarea, 275 | select, 276 | button, 277 | option, 278 | html, 279 | body { 280 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 281 | font-size: 18px; 282 | font-stretch: normal; 283 | font-style: normal; 284 | font-weight: 400; 285 | line-height: 29.7px; 286 | } 287 | 288 | h1, 289 | h2, 290 | h3, 291 | h4, 292 | h5, 293 | h6 { 294 | color: #000; 295 | margin-bottom: 18px; 296 | } 297 | 298 | h1 { 299 | font-size: 36px; 300 | font-weight: 500; 301 | line-height: 41.4px; 302 | margin-top: 72px; 303 | } 304 | 305 | h2 { 306 | font-size: 25.2px; 307 | font-weight: 400; 308 | line-height: 30.6px; 309 | margin-top: 54px; 310 | } 311 | 312 | h3 { 313 | font-size: 21.6px; 314 | line-height: 27px; 315 | margin-top: 36px; 316 | } 317 | 318 | h4 { 319 | font-size: 18px; 320 | line-height: 23.4px; 321 | margin-top: 18px; 322 | } 323 | 324 | h5 { 325 | font-size: 14.4px; 326 | font-weight: bold; 327 | line-height: 21.6px; 328 | text-transform: uppercase; 329 | } 330 | 331 | h6 { 332 | color: #595959; 333 | font-size: 14.4px; 334 | font-weight: bold; 335 | line-height: 18px; 336 | text-transform: uppercase; 337 | } 338 | 339 | input, 340 | textarea, 341 | select, 342 | button, 343 | option, 344 | html, 345 | body { 346 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 347 | font-size: 18px; 348 | font-stretch: normal; 349 | font-style: normal; 350 | font-weight: 400; 351 | line-height: 29.7px; 352 | } 353 | 354 | a { 355 | color: #275a90; 356 | text-decoration: none; 357 | } 358 | 359 | a:hover { 360 | text-decoration: underline; 361 | } 362 | 363 | hr { 364 | border-bottom: 1px solid #595959; 365 | } 366 | 367 | figcaption, 368 | small { 369 | font-size: 15.3px; 370 | } 371 | 372 | figcaption { 373 | color: #595959; 374 | } 375 | 376 | var, 377 | em, 378 | i { 379 | font-style: italic; 380 | } 381 | 382 | dt, 383 | strong, 384 | b { 385 | font-weight: 600; 386 | } 387 | 388 | del, 389 | s { 390 | text-decoration: line-through; 391 | } 392 | 393 | ins, 394 | u { 395 | text-decoration: underline; 396 | } 397 | 398 | sub, 399 | sup { 400 | font-size: 75%; 401 | line-height: 0; 402 | position: relative; 403 | vertical-align: baseline; 404 | } 405 | 406 | sup { 407 | top: -0.5em; 408 | } 409 | 410 | sub { 411 | bottom: -0.25em; 412 | } 413 | 414 | * { 415 | border: 0; 416 | border-collapse: separate; 417 | border-spacing: 0; 418 | box-sizing: border-box; 419 | margin: 0; 420 | max-width: 100%; 421 | padding: 0; 422 | vertical-align: baseline; 423 | } 424 | 425 | html, 426 | body { 427 | width: 100%; 428 | } 429 | 430 | html { 431 | height: 100%; 432 | } 433 | 434 | body { 435 | background: #f5f5f5; 436 | color: #1a1919; 437 | padding: 36px; 438 | } 439 | 440 | p, 441 | ul, 442 | ol, 443 | dl, 444 | blockquote, 445 | hr, 446 | pre, 447 | table, 448 | form, 449 | fieldset, 450 | figure, 451 | address { 452 | margin-bottom: 29.7px; 453 | } 454 | 455 | section { 456 | margin-left: auto; 457 | margin-right: auto; 458 | width: 900px; 459 | } 460 | 461 | article, 462 | header, 463 | footer { 464 | padding: 43.2px; 465 | } 466 | 467 | article { 468 | background: #fff; 469 | border: 1px solid #d9d9d9; 470 | border-radius: 7.2px; 471 | } 472 | 473 | nav { 474 | text-align: center; 475 | } 476 | 477 | nav ul { 478 | list-style: none; 479 | margin-left: 0; 480 | text-align: center; 481 | } 482 | 483 | nav ul li { 484 | display: inline-block; 485 | margin-left: 9px; 486 | margin-right: 9px; 487 | vertical-align: middle; 488 | } 489 | 490 | nav ul li:first-child { 491 | margin-left: 0; 492 | } 493 | 494 | nav ul li:last-child { 495 | margin-right: 0; 496 | } 497 | 498 | ol, 499 | ul { 500 | margin-left: 31.5px; 501 | } 502 | 503 | li dl, 504 | li ol, 505 | li ul { 506 | margin-bottom: 0; 507 | } 508 | 509 | dl { 510 | display: inline-block; 511 | } 512 | 513 | dt { 514 | padding: 0 18px; 515 | } 516 | 517 | dd { 518 | padding: 0 18px 4.5px; 519 | } 520 | 521 | dd:last-of-type { 522 | border-bottom: 1.08px solid #595959; 523 | } 524 | 525 | dd + dt { 526 | border-top: 1.08px solid #595959; 527 | padding-top: 9px; 528 | } 529 | 530 | blockquote { 531 | border-left: 2.16px solid #595959; 532 | padding: 4.5px 18px 4.5px 15.84px; 533 | } 534 | 535 | blockquote footer { 536 | color: #595959; 537 | font-size: 13.5px; 538 | margin: 0; 539 | } 540 | 541 | blockquote p { 542 | margin-bottom: 0; 543 | } 544 | 545 | img { 546 | height: auto; 547 | margin: 0 auto; 548 | } 549 | 550 | figure img { 551 | display: block; 552 | } 553 | 554 | @media (max-width: 767px) { 555 | body { 556 | padding: 18px 0; 557 | } 558 | article { 559 | border: 0; 560 | padding: 18px; 561 | } 562 | header, 563 | footer { 564 | padding: 18px; 565 | } 566 | textarea, 567 | input, 568 | select { 569 | min-width: 0; 570 | } 571 | fieldset { 572 | min-width: 0; 573 | } 574 | fieldset * { 575 | flex-grow: 1; 576 | page-break-before: auto; 577 | } 578 | section { 579 | width: auto; 580 | } 581 | x:-moz-any-link { 582 | display: table-cell; 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /src/commands/css/night-owl.scss: -------------------------------------------------------------------------------- 1 | code[class*='language-'], 2 | pre[class*='language-'] { 3 | color: #d6deeb; 4 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 5 | text-align: left; 6 | white-space: pre; 7 | word-spacing: normal; 8 | word-break: normal; 9 | word-wrap: normal; 10 | line-height: 1.5; 11 | 12 | -moz-tab-size: 4; 13 | -o-tab-size: 4; 14 | tab-size: 4; 15 | 16 | -webkit-hyphens: none; 17 | -moz-hyphens: none; 18 | -ms-hyphens: none; 19 | hyphens: none; 20 | } 21 | 22 | pre[class*='language-']::-moz-selection, 23 | pre[class*='language-'] ::-moz-selection, 24 | code[class*='language-']::-moz-selection, 25 | code[class*='language-'] ::-moz-selection { 26 | text-shadow: none; 27 | background: #011627; 28 | } 29 | 30 | pre[class*='language-']::selection, 31 | pre[class*='language-'] ::selection, 32 | code[class*='language-']::selection, 33 | code[class*='language-'] ::selection { 34 | text-shadow: none; 35 | background: #011627; 36 | } 37 | 38 | @media print { 39 | code[class*='language-'], 40 | pre[class*='language-'] { 41 | text-shadow: none; 42 | } 43 | } 44 | 45 | /* Code blocks */ 46 | pre { 47 | padding: 1em; 48 | margin: 0.5em 0; 49 | overflow: auto; 50 | } 51 | 52 | :not(pre) > code, 53 | pre { 54 | background: #011627; 55 | } 56 | 57 | :not(pre) > code { 58 | padding: 0.1em; 59 | border-radius: 0.3em; 60 | white-space: normal; 61 | } 62 | 63 | .token.comment, 64 | .token.prolog, 65 | .token.cdata { 66 | color: rgb(99, 119, 119); 67 | font-style: italic; 68 | } 69 | 70 | .token.punctuation { 71 | color: rgb(199, 146, 234); 72 | } 73 | 74 | .namespace { 75 | color: rgb(178, 204, 214); 76 | } 77 | 78 | .token.deleted { 79 | color: rgba(239, 83, 80, 0.56); 80 | font-style: italic; 81 | } 82 | 83 | .token.symbol, 84 | .token.property { 85 | color: rgb(128, 203, 196); 86 | } 87 | 88 | .token.tag, 89 | .token.operator, 90 | .token.keyword { 91 | color: rgb(127, 219, 202); 92 | } 93 | 94 | .token.boolean { 95 | color: rgb(255, 88, 116); 96 | } 97 | 98 | .token.number { 99 | color: rgb(247, 140, 108); 100 | } 101 | 102 | .token.constant, 103 | .token.function, 104 | .token.builtin, 105 | .token.char { 106 | color: rgb(130, 170, 255); 107 | } 108 | 109 | .token.selector, 110 | .token.doctype { 111 | color: rgb(199, 146, 234); 112 | font-style: italic; 113 | } 114 | 115 | .token.attr-name, 116 | .token.inserted { 117 | color: rgb(173, 219, 103); 118 | font-style: italic; 119 | } 120 | 121 | .token.string, 122 | .token.url, 123 | .token.entity, 124 | .language-css .token.string, 125 | .style .token.string { 126 | color: rgb(173, 219, 103); 127 | } 128 | 129 | .token.class-name, 130 | .token.atrule, 131 | .token.attr-value { 132 | color: rgb(255, 203, 139); 133 | } 134 | 135 | .token.regex, 136 | .token.important, 137 | .token.variable { 138 | color: rgb(214, 222, 235); 139 | } 140 | 141 | .token.important, 142 | .token.bold { 143 | font-weight: bold; 144 | } 145 | 146 | .token.italic { 147 | font-style: italic; 148 | } 149 | -------------------------------------------------------------------------------- /src/commands/css/prism-theme.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * prism.js default theme for JavaScript, CSS and HTML 3 | * Based on dabblet (http://dabblet.com) 4 | * @author Lea Verou 5 | */ 6 | 7 | code[class*='language-'], 8 | pre[class*='language-'] { 9 | color: black; 10 | background: none; 11 | text-shadow: 0 1px white; 12 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | word-break: normal; 17 | word-wrap: normal; 18 | line-height: 1.5; 19 | 20 | -moz-tab-size: 4; 21 | -o-tab-size: 4; 22 | tab-size: 4; 23 | 24 | -webkit-hyphens: none; 25 | -moz-hyphens: none; 26 | -ms-hyphens: none; 27 | hyphens: none; 28 | } 29 | 30 | pre[class*='language-']::-moz-selection, 31 | pre[class*='language-'] ::-moz-selection, 32 | code[class*='language-']::-moz-selection, 33 | code[class*='language-'] ::-moz-selection { 34 | text-shadow: none; 35 | background: #b3d4fc; 36 | } 37 | 38 | pre[class*='language-']::selection, 39 | pre[class*='language-'] ::selection, 40 | code[class*='language-']::selection, 41 | code[class*='language-'] ::selection { 42 | text-shadow: none; 43 | background: #b3d4fc; 44 | } 45 | 46 | @media print { 47 | code[class*='language-'], 48 | pre[class*='language-'] { 49 | text-shadow: none; 50 | } 51 | } 52 | 53 | /* Code blocks */ 54 | pre[class*='language-'] { 55 | padding: 1em; 56 | margin: 0.5em 0; 57 | overflow: auto; 58 | } 59 | 60 | :not(pre) > code[class*='language-'], 61 | pre[class*='language-'] { 62 | background: #f5f2f0; 63 | } 64 | 65 | /* Inline code */ 66 | :not(pre) > code[class*='language-'] { 67 | padding: 0.1em; 68 | border-radius: 0.3em; 69 | white-space: normal; 70 | } 71 | 72 | .token.comment, 73 | .token.prolog, 74 | .token.doctype, 75 | .token.cdata { 76 | color: slategray; 77 | } 78 | 79 | .token.punctuation { 80 | color: #999; 81 | } 82 | 83 | .namespace { 84 | opacity: 0.7; 85 | } 86 | 87 | .token.property, 88 | .token.tag, 89 | .token.boolean, 90 | .token.number, 91 | .token.constant, 92 | .token.symbol, 93 | .token.deleted { 94 | color: #905; 95 | } 96 | 97 | .token.selector, 98 | .token.attr-name, 99 | .token.string, 100 | .token.char, 101 | .token.builtin, 102 | .token.inserted { 103 | color: #690; 104 | } 105 | 106 | .token.operator, 107 | .token.entity, 108 | .token.url, 109 | .language-css .token.string, 110 | .style .token.string { 111 | color: #9a6e3a; 112 | background: hsla(0, 0%, 100%, 0.5); 113 | } 114 | 115 | .token.atrule, 116 | .token.attr-value, 117 | .token.keyword { 118 | color: #07a; 119 | } 120 | 121 | .token.function, 122 | .token.class-name { 123 | color: #dd4a68; 124 | } 125 | 126 | .token.regex, 127 | .token.important, 128 | .token.variable { 129 | color: #e90; 130 | } 131 | 132 | .token.important, 133 | .token.bold { 134 | font-weight: bold; 135 | } 136 | .token.italic { 137 | font-style: italic; 138 | } 139 | 140 | .token.entity { 141 | cursor: help; 142 | } 143 | -------------------------------------------------------------------------------- /src/commands/css/style.scss: -------------------------------------------------------------------------------- 1 | @import 'tacit'; 2 | @import 'main'; 3 | @import 'gh-corner'; 4 | @import 'dark'; 5 | 6 | .body.light { 7 | @import 'prism-theme'; 8 | } 9 | 10 | .body.dark { 11 | @import './night-owl'; 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/deploy.js: -------------------------------------------------------------------------------- 1 | const ghpages = require('gh-pages') 2 | const { run } = require('./fiddly') 3 | 4 | module.exports = { 5 | name: 'deploy', 6 | alias: 'd', 7 | description: 'Deploy your static site to Github pages', 8 | run: async (toolbox) => { 9 | const { 10 | print: { spin, warning }, 11 | filesystem, 12 | } = toolbox 13 | 14 | const packageJSON = 15 | filesystem.read(`${process.cwd()}/package.json`, 'json') || {} 16 | 17 | const options = { 18 | ...{ dist: 'public' }, 19 | ...(packageJSON.fiddly || {}), 20 | ...(filesystem.read(`${process.cwd()}/.fiddly.config.json`, 'json') || 21 | {}), 22 | } 23 | 24 | const dist = options.dist 25 | const distFolder = `${process.cwd()}/${dist}` 26 | const deploymentOptions = options.deployment || {} 27 | 28 | if (!filesystem.exists(distFolder)) { 29 | warning(`Seems there is no ${dist} folder. Building it...`) 30 | await run(toolbox) 31 | } 32 | 33 | const spinner = spin('Deploying your site') 34 | ghpages.publish(distFolder, deploymentOptions, (err) => { 35 | if (err) { 36 | return spinner.fail('There was an error publishing your site 😢') 37 | } 38 | return spinner.succeed('Website Published 🎉') 39 | }) 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/fiddly.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-path-concat */ 2 | const toCss = require('to-css') 3 | const CleanCSS = require('clean-css') 4 | const imageminJpegtran = require('imagemin-jpegtran') 5 | const imageminPngquant = require('imagemin-pngquant') 6 | const sass = require('node-sass') 7 | const createHTML = require('create-html') 8 | const corner = require('../utils/githubCorner') 9 | const fiddlyImports = require('../utils/fiddlyImports') 10 | const head = require('../utils/head') 11 | const header = require('../utils/header') 12 | const DEFAULT_FILENAMES = require('../utils/DEFAULT_FILENAMES') 13 | const getRemoteStyles = require('../utils/remoteStyles') 14 | const snarkdown = require('snarkdown') 15 | const iterator = require('markdown-it-for-inline') 16 | const md = require('markdown-it')({ 17 | html: true, 18 | xhtmlOut: true, 19 | linkify: true, 20 | }) 21 | const prism = require('markdown-it-prism') 22 | 23 | md.use(prism, {}) 24 | md.use(require('markdown-it-inline-comments')) 25 | md.use(require('markdown-it-checkbox')) 26 | md.use(require('markdown-it-github-headings')) 27 | md.use(require('markdown-it-anchor'), { 28 | level: 1, 29 | }) 30 | md.use(require('markdown-it-emoji')) 31 | md.linkify.tlds('.md', false) 32 | md.linkify.tlds('.MD', false) 33 | md.use(iterator, 'url_new_win', 'link_open', function (tokens, idx) { 34 | const aIndex = tokens[idx].attrIndex('target') 35 | 36 | if (aIndex < 0) { 37 | tokens[idx].attrPush(['target', '_blank']) 38 | } else { 39 | tokens[idx].attrs[aIndex][1] = '_blank' 40 | } 41 | }) 42 | const defaultRender = 43 | md.renderer.rules.html_block || 44 | function (tokens, idx, options, env, self) { 45 | return self.renderToken(tokens, idx, options) 46 | } 47 | 48 | md.renderer.rules.html_block = function (tokens, idx, options, env, self) { 49 | const htmlBlock = tokens[idx] 50 | tokens[idx].content = snarkdown(htmlBlock.content) 51 | 52 | return defaultRender(tokens, idx, options, env, self) 53 | } 54 | 55 | const defaultOptions = { 56 | dist: 'public', 57 | darkTheme: false, 58 | noHeader: false, 59 | file: null, 60 | name: null, 61 | description: null, 62 | styles: {}, 63 | logo: '', 64 | shareCard: '', 65 | favicon: '', 66 | additionalFiles: [], 67 | homepage: null, 68 | repo: null, 69 | pathPrefix: `${process.env.PATH_PREFIX || ''}`, 70 | meta: [], 71 | remoteStyles: [], 72 | remoteScripts: [], 73 | } 74 | 75 | module.exports = { 76 | name: 'fiddly', 77 | description: 'Build your static website from markdown', 78 | run: async (toolbox) => { 79 | const { 80 | print: { info, success, error }, 81 | filesystem, 82 | } = toolbox 83 | 84 | const packageJSON = 85 | filesystem.read(`${process.cwd()}/package.json`, 'json') || {} 86 | 87 | const options = { 88 | ...defaultOptions, 89 | ...(packageJSON.fiddly || {}), 90 | ...(filesystem.read(`${process.cwd()}/.fiddly.config.json`, 'json') || 91 | {}), 92 | } 93 | 94 | const dist = options.dist 95 | const distFolder = `${process.cwd()}/${dist}` 96 | 97 | // CSS 98 | 99 | // Get CSS defined by the user 100 | const getAdditionalStyles = () => { 101 | // if string read the file path 102 | if (typeof options.styles === 'string') { 103 | return filesystem.read(`${process.cwd()}/${options.styles}`) 104 | } 105 | 106 | // If not read object and turn into css 107 | return toCss(options.styles, { 108 | selector: (s) => `#fiddly ${s}`, 109 | property: (p) => 110 | p.replace(/([A-Z])/g, (matches) => `-${matches[0].toLowerCase()}`), 111 | }) 112 | } 113 | 114 | // Get normalize and google fonts 115 | const remoteStyles = await getRemoteStyles() 116 | 117 | // Transform sass to css 118 | const css = sass 119 | .renderSync({ 120 | data: remoteStyles 121 | .concat(filesystem.read(`${__dirname}/css/style.scss`)) 122 | .concat(getAdditionalStyles()), 123 | includePaths: [`${__dirname}/css`], 124 | }) 125 | .css.toString() 126 | 127 | // minify css 128 | const minifiedCSS = new CleanCSS().minify(css).styles 129 | 130 | // write the css 131 | await filesystem.write(`${distFolder}/style.css`, minifiedCSS) 132 | 133 | // HTML 134 | 135 | // Add trailing slash if missing 136 | options.pathPrefix = options.pathPrefix.replace(/\/?$/, '/') 137 | 138 | // Check for `options.file`, if null, check if a default file exists, or error 139 | if (options.file === null) { 140 | options.additionalFiles.unshift( 141 | DEFAULT_FILENAMES.find((filename) => { 142 | return filesystem.exists(filename) ? filename : null 143 | }) 144 | ) 145 | // Throw error if no default file could be found 146 | if ( 147 | options.additionalFiles[options.additionalFiles.length - 1] === null 148 | ) { 149 | return error( 150 | 'No default file ("readme.md", "Readme.md", or "README.md") can be found. Please use the "file" option if using a differing filename.' 151 | ) 152 | } 153 | } else { 154 | // Set file to the given `options.file` value 155 | options.additionalFiles.unshift(options.file) 156 | } 157 | 158 | // Map through all files 159 | options.additionalFiles.map(async (file) => { 160 | // Get markdown contents of given file 161 | const markdown = filesystem.read(`${process.cwd()}/${file}`) 162 | 163 | // Throw error if file does not exist and subsequently can't get markdown from the file. 164 | if (typeof markdown === 'undefined') { 165 | return error(`Cannot find file "${file}". Please ensure file exists.`) 166 | } 167 | 168 | const description = options.description || packageJSON.description 169 | const name = options.name || packageJSON.name 170 | const repo = options.repo || (packageJSON.repository || {}).url 171 | const githubCorner = repo ? corner(repo, options.darkTheme) : '' 172 | const dark = options.darkTheme ? 'dark' : 'light' 173 | 174 | // Get all images that are not from the internet ™️ 175 | const images = ( 176 | markdown.match(/(?:!\[(.*?)\]\((?!http)(.*?)\))/gim) || [] 177 | ) 178 | .filter((i) => !i.includes('https')) 179 | .map((image) => (image.split('./')[1] || '').split(')')[0]) 180 | 181 | // Map through them and if that file exists minify it and copy it 182 | images.map(async (i) => { 183 | const imagemin = (await import('imagemin')).default 184 | if (filesystem.exists(`${process.cwd()}/${i}`)) { 185 | await imagemin([`${process.cwd()}/${i}`], { 186 | destination: `${distFolder}/${i.substring(0, i.lastIndexOf('/'))}/`, 187 | plugins: [ 188 | imageminJpegtran(), 189 | imageminPngquant({ quality: [0.65, 0.8] }), 190 | ], 191 | }) 192 | } 193 | }) 194 | 195 | // Copy favicon 196 | if (!options.favicon.includes('http') && options.favicon !== '') { 197 | filesystem.copy( 198 | `${process.cwd()}/${options.favicon}`, 199 | `${distFolder}/${options.favicon}`, 200 | { overwrite: true } 201 | ) 202 | } 203 | 204 | // Copy logo 205 | if (!options.logo.includes('http') && options.logo !== '') { 206 | filesystem.copy( 207 | `${process.cwd()}/${options.logo}`, 208 | `${distFolder}/${options.logo}`, 209 | { overwrite: true } 210 | ) 211 | } 212 | 213 | // Copy shareCard 214 | if (!options.shareCard.includes('http') && options.shareCard !== '') { 215 | filesystem.copy( 216 | `${process.cwd()}/${options.shareCard}`, 217 | `${distFolder}/${options.shareCard}`, 218 | { overwrite: true } 219 | ) 220 | } 221 | 222 | const isIndex = DEFAULT_FILENAMES.includes(file) 223 | 224 | const fileName = isIndex 225 | ? 'index' 226 | : (file.substring(file.lastIndexOf('/') + 1) || '').split('.md')[0] 227 | 228 | const title = name ? name.charAt(0).toUpperCase() + name.slice(1) : '' 229 | 230 | const html = createHTML({ 231 | title, 232 | css: fiddlyImports.css, 233 | lang: 'en', 234 | head: head( 235 | description, 236 | name, 237 | options, 238 | options.homepage || packageJSON.homepage, 239 | options.meta, 240 | options.remoteStyles, 241 | options.remoteScripts 242 | ), 243 | body: `
${githubCorner}${header( 244 | options, 245 | name, 246 | options.additionalFiles 247 | )}${md.render(markdown)}
`, 248 | favicon: options.favicon, 249 | }) 250 | try { 251 | await filesystem.write( 252 | `${distFolder}/${fileName.toLowerCase()}.html`, 253 | html 254 | ) 255 | } catch (e) { 256 | error('Oh no, there has been an error making your file', file) 257 | } 258 | }) 259 | 260 | info(` 261 | 262 | Generated your static files at ${dist}/ 263 | `) 264 | success(` 265 | 🎉 You can deploy the ${dist} folder to a static server 🎉 266 | 267 | `) 268 | }, 269 | } 270 | 271 | exports.DEFAULT_FILENAMES = DEFAULT_FILENAMES 272 | -------------------------------------------------------------------------------- /src/commands/lint.js: -------------------------------------------------------------------------------- 1 | const markdownlint = require('markdownlint') 2 | const DEFAULT_FILENAMES = require('../utils/DEFAULT_FILENAMES') 3 | 4 | module.exports = { 5 | name: 'lint', 6 | alias: 'l', 7 | description: 'Lint your linked markdown files', 8 | run: async (toolbox) => { 9 | const { 10 | print: { success, error }, 11 | filesystem, 12 | } = toolbox 13 | 14 | const packageJSON = 15 | filesystem.read(`${process.cwd()}/package.json`, 'json') || {} 16 | 17 | const options = { 18 | ...(packageJSON.fiddly || {}), 19 | ...(filesystem.read(`${process.cwd()}/.fiddly.config.json`, 'json') || 20 | {}), 21 | } 22 | const files = [] 23 | 24 | DEFAULT_FILENAMES.find((filename) => { 25 | return filesystem.exists(filename) ? files.push(filename) : null 26 | }) 27 | 28 | if (options.additionalFiles) { 29 | files.push(options.additionalFiles) 30 | } 31 | 32 | markdownlint({ files }, (err, result) => { 33 | if (!err) { 34 | if (result.toString()) { 35 | error('We found some errors 😢') 36 | error(result.toString()) 37 | } else { 38 | success('All clear ✅') 39 | } 40 | } 41 | }) 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/watch.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_FILENAMES = require('../utils/DEFAULT_FILENAMES') 2 | const fs = require('fs') 3 | const { run } = require('./fiddly') 4 | 5 | module.exports = { 6 | name: 'watch', 7 | alias: 'w', 8 | description: 'Watch your markdown files for changes and build automatically', 9 | run: async (toolbox) => { 10 | const { 11 | print: { success }, 12 | filesystem, 13 | } = toolbox 14 | success('Watching your files') 15 | const packageJSON = 16 | filesystem.read(`${process.cwd()}/package.json`, 'json') || {} 17 | 18 | const options = { 19 | ...(packageJSON.fiddly || {}), 20 | ...(filesystem.read(`${process.cwd()}/.fiddly.config.json`, 'json') || 21 | {}), 22 | } 23 | const files = [] 24 | 25 | DEFAULT_FILENAMES.find((filename) => { 26 | return filesystem.exists(filename) ? files.push(filename) : null 27 | }) 28 | 29 | if (options.additionalFiles) { 30 | files.push(...options.additionalFiles) 31 | } 32 | 33 | files.map((file) => { 34 | fs.watch(file, (e, filename) => { 35 | if (filename && e === 'change') { 36 | success(`${filename} changed. Building`) 37 | run(toolbox) 38 | } 39 | }) 40 | 41 | return null 42 | }) 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/DEFAULT_FILENAMES.js: -------------------------------------------------------------------------------- 1 | module.exports = ['readme.md', 'Readme.md', 'README.md'] 2 | -------------------------------------------------------------------------------- /src/utils/fiddlyImports.js: -------------------------------------------------------------------------------- 1 | exports.css = ['style.css'] 2 | -------------------------------------------------------------------------------- /src/utils/githubCorner.js: -------------------------------------------------------------------------------- 1 | module.exports = (url, dark) => { 2 | const corners = { 3 | light: ``, 4 | dark: ``, 5 | } 6 | 7 | return dark ? corners.dark : corners.light 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/head.js: -------------------------------------------------------------------------------- 1 | module.exports = ( 2 | description, 3 | name, 4 | options = {}, 5 | homepage, 6 | meta = [], 7 | remoteStyles = [], 8 | remoteScripts = [] 9 | ) => { 10 | const metaTags = meta 11 | .map((value) => { 12 | if (!value.content) return null 13 | const content = value.content 14 | delete value.content 15 | const key = Object.keys(value)[0] 16 | const v = Object.values(value)[0] 17 | return `` 18 | }) 19 | .filter((exists) => exists) 20 | const remoteStylesCleaned = typeof remoteStyles === 'string' ? [remoteStyles] : remoteStyles 21 | const remoteScriptsCleaned = typeof remoteScripts === 'string' ? [remoteScripts] : remoteScripts 22 | return ` 23 | 24 | 25 | 26 | ${ 27 | options.favicon 28 | ? ` 29 | 30 | 31 | ` 32 | : '' 33 | } 34 | 35 | ${ 36 | name 37 | ? ` 38 | 39 | 40 | 41 | ` 42 | : '' 43 | } 44 | ${ 45 | description 46 | ? ` 47 | 48 | 49 | 50 | ` 51 | : '' 52 | } 53 | ${homepage ? `` : ''} 54 | ${ 55 | options.shareCard && options.shareCard.includes('http') 56 | ? ` 57 | 58 | 59 | ` 60 | : '' 61 | } 62 | 63 | 66 | ${metaTags.join('')} 67 | ${ 68 | remoteStylesCleaned.length 69 | ? remoteStylesCleaned 70 | .map((link) => ``) 71 | .join('') 72 | : '' 73 | } 74 | ${ 75 | remoteScriptsCleaned.length 76 | ? remoteScriptsCleaned.map((link) => ``).join('') 77 | : '' 78 | } 79 | ` 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/header.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_FILENAMES = require('./DEFAULT_FILENAMES') 2 | 3 | const capitalize = (name) => 4 | name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() 5 | 6 | module.exports = (options, name, pages) => { 7 | const fileName = (file) => { 8 | const isIndex = DEFAULT_FILENAMES.includes(file) 9 | return isIndex 10 | ? 'Home' 11 | : (file.split('/')[file.split('/').length - 1] || '').split('.md')[0] 12 | } 13 | 14 | const absolutePath = (path) => `${options.pathPrefix}${path}` 15 | 16 | const fileHref = (file) => 17 | fileName(file) === 'Home' ? '' : `${fileName(file).toLowerCase()}.html` 18 | 19 | return options && !options.noHeader 20 | ? `
${name ? `

${name}

` : ''}${ 21 | options.logo !== '' 22 | ? `` 25 | : '' 26 | } 27 | ${ 28 | pages.length > 1 29 | ? `` 37 | : '' 38 | } 39 |
` 40 | : '' 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/remoteStyles.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | 3 | const getRemoteStyles = async () => { 4 | const { data: normalize } = await axios.get( 5 | 'https://unpkg.com/normalize.css@8.0.1/normalize.css' 6 | ) 7 | const { data: lato } = await axios.get( 8 | 'https://fonts.googleapis.com/css?family=Lato' 9 | ) 10 | const { data: playfair } = await axios.get( 11 | 'https://fonts.googleapis.com/css?family=Playfair+Display:700i,900' 12 | ) 13 | 14 | return normalize.concat(lato).concat(playfair) 15 | } 16 | 17 | module.exports = getRemoteStyles 18 | --------------------------------------------------------------------------------