├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yml │ ├── 2-feature-request.md │ └── 3-help.md ├── PULL_REQUEST_TEMPLATE.md ├── gigsboat-2.png ├── gigsboat-screenshot.png └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg ├── post-merge ├── pre-commit └── pre-push ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GENERATE_11ty_STATIC_SITE.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __tests__ ├── __fixtures__ │ ├── datafiles │ │ └── event-ok.md │ ├── filesglobbing │ │ └── pages │ │ │ ├── 2020 │ │ │ └── 2020.md │ │ │ └── 2021 │ │ │ └── 2021.md │ └── main-datafiles │ │ └── myapp │ │ └── pages │ │ ├── 2019 │ │ ├── 2019-09-12.md │ │ ├── 2019-09-15.md │ │ └── 2019-09-27.md │ │ └── 2021 │ │ ├── 2021-01-01.md │ │ ├── 2021-06-13.md │ │ └── 2021-07-21.md ├── __snapshots__ │ ├── json-formatter.test.js.snap │ ├── main-e2e.test.js.snap │ ├── main.test.js.snap │ ├── md-formatter.test.js.snap │ └── yaml-parser.test.js.snap ├── config-manager.test.js ├── fs.test.js ├── json-formatter.test.js ├── main-e2e.test.js ├── main.test.js ├── md-formatter.test.js ├── output-handler.test.js └── yaml-parser.test.js ├── bin ├── cli-parser.js ├── cli.js ├── config-manager.js └── output-handler.js ├── package-lock.json ├── package.json └── src ├── main.js └── utils ├── content-manager.js ├── fs.js ├── json-parser.js ├── md-formatter.js └── yaml-parser.js /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lirantal 2 | src/utils/content-manager.js @gep13 3 | src/utils/md-formatter.js @gep13 4 | __tests__/md-formatter.test.js @gep13 5 | __tests__/__snapshots__/md-formatter.test.* @gep13 6 | bin/config-manager.js @gep13 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | - **Library Version**: 8 | - **OS**: 9 | - **Node.js Version**: 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: "🐞 Bug Report" 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | assignees: 6 | - lirantal 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | - type: textarea 13 | id: expected-behavior 14 | attributes: 15 | label: Expected Behavior 16 | description: What did you expect to happen? 17 | placeholder: Tell us what you see! 18 | value: "A bug happened!" 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: actual-behavior 23 | attributes: 24 | label: Actual Behavior 25 | description: What actually happened? 26 | placeholder: Paste any screenshots that are helpful, but becareful about exposing sensitive information 27 | value: "It didn't work" 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: reproduce 32 | attributes: 33 | label: Steps to reproduce 34 | description: How can we reproduce this issue? 35 | value: | 36 | 1. npm install cli 37 | 2. ... 38 | 3. ... 39 | render: markdown 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: possible-solution 44 | attributes: 45 | label: Suggest a solution 46 | description: Would you like to suggest a solution for how this should be solved? 47 | validations: 48 | required: false 49 | - type: dropdown 50 | id: node-version 51 | attributes: 52 | label: Which Node.js version are you using? 53 | multiple: false 54 | options: 55 | - Node.js 10 56 | - Node.js 12 57 | - Node.js 14 58 | - Node.js 16 59 | - Node.js 18 60 | - type: textarea 61 | id: logs 62 | attributes: 63 | label: Relevant log output 64 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Please scrutinize input to avoid potentially leaking and exposing sensitive information. 65 | render: shell 66 | - type: checkboxes 67 | id: terms 68 | attributes: 69 | label: Code of Conduct 70 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com) 71 | options: 72 | - label: I agree to follow this project's Code of Conduct 73 | required: true 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | Please describe the problem you are trying to solve. 14 | 15 | **Describe the solution you'd like** 16 | Please describe the desired behavior. 17 | 18 | **Describe alternatives you've considered** 19 | Please describe alternative solutions or features you have considered. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "⁉️ Need help?" 3 | about: Please describe the problem. 4 | --- 5 | 6 | 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Types of changes 8 | 9 | 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 14 | 15 | ## Related Issue 16 | 17 | 18 | 19 | 20 | 21 | 22 | ## Motivation and Context 23 | 24 | 25 | 26 | ## How Has This Been Tested? 27 | 28 | 29 | 30 | 31 | 32 | 33 | ## Screenshots (if appropriate): 34 | 35 | ## Checklist: 36 | 37 | 38 | 39 | 40 | - [ ] I have updated the documentation (if required). 41 | - [ ] I have read the **CONTRIBUTING** document. 42 | - [ ] I have added tests to cover my changes. 43 | - [ ] All new and existing tests passed. 44 | - [ ] I added a picture of a cute animal cause it's fun 45 | -------------------------------------------------------------------------------- /.github/gigsboat-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gigsboat/cli/5d233efdcfdef10040760ebcde0a16ac2a9a1d41/.github/gigsboat-2.png -------------------------------------------------------------------------------- /.github/gigsboat-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gigsboat/cli/5d233efdcfdef10040760ebcde0a16ac2a9a1d41/.github/gigsboat-screenshot.png -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: [ubuntu-latest] 10 | node: ['14', '16'] 11 | name: Node ${{ matrix.node }} (${{ matrix.platform }}) 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node }} 18 | - name: install dependencies 19 | run: yarn install --frozen-lockfile --ignore-engines 20 | - name: lint code 21 | run: npm run lint 22 | - name: run tests 23 | run: npm run test 24 | - name: get code coverage report 25 | run: npx codecov 26 | env: 27 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 28 | 29 | release: 30 | name: do semantic release 31 | runs-on: 'ubuntu-latest' 32 | environment: publish 33 | needs: build 34 | steps: 35 | - uses: actions/checkout@v1 36 | - uses: actions/setup-node@v1 37 | with: 38 | node-version: '16' 39 | - name: install dependencies 40 | run: yarn install --frozen-lockfile --ignore-engines 41 | - name: release 42 | run: npx semantic-release 43 | env: 44 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 45 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 46 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | 82 | # Snyk Code 83 | .dccache 84 | 85 | # Developing on a Mac 86 | .DS_Store 87 | 88 | # Project files 89 | README.json 90 | README-gigs*.* -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm install 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run test 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "semi": false, 5 | "trailingComma": "none", 6 | "useTabs": false, 7 | "bracketSpacing": true 8 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at liran.tal@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | The following is a set of guidelines for contributing to @gigsboat/cli. 6 | These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 7 | 8 | ## Code of Conduct 9 | 10 | This project and everyone participating in it is governed by a [Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 11 | 12 | ## How to contribute to @gigsboat/cli 13 | 14 | 15 | 16 | ### Tests 17 | 18 | Make sure the code you're adding has decent test coverage. 19 | 20 | Running project tests and coverage: 21 | 22 | ```bash 23 | npm run test 24 | ``` 25 | 26 | ### Commit Guidelines 27 | 28 | The project uses the commitizen tool for standardizing changelog style commit and a git pre-commit hook to enforce them. 29 | -------------------------------------------------------------------------------- /GENERATE_11ty_STATIC_SITE.md: -------------------------------------------------------------------------------- 1 | # Generating a static site with 11ty 2 | 3 | 11ty is a very flexible static site generator, with loads of different templating and layout options. Below are some instructions to get you started but check the [11ty docs](https://www.11ty.dev/docs/getting-started/) for further customisation. 4 | 5 | ## To create the 11ty starter 6 | 1. Create a file name `.eleventy.js` at the root of your project 7 | 1. Start with some basic config: 8 | ``` 9 | const GIGSBOAT_INPUT_DIR = 'gigsboat-eleventy-starter' 10 | 11 | module.exports = function(eleventyConfig) { 12 | const markdownIt = require('markdown-it') 13 | const options = { html: true } 14 | 15 | eleventyConfig.setLibrary('md', markdownIt(options).use(require('markdown-it-anchor'), { permalink: false })) 16 | 17 | eleventyConfig.addPassthroughCopy(`${GIGSBOAT_INPUT_DIR}/styles.css`) 18 | 19 | return { 20 | dir: { input: GIGSBOAT_INPUT_DIR } 21 | } 22 | } 23 | ``` 24 | 1. Create a directory `gigsboat-eleventy-starter` where your 11ty starter files will live 25 | 1. In the `gigsboat-eleventy-starter` directory create a file `index.json` and declare your layout file name 26 | `{ "layout": "gigsboat-layout" }` 27 | 1. Create a file `styles.css` and add some basic css 28 | ``` 29 | :root { 30 | --nightrider-gray: #333; 31 | --white-smoke: #eee; 32 | --blue: #0ff; 33 | --cornflower-blue: #539bf5; 34 | } 35 | 36 | @media (prefers-color-scheme: light) { 37 | :root { 38 | --color-bg: var(--white-smoke); 39 | --color-text: var(--nightrider-gray); 40 | --color-link: var(--cornflower-blue); 41 | } 42 | } 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --color-bg: var(--nightrider-gray); 46 | --color-text: var(--white-smoke); 47 | --color-link: var(--cornflower-blue); 48 | } 49 | } 50 | 51 | * { 52 | box-sizing: border-box; 53 | } 54 | 55 | html, 56 | body { 57 | padding: 0; 58 | margin: 0; 59 | font-family: -apple-system, system-ui, sans-serif; 60 | color: var(--color-text); 61 | background-color: var(--color-bg); 62 | } 63 | 64 | main { 65 | max-width: calc(900px - (20px * 2)); 66 | margin: 0 auto; 67 | padding-right: 20px; 68 | padding-left: 20px; 69 | } 70 | 71 | p:last-child { 72 | margin-bottom: 0; 73 | } 74 | 75 | a { 76 | color: var(--color-link); 77 | text-decoration: none; 78 | } 79 | 80 | a:hover { 81 | text-decoration: underline; 82 | } 83 | 84 | table { 85 | border-collapse: collapse; 86 | } 87 | 88 | table td, 89 | table th { 90 | border: 1px solid var(--color-text); 91 | padding: 5px; 92 | text-align: left; 93 | } 94 | ``` 95 | 1. Create a new directory named `_includes` (this is the default name where 11ty looks for layout files) and in that directory create a file named `gigsboat-layout.liquid` 96 | 1. In the `gigsboat-layout.liquid` file add the code for a basic template and edit as you wish. 97 | ``` 98 | --- 99 | title: Public Speaking 100 | --- 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {{ title }} 110 | {% if description %} 111 | 112 | {% endif %} 113 | 114 | 115 | 116 | 117 | 118 | 119 |
120 | 121 | {% block content %} 122 | {{ content }} 123 | {% endblock %} 124 |
125 | 126 | 127 | ``` 128 | That's it! Your gigsboat 11ty starter is ready 129 | 130 | ## To generate the html 131 | After you run `npx @gigsboat/cli` and your markdown file is generated 132 | 133 | Run 134 | ```bash 135 | cp README.md gigsboat-eleventy-starter/index.md 136 | npx @11ty/eleventy 137 | ``` 138 | 139 | The above will copy your generated README file into the `gigsboat-eleventy-starter` directory and then run 11ty to generate your html. 140 | 141 | Your newly generated html and css files will be in a new directory rceated by 11ty named `_site`. If desired you can define a different output directory name in *.eleventy.js* 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2021 Liran Tal . 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | https://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | @gigsboat/cli 3 |

4 | 5 |

6 | Do you have a boatload of speaking gigs? 7 |
8 | Use the gigsboat CLI to manage them all via GitHub in the open source way! 9 |

10 | 11 |

12 | 13 |

14 | 15 |

16 | npm version 17 | license 18 | downloads 19 | build 20 | codecov 21 | Known Vulnerabilities 22 | Responsible Disclosure Policy 23 |

24 | 25 | # The Hook 26 | 27 | Track your speaking activities all within your GitHub opensource repository! 28 | 29 | Here is how [I'm doing it](https://github.com/lirantal/public-speaking): 30 | 31 | ![](https://github.com/gigsboat/cli/blob/main/.github/gigsboat-screenshot.png) 32 | 33 | 34 | # About 35 | 36 | - Are you a Developer Advocate 🥑 ? 37 | - Enjoy doing conference talks, meetups presentations, or joining a podcast? 38 | - Do you have a boatload of speaking gigs? 39 | 40 | Gigsboat is for you because: 41 | - It is difficult for you to keep track of all of your gigs 42 | - You want some stats and hard cold data for all of your speaking engagements 43 | 44 | Welcome to gigsboat ⛵️ 🎉 45 | 46 | ## Why gigsboat? 47 | 48 | Even more reasons to use Gigsboat: 49 | 50 | - **You own your data** - do you use a 3rd-party app, or tool to manage your talks? Maybe a Trello board? With gigsboat, you own and manage your data via YAML files, and gigsboat is here to help you transform that into a beautiful Markdown page. 51 | 52 | - **It's all open source** - Well, gigsboat is open source but also all of your speaking activities, they're all open source and all managed right here in GitHub where it's easy to manage your talks, just like you manage your code projects. It's all transparent and you can share it with your friends, conference organizers, and so on. 53 | 54 | - **Zero config** - just run the CLI tool and it'll automatically detect source files, and generate a Markdown document for you with all of them. 55 | 56 | - **Statistics** - gigsboat runs some numbers crunching and gives you stats about your speaking engagements! 57 | 58 | # How does it work? 59 | 60 | 1. You run the CLI 61 | 2. It finds your events' data files 62 | 3. It parses them into JSON 63 | 4. It extracts, and sorts the data in all kinds of way 64 | 5. Exports the JSON into Markdown format 65 | 6. Results are printed to STDOUT or to a `README.md` file 66 | 7. You now have a fancy looking Markdown document that lists all of your events 67 | 68 | # How to get started? 69 | 70 | ## Option 1: Zero-to-Hero in a heartbeat! 71 | 72 | We have a template repository for you to get started in a matter of seconds! 🚤 73 | 74 | Head over to the [gigs-template](https://github.com/gigsboat/gigs-template) repository which we created as a template for you to quickly get started. It's just a few steps, and you can start tracking all of your speaking engagements. 75 | 76 | If you would like to know the gritty details of how the gigsboat CLI works, continue to read for the _starting from scratch_ section. 77 | 78 | ## Option 2: Starting from scratch 79 | 80 | If you're just getting started tracking your events and you don't have any actual data files that hold the history of your events (or your upcoming events), then this section is for you. 81 | 82 | It boils down to the following: 83 | 1. You need a directory structure (any structure you choose) with data files inside it 84 | 2. Those data files need to be in [YAML Front Matter format](https://docs.zettlr.com/en/core/yaml-frontmatter/#:~:text=A%20YAML%20frontmatter%20is%20a,%2C%20keywords%2C%20and%20the%20title.&text=They%20contain%20valid%20YAML%20and%20can%20be%20used%20to%20define%20arbitrary%20variables.). Those YAML files need to have a specific format. 85 | 86 | Now, let's break it down further to give you example code you can get started with. 87 | 88 | ### 1. Directory structure 89 | 90 | You are probably going to manage it all via a repository, so once you have one created, here's a suggested directory structure: 91 | 92 | ``` 93 | - gigsboat.json 94 | - pages 95 | | 96 | - 2020 97 | | 98 | - 2020-02-03.md 99 | - 2020-06-03.md 100 | - 2021 101 | | 102 | - 2021-11-26.md 103 | ``` 104 | 105 | In the above we are nesting all of the data files under `pages//` directories. 106 | 107 | You are free to create whatever directory structure that makes sense for you, but note that `gigsboat` will by default search the data files under the `pages` root directory in which it runs. 108 | 109 | The `gigsboat.json` file is used as a configuration file for the gigsboat cli. 110 | 111 | Note: the data filenames can be anything. In this example, it shows a simple date format of `yyyy-mm-dd` however that may not always work - for example, when you have two speaking engagements (a podcast recording, and a meetup talk) on the same day. That said, the filename doesn't matter at all, and you can then follow a convention, of say, `2021-11-26-2.md` which adds a suffix of incrementing numbers to the day. 112 | 113 | ### 2. YAML Front Matter 114 | 115 | The data files are markdown, but they need to have the following YAML Front Matter format: 116 | 117 | ```md 118 | --- 119 | date: 2019-05-30 120 | tags: post 121 | name: OWASP Global AppSec 122 | url: https://telaviv.appsecglobal.org/ 123 | type: conference 124 | title: Black Clouds Silver Linings In Nodejs Security 125 | slides_url: https://drive.google.com/file/d/1s0YIvnlF7ByoESu3rHV2i5M9_jQSXjyR/view 126 | recording_url: https://www.youtube.com/watch?v=4XdF4OiAAzU&feature=emb_logo&ab_channel=OWASP 127 | city: Tel Aviv 128 | country: Israel 129 | country_code: IL 130 | language: English 131 | recognitions: 132 | twitter: 133 | - https://twitter.com/_r3ggi/status/1134057317538942978 134 | image_header: https://pbs.twimg.com/media/D7z7G5dXsAA3ulw?format=jpg&name=small 135 | images: 136 | - https://pbs.twimg.com/media/D7z7G5dXsAA3ulw?format=jpg&name=small 137 | --- 138 | ``` 139 | 140 | For brevity, the above also includes example values to each of the field so that the schema for the data files is easily understood. 141 | 142 | You can save this data file as say `2019-05-30.md` in your `pages/` directory somewhere. As you can see, there's a dedicated `date` field, which `gigsboat` will parse and is the reason that the filename convention doesn't actually matter. 143 | 144 | You might wonder why is this referred to as a markdown file? because the front matter piece of it all the YAML structure between the opening and closing `---`, after which, it can have a markdown-formatted content, so you can treat it as a markdown document for any purpose. Some ideas: you may want to capture your own personal notes of that event, maybe add pictures, add your summary and so on. 145 | 146 | #### type property 147 | 148 | There are currently 6 supported values for the `type` property within the YAML Front Matter. The gigsboat cli will count how many of each of these exist, and create a badge with that count on the generated README.md file. 149 | 150 | These values are: 151 | 152 | * conference 153 | * podcast 154 | * webinar 155 | * meetup 156 | * article 157 | * workshop 158 | 159 | It is possible to use any free form text in the type property, however, when these aren't matched against the above, they will be counted in a fallback type called 'other'. 160 | 161 | ### gigsboat.json 162 | 163 | This is the configuration file for the gigsboat CLI. This contains four sections: 164 | 165 | * `input` 166 | * `sourceDirectory` - this is the relative path to the directory in which the data files for the generated file are located 167 | * `output` 168 | * `markdownFile` - this is the name of the file that should be generated 169 | * `preContent` - a collection of raw or formatted HTML to place at the start of the generated file 170 | * `postContent` - a collection of raw or formatted HTML to place at the end of the generated file 171 | 172 | A complete example of this configuration file: 173 | 174 | ```json 175 | { 176 | "input": { 177 | "sourceDirectory": "pages" 178 | }, 179 | "output": { 180 | "markdownFile": "README-gigs.md" 181 | }, 182 | "preContent": [ 183 | { 184 | "raw": "

This will appear at the top of the generated README.md file

" 185 | }, 186 | { 187 | "raw": "

Let's add some badges!

Twitter Follow

" 188 | }, 189 | { 190 | "format": [ 191 | { 192 | "ul": [ 193 | "In addition to raw HTML elements, you can use format sections", 194 | "Using HTML elements to construct the output" 195 | ] 196 | } 197 | ] 198 | } 199 | ], 200 | "postContent": [ 201 | { 202 | "raw": "

This will appear at the bottom of the generated README.md file

" 203 | } 204 | ] 205 | } 206 | ``` 207 | 208 | ### Gigsboat CLI arguments 209 | 210 | You can provide the gigsboat CLI with arguments to control the behaviour of the CLI. 211 | 212 | Run the CLI with the `--help` argument to see a list of available arguments: 213 | 214 | ```sh 215 | Options: 216 | --help Show help [boolean] 217 | --version Show version number [boolean] 218 | -o, --output-file [string] [default: "README-gigsfile.md"] 219 | -s, --source-directory [string] [default: "pages"] 220 | ``` 221 | 222 | As such, you can control the source file directory and the output file name, 223 | for example: 224 | 225 | ```sh 226 | npx gigsboat -s ~/myProject/data/pages -o README-gigs.md 227 | ``` 228 | 229 | ## I already track events via YAML / Markdown 230 | 231 | Use the `gigsboat` CLI to manage them all! 232 | 233 | Use npm's built-in `npx` command to fetch, install, and run gigsboat with no configuration on your current project: 234 | 235 | ```bash 236 | npx @gigsboat/cli 237 | ``` 238 | 239 | ## Deploying to GitHub Pages 240 | 241 | It is possible to deploy the generated README.md file to github pages. All that is needed, is to enable github-pages in the repository's *settings* > *pages*, as is described in the [github pages docs](https://docs.github.com/en/pages/quickstart#creating-your-website), starting from _step 3_. 242 | 243 | To access the generated site visit 244 | `https://.github.io/` 245 | 246 | To select a different theme than the default or to customise it, add a __config.yml_ file and include `theme: jekyll-theme-minimal`. From the same __config.yml_ file, it's also possible to set a `title` and `description` for the gerated html. For more details on theme customisation follow the steps described in [adding a theme to your jekyll site](https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/adding-a-theme-to-your-github-pages-site-using-jekyll). 247 | 248 | ## Generating a static site 249 | 250 | You can use several static site generators to output an html from the generated README.md file. Using jekyll, is great if you want to deploy to github pages. But if you're looking to deploy somewhere else, you might want to use a different tool like [11ty](https://www.11ty.dev/). We have created a guide in [GENERATE_11ty_STATIC_SITE.md](GENERATE_11ty_STATIC_SITE.md) to get you started. 251 | 252 | # Contributing 253 | 254 | Please consult [CONTRIBUTING](./CONTRIBUTING.md) for guidelines on contributing to this project. 255 | 256 | # Author 257 | 258 | **@gigsboat/cli** © [Liran Tal](https://github.com/lirantal), Released under the [Apache-2.0](./LICENSE) License. 259 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | **Please do not report security vulnerabilities through public GitHub issues**. 4 | 5 | ## Responsible disclosure security policy 6 | 7 | A responsible disclosure policy helps protect users of the project from public disclosure of security vulnerabilities without a fix available. We achieve that by following the process where vulnerabilities are first triaged in a private manner, and are only publicly disclosed after a reasonable time period of the patch being available for users. 8 | 9 | We kindly ask you to refrain from malicious acts that put our users, the project, or any of the project’s team members at risk. 10 | 11 | ## Reporting a security issue 12 | 13 | We consider the security of the project a top priority. 14 | 15 | If you discover a security vulnerability, please use one of the following means of communications to report it to us: 16 | 17 | - Report the security issue to the [Snyk Security Team](https://snyk.io/vulnerability-disclosure). They will help triage the security issue and work with all involved parties to remediate and release a fix. 18 | 19 | We sincerely appreciate your efforts to responsibly disclose your findings with us. 20 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/datafiles/event-ok.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2020-02-18 3 | tags: post 4 | name: SAP Labs dkom 2020 Israel 5 | url: 6 | type: conference 7 | title: The State of Open Source Security 8 | slides_url: 9 | recording_url: 10 | city: Ra'anana 11 | country: Israel 12 | country_code: IL 13 | language: English 14 | recognitions: 15 | twitter: 16 | - https://twitter.com/liran_tal/status/1220242496703385600?s=20 17 | images: 18 | - https://pbs.twimg.com/media/EO8sCpbWAAAUAw0?format=jpg&name=4096x4096 19 | --- 20 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/filesglobbing/pages/2020/2020.md: -------------------------------------------------------------------------------- 1 | # just a placeholder 2 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/filesglobbing/pages/2021/2021.md: -------------------------------------------------------------------------------- 1 | # just a placeholder 2 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/main-datafiles/myapp/pages/2019/2019-09-12.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2019-09-12 3 | tags: post 4 | name: APIdays Barcelona 5 | url: https://www.apidays.co/barcelona2019/ 6 | type: webinar 7 | title: 'Consumer-Driven Contracts: A better approach for API Testing' 8 | slides_url: https://slides.com/lirantal/consumer-driven-contracts 9 | recording_url: https://www.youtube.com/watch?v=zfGKX5iKSis&list=PLmEaqnTJ40Oqo9VlbcUakVmFZgx5weLrs&index=25&t=141s&ab_channel=apidays 10 | city: Barcelona 11 | country: Spain 12 | country_code: ES 13 | language: English 14 | recognitions: 15 | twitter: 16 | - https://twitter.com/anyulled/status/1172079037189152768 17 | - https://twitter.com/APIdaysGlobal/status/1162621839933358080 18 | - https://twitter.com/liran_tal/status/1173942213086076928 19 | - https://twitter.com/liran_tal/status/1199047628530556931 20 | - https://twitter.com/snyksec/status/1172033103528898561 21 | image_header: https://pbs.twimg.com/media/EEQPqXlXUAEoafi?format=jpg&name=large 22 | images: 23 | - https://pbs.twimg.com/media/EEQPqXlXUAEoafi?format=jpg&name=large 24 | - https://pbs.twimg.com/media/ECJ2Zv6WsAEPUFF?format=jpg&name=large 25 | --- 26 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/main-datafiles/myapp/pages/2019/2019-09-15.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2019-09-15 3 | tags: post 4 | name: JSConf Budapest 5 | url: https://2019.jsconfbp.com/ 6 | type: podcast 7 | title: 'StrangerDanger: Finding Security Vulnerabilities Before They Find You!' 8 | slides_url: 9 | recording_url: https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf 10 | city: Budapest 11 | country: Hungary 12 | country_code: HU 13 | language: English 14 | recognitions: 15 | twitter: 16 | - https://twitter.com/panay_georgiou/status/1177572765664194560 17 | image_header: https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large 18 | images: 19 | - https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large 20 | --- 21 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/main-datafiles/myapp/pages/2019/2019-09-27.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2019-09-27 3 | tags: post 4 | name: JSConf Budapest 5 | url: https://2019.jsconfbp.com/ 6 | type: meetup 7 | title: 'StrangerDanger: Finding Security Vulnerabilities Before They Find You!' 8 | slides_url: 9 | recording_url: https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf 10 | city: Budapest 11 | country: Hungary 12 | country_code: HU 13 | language: English 14 | recognitions: 15 | twitter: 16 | - https://twitter.com/panay_georgiou/status/1177572765664194560 17 | image_header: https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large 18 | images: 19 | - https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large 20 | --- 21 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/main-datafiles/myapp/pages/2021/2021-01-01.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2021-01-01 3 | tags: post 4 | name: Cyber Week 5 | url: https://cw2021.b2b-wizard.com/expo 6 | type: other 7 | title: Are We Forever Doomed By Software Supply Chain Risks? 8 | slides_url: 9 | recording_url: https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk 10 | city: Tel Aviv 11 | country: Israel 12 | country_code: IL 13 | language: English 14 | recognitions: 15 | twitter: 16 | - https://twitter.com/liran_tal/status/1417874639859109894 17 | image_header: 18 | images: 19 | - 20 | --- 21 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/main-datafiles/myapp/pages/2021/2021-06-13.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2021-06-13 3 | tags: post 4 | name: Cyber Week 5 | url: https://cw2021.b2b-wizard.com/expo 6 | type: article 7 | title: Are We Forever Doomed By Software Supply Chain Risks? 8 | slides_url: 9 | recording_url: https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk 10 | city: Tel Aviv 11 | country: Israel 12 | country_code: IL 13 | language: English 14 | recognitions: 15 | twitter: 16 | - https://twitter.com/liran_tal/status/1417874639859109894 17 | image_header: 18 | images: 19 | - 20 | --- 21 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/main-datafiles/myapp/pages/2021/2021-07-21.md: -------------------------------------------------------------------------------- 1 | --- 2 | date: 2021-07-21 3 | tags: post 4 | name: Cyber Week 5 | url: https://cw2021.b2b-wizard.com/expo 6 | type: conference 7 | title: Are We Forever Doomed By Software Supply Chain Risks? 8 | slides_url: 9 | recording_url: https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk 10 | city: Tel Aviv 11 | country: Israel 12 | country_code: IL 13 | language: English 14 | recognitions: 15 | twitter: 16 | - https://twitter.com/liran_tal/status/1417874639859109894 17 | image_header: 18 | images: 19 | - 20 | --- 21 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/json-formatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`given a directory of markdown files ensure the JSON output is created 1`] = ` 4 | Object { 5 | "generatedAt": "2022-12-14T00:00:00.000Z", 6 | "items": Array [ 7 | Object { 8 | "city": "Tel Aviv", 9 | "country": "Israel", 10 | "country_code": "IL", 11 | "date": "2021-07-21T00:00:00.000Z", 12 | "image_header": null, 13 | "images": Array [ 14 | null, 15 | ], 16 | "language": "English", 17 | "name": "Cyber Week", 18 | "recognitions": Object { 19 | "twitter": Array [ 20 | "https://twitter.com/liran_tal/status/1417874639859109894", 21 | ], 22 | }, 23 | "recording_url": "https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk", 24 | "slides_url": null, 25 | "tags": "post", 26 | "title": "Are We Forever Doomed By Software Supply Chain Risks?", 27 | "type": "conference", 28 | "url": "https://cw2021.b2b-wizard.com/expo", 29 | }, 30 | Object { 31 | "city": "Tel Aviv", 32 | "country": "Israel", 33 | "country_code": "IL", 34 | "date": "2021-06-13T00:00:00.000Z", 35 | "image_header": null, 36 | "images": Array [ 37 | null, 38 | ], 39 | "language": "English", 40 | "name": "Cyber Week", 41 | "recognitions": Object { 42 | "twitter": Array [ 43 | "https://twitter.com/liran_tal/status/1417874639859109894", 44 | ], 45 | }, 46 | "recording_url": "https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk", 47 | "slides_url": null, 48 | "tags": "post", 49 | "title": "Are We Forever Doomed By Software Supply Chain Risks?", 50 | "type": "article", 51 | "url": "https://cw2021.b2b-wizard.com/expo", 52 | }, 53 | Object { 54 | "city": "Tel Aviv", 55 | "country": "Israel", 56 | "country_code": "IL", 57 | "date": "2021-01-01T00:00:00.000Z", 58 | "image_header": null, 59 | "images": Array [ 60 | null, 61 | ], 62 | "language": "English", 63 | "name": "Cyber Week", 64 | "recognitions": Object { 65 | "twitter": Array [ 66 | "https://twitter.com/liran_tal/status/1417874639859109894", 67 | ], 68 | }, 69 | "recording_url": "https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk", 70 | "slides_url": null, 71 | "tags": "post", 72 | "title": "Are We Forever Doomed By Software Supply Chain Risks?", 73 | "type": "other", 74 | "url": "https://cw2021.b2b-wizard.com/expo", 75 | }, 76 | Object { 77 | "city": "Budapest", 78 | "country": "Hungary", 79 | "country_code": "HU", 80 | "date": "2019-09-27T00:00:00.000Z", 81 | "image_header": "https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large", 82 | "images": Array [ 83 | "https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large", 84 | ], 85 | "language": "English", 86 | "name": "JSConf Budapest", 87 | "recognitions": Object { 88 | "twitter": Array [ 89 | "https://twitter.com/panay_georgiou/status/1177572765664194560", 90 | ], 91 | }, 92 | "recording_url": "https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf", 93 | "slides_url": null, 94 | "tags": "post", 95 | "title": "StrangerDanger: Finding Security Vulnerabilities Before They Find You!", 96 | "type": "meetup", 97 | "url": "https://2019.jsconfbp.com/", 98 | }, 99 | Object { 100 | "city": "Budapest", 101 | "country": "Hungary", 102 | "country_code": "HU", 103 | "date": "2019-09-15T00:00:00.000Z", 104 | "image_header": "https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large", 105 | "images": Array [ 106 | "https://pbs.twimg.com/media/EFeULhIW4AARwZa?format=jpg&name=large", 107 | ], 108 | "language": "English", 109 | "name": "JSConf Budapest", 110 | "recognitions": Object { 111 | "twitter": Array [ 112 | "https://twitter.com/panay_georgiou/status/1177572765664194560", 113 | ], 114 | }, 115 | "recording_url": "https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf", 116 | "slides_url": null, 117 | "tags": "post", 118 | "title": "StrangerDanger: Finding Security Vulnerabilities Before They Find You!", 119 | "type": "podcast", 120 | "url": "https://2019.jsconfbp.com/", 121 | }, 122 | Object { 123 | "city": "Barcelona", 124 | "country": "Spain", 125 | "country_code": "ES", 126 | "date": "2019-09-12T00:00:00.000Z", 127 | "image_header": "https://pbs.twimg.com/media/EEQPqXlXUAEoafi?format=jpg&name=large", 128 | "images": Array [ 129 | "https://pbs.twimg.com/media/EEQPqXlXUAEoafi?format=jpg&name=large", 130 | "https://pbs.twimg.com/media/ECJ2Zv6WsAEPUFF?format=jpg&name=large", 131 | ], 132 | "language": "English", 133 | "name": "APIdays Barcelona", 134 | "recognitions": Object { 135 | "twitter": Array [ 136 | "https://twitter.com/anyulled/status/1172079037189152768", 137 | "https://twitter.com/APIdaysGlobal/status/1162621839933358080", 138 | "https://twitter.com/liran_tal/status/1173942213086076928", 139 | "https://twitter.com/liran_tal/status/1199047628530556931", 140 | "https://twitter.com/snyksec/status/1172033103528898561", 141 | ], 142 | }, 143 | "recording_url": "https://www.youtube.com/watch?v=zfGKX5iKSis&list=PLmEaqnTJ40Oqo9VlbcUakVmFZgx5weLrs&index=25&t=141s&ab_channel=apidays", 144 | "slides_url": "https://slides.com/lirantal/consumer-driven-contracts", 145 | "tags": "post", 146 | "title": "Consumer-Driven Contracts: A better approach for API Testing", 147 | "type": "webinar", 148 | "url": "https://www.apidays.co/barcelona2019/", 149 | }, 150 | ], 151 | "stats": Object { 152 | "total": 6, 153 | "total_article": 1, 154 | "total_conference": 1, 155 | "total_meetup": 1, 156 | "total_other": 1, 157 | "total_podcast": 1, 158 | "total_webinar": 1, 159 | "total_workshop": 0, 160 | }, 161 | } 162 | `; 163 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/main-e2e.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`spawn the cli to process data and ensure markdown file is generated correctly 1`] = ` 4 | "

\\"Total \\"Total \\"Total \\"Total \\"Total \\"Total

5 |
6 | 7 | # Table of Contents 8 | 9 | 10 | - [Year of 2021](#2021) - total events 3 11 | - [Year of 2019](#2019) - total events 3 12 | 13 | # 2021 14 | 15 | 16 | ![Total Events](https://img.shields.io/badge/total-3-blue?style=flat-square) ![Total Conferences](https://img.shields.io/badge/conferences-1-red?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/articles-1-green?style=flat-square) 17 | 18 | 19 | 20 | 21 | | Date | Event | Title | Slides | Recording | Location | Language | 22 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 23 | | 2021-7-21 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](2021/2021-07-21.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 24 | | 2021-6-13 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](2021/2021-06-13.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 25 | | 2021-1-1 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](2021/2021-01-01.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 26 | 27 | 28 | # 2019 29 | 30 | 31 | ![Total Events](https://img.shields.io/badge/total-3-blue?style=flat-square) ![Total Meetups](https://img.shields.io/badge/meetups-1-violet?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/podcasts-1-yellow?style=flat-square) ![Total Webinars](https://img.shields.io/badge/webinars-1-lightgrey?style=flat-square) 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | | Date | Event | Title | Slides | Recording | Location | Language | 45 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 46 | | 2019-9-27 | JSConf Budapest | [StrangerDanger: Finding Security Vulnerabilities Before They Find You!](2019/2019-09-27.md) | | [Recording](https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf) | [🇭🇺](## \\"Hungary\\") | English | 47 | | 2019-9-15 | JSConf Budapest | [StrangerDanger: Finding Security Vulnerabilities Before They Find You!](2019/2019-09-15.md) | | [Recording](https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf) | [🇭🇺](## \\"Hungary\\") | English | 48 | | 2019-9-12 | APIdays Barcelona | [Consumer-Driven Contracts: A better approach for API Testing](2019/2019-09-12.md) | [Slides](https://slides.com/lirantal/consumer-driven-contracts) | [Recording](https://www.youtube.com/watch?v=zfGKX5iKSis&list=PLmEaqnTJ40Oqo9VlbcUakVmFZgx5weLrs&index=25&t=141s&ab_channel=apidays) | [🇪🇸](## \\"Spain\\") | English | 49 | 50 | 51 | 52 | 53 | powered by [gigsboat/cli](https://github.com/gigsboat/cli)" 54 | `; 55 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/main.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`given a directory of markdown files a full document is rendered 1`] = ` 4 | "

\\"Total \\"Total \\"Total \\"Total \\"Total \\"Total

5 |
6 | 7 | # Table of Contents 8 | 9 | 10 | - [Year of 2021](#2021) - total events 3 11 | - [Year of 2019](#2019) - total events 3 12 | 13 | # 2021 14 | 15 | 16 | ![Total Events](https://img.shields.io/badge/total-3-blue?style=flat-square) ![Total Conferences](https://img.shields.io/badge/conferences-1-red?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/articles-1-green?style=flat-square) 17 | 18 | 19 | 20 | 21 | | Date | Event | Title | Slides | Recording | Location | Language | 22 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 23 | | 2021-7-21 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](myapp/pages/2021/2021-07-21.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 24 | | 2021-6-13 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](myapp/pages/2021/2021-06-13.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 25 | | 2021-1-1 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](myapp/pages/2021/2021-01-01.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 26 | 27 | 28 | # 2019 29 | 30 | 31 | ![Total Events](https://img.shields.io/badge/total-3-blue?style=flat-square) ![Total Meetups](https://img.shields.io/badge/meetups-1-violet?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/podcasts-1-yellow?style=flat-square) ![Total Webinars](https://img.shields.io/badge/webinars-1-lightgrey?style=flat-square) 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 | | Date | Event | Title | Slides | Recording | Location | Language | 45 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 46 | | 2019-9-27 | JSConf Budapest | [StrangerDanger: Finding Security Vulnerabilities Before They Find You!](myapp/pages/2019/2019-09-27.md) | | [Recording](https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf) | [🇭🇺](## \\"Hungary\\") | English | 47 | | 2019-9-15 | JSConf Budapest | [StrangerDanger: Finding Security Vulnerabilities Before They Find You!](myapp/pages/2019/2019-09-15.md) | | [Recording](https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf) | [🇭🇺](## \\"Hungary\\") | English | 48 | | 2019-9-12 | APIdays Barcelona | [Consumer-Driven Contracts: A better approach for API Testing](myapp/pages/2019/2019-09-12.md) | [Slides](https://slides.com/lirantal/consumer-driven-contracts) | [Recording](https://www.youtube.com/watch?v=zfGKX5iKSis&list=PLmEaqnTJ40Oqo9VlbcUakVmFZgx5weLrs&index=25&t=141s&ab_channel=apidays) | [🇪🇸](## \\"Spain\\") | English | 49 | 50 | 51 | 52 | 53 | *page updated on 2021-12-14T00:00:00.000Z* 54 | 55 | powered by [gigsboat/cli](https://github.com/gigsboat/cli)" 56 | `; 57 | 58 | exports[`given a directory of markdown files a full document is rendered along with pre and post content 1`] = ` 59 | "

\\"Total \\"Total \\"Total \\"Total \\"Total \\"Total

60 |
61 |

actual html is allowed too

62 | 63 | Liran Tal 64 | 65 | 66 | # Table of Contents 67 | 68 | 69 | - [Year of 2021](#2021) - total events 3 70 | - [Year of 2019](#2019) - total events 3 71 | 72 | # 2021 73 | 74 | 75 | ![Total Events](https://img.shields.io/badge/total-3-blue?style=flat-square) ![Total Conferences](https://img.shields.io/badge/conferences-1-red?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/articles-1-green?style=flat-square) 76 | 77 | 78 | 79 | 80 | | Date | Event | Title | Slides | Recording | Location | Language | 81 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 82 | | 2021-7-21 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](myapp/pages/2021/2021-07-21.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 83 | | 2021-6-13 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](myapp/pages/2021/2021-06-13.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 84 | | 2021-1-1 | Cyber Week | [Are We Forever Doomed By Software Supply Chain Risks?](myapp/pages/2021/2021-01-01.md) | | [Recording](https://www.youtube.com/watch?v=x74sMCaZKbg&ab_channel=Snyk) | [🇮🇱](## \\"Israel\\") | English | 85 | 86 | 87 | # 2019 88 | 89 | 90 | ![Total Events](https://img.shields.io/badge/total-3-blue?style=flat-square) ![Total Meetups](https://img.shields.io/badge/meetups-1-violet?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/podcasts-1-yellow?style=flat-square) ![Total Webinars](https://img.shields.io/badge/webinars-1-lightgrey?style=flat-square) 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 102 | 103 | | Date | Event | Title | Slides | Recording | Location | Language | 104 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 105 | | 2019-9-27 | JSConf Budapest | [StrangerDanger: Finding Security Vulnerabilities Before They Find You!](myapp/pages/2019/2019-09-27.md) | | [Recording](https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf) | [🇭🇺](## \\"Hungary\\") | English | 106 | | 2019-9-15 | JSConf Budapest | [StrangerDanger: Finding Security Vulnerabilities Before They Find You!](myapp/pages/2019/2019-09-15.md) | | [Recording](https://www.youtube.com/watch?v=3H8pF6yoSgU&list=PL37ZVnwpeshEMCvdYDdZ09Sy-toTftWu0&index=15&ab_channel=JSConf) | [🇭🇺](## \\"Hungary\\") | English | 107 | | 2019-9-12 | APIdays Barcelona | [Consumer-Driven Contracts: A better approach for API Testing](myapp/pages/2019/2019-09-12.md) | [Slides](https://slides.com/lirantal/consumer-driven-contracts) | [Recording](https://www.youtube.com/watch?v=zfGKX5iKSis&list=PLmEaqnTJ40Oqo9VlbcUakVmFZgx5weLrs&index=25&t=141s&ab_channel=apidays) | [🇪🇸](## \\"Spain\\") | English | 108 | 109 | 110 | 111 |

rendered in postcontent

112 | 113 | Thank you! 114 | 115 | 116 | *page updated on 2021-12-14T00:00:00.000Z* 117 | 118 | powered by [gigsboat/cli](https://github.com/gigsboat/cli)" 119 | `; 120 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/md-formatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`convert json object to markdown formatting 1`] = ` 4 | "# great title 5 | 6 | 7 | and my paragraph is cool 8 | " 9 | `; 10 | 11 | exports[`process events images 1`] = ` 12 | "# Table of Contents 13 | 14 | 15 | - [Year of 2016](#2016) - total events 1 16 | - [Year of 2017](#2017) - total events 1 17 | - [Year of 2021](#2021) - total events 2 18 | 19 | # 2016 20 | 21 | 22 | ![Total Events](https://img.shields.io/badge/total-1-blue?style=flat-square) 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | | Date | Event | Title | Slides | Recording | Location | Language | 34 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 35 | | 2016-1-1 | name | title | [Slides](https://example.com) | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | | 36 | 37 | 38 | # 2017 39 | 40 | 41 | ![Total Events](https://img.shields.io/badge/total-1-blue?style=flat-square) 42 | 43 | 44 | 45 | 46 | | Date | Event | Title | Slides | Recording | Location | Language | 47 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 48 | | 2017-1-1 | name | title | [Slides](https://example.com) | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | | 49 | 50 | 51 | # 2021 52 | 53 | 54 | ![Total Events](https://img.shields.io/badge/total-2-blue?style=flat-square) ![Total Meetups](https://img.shields.io/badge/meetups-1-violet?style=flat-square) ![Total Conferences](https://img.shields.io/badge/conferences-1-red?style=flat-square) 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | 64 | 65 | | Date | Event | Title | Slides | Recording | Location | Language | 66 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 67 | | 2021-2-1 | name | title | [Slides](https://example.com) | | | English | 68 | | 2021-2-2 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 69 | 70 | 71 | " 72 | `; 73 | 74 | exports[`process events json data to provide a complete markdown document 1`] = ` 75 | "# Table of Contents 76 | 77 | 78 | - [Year of 2016](#2016) - total events 1 79 | - [Year of 2021](#2021) - total events 8 80 | 81 | # 2016 82 | 83 | 84 | ![Total Events](https://img.shields.io/badge/total-1-blue?style=flat-square) 85 | 86 | 87 | 88 | 89 | | Date | Event | Title | Slides | Recording | Location | Language | 90 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 91 | | 2016-1-1 | name | title | [Slides](https://example.com) | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | | 92 | 93 | 94 | # 2021 95 | 96 | 97 | ![Total Events](https://img.shields.io/badge/total-8-blue?style=flat-square) ![Total Meetups](https://img.shields.io/badge/meetups-1-violet?style=flat-square) ![Total Conferences](https://img.shields.io/badge/conferences-1-red?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/podcasts-1-yellow?style=flat-square) ![Total Webinars](https://img.shields.io/badge/webinars-2-lightgrey?style=flat-square) ![Total Podcasts](https://img.shields.io/badge/articles-1-green?style=flat-square) ![Total Workshops](https://img.shields.io/badge/workshops-2-orange?style=flat-square) 98 | 99 | 100 | 101 | 102 | | Date | Event | Title | Slides | Recording | Location | Language | 103 | | ---- | ----- | ----- | ------ | --------- | -------- | -------- | 104 | | 2021-2-1 | name | title | [Slides](https://example.com) | | | English | 105 | | 2021-2-2 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 106 | | 2021-2-2 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 107 | | 2021-2-2 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 108 | | 2021-2-2 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 109 | | 2021-2-2 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 110 | | 2021-2-2 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 111 | | 2021-2-3 | name | title | | [Recording](https://example.com) | [🇺🇸](## \\"United States\\") | English | 112 | 113 | 114 | " 115 | `; 116 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/yaml-parser.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`convert markdown file to json 1`] = ` 4 | Object { 5 | "attributes": Object { 6 | "city": "Ra'anana", 7 | "country": "Israel", 8 | "country_code": "IL", 9 | "date": 2020-02-18T00:00:00.000Z, 10 | "images": Array [ 11 | "https://pbs.twimg.com/media/EO8sCpbWAAAUAw0?format=jpg&name=4096x4096", 12 | ], 13 | "language": "English", 14 | "name": "SAP Labs dkom 2020 Israel", 15 | "recognitions": Object { 16 | "twitter": Array [ 17 | "https://twitter.com/liran_tal/status/1220242496703385600?s=20", 18 | ], 19 | }, 20 | "recording_url": null, 21 | "slides_url": null, 22 | "tags": "post", 23 | "title": "The State of Open Source Security", 24 | "type": "conference", 25 | "url": null, 26 | }, 27 | "body": "", 28 | "bodyBegin": 20, 29 | "frontmatter": "date: 2020-02-18 30 | tags: post 31 | name: SAP Labs dkom 2020 Israel 32 | url: 33 | type: conference 34 | title: The State of Open Source Security 35 | slides_url: 36 | recording_url: 37 | city: Ra'anana 38 | country: Israel 39 | country_code: IL 40 | language: English 41 | recognitions: 42 | twitter: 43 | - https://twitter.com/liran_tal/status/1220242496703385600?s=20 44 | images: 45 | - https://pbs.twimg.com/media/EO8sCpbWAAAUAw0?format=jpg&name=4096x4096", 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /__tests__/config-manager.test.js: -------------------------------------------------------------------------------- 1 | import { getConfig } from '../bin/config-manager.js' 2 | import { promises as fs } from 'fs' 3 | // eslint-disable-next-line node/no-extraneous-import 4 | import { jest } from '@jest/globals' 5 | 6 | test('returns the result of a config file', async () => { 7 | const mockedConfig = { 8 | output: { 9 | markdownFile: 'README-gigs.md' 10 | }, 11 | preContent: [ 12 | { 13 | raw: "

hello

" 14 | }, 15 | { 16 | format: [ 17 | { 18 | h1: 'hello' 19 | } 20 | ] 21 | } 22 | ] 23 | } 24 | 25 | jest 26 | .spyOn(fs, 'readFile') 27 | .mockImplementation(() => Promise.resolve(JSON.stringify(mockedConfig))) 28 | 29 | const gigsConfig = await getConfig() 30 | expect(gigsConfig).toMatchObject(mockedConfig) 31 | }) 32 | -------------------------------------------------------------------------------- /__tests__/fs.test.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import path from 'path' 3 | import { getAllFiles } from '../src/utils/fs.js' 4 | 5 | const __dirname = new URL('.', import.meta.url).pathname 6 | 7 | test('fs utility is able to successfully recursive directories to find markdown files', async () => { 8 | const directoryPath = path.join(__dirname, './__fixtures__/filesglobbing') 9 | const allFiles = await getAllFiles(directoryPath) 10 | for (const file of allFiles) { 11 | expect(file.endsWith('.md')).toBe(true) 12 | } 13 | expect(allFiles).toHaveLength(2) 14 | }) 15 | 16 | test('fs utility returns an empty array if it cant find a directory to work at', async () => { 17 | const directoryPath = path.join( 18 | __dirname, 19 | '/tmp/a/b/c/d/nonexistent/and/madeup' 20 | ) 21 | 22 | const allFiles = await getAllFiles(directoryPath) 23 | expect(allFiles).toHaveLength(0) 24 | }) 25 | -------------------------------------------------------------------------------- /__tests__/json-formatter.test.js: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals' 2 | import path from 'path' 3 | import { readFile } from 'fs/promises' 4 | 5 | import { generateDocument } from '../src/main.js' 6 | 7 | const __dirname = new URL('.', import.meta.url).pathname 8 | 9 | beforeEach(() => { 10 | jest.useFakeTimers('modern') 11 | jest.setSystemTime(new Date(Date.UTC(2022, 11, 14))) 12 | }) 13 | 14 | afterAll(() => jest.useRealTimers()) 15 | 16 | test('given a directory of markdown files ensure the JSON output is created', async () => { 17 | const filePath = path.join(__dirname, './__fixtures__/main-datafiles') 18 | await generateDocument({ sourceDirectory: filePath }) 19 | 20 | const jsonStringData = await readFile(path.join('README.json'), 'utf8') 21 | const jsonResult = JSON.parse(jsonStringData) 22 | expect(jsonResult).toMatchSnapshot() 23 | }) 24 | -------------------------------------------------------------------------------- /__tests__/main-e2e.test.js: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process' 2 | import { tmpdir } from 'os' 3 | import { unlinkSync, readFileSync } from 'fs' 4 | import path from 'path' 5 | 6 | const __dirname = new URL('.', import.meta.url).pathname 7 | test('spawn the cli to process data and ensure markdown file is generated correctly', async () => { 8 | const cliExecutable = path.join(__dirname, '/../bin/cli.js') 9 | const projectFixtures = path.join( 10 | __dirname, 11 | './__fixtures__/main-datafiles/myapp/pages' 12 | ) 13 | const projectOutputFile = path.join(tmpdir(), 'test.md') 14 | 15 | // specifically need to mutate the process.argv array to avoid the cli 16 | // generating an "updated at" timestamp so we can always match the snapshot 17 | // and so we add a TEST_E2E flag to it 18 | const spawnedCliEnvironmentVariables = process.env 19 | spawnedCliEnvironmentVariables.TEST_E2E = 'true' 20 | spawnedCliEnvironmentVariables.DEBUG = 'gigsboat:*' 21 | 22 | const cliData = spawnSync( 23 | cliExecutable, 24 | ['--source-directory', projectFixtures, '--output-file', projectOutputFile], 25 | { shell: true, env: spawnedCliEnvironmentVariables } 26 | ) 27 | 28 | const cliOutput = cliData.output.toString() 29 | 30 | console.log(cliOutput) 31 | expect(cliOutput).toContain('loaded configuration:') 32 | expect(cliOutput).toContain('source directory: ' + projectFixtures) 33 | expect(cliOutput).toContain('output file: ' + projectOutputFile) 34 | 35 | const expectedMarkdownOutput = readFileSync(projectOutputFile, 'utf8') 36 | expect(expectedMarkdownOutput).toMatchSnapshot() 37 | 38 | // cleanup temporary files 39 | unlinkSync(projectOutputFile) 40 | }) 41 | -------------------------------------------------------------------------------- /__tests__/main.test.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { jest } from '@jest/globals' 3 | import path from 'path' 4 | import { generateDocument } from '../src/main.js' 5 | 6 | const __dirname = new URL('.', import.meta.url).pathname 7 | 8 | beforeEach(() => { 9 | jest.useFakeTimers('modern') 10 | jest.setSystemTime(new Date(Date.UTC(2021, 11, 14))) 11 | }) 12 | 13 | afterAll(() => jest.useRealTimers()) 14 | 15 | test('given a directory of markdown files a full document is rendered', async () => { 16 | const filePath = path.join(__dirname, './__fixtures__/main-datafiles') 17 | const jsonResult = await generateDocument({ sourceDirectory: filePath }) 18 | expect(jsonResult).toMatchSnapshot() 19 | }) 20 | 21 | test('given a directory of markdown files a full document is rendered along with pre and post content', async () => { 22 | const filePath = path.join(__dirname, './__fixtures__/main-datafiles') 23 | const jsonResult = await generateDocument({ 24 | sourceDirectory: filePath, 25 | preContent: [ 26 | { 27 | raw: '

actual html is allowed too

' 28 | }, 29 | { 30 | format: [ 31 | { 32 | p: 'Liran Tal' 33 | } 34 | ] 35 | } 36 | ], 37 | postContent: [ 38 | { 39 | raw: '

rendered in postcontent

' 40 | }, 41 | { 42 | format: [ 43 | { 44 | p: 'Thank you!' 45 | } 46 | ] 47 | } 48 | ] 49 | }) 50 | expect(jsonResult).toMatchSnapshot() 51 | }) 52 | -------------------------------------------------------------------------------- /__tests__/md-formatter.test.js: -------------------------------------------------------------------------------- 1 | import { getConfig } from '../bin/config-manager.js' 2 | import { formatToMarkdown, getEventsMd } from '../src/utils/md-formatter.js' 3 | 4 | test('convert json object to markdown formatting', async () => { 5 | const json = [ 6 | { 7 | h1: 'great title' 8 | }, 9 | { 10 | p: 'and my paragraph is cool' 11 | } 12 | ] 13 | const markdown = formatToMarkdown(json) 14 | expect(markdown).toMatchSnapshot() 15 | }) 16 | 17 | test('process events json data to provide a complete markdown document', async () => { 18 | const events = [ 19 | { 20 | year: 2016, 21 | items: [ 22 | { 23 | attributes: { 24 | date: new Date('2016-01-01'), 25 | title: 'title', 26 | name: 'name', 27 | slides_url: 'https://example.com', 28 | recording_url: 'https://example.com', 29 | country_code: 'US', 30 | language: null 31 | } 32 | } 33 | ], 34 | stats: { 35 | total: 1 36 | } 37 | }, 38 | { 39 | year: 2021, 40 | items: [ 41 | { 42 | attributes: { 43 | date: new Date('2021-02-01'), 44 | title: 'title', 45 | type: 'conference', 46 | name: 'name', 47 | slides_url: 'https://example.com', 48 | recording_url: null, 49 | country_code: null, 50 | language: 'English' 51 | } 52 | }, 53 | { 54 | attributes: { 55 | date: new Date('2021-02-02'), 56 | title: 'title', 57 | type: 'meetup', 58 | name: 'name', 59 | slides_url: null, 60 | recording_url: 'https://example.com', 61 | country_code: 'US', 62 | language: 'English' 63 | } 64 | }, 65 | { 66 | attributes: { 67 | date: new Date('2021-02-02'), 68 | title: 'title', 69 | type: 'podcast', 70 | name: 'name', 71 | slides_url: null, 72 | recording_url: 'https://example.com', 73 | country_code: 'US', 74 | language: 'English' 75 | } 76 | }, 77 | { 78 | attributes: { 79 | date: new Date('2021-02-02'), 80 | title: 'title', 81 | type: 'article', 82 | name: 'name', 83 | slides_url: null, 84 | recording_url: 'https://example.com', 85 | country_code: 'US', 86 | language: 'English' 87 | } 88 | }, 89 | { 90 | attributes: { 91 | date: new Date('2021-02-02'), 92 | title: 'title', 93 | type: 'webinar', 94 | name: 'name', 95 | slides_url: null, 96 | recording_url: 'https://example.com', 97 | country_code: 'US', 98 | language: 'English' 99 | } 100 | }, 101 | { 102 | attributes: { 103 | date: new Date('2021-02-02'), 104 | title: 'title', 105 | type: 'webinar', 106 | name: 'name', 107 | slides_url: null, 108 | recording_url: 'https://example.com', 109 | country_code: 'US', 110 | language: 'English' 111 | } 112 | }, 113 | { 114 | attributes: { 115 | date: new Date('2021-02-02'), 116 | title: 'title', 117 | type: 'workshop', 118 | name: 'name', 119 | slides_url: null, 120 | recording_url: 'https://example.com', 121 | country_code: 'US', 122 | language: 'English' 123 | } 124 | }, 125 | { 126 | attributes: { 127 | date: new Date('2021-02-03'), 128 | title: 'title', 129 | type: 'workshop', 130 | name: 'name', 131 | slides_url: null, 132 | recording_url: 'https://example.com', 133 | country_code: 'US', 134 | language: 'English' 135 | } 136 | } 137 | ], 138 | stats: { 139 | total: 8, 140 | total_podcast: 1, 141 | total_conference: 1, 142 | total_webinar: 2, 143 | total_meetup: 1, 144 | total_article: 1, 145 | total_workshop: 2, 146 | total_other: 0 147 | } 148 | } 149 | ] 150 | 151 | const markdown = await getEventsMd(events) 152 | expect(markdown).toMatchSnapshot() 153 | }) 154 | 155 | test('process events images', async () => { 156 | const myConfig = { 157 | output: { 158 | includePictureGalleryYearly: true 159 | } 160 | } 161 | 162 | // @TODO what we actually need to do is refactor config-manager.js 163 | // to really have a getConfig that reads from a processed config 164 | // so it doesn't re-evaluate all the time, but then also have a 165 | // setConfig that allows to override the config 166 | await getConfig(myConfig) 167 | 168 | const events = [ 169 | { 170 | year: 2016, 171 | items: [ 172 | { 173 | attributes: { 174 | date: new Date('2016-01-01'), 175 | title: 'title', 176 | type: 'conference', 177 | name: 'name', 178 | slides_url: 'https://example.com', 179 | recording_url: 'https://example.com', 180 | country_code: 'US', 181 | language: null, 182 | images: [ 183 | 'https://pbs.twimg.com/media/CbgOxYzWAAAjvgp?format=jpg&name=4096x4096', 184 | 'https://pbs.twimg.com/media/CbgOy-pXEAE4PVp?format=jpg&name=4096x4096' 185 | ] 186 | } 187 | } 188 | ], 189 | stats: { 190 | total: 1 191 | } 192 | }, 193 | { 194 | year: 2017, 195 | items: [ 196 | { 197 | attributes: { 198 | date: new Date('2017-01-01'), 199 | title: 'title', 200 | name: 'name', 201 | type: 'conference', 202 | slides_url: 'https://example.com', 203 | recording_url: 'https://example.com', 204 | country_code: 'US', 205 | language: null 206 | } 207 | } 208 | ], 209 | stats: { 210 | total: 1 211 | } 212 | }, 213 | { 214 | year: 2021, 215 | items: [ 216 | { 217 | attributes: { 218 | date: new Date('2021-02-01'), 219 | title: 'title', 220 | type: 'conference', 221 | name: 'name', 222 | slides_url: 'https://example.com', 223 | recording_url: null, 224 | country_code: null, 225 | language: 'English', 226 | images: [ 227 | 'https://pbs.twimg.com/media/CbgOxYzWAAAjvgp?format=jpg&name=4096x4096' 228 | ] 229 | } 230 | }, 231 | { 232 | attributes: { 233 | date: new Date('2021-02-02'), 234 | title: 'title', 235 | type: 'meetup', 236 | name: 'name', 237 | slides_url: null, 238 | recording_url: 'https://example.com', 239 | country_code: 'US', 240 | language: 'English', 241 | images: [ 242 | 'https://pbs.twimg.com/media/CbgOy-pXEAE4PVp?format=jpg&name=4096x4096' 243 | ] 244 | } 245 | } 246 | ], 247 | stats: { 248 | total: 2, 249 | total_podcast: 0, 250 | total_conference: 1, 251 | total_webinar: 0, 252 | total_meetup: 1, 253 | total_article: 0, 254 | total_workshop: 0, 255 | total_other: 0 256 | } 257 | } 258 | ] 259 | 260 | const markdown = await getEventsMd(events) 261 | expect(markdown).toMatchSnapshot() 262 | }) 263 | -------------------------------------------------------------------------------- /__tests__/output-handler.test.js: -------------------------------------------------------------------------------- 1 | import { processOutput } from '../bin/output-handler.js' 2 | import { jest } from '@jest/globals' 3 | import fs from 'fs/promises' 4 | 5 | jest.spyOn(fs, 'writeFile') 6 | 7 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation() 8 | 9 | test('document is saved to file', async () => { 10 | fs.writeFile.mockReturnValue(null) 11 | 12 | await processOutput({ 13 | document: 'something something', 14 | outputFile: 'test.txt' 15 | }) 16 | expect(fs.writeFile).toHaveBeenCalledTimes(1) 17 | }) 18 | 19 | test('document is outputted to the stdout', async () => { 20 | fs.writeFile.mockReturnValue(null) 21 | 22 | await processOutput({ document: 'nothing at all' }) 23 | expect(consoleSpy).toHaveBeenCalledTimes(1) 24 | expect(consoleSpy).toHaveBeenCalledWith('nothing at all') 25 | }) 26 | -------------------------------------------------------------------------------- /__tests__/yaml-parser.test.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import path from 'path' 3 | 4 | import { convertToJson } from '../src/utils/yaml-parser.js' 5 | 6 | const __dirname = new URL('.', import.meta.url).pathname 7 | 8 | test('convert markdown file to json', async () => { 9 | const filePath = path.join(__dirname, './__fixtures__/datafiles/event-ok.md') 10 | const jsonResult = await convertToJson(filePath) 11 | expect(jsonResult).toMatchSnapshot() 12 | }) 13 | -------------------------------------------------------------------------------- /bin/cli-parser.js: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs' 2 | import DebugLogger from 'debug' 3 | import { hideBin } from 'yargs/helpers' 4 | 5 | const debug = DebugLogger('gigsboat:cli') 6 | 7 | export function parseCliArgs() { 8 | // supporting right now: 9 | // -s --source-directory= 10 | // -o --output-file= 11 | const cliOptions = yargs(hideBin(process.argv)) 12 | .options({ 13 | o: { 14 | alias: 'output-file', 15 | type: 'string' 16 | }, 17 | s: { 18 | alias: 'source-directory', 19 | type: 'string' 20 | } 21 | }) 22 | .parse() 23 | 24 | debug('processed cli args: ' + JSON.stringify(cliOptions)) 25 | return cliOptions 26 | } 27 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { parseCliArgs } from './cli-parser.js' 3 | import { getConfig } from './config-manager.js' 4 | import { processOutput } from './output-handler.js' 5 | import { generateDocument } from '../src/main.js' 6 | import DebugLogger from 'debug' 7 | 8 | const debug = DebugLogger('gigsboat:app') 9 | if (!DebugLogger.enabled('gigsboat:app')) { 10 | DebugLogger.enable('gigsboat:app') 11 | } 12 | 13 | async function init() { 14 | const cliArgs = parseCliArgs() 15 | 16 | // command line arguments always override config file 17 | // in order of precedence, and so: 18 | let gigsConfig = await getConfig() 19 | if (cliArgs.sourceDirectory) { 20 | gigsConfig.input.sourceDirectory = cliArgs.sourceDirectory 21 | } 22 | if (cliArgs.outputFile) { 23 | gigsConfig.output.markdownFile = cliArgs.outputFile 24 | } 25 | 26 | debug('loaded configuration:') 27 | debug(' - source directory: %s', gigsConfig.input.sourceDirectory) 28 | debug(' - output file: %s', gigsConfig.output.markdownFile) 29 | const document = await generateDocument({ 30 | sourceDirectory: gigsConfig.input.sourceDirectory, 31 | preContent: gigsConfig.preContent, 32 | postContent: gigsConfig.postContent 33 | }) 34 | 35 | await processOutput({ document, outputFile: gigsConfig.output.markdownFile }) 36 | debug('finished') 37 | } 38 | 39 | init() 40 | -------------------------------------------------------------------------------- /bin/config-manager.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import ajv from 'ajv' 3 | import path from 'path' 4 | import DebugLogger from 'debug' 5 | 6 | const configFileName = 'gigsboat.json' 7 | const __dirname = process.cwd() 8 | let gigsConfig = undefined 9 | 10 | const debug = DebugLogger('gigsboat:config') 11 | 12 | export async function getConfig(providedConfig) { 13 | if (gigsConfig) { 14 | debug('using config from cache') 15 | return gigsConfig 16 | } 17 | 18 | if (providedConfig) { 19 | gigsConfig = providedConfig 20 | } else { 21 | try { 22 | debug('opening config file for reading: %s', configFileName) 23 | const jsonConfigFileContents = await fs.readFile( 24 | path.join(__dirname, configFileName), 25 | 'utf8' 26 | ) 27 | gigsConfig = JSON.parse(jsonConfigFileContents) 28 | } catch (error) { 29 | // debug message and silently ignore 30 | debug('encountered error while processing JSON config file:') 31 | debug(error) 32 | } 33 | } 34 | 35 | gigsConfig = validateConfig(gigsConfig) 36 | return gigsConfig 37 | } 38 | 39 | function validateConfig(config) { 40 | if (!config) { 41 | debug('config is empty so falling back to defaults') 42 | config = {} 43 | } 44 | 45 | const gigsConfigSchema = { 46 | type: 'object', 47 | properties: { 48 | input: { 49 | type: 'object', 50 | properties: { 51 | sourceDirectory: { type: 'string', default: 'pages' } 52 | }, 53 | default: {} 54 | }, 55 | output: { 56 | type: 'object', 57 | properties: { 58 | markdownFile: { type: 'string', default: 'README-gigsfile.md' }, 59 | includePictureGalleryYearly: { type: 'boolean', default: true } 60 | }, 61 | default: {} 62 | }, 63 | preContent: { 64 | type: 'array', 65 | items: { 66 | type: 'object', 67 | properties: { 68 | raw: { type: 'string' }, 69 | format: { type: 'array', items: { type: 'object' } } 70 | } 71 | } 72 | }, 73 | postContent: { 74 | type: 'array', 75 | items: { 76 | type: 'object', 77 | properties: { 78 | raw: { type: 'string' }, 79 | format: { type: 'array', items: { type: 'object' } } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | const schemaValidator = new ajv({ useDefaults: true }) 87 | const schemaValidation = schemaValidator.compile(gigsConfigSchema) 88 | const isValid = schemaValidation(config) 89 | debug('is configuration schema valid: %s', isValid) 90 | return config 91 | } 92 | -------------------------------------------------------------------------------- /bin/output-handler.js: -------------------------------------------------------------------------------- 1 | import DebugLogger from 'debug' 2 | import { promises as fs } from 'fs' 3 | import path from 'path' 4 | 5 | const debug = DebugLogger('gigsboat:app') 6 | 7 | export async function processOutput({ document, outputFile }) { 8 | if (outputFile) { 9 | if (path.isAbsolute(outputFile)) { 10 | debug('detected absolute path for output file') 11 | } else { 12 | const __dirname = process.cwd() 13 | outputFile = path.join(__dirname, outputFile) 14 | } 15 | 16 | await fs.writeFile(outputFile, document) 17 | } else { 18 | console.log(document) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gigsboat/cli", 3 | "version": "0.0.0-development", 4 | "description": "Do you have a boatload of speaking gigs? Use this CLI to manage them all!", 5 | "main": "main.js", 6 | "bin": { 7 | "gigsboat": "bin/cli.js" 8 | }, 9 | "exports": { 10 | ".": "./src/main.js" 11 | }, 12 | "engines": { 13 | "node": ">=14.0.0" 14 | }, 15 | "type": "module", 16 | "files": [ 17 | "src", 18 | "bin" 19 | ], 20 | "scripts": { 21 | "lint": "eslint . && npm run lint:lockfile", 22 | "lint:fix": "eslint . --fix", 23 | "format": "prettier --config .prettierrc.json --write '**/*.js'", 24 | "test": "NODE_OPTIONS=--experimental-vm-modules npx jest", 25 | "test:watch": "NODE_OPTIONS=--experimental-vm-modules npx jest --watch", 26 | "coverage:view": "open-cli coverage/lcov-report/index.html", 27 | "semantic-release": "npx semantic-release", 28 | "lint:lockfile": "lockfile-lint --path package-lock.json --validate-https --allowed-hosts npm yarn", 29 | "prepare": "husky install" 30 | }, 31 | "author": { 32 | "name": "Liran Tal", 33 | "email": "liran.tal@gmail.com", 34 | "url": "https://github.com/lirantal" 35 | }, 36 | "license": "Apache-2.0", 37 | "keywords": [ 38 | "speaking" 39 | ], 40 | "homepage": "https://github.com/gigsboat/cli", 41 | "bugs": { 42 | "url": "https://github.com/gigsboat/cli/issues" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/gigsboat/cli.git" 47 | }, 48 | "dependencies": { 49 | "ajv": "^8.6.3", 50 | "country-emoji": "^1.5.6", 51 | "debug": "^4.3.4", 52 | "front-matter": "^4.0.2", 53 | "json2md": "^1.12.0", 54 | "marked": "^2.1.3", 55 | "yargs": "^17.5.1" 56 | }, 57 | "devDependencies": { 58 | "@babel/core": "^7.15.8", 59 | "@babel/eslint-parser": "^7.15.8", 60 | "@babel/plugin-syntax-top-level-await": "^7.14.5", 61 | "@commitlint/cli": "^13.2.1", 62 | "@commitlint/config-conventional": "^13.2.0", 63 | "@semantic-release/changelog": "^6.0.0", 64 | "@semantic-release/commit-analyzer": "^9.0.1", 65 | "@semantic-release/git": "^10.0.0", 66 | "@semantic-release/github": "^8.0.1", 67 | "@semantic-release/npm": "^8.0.2", 68 | "@semantic-release/release-notes-generator": "^10.0.2", 69 | "eslint": "^8.0.1", 70 | "eslint-plugin-anti-trojan-source": "^1.0.1", 71 | "eslint-plugin-jest": "^25.2.2", 72 | "eslint-plugin-node": "^11.1.0", 73 | "eslint-plugin-security": "^1.4.0", 74 | "eslint-plugin-standard": "^4.1.0", 75 | "husky": "^7.0.0", 76 | "jest": "^27.4.7", 77 | "lint-staged": "^11.2.3", 78 | "lockfile-lint": "^4.6.2", 79 | "open-cli": "^7.0.1", 80 | "prettier": "^2.4.1" 81 | }, 82 | "jest": { 83 | "transform": {}, 84 | "testEnvironment": "node", 85 | "verbose": true, 86 | "collectCoverage": true, 87 | "coverageThreshold": { 88 | "global": { 89 | "branches": 80, 90 | "functions": 80, 91 | "lines": 80, 92 | "statements": 80 93 | } 94 | }, 95 | "testPathIgnorePatterns": [ 96 | "/__tests__/.*/__fixtures__/.*" 97 | ], 98 | "collectCoverageFrom": [ 99 | "index.js", 100 | "src/**/*.{js,ts}" 101 | ], 102 | "testMatch": [ 103 | "**/*.test.js" 104 | ] 105 | }, 106 | "lint-staged": { 107 | "**/*.js": [ 108 | "npm run format" 109 | ] 110 | }, 111 | "commitlint": { 112 | "extends": [ 113 | "@commitlint/config-conventional" 114 | ] 115 | }, 116 | "eslintIgnore": [ 117 | "coverage/**" 118 | ], 119 | "babel": { 120 | "plugins": [ 121 | "@babel/plugin-syntax-top-level-await" 122 | ] 123 | }, 124 | "eslintConfig": { 125 | "plugins": [ 126 | "node", 127 | "security", 128 | "jest", 129 | "anti-trojan-source" 130 | ], 131 | "extends": [ 132 | "plugin:node/recommended" 133 | ], 134 | "rules": { 135 | "anti-trojan-source/no-bidi": "error", 136 | "node/no-unsupported-features/es-syntax": [ 137 | "error", 138 | { 139 | "ignores": [ 140 | "dynamicImport", 141 | "modules" 142 | ] 143 | } 144 | ], 145 | "no-process-exit": "warn", 146 | "jest/no-disabled-tests": "error", 147 | "jest/no-focused-tests": "error", 148 | "jest/no-identical-title": "error", 149 | "node/no-unsupported-features": "off", 150 | "node/no-unpublished-require": "off", 151 | "node/no-extraneous-import": "off", 152 | "security/detect-non-literal-fs-filename": "warn", 153 | "security/detect-unsafe-regex": "error", 154 | "security/detect-buffer-noassert": "error", 155 | "security/detect-child-process": "error", 156 | "security/detect-disable-mustache-escape": "error", 157 | "security/detect-eval-with-expression": "error", 158 | "security/detect-no-csrf-before-method-override": "error", 159 | "security/detect-non-literal-regexp": "error", 160 | "security/detect-object-injection": "warn", 161 | "security/detect-possible-timing-attacks": "error", 162 | "security/detect-pseudoRandomBytes": "error", 163 | "space-before-function-paren": "off", 164 | "object-curly-spacing": "off" 165 | }, 166 | "parser": "@babel/eslint-parser", 167 | "parserOptions": { 168 | "sourceType": "module", 169 | "ecmaFeatures": { 170 | "impliedStrict": true 171 | } 172 | } 173 | }, 174 | "release": { 175 | "branches": [ 176 | "main", 177 | "master" 178 | ], 179 | "analyzeCommits": { 180 | "preset": "angular", 181 | "releaseRules": [ 182 | { 183 | "type": "docs", 184 | "release": "patch" 185 | }, 186 | { 187 | "type": "refactor", 188 | "release": "patch" 189 | }, 190 | { 191 | "type": "style", 192 | "release": "patch" 193 | } 194 | ] 195 | } 196 | }, 197 | "plugins": [ 198 | "@semantic-release/commit-analyzer", 199 | "@semantic-release/release-notes-generator", 200 | [ 201 | "@semantic-release/changelog", 202 | { 203 | "changelogFile": "CHANGELOG.md" 204 | } 205 | ], 206 | "@semantic-release/npm", 207 | [ 208 | "@semantic-release/git", 209 | { 210 | "assets": [ 211 | "CHANGELOG.md" 212 | ] 213 | } 214 | ], 215 | "@semantic-release/github" 216 | ], 217 | "publishConfig": { 218 | "access": "public", 219 | "registry": "https://registry.npmjs.org/" 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import marked from 'marked' 3 | import DebugLogger from 'debug' 4 | 5 | import { getAllFiles } from './utils/fs.js' 6 | import { convertToJson } from './utils/yaml-parser.js' 7 | import { 8 | getEventsMd, 9 | formatToMarkdown, 10 | getStatsBadges 11 | } from './utils/md-formatter.js' 12 | import { createYearBuckets, getEventsStats } from './utils/content-manager.js' 13 | import { getJsonFormat } from './utils/json-parser.js' 14 | import { writeFile } from 'fs/promises' 15 | 16 | export { formatToMarkdown, generateGigs, generateDocument } 17 | 18 | const debug = DebugLogger('gigsboat:app') 19 | 20 | async function generateDocument({ sourceDirectory, preContent, postContent }) { 21 | let markdownOutputPreContent = '' 22 | let markdownOutputPostContent = '' 23 | let document = '' 24 | 25 | const { entriesForYearMarkdown, entriesByBucket } = await generateGigs({ 26 | sourceDirectory 27 | }) 28 | 29 | const gigsJsonData = await getJsonFormat({ gigsData: entriesByBucket }) 30 | gigsJsonData.generatedAt = new Date().toISOString() 31 | await writeFile('README.json', JSON.stringify(gigsJsonData, null, 2)) 32 | 33 | if (preContent) { 34 | markdownOutputPreContent = processCustomContent({ 35 | contents: preContent 36 | }) 37 | } 38 | 39 | if (postContent) { 40 | markdownOutputPostContent = processCustomContent({ 41 | contents: postContent 42 | }) 43 | } 44 | 45 | const statsHeader = `
${marked.parse( 46 | getStatsBadges(entriesByBucket) 47 | )}
48 | ` 49 | 50 | const currentDate = new Date().toISOString() 51 | let footer = '' 52 | if (!process.env.TEST_E2E) { 53 | footer += `*page updated on ${currentDate}*\n\n` 54 | } 55 | footer += `powered by [gigsboat/cli](https://github.com/gigsboat/cli)` 56 | 57 | document += 58 | statsHeader + 59 | markdownOutputPreContent + 60 | '\n' + 61 | entriesForYearMarkdown + 62 | '\n' + 63 | markdownOutputPostContent + 64 | '\n' + 65 | footer 66 | 67 | return document 68 | } 69 | 70 | function processCustomContent({ contents }) { 71 | let markdownContent = '' 72 | for (const content of contents) { 73 | if (content.hasOwnProperty('raw')) { 74 | markdownContent += content.raw + '\n' 75 | } 76 | 77 | if (content.hasOwnProperty('format')) { 78 | markdownContent += formatToMarkdown(content.format) + '\n' 79 | } 80 | } 81 | 82 | return markdownContent 83 | } 84 | 85 | async function generateGigs({ sourceDirectory }) { 86 | let directoryPath = path.join(process.cwd(), sourceDirectory) 87 | if (path.isAbsolute(sourceDirectory)) { 88 | debug('detected absolute path for source directory') 89 | directoryPath = sourceDirectory 90 | } 91 | 92 | const allFilesFlatList = await getAllFiles(directoryPath) 93 | 94 | const entries = [] 95 | for (const filePath of allFilesFlatList) { 96 | const json = await convertToJson(filePath) 97 | 98 | // push the relative path to the file as part of the data object 99 | let fileDirectoryPath = path.relative(directoryPath, filePath) 100 | if (!path.isAbsolute(sourceDirectory)) { 101 | fileDirectoryPath = path.join(sourceDirectory, fileDirectoryPath) 102 | } 103 | json.fileRelativePath = fileDirectoryPath 104 | 105 | entries.push(json) 106 | } 107 | 108 | const entriesByBucket = await getEntriesByBuckets(entries) 109 | const entriesForYearMarkdown = await getEventsMd( 110 | entriesByBucket.bucketsByYear 111 | ) 112 | return { 113 | entriesForYearMarkdown, 114 | entriesByBucket 115 | } 116 | } 117 | 118 | async function getEntriesByBuckets(entries) { 119 | // @TODO these buckets: 120 | // const bucketsLanguage = {} 121 | // const bucketsCountry = {} 122 | 123 | const bucketsByYear = createYearBuckets(entries) 124 | const statsTotal = getEventsStats(entries) 125 | 126 | return { 127 | bucketsByYear, 128 | stats: statsTotal 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/utils/content-manager.js: -------------------------------------------------------------------------------- 1 | export { createYearBuckets, getEventsStats } 2 | 3 | function sortByDateOrYear(a, b) { 4 | if (a.hasOwnProperty('year') && b.hasOwnProperty('year')) { 5 | return new Date(b.year) - new Date(a.year) 6 | } else { 7 | return new Date(b.attributes.date) - new Date(a.attributes.date) 8 | } 9 | } 10 | 11 | function getEventsStats(entries) { 12 | const stats = { 13 | total: entries.length, 14 | total_podcast: 0, 15 | total_conference: 0, 16 | total_webinar: 0, 17 | total_meetup: 0, 18 | total_article: 0, 19 | total_workshop: 0, 20 | total_other: 0 21 | } 22 | 23 | for (const entry of entries) { 24 | const { attributes } = entry 25 | const { type } = attributes 26 | 27 | if (type === 'podcast') { 28 | stats.total_podcast += 1 29 | } else if (type === 'conference') { 30 | stats.total_conference += 1 31 | } else if (type === 'webinar') { 32 | stats.total_webinar += 1 33 | } else if (type === 'meetup') { 34 | stats.total_meetup += 1 35 | } else if (type === 'article') { 36 | stats.total_article += 1 37 | } else if (type === 'workshop') { 38 | stats.total_workshop += 1 39 | } else { 40 | stats.total_other += 1 41 | } 42 | } 43 | 44 | return stats 45 | } 46 | 47 | function createYearBuckets(entries) { 48 | const bucketsYear = {} 49 | 50 | // sort entries by date in descending order 51 | entries.sort(sortByDateOrYear) 52 | 53 | // loop them to gather them into yearly buckets 54 | for (const entry of entries) { 55 | const { attributes } = entry 56 | const { date } = attributes 57 | 58 | const year = String(new Date(date).getFullYear()) 59 | if (bucketsYear[year] === undefined) { 60 | bucketsYear[year] = { 61 | year: year, 62 | items: [entry] 63 | } 64 | } else { 65 | bucketsYear[year].items.push(entry) 66 | } 67 | } 68 | 69 | // augment the yearlyItems with stats 70 | for (const year of Object.keys(bucketsYear)) { 71 | const { items } = bucketsYear[year] 72 | bucketsYear[year].stats = getEventsStats(items) 73 | } 74 | 75 | // loop over the yearly buckets and sort those in yearly descending order 76 | const yearlyItems = Object.values(bucketsYear) 77 | yearlyItems.sort(sortByDateOrYear) 78 | 79 | return yearlyItems 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/fs.js: -------------------------------------------------------------------------------- 1 | import { readdir } from 'fs/promises' 2 | import path from 'path' 3 | 4 | /** 5 | * 6 | * @param {*} directoryPath 7 | * @returns array of files 8 | */ 9 | export async function getAllFiles(directoryPath) { 10 | const filesFound = [] 11 | try { 12 | const files = await readdir(directoryPath, { withFileTypes: true }) 13 | for (const file of files) { 14 | if (file.isDirectory()) { 15 | const filesFoundRecursively = await getAllFiles( 16 | path.join(directoryPath, file.name) 17 | ) 18 | filesFound.push(...filesFoundRecursively) 19 | } else { 20 | filesFound.push(path.join(directoryPath, file.name)) 21 | } 22 | } 23 | 24 | return filesFound 25 | } catch (err) { 26 | // console.error(err) 27 | return [] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/json-parser.js: -------------------------------------------------------------------------------- 1 | export async function getJsonFormat({ gigsData }) { 2 | const totalEventsData = formatJson(gigsData) 3 | return totalEventsData 4 | } 5 | 6 | function formatJson(gigsData) { 7 | let data = {} 8 | data.items = [] 9 | 10 | for (const yearlyEntries of gigsData.bucketsByYear) { 11 | for (const yearlyItem of yearlyEntries.items) { 12 | data.items.push(yearlyItem.attributes) 13 | } 14 | } 15 | 16 | data.stats = gigsData.stats 17 | 18 | return data 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/md-formatter.js: -------------------------------------------------------------------------------- 1 | import json2md from 'json2md' 2 | import { getConfig } from '../../bin/config-manager.js' 3 | import { flag as countryFlag, name as countryName } from 'country-emoji' 4 | 5 | json2md.converters.rawHTML = function (input) { 6 | return input 7 | } 8 | 9 | export function formatToMarkdown(data) { 10 | return json2md(data) 11 | } 12 | 13 | export async function getEventsMd(events) { 14 | let markdownEventsContent = '' 15 | for (const yearlyItems of events) { 16 | const eventsByYear = await eventsListForYear(yearlyItems) 17 | markdownEventsContent += eventsByYear + '\n' 18 | } 19 | 20 | const markdown = getTableOfContents(events) + '\n' + markdownEventsContent 21 | return markdown 22 | } 23 | 24 | function getTableOfContents(events) { 25 | const tableOfContents = [] 26 | tableOfContents.push({ 27 | h1: 'Table of Contents' 28 | }) 29 | 30 | const markdownYearsItems = [] 31 | for (const yearlyItems of events) { 32 | markdownYearsItems.push( 33 | `[Year of ${yearlyItems.year}](#${yearlyItems.year}) - total events ${yearlyItems.stats.total}` 34 | ) 35 | } 36 | 37 | tableOfContents.push({ 38 | ul: markdownYearsItems 39 | }) 40 | 41 | return json2md(tableOfContents) 42 | } 43 | 44 | export function getStatsBadges(events) { 45 | const statsBadges = [] 46 | 47 | const badgeForTotalEvents = events.stats.total 48 | ? `![Total Events](https://img.shields.io/badge/total-${events.stats.total}-blue?style=flat-square)` 49 | : 0 50 | const badgeForTotalConferences = events.stats.total_conference 51 | ? `![Total Conferences](https://img.shields.io/badge/conferences-${events.stats.total_conference}-red?style=flat-square)` 52 | : 0 53 | const badgeForTotalPodcasts = events.stats.total_podcast 54 | ? `![Total Podcasts](https://img.shields.io/badge/podcasts-${events.stats.total_podcast}-yellow?style=flat-square)` 55 | : 0 56 | const badgeForTotalWebinars = events.stats.total_webinar 57 | ? `![Total Webinars](https://img.shields.io/badge/webinars-${events.stats.total_webinar}-lightgrey?style=flat-square)` 58 | : 0 59 | const badgeForTotalMeetups = events.stats.total_meetup 60 | ? `![Total Meetups](https://img.shields.io/badge/meetups-${events.stats.total_meetup}-violet?style=flat-square)` 61 | : 0 62 | const badgeForTotalArticles = events.stats.total_article 63 | ? `![Total Podcasts](https://img.shields.io/badge/articles-${events.stats.total_article}-green?style=flat-square)` 64 | : 0 65 | const badgeForTotalWorkshops = events.stats.total_workshop 66 | ? `![Total Workshops](https://img.shields.io/badge/workshops-${events.stats.total_workshop}-orange?style=flat-square)` 67 | : 0 68 | 69 | statsBadges.push({ 70 | p: `${badgeForTotalEvents ? badgeForTotalEvents : ''} ${ 71 | badgeForTotalMeetups ? badgeForTotalMeetups : '' 72 | } ${badgeForTotalConferences ? badgeForTotalConferences : ''} ${ 73 | badgeForTotalPodcasts ? badgeForTotalPodcasts : '' 74 | } ${badgeForTotalWebinars ? badgeForTotalWebinars : ''} ${ 75 | badgeForTotalArticles ? badgeForTotalArticles : '' 76 | } ${badgeForTotalWorkshops ? badgeForTotalWorkshops : ''}` 77 | }) 78 | 79 | return json2md(statsBadges) + '\n' 80 | } 81 | 82 | async function eventsListForYear(eventsOfYear) { 83 | const gigsConfig = await getConfig() 84 | 85 | const eventsByYear = [] 86 | let eventsImageGalleryItems = [] 87 | 88 | eventsByYear.push({ 89 | h1: eventsOfYear.year 90 | }) 91 | eventsByYear.push(getStatsBadges(eventsOfYear)) 92 | 93 | const eventsTableEntries = [] 94 | eventsOfYear.items.forEach((event) => { 95 | const eventDate = `${event.attributes.date.getUTCFullYear()}-${ 96 | event.attributes.date.getMonth() + 1 97 | }-${event.attributes.date.getDate()}` 98 | 99 | const pathToFileOnGitHub = encodeURI(event.fileRelativePath) 100 | 101 | eventsTableEntries.push({ 102 | Date: eventDate, 103 | Event: event.attributes.name, 104 | Title: event.fileRelativePath 105 | ? `[${event.attributes.title}](${pathToFileOnGitHub})` 106 | : event.attributes.title, 107 | Slides: event.attributes.slides_url 108 | ? `[Slides](${event.attributes.slides_url})` 109 | : '', 110 | Recording: event.attributes.recording_url 111 | ? `[Recording](${event.attributes.recording_url})` 112 | : '', 113 | Location: event.attributes.country_code 114 | ? `[${countryFlag(event.attributes.country_code)}](## "${countryName( 115 | event.attributes.country_code 116 | )}")` 117 | : '', 118 | Language: event.attributes.language ? event.attributes.language : '' 119 | }) 120 | 121 | if (event.attributes.images) { 122 | const imagesList = event.attributes.images.filter((item) => !!item) 123 | eventsImageGalleryItems = eventsImageGalleryItems.concat(imagesList) 124 | } 125 | }) 126 | 127 | if (gigsConfig?.output?.includePictureGalleryYearly === true) { 128 | let tableItems = [] 129 | let tableHTML = '' 130 | 131 | for (let i = 0; i <= eventsImageGalleryItems.length - 1; i + 7) { 132 | tableItems.push(eventsImageGalleryItems.splice(i, 8)) 133 | } 134 | 135 | if (tableItems.length) { 136 | tableHTML += '' + '\n' 137 | tableItems.forEach((itemsRow) => { 138 | if (itemsRow.length) { 139 | tableHTML += ' ' + '\n' 140 | itemsRow.forEach((item) => { 141 | tableHTML += 142 | ` ` + 143 | '\n' 144 | }) 145 | tableHTML += ' ' + '\n' 146 | } 147 | }) 148 | tableHTML += '
' + '\n' 149 | } 150 | 151 | eventsByYear.push(json2md({ rawHTML: tableHTML })) 152 | } 153 | 154 | eventsByYear.push({ 155 | table: { 156 | headers: [ 157 | 'Date', 158 | 'Event', 159 | 'Title', 160 | 'Slides', 161 | 'Recording', 162 | 'Location', 163 | 'Language' 164 | ], 165 | rows: eventsTableEntries 166 | } 167 | }) 168 | 169 | return json2md(eventsByYear) + '\n' 170 | } 171 | -------------------------------------------------------------------------------- /src/utils/yaml-parser.js: -------------------------------------------------------------------------------- 1 | import frontMatter from 'front-matter' 2 | import { readFile } from 'fs/promises' 3 | 4 | export async function convertToJson(filePath) { 5 | const fileContent = await readFile(filePath, 'utf8') 6 | const json = frontMatter(fileContent) 7 | return json 8 | } 9 | --------------------------------------------------------------------------------