├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── architecture.md ├── docs ├── app.css ├── app.js ├── fontawesome-webfont-1nyrICN.eot ├── fontawesome-webfont-1odHW_R.woff2 ├── fontawesome-webfont-3WIM-CR.ttf ├── fontawesome-webfont-FXn0DBT.svg ├── fontawesome-webfont-o-InSMT.woff ├── index.html ├── page_structure-Q9TefGA.svg ├── style.css └── style.js ├── example ├── .gitignore ├── css │ ├── exerslide.css │ ├── homepage.css │ ├── normalize.css │ └── style.css ├── exerslide-help.md ├── exerslide.config.js ├── index.html ├── js │ ├── MasterLayout.js │ ├── SlideLayout.js │ ├── a11yExamples.js │ ├── components │ │ ├── PageHeader.js │ │ ├── TOC.js │ │ ├── Toolbar.js │ │ └── css │ │ │ ├── toc.css │ │ │ └── toolbar.css │ └── presentation.js ├── layouts │ └── Editor.js ├── package.json ├── references.yml ├── slides │ ├── 000-Intro │ │ ├── 000-exerslide.md │ │ └── 005-quick_start.md │ ├── 005-slides │ │ ├── 000-slides.md │ │ ├── 010-organize_slides.md │ │ ├── 020-layouts.md │ │ ├── 030-content_type.md │ │ └── 040-meta_data.md │ ├── 007-building │ │ └── 000-build_presentation.md │ ├── 010-configuration │ │ ├── 000-configuration.md │ │ ├── 010-exerslide.config.js.md │ │ └── 020-webpack.config.js.md │ ├── 020-layouts │ │ ├── 000-layouts_explained.md │ │ ├── 010-center_layout.md │ │ ├── 014-center_layout_example.md │ │ ├── 015-image_center_layout_example.md │ │ ├── 016-full_screen_center_layout_example.md │ │ ├── 020-column_layout.md │ │ ├── 025-column_layout_example.md │ │ ├── 026-column_layout_example2.md │ │ ├── 030-bulletlist_layout.md │ │ └── 035-bulletlist_layout_example.md │ ├── 025-customization │ │ ├── 000-intro.md │ │ ├── 005-custom_css.md │ │ ├── 010-custom_master.md │ │ └── images │ │ │ └── page_structure.svg │ ├── 030-advanced │ │ ├── 010-custom_layouts.md │ │ └── 015-custom_layouts_example.md │ └── 050-features │ │ ├── 000-image_size.md │ │ ├── 010-slide_references.md │ │ ├── 020-foundation_fontawesome.md │ │ ├── 030-a11y.md │ │ └── 10-error_example.md ├── statics │ └── index.html └── webpack.config.js ├── package.json ├── packages ├── .eslintrc.yml ├── exerslide-cli │ ├── bin │ │ ├── __tests__ │ │ │ ├── .eslintrc │ │ │ └── exerslide-test.js │ │ └── exerslide.js │ ├── package.json │ └── scripts │ │ └── test-utils.js ├── exerslide-plugin-bulletlist-layout │ ├── README.md │ ├── layouts │ │ ├── BulletList.js │ │ └── css │ │ │ └── bulletList.css │ └── package.json ├── exerslide-plugin-center-layout │ ├── README.md │ ├── layouts │ │ ├── Center.js │ │ └── css │ │ │ └── Center.css │ └── package.json ├── exerslide-plugin-column-layout │ ├── README.md │ ├── layouts │ │ ├── Column.js │ │ └── css │ │ │ └── Column.css │ └── package.json ├── exerslide-plugin-html-converter │ ├── README.md │ ├── contentTypes │ │ └── text_html.js │ └── package.json ├── exerslide-plugin-javascriptexercise-layout │ ├── README.md │ ├── js │ │ └── withoutComments.js │ ├── layouts │ │ └── JavaScriptExercise.js │ └── package.json ├── exerslide-plugin-markdown-converter │ ├── README.md │ ├── __tests__ │ │ ├── .eslintrc │ │ └── extractLanguageHighlights-test.js │ ├── contentTypes │ │ └── text_x-markdown.js │ ├── extractLanguageHighlights.js │ ├── init.js │ ├── package.json │ ├── reactFence.js │ └── utils │ │ ├── markdownItBlankLinks.js │ │ ├── markdownItFillSlideTitle.js │ │ ├── markdownItReactFence.js │ │ ├── registerLanguage.js │ │ └── renderIntoDOM.js └── exerslide │ ├── .babelrc │ ├── README.md │ ├── browser-plugins │ ├── .eslintrc.yml │ ├── __tests__ │ │ ├── .eslintrc.yml │ │ └── scaledContent-test.js │ ├── contentVisibility.js │ ├── debugInformation.js │ ├── keyboardNavigation.js │ ├── scaledContent.js │ └── scriptStore.js │ ├── browser.js │ ├── browser │ ├── .eslintrc.yml │ ├── Presentation.js │ ├── Store.js │ ├── extensionManager.js │ ├── navigation.js │ ├── pluginManager.js │ ├── present.js │ ├── pubSub.js │ ├── slideDataCache.js │ └── utils │ │ ├── __tests__ │ │ ├── .eslintrc │ │ ├── chapterHelper-test.js │ │ ├── imageDataHelper-test.js │ │ └── optionHelper-test.js │ │ ├── chapterHelper.js │ │ ├── deviceHelper.js │ │ ├── imageDataHelper.js │ │ ├── optionHelper.js │ │ ├── scriptHelper.js │ │ └── styleHelper.js │ ├── cli │ ├── __tests__ │ │ ├── .eslintrc │ │ ├── init-test.js │ │ └── test-cli.js │ ├── build.js │ ├── copy-defaults.js │ ├── index.js │ ├── init.js │ ├── serve.js │ ├── utils.js │ └── watch.js │ ├── components │ ├── .eslintrc.yml │ ├── ContentRenderer.js │ ├── Editor.js │ ├── ExtensionPoint.js │ ├── Output.js │ ├── Slide.js │ ├── __tests__ │ │ ├── .eslintrc.yml │ │ └── ExtensionPoint-test.js │ └── css │ │ └── editor.css │ ├── index.js │ ├── layouts │ ├── __ExerslideError__.js │ └── css │ │ └── __exerslideError__.css │ ├── lib │ ├── __tests__ │ │ ├── .eslintrc │ │ ├── initPlugins-test.js │ │ ├── initTransforms-test.js │ │ ├── scaffolder-test.js │ │ ├── slide-loader-test.js │ │ └── toSlideObject-test.js │ ├── builder.js │ ├── fs │ │ ├── __tests__ │ │ │ ├── .eslintrc │ │ │ ├── copyDir-test.js │ │ │ └── mkdirp-test.js │ │ ├── copyDir.js │ │ ├── mkdirp.js │ │ └── pathExists.js │ ├── initPlugins.js │ ├── initTransforms.js │ ├── scaffolder.js │ ├── slide-loader.js │ ├── slide_transforms │ │ ├── __tests__ │ │ │ ├── .eslintrc │ │ │ ├── atRequireExpansion-test.js │ │ │ ├── hashPath-test.js │ │ │ ├── registerContentType-test.js │ │ │ ├── registerLayout-test.js │ │ │ └── requireAssets-test.js │ │ ├── atRequireExpansion.js │ │ ├── hashPath.js │ │ ├── index.js │ │ ├── registerContentType.js │ │ ├── registerLayout.js │ │ ├── requireAssets.js │ │ └── utils │ │ │ ├── getErrorSlideObject.js │ │ │ └── transformHelper.js │ ├── toSlideObject.js │ └── utils │ │ ├── __tests__ │ │ ├── .eslintrc │ │ ├── fileLookupHelper-test.js │ │ ├── optionsHelper-test.js │ │ └── resolvePlugin-test.js │ │ ├── diff.js │ │ ├── fileLookupHelper.js │ │ ├── indent.js │ │ ├── optionHelper.js │ │ └── resolvePlugin.js │ ├── package.json │ ├── scaffolding │ ├── .eslintrc.yml │ ├── .gitignore │ ├── css │ │ ├── exerslide.css │ │ └── style.css │ ├── exerslide-help.md │ ├── exerslide.config.js │ ├── index.html │ ├── js │ │ ├── MasterLayout.js │ │ ├── SlideLayout.js │ │ ├── components │ │ │ ├── TOC.js │ │ │ ├── Toolbar.js │ │ │ ├── __tests__ │ │ │ │ ├── .eslintrc.yml │ │ │ │ └── TOC-test.js │ │ │ └── css │ │ │ │ ├── toc.css │ │ │ │ └── toolbar.css │ │ └── presentation.js │ ├── package.json │ ├── references.yml │ ├── slides │ │ └── 00-example.md │ └── webpack.config.js │ └── scripts │ ├── browser-test-setup.js │ └── test-utils.js └── scripts ├── check.sh ├── publish.sh ├── setupDevEnv.js ├── shared.sh ├── syncPackageVersion.js └── testNewProject.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 4 5 | - 5 6 | - 6 7 | - 7 8 | - stable 9 | install: 10 | - npm install 11 | - ./scripts/setupDevEnv.js ./example --test 12 | script: 13 | - ./scripts/check.sh 14 | - ./scripts/testNewProject.sh 15 | matrix: 16 | allow_failures: 17 | - node_js: stable 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to exerslide 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Our Development Process 7 | 8 | exerslide is complete developed on GitHub. If you want to contribute please send 9 | pull request or create issues. 10 | 11 | ## Pull Requests 12 | We actively welcome your pull requests. 13 | 14 | 1. Fork the repo and create your branch from `master` for core changes, 15 | or `gh-pages` for docs and website changes. 16 | 2. If you've added code that should be tested, add tests. 17 | 3. If you've changed APIs, update the documentation. 18 | 4. Ensure the test suite passes by running `npm test`. 19 | 5. Make sure your JavaScript code lints by running `npm run lint`. 20 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 21 | 22 | ## Contributor License Agreement ("CLA") 23 | 24 | In order to accept your pull request, we need you to submit a CLA. You only need 25 | to do this once to work on any of Facebook's open source projects. 26 | 27 | Complete your CLA here: 28 | 29 | ## Issues 30 | We use GitHub issues to track public bugs. Please ensure your description is 31 | clear and has sufficient instructions to be able to reproduce the issue. 32 | 33 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 34 | disclosure of security bugs. In those cases, please go through the process 35 | outlined on that page and do not file a public issue. 36 | 37 | ## Coding Style 38 | * Spaces for indentation rather than tabs - 2 for JavaScript. 39 | * 80 character line length 40 | 41 | ## License 42 | By contributing to exerslide, you agree that your contributions will be licensed 43 | under the license outlined in the LICENSE file in the same directory as this 44 | file. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE AGREEMENT 2 | 3 | For exerslide software 4 | 5 | Copyright (c) 2016, Facebook, Inc. All rights reserved. 6 | 7 | Facebook, Inc. ("Facebook") owns all right, title and interest, including all 8 | intellectual property and other proprietary rights, in and to the exerslide 9 | software (the "Software"). Subject to your compliance with these terms, you are 10 | hereby granted a non-exclusive, worldwide, royalty-free copyright license to 11 | (1) use and copy the Software; and (2) reproduce and distribute the Software as part 12 | of your own software ("Your Software"), provided Your Software does not consist 13 | solely of the Software; and (3) modify the Software for your own internal use. 14 | Facebook reserves all rights not expressly granted to you in this license agreement. 15 | 16 | THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. 19 | IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR EMPLOYEES 20 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 25 | IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 26 | OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /docs/fontawesome-webfont-1nyrICN.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/exerslide/785e240a60139f5788558cfae65b01a69dc03184/docs/fontawesome-webfont-1nyrICN.eot -------------------------------------------------------------------------------- /docs/fontawesome-webfont-1odHW_R.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/exerslide/785e240a60139f5788558cfae65b01a69dc03184/docs/fontawesome-webfont-1odHW_R.woff2 -------------------------------------------------------------------------------- /docs/fontawesome-webfont-3WIM-CR.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/exerslide/785e240a60139f5788558cfae65b01a69dc03184/docs/fontawesome-webfont-3WIM-CR.ttf -------------------------------------------------------------------------------- /docs/fontawesome-webfont-o-InSMT.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/exerslide/785e240a60139f5788558cfae65b01a69dc03184/docs/fontawesome-webfont-o-InSMT.woff -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | exerslide 19 | 20 | 21 | 22 |
23 | 24 |
25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # This hash helps exerslide to determine whether the file needs to be updated 2 | # or not. Please don't remove it. 3 | # @exerslide-file-hash 461156649f6073516a3ff976c72d3090 4 | 5 | /node_modules 6 | *.sketch 7 | -------------------------------------------------------------------------------- /example/css/homepage.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | #exerslide-page { 10 | flex-direction: column; 11 | } 12 | 13 | #exerslide-page > .page-header { 14 | margin: 0; 15 | padding: 0 10px; 16 | background-color: #555 17 | } 18 | 19 | #exerslide-page > .page-header a { 20 | color: white; 21 | } 22 | 23 | #exerslide-page > .page-wrapper { 24 | min-height: 0; 25 | flex-grow: 1; 26 | } 27 | 28 | #exerslide-page > .page-header > .title { 29 | color: #EFEFEF; 30 | font-size: 1.4em; 31 | font-weight: bold; 32 | padding: 10px; 33 | } 34 | 35 | #exerslide-page > .page-header > .navigation { 36 | padding: 10px; 37 | font-size: 1.4em; 38 | } 39 | 40 | #exerslide-page > .page-header > .navigation > a { 41 | display: inline-block; 42 | font-size: 0.8em; 43 | padding: 0 10px; 44 | } 45 | 46 | #exerslide-slide #exerslide-slide-title { 47 | text-align: left; 48 | } 49 | 50 | #exerslide-slide pre > code { 51 | font-size: 0.8em; 52 | } 53 | 54 | .pull-left { 55 | float: left; 56 | } 57 | 58 | .pull-right { 59 | float: right; 60 | } 61 | 62 | .accessible-example input[type="checkbox"] { 63 | margin: 0; 64 | } 65 | 66 | .accessible-example { 67 | margin-bottom: 1rem; 68 | } 69 | 70 | .accessible-example label + pre > code, 71 | .accessible-example label + div > pre > code, 72 | .accessible-example label + pre + pre > code, 73 | .accessible-example label + div + div > code { 74 | margin-top: 0; 75 | } 76 | 77 | .accessible-example label + div + div, 78 | .accessible-example label + pre + pre { 79 | display: none; 80 | } 81 | -------------------------------------------------------------------------------- /example/exerslide.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /* 10 | * This hash helps exerslide to determine whether the file needs to be updated 11 | * or not. Please don't remove it. 12 | * @exerslide-file-hash 2c68270c19bbcd837746864a3b15e7f2 13 | */ 14 | 15 | 'use strict'; 16 | 17 | const isTextPath = require('is-text-path'); 18 | const path = require('path'); 19 | 20 | module.exports = { 21 | 22 | /** Standard configuration options **/ 23 | 24 | /** 25 | * Paths to stylesheets. Can refer to modules. 26 | */ 27 | stylesheets: [ 28 | 'foundation-sites/dist/css/foundation.css', 29 | 'font-awesome/css/font-awesome.css', 30 | 'highlight.js/styles/solarized-light.css', 31 | './css/exerslide.css', 32 | './css/homepage.css', 33 | ], 34 | 35 | /** 36 | * This map allows you to automatically assign layouts based on file 37 | * extension. 38 | */ 39 | defaultLayouts: { 40 | }, 41 | 42 | /** 43 | 44 | * List of plugins to load. Plugins provide layouts, content type converters, 45 | * or other extensions to the exerslide or webpack config. 46 | * 47 | * A list of module names (exerslide-plugin-* can be omitted) or paths. 48 | */ 49 | plugins: [ 50 | 'bulletlist-layout', 51 | 'center-layout', 52 | 'column-layout', 53 | 'html-converter', 54 | 'markdown-converter', 55 | ], 56 | 57 | /** Advanced configuration options **/ 58 | 59 | /** 60 | * Absolute path to save the built presentation. 61 | */ 62 | out: path.join(__dirname, '../docs/'), 63 | 64 | /** 65 | * File path patterns used to watch slides for changes while creating the 66 | * presentation. 67 | */ 68 | slidePaths: [ 69 | './slides/*', 70 | './slides/*/*', 71 | ], 72 | 73 | /** 74 | * A function with which you can filter and reorder the file paths matched by 75 | * the patterns in "slidePaths". 76 | */ 77 | processSlides(paths) { 78 | return paths.filter(isTextPath).sort(); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | exerslide 19 | 20 | 21 | 22 |
23 | 24 |
25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/js/MasterLayout.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /* 10 | * This hash helps exerslide to determine whether the file needs to be updated 11 | * or not. Please don't remove it. 12 | * @exerslide-file-hash 9fb0ace1842e8bf7bee9c504e5f7a2f7 13 | */ 14 | 15 | import PageHeader from './components/PageHeader'; 16 | import ExtensionPoint from 'exerslide/components/ExtensionPoint'; 17 | import React from 'react'; 18 | import TOC from './components/TOC'; 19 | import Toolbar from './components/Toolbar'; 20 | 21 | /** 22 | * The master layout specifies the overall layout of the page, such as 23 | * the table of contents, a progress indicator and of course the slide itself. 24 | * The current slide component is passed as child to it. 25 | * 26 | * Be default the master layout renders a table of contents, navigation buttons 27 | * and the slide content: 28 | * 29 | * +----------------------------------------+ 30 | * |+---------+ +--------------------------+| 31 | * || | | +-----------------------+|| 32 | * || | | | ||| 33 | * || | | | Slide ||| 34 | * || TOC | | | ||| 35 | * || | | +-----------------------+|| 36 | * || | | +-----------------------+|| 37 | * || | | | Toolbar ||| 38 | * || | | +-----------------------+|| 39 | * |+---------+ +--------------------------+| 40 | * +----------------------------------------+ 41 | * 42 | */ 43 | export default function MasterLayout({className, children}) { 44 | return ( 45 |
46 | 47 |
48 | 49 | 50 |
51 | {children} 52 | 53 |
54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | MasterLayout.propTypes = { 61 | /** 62 | * CSS class names to add to the page. 63 | */ 64 | className: React.PropTypes.string, 65 | 66 | /** 67 | * The rendered slide is passed as child to the master layout. 68 | */ 69 | children: React.PropTypes.node, 70 | }; 71 | -------------------------------------------------------------------------------- /example/js/SlideLayout.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | /* 10 | * This hash helps exerslide to determine whether the file needs to be updated 11 | * or not. Please don't remove it. 12 | * @exerslide-file-hash 3280f175535da1ea028db9a841205223 13 | */ 14 | 15 | import ExtensionPoint from 'exerslide/components/ExtensionPoint'; 16 | import React from 'react'; 17 | 18 | /** 19 | * The base layout for every slide. This allows you do add additional 20 | * content to all slides before or after the content. 21 | */ 22 | export default function SlideLayout({children}) { 23 | return ( 24 | 25 |
26 | {children} 27 |
28 |
29 | ); 30 | } 31 | 32 | SlideLayout.propTypes = { 33 | /** 34 | * The current slide content and header are passed in as children by exerslide 35 | */ 36 | children: React.PropTypes.node, 37 | }; 38 | -------------------------------------------------------------------------------- /example/js/a11yExamples.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | export default function a11yExamples(exerslide) { 10 | exerslide.subscribe('CONFIG.SET', config => { 11 | config['markdown-converter'] = md => { 12 | md.use(require('markdown-it-container'), 'a11y', { 13 | render: function(tokens, index) { 14 | const token = tokens[index]; 15 | if (token.nesting === 1) { 16 | return '
' + 17 | ''; 20 | } 21 | return '
'; 22 | }, 23 | }); 24 | }; 25 | }); 26 | 27 | exerslide.subscribe('SLIDE.DID_MOUNT', setup); 28 | exerslide.subscribe('SLIDE.WILL_UNMOUNT', teardown); 29 | } 30 | 31 | function setup({slide}) { 32 | if (/^::: a11y/m.test(slide.content)) { 33 | setTimeout(() => { 34 | forEachInput(function(input) { 35 | console.dir(input); 36 | input.addEventListener('change', toggleCodeExample); 37 | }); 38 | }, 0); 39 | } 40 | } 41 | 42 | function teardown({slide}) { 43 | if (/^::: a11y/m.test(slide.content)) { 44 | forEachInput(function(input) { 45 | input.removeEventListener('change', toggleCodeExample); 46 | }); 47 | } 48 | }; 49 | 50 | function toggleCodeExample() { 51 | var defaultExample = this.parentNode.nextElementSibling; 52 | var accessibleExample = defaultExample.nextElementSibling; 53 | 54 | var toShow = this.checked ? accessibleExample : defaultExample; 55 | var toHide = this.checked ? defaultExample : accessibleExample; 56 | toShow.style.display = 'block'; 57 | toHide.style.display = 'none'; 58 | } 59 | 60 | function forEachInput(callback) { 61 | var inputs = global.document.querySelectorAll('.meta-data-code-toggle > input'); 62 | for (var i = 0; i < inputs.length; i++) { 63 | callback(inputs[i]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/js/components/PageHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function() { 4 | return ( 5 |
6 |
exerslide
7 |
8 | Docs 9 | GitHub 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /example/js/components/Toolbar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | /* 9 | * This hash helps exerslide to determine whether the file needs to be updated 10 | * or not. Please don't remove it. 11 | * @exerslide-file-hash ca97a5ed94cbd4463bea871d1d921eca 12 | */ 13 | 14 | import React from 'react'; 15 | import ExtensionPoint from 'exerslide/components/ExtensionPoint'; 16 | import {forward, back} from 'exerslide/browser'; 17 | 18 | import './css/toolbar.css'; 19 | 20 | /** 21 | * This components generates a previous and next buttons (rendered as arrows, 22 | * using Font Awesome) to navigate the presentation. 23 | */ 24 | export default function Toolbar({className}, {slideIndex, slides}) { 25 | const numberOfSlides = slides.length; 26 | 27 | return ( 28 | 29 |
33 | 41 | 46 | {' ' + (slideIndex + 1) + '/' + numberOfSlides + ' '} 47 | 48 | 56 |
57 |
58 | ); 59 | } 60 | 61 | Toolbar.propTypes = { 62 | className: React.PropTypes.string, 63 | }; 64 | 65 | Toolbar.contextTypes = { 66 | /** 67 | * This index of the current slide. 68 | */ 69 | slideIndex: React.PropTypes.number.isRequired, 70 | 71 | /** 72 | * Number of slides. 73 | */ 74 | slides: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, 75 | }; 76 | -------------------------------------------------------------------------------- /example/js/components/css/toc.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | /* 9 | * This hash helps exerslide to determine whether the file needs to be updated 10 | * or not. Please don't remove it. 11 | * @exerslide-file-hash c7626160cf3d53da56869dd8e56f3443 12 | */ 13 | 14 | .exerslide-toc-container { 15 | background-color: #F4F4F4; 16 | border-right: 1px solid #CCC; 17 | display: flex; 18 | flex-direction: column; 19 | max-width: 30%; 20 | min-width: 20em; 21 | } 22 | 23 | .exerslide-toc-container.collapsed { 24 | background-color: inherit; 25 | border-right: none; 26 | min-width: 0; 27 | position: absolute; 28 | z-index: 100; 29 | } 30 | 31 | ol.exerslide-toc-entries, 32 | ol.exerslide-toc-list { 33 | list-style: none; 34 | counter-reset: item; 35 | overflow-y: auto; 36 | margin: 0; 37 | } 38 | 39 | .exerslide-toc-list { 40 | padding-left: 1em; 41 | padding-right: 1em; 42 | } 43 | 44 | .exerslide-toc-entries { 45 | padding-left: 0; 46 | } 47 | 48 | .exerslide-toc-list > * { 49 | margin-bottom: 1em; 50 | } 51 | 52 | .exerslide-toc-title, 53 | .exerslide-toc-container.collapsed > .exerslide-toc-list { 54 | display: none; 55 | } 56 | 57 | .exerslide-toc-toggleButton { 58 | align-self: flex-end; 59 | background-color: transparent; 60 | border: none; 61 | color: #AAA; 62 | cursor: pointer; 63 | flex-shrink: 0; 64 | font-size: 1em; 65 | outline-width: thin; 66 | padding: 0.5em; 67 | } 68 | 69 | .exerslide-toc-toggleButton:hover { 70 | color: inherit; 71 | } 72 | 73 | .exerslide-toc-list > :first-child { 74 | margin-top: 0; 75 | } 76 | 77 | .exerslide-toc-entry, 78 | .exerslide-toc-chapter { 79 | display: block; 80 | } 81 | 82 | .exerslide-toc-entry::before, 83 | .exerslide-toc-chapter::before { 84 | content: counters(item, '.') ". "; 85 | counter-increment: item; 86 | } 87 | 88 | .exerslide-toc-heading { 89 | display: inline-block; 90 | margin: 0; 91 | margin-bottom: 0.5em; 92 | } 93 | 94 | .exerslide-toc-list > .exerslide-toc-entry::before, 95 | .exerslide-toc-list > .exerslide-toc-entry, 96 | .exerslide-toc-chapter::before, 97 | .exerslide-toc-heading { 98 | font-weight: bold; 99 | font-size: 1em; 100 | } 101 | 102 | .exerslide-toc-entry { 103 | padding: 0.1em 0; 104 | } 105 | 106 | .exerslide-toc-entry > a { 107 | color: inherit; 108 | } 109 | 110 | .exerslide-toc-entry, 111 | .exerslide-toc-entry > a { 112 | text-decoration: none; 113 | outline-width: thin; 114 | } 115 | 116 | .exerslide-toc-entry.active, 117 | .exerslide-toc-entry.active > a, 118 | .exerslide-toc-entry:hover { 119 | color: #428bca; 120 | } 121 | -------------------------------------------------------------------------------- /example/js/components/css/toolbar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | /* 9 | * This hash helps exerslide to determine whether the file needs to be updated 10 | * or not. Please don't remove it. 11 | * @exerslide-file-hash d664bd6574d5ed3e46c9a44d23794c19 12 | */ 13 | 14 | .exerslide-toolbar { 15 | box-sizing: border-box; 16 | color: #BBB; 17 | padding: 0 1.5em; 18 | text-align: right; 19 | width: 100%; 20 | } 21 | 22 | .exerslide-toolbar-button, 23 | .exerslide-toolbar-text { 24 | font-size: 1.05rem; 25 | } 26 | .exerslide-toolbar-button { 27 | background-color: transparent; 28 | cursor: pointer; 29 | color: #BBB; 30 | border: none; 31 | padding: 10px; 32 | } 33 | 34 | .exerslide-toolbar-button:focus, 35 | .exerslide-toolbar-button:hover { 36 | color: #555; 37 | } 38 | 39 | .exerslide-toolbar-button[disabled] { 40 | visibility: hidden; 41 | } 42 | 43 | @media(max-width: 768px) { 44 | .exerslide-toolbar { 45 | text-align: center; 46 | margin-bottom: 1em; 47 | } 48 | 49 | .exerslide-toolbar-button, 50 | .exerslide-toolbar-text { 51 | font-size: 1.5rem; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/layouts/Editor.js: -------------------------------------------------------------------------------- 1 | import Editor from 'exerslide/components/Editor'; 2 | import React from 'react'; 3 | import Output from 'exerslide/components/Output'; 4 | 5 | import 'codemirror/mode/htmlmixed/htmlmixed'; 6 | 7 | /** 8 | * Example layout that uses an editor 9 | */ 10 | export default class extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | value: props.content, 15 | }; 16 | 17 | this._onChange = this._onChange.bind(this); 18 | } 19 | 20 | _onChange(value) { 21 | this.setState({ 22 | value, 23 | }); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 | {this.props.title} 30 | 37 | 38 |
39 | 40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "dependencies": { 7 | "codemirror": "^5.7.0", 8 | "exerslide": "~1.1.2", 9 | "exerslide-plugin-bulletlist-layout": "^1.1.0", 10 | "exerslide-plugin-center-layout": "^1.1.0", 11 | "exerslide-plugin-column-layout": "^1.1.0", 12 | "exerslide-plugin-html-converter": "^1.0.0", 13 | "exerslide-plugin-markdown-converter": "^1.1.0", 14 | "font-awesome": "^4.4.0", 15 | "foundation-sites": "^6.3.0", 16 | "highlight.js": "^9.6.0", 17 | "markdown-it-container": "^2.0.0", 18 | "react": "^15.0.0", 19 | "react-dom": "^15.0.0" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer-loader": "^3.1.0", 23 | "babel-core": "^6.2.1", 24 | "babel-loader": "^6.2.0", 25 | "babel-plugin-transform-runtime": "^6.3.13 ", 26 | "babel-preset-es2015": "^6.1.18", 27 | "babel-preset-react": "^6.1.18", 28 | "babel-preset-stage-0": "^6.1.18", 29 | "babel-runtime": "^6.3.13", 30 | "css-loader": "^0.23.0", 31 | "extract-text-webpack-plugin": "^0.9.1", 32 | "file-loader": "^0.8.5", 33 | "html-loader": "^0.4.3", 34 | "html-webpack-plugin": "^2.22.0", 35 | "is-text-path": "^1.0.1", 36 | "json-loader": "^0.5.4", 37 | "raw-loader": "^0.5.1", 38 | "style-loader": "^0.13.0", 39 | "webpack": "^1.12.9", 40 | "yaml-loader": "^0.4.0" 41 | }, 42 | "exerslide": "@exerslide-file-hash e6ef250ab036a57c64fb8004065b237f" 43 | } 44 | -------------------------------------------------------------------------------- /example/references.yml: -------------------------------------------------------------------------------- 1 | # You can keep links to external sources here. This lets you avoid repeating the 2 | # same URL on different slides. The default markdown parser takes these into 3 | # account.The format is: "name: URL" 4 | # 5 | # Example: 6 | # 7 | # example: http://example.org 8 | 9 | gulp: http://gulpjs.com 10 | html: https://en.wikipedia.org/wiki/HTML 11 | markdown: https://en.wikipedia.org/wiki/Markdown 12 | media type: https://en.wikipedia.org/wiki/Media_type 13 | react: http://facebook.github.io/react/ 14 | webpack: http://webpack.github.io 15 | yaml: https://en.wikipedia.org/wiki/YAML 16 | node: https://nodejs.org/en/ 17 | nvm: https://github.com/creationix/nvm 18 | snake case: https://en.wikipedia.org/wiki/Snake_case 19 | camel case: https://en.wikipedia.org/wiki/CamelCase 20 | -------------------------------------------------------------------------------- /example/slides/000-Intro/000-exerslide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: exerslide 3 | chapter: Exerslide 4 | toc: What is exerslide? 5 | hide_toc: true 6 | style: | 7 | #exerslide-slide h1#exerslide-slide-title { 8 | font-size: 2.5rem; 9 | text-align: center; 10 | } 11 | #exerslide-slide { 12 | padding-bottom: 20px; 13 | } 14 | --- 15 | 16 | exerslide is a presentation / tutorial generation tool which converts a set of 17 | files into a self-contained HTML page. It uses [webpack][] and [React][]. 18 | 19 | ## Features 20 | 21 | ### Content first 22 | 23 | Creating the content of a slide should be as easy as possible. You should be 24 | able to focus on **what** to write, not get distracted by *how* it looks. 25 | 26 | Every slide is represented by a (text) file, written in your favorite markup 27 | language (e.g. [Markdown][]). The content (and metadata) is passed to a 28 | *layout*, which determines *how* the slide looks like and what it can do. 29 | 30 | Each slide can have its own layout, and more importantly, layouts are easy to 31 | create and can be shared. 32 | 33 | ### Customization 34 | 35 | exerslide tries to provide sensible defaults for overall page structure, 36 | layouts and CSS, while also providing a wide range of customization options, 37 | from simple CSS changes to elaborate build process changes. 38 | 39 | ### Accessibility 40 | 41 | All the components coming with exerslide pay special attention to 42 | accessibility. exerslide presentations work well with screen readers, whether 43 | you read the presentation or give it. 44 | 45 | ### Short feedback cycle 46 | 47 | Because exerslide uses [webpack][], it comes with a development server built 48 | in. Whenever you change one of your slides or presentation code, the browser 49 | will automatically reload the presentation. 50 | 51 | 55 | Get started! 56 | 57 | -------------------------------------------------------------------------------- /example/slides/000-Intro/005-quick_start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: get_started 3 | title: Get started 4 | --- 5 | 6 | exerslide is a set of [React][] components, JavaScript modules, and [webpack][] 7 | configuration files. The **command line tool** will copy these files into the 8 | project folder upon initalization. 9 | 10 | 0. Install [Node][] if you haven't already (consider using [nvm][] for simpler 11 | installation). 12 | 13 | 1. Install `exerslide-cli`. 14 | ```bash 15 | npm install -g exerslide-cli 16 | ``` 17 | 18 | 2. Create a new directory for your project and change into that directory. 19 | ```bash 20 | mkdir myPresentation 21 | cd myPresentation 22 | ``` 23 | 24 | 3. Initialize a new project. This will copy all necessary configuration and 25 | JavaScript files which you can edit later, and install all dependencies. 26 | ```bash 27 | exerslide init MyPresentation 28 | ``` 29 | 30 | 4. Start the local server. This will start webpack's development server and 31 | opens your default browser. Changes to slides or JavaScript files cause the 32 | browser to refresh. 33 | ```bash 34 | exerslide serve 35 | ``` 36 | 37 | 5. Add/edit slides in `slides`. 38 | -------------------------------------------------------------------------------- /example/slides/005-slides/000-slides.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: docs 3 | title: Structure of a slide 4 | chapter: Making Slides 5 | --- 6 | Each slide or page is created from a **text file**. Each file can contain a 7 | metadata header, delimited by lines containing only `---`. 8 | The metadata header is expressed in [YAML][]. You may already be familiar with 9 | this if you used other static site generators. 10 | 11 |
12 | 13 | **Note:** Depending on how you write YAML, whitespaces and indentation can be 14 | important. Screenreaders might not support these very well though, which can 15 | complicate creating slide headers. Because of this we are providing 16 | additional examples of slide headers with extra punctuation that better 17 | indicate where new lines and values start. Select the "Show accessible 18 | version" before each example to enable it. 19 | See [YAML language 20 | elements](https://en.wikipedia.org/wiki/YAML#Language_elements) for more 21 | information. 22 | 23 |
24 | 25 | 26 | ### Example 27 | 28 | ::: a11y 29 | ```yaml 30 | --- 31 | title: "This is the metadata section" 32 | --- 33 | Here is the content 34 | ``` 35 | 36 | ```yaml 37 | --- 38 | { 39 | title: "This is the metadata section" 40 | } 41 | --- 42 | Here is the content 43 | ``` 44 | ::: 45 | 46 | ## Metadata 47 | 48 | See [](#/metadata) for a full list of metadata options. In the above example, 49 | the `title` option defines the title to render above the content and in the 50 | navigation section. 51 | 52 | ## Content 53 | 54 | Out of the box, exerslide supports slides written in HTML or Markdown, but with 55 | the right plugin it could be something else completely. How to *structure* the 56 | content primarily depends on the **[layout](#/layouts)** that is used to render 57 | the slide. We will talk about this more in the following slides. 58 | -------------------------------------------------------------------------------- /example/slides/005-slides/010-organize_slides.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Order and organization of slides 3 | --- 4 | By default, the order in which the slides appear is determined by the 5 | lexicographical order of the file names. Aside from that you can name each file 6 | however you want. 7 | 8 | ### Example 9 | 10 | If your slides are in a folder with the following structure: 11 | 12 | ``` 13 | slides/ 14 | 00-intro.md 15 | 01-example.md 16 | 02-end.md 17 | ``` 18 | 19 | Then the order of slides will be 20 | 21 | 1. 00-intro 22 | 2. 01-example 23 | 3. 02-end 24 | 25 |
26 | 27 | This can be changed in `exerslide.config.js`. 28 | 29 |
30 | 31 | ## Chapters 32 | 33 | You can group slides into chapters by putting the corresponding files into a 34 | folder. The order of chapters is determined in the same way as the order of 35 | slides. 36 | 37 | ### Example 38 | 39 | ``` 40 | slides/ 41 | 00-Intro/ 42 | 00-slide1.md 43 | 01-slide2.md 44 | 01-Chapter1/ 45 | 00-slide1.md 46 | 01-slide2.md 47 | ``` 48 | 49 | The **name** of the chapter can be defined using the `chapter` metadata 50 | option in the *first slide* of the chapter: 51 | 52 | ### Example 53 | 54 | ``` 55 | --- 56 | title: Some title 57 | chapter: Intro 58 | --- 59 | Content goes here 60 | ``` 61 | -------------------------------------------------------------------------------- /example/slides/005-slides/020-layouts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Layouts 3 | --- 4 | 5 | You are probably familiar with the concept of layouts from existing 6 | presentation software products. Layouts define how a slide is structured. 7 | 8 | In exerslide, layouts are implemented as [React][] components. The component 9 | gets passed the slide content and other metadata. See [](#/layouts) for more 10 | details. 11 | 12 | Which layout to use for a slide can be configured in the metadata section via 13 | the `layout` option: 14 | 15 | ::: a11y 16 | ```yaml 17 | --- 18 | title: "Example title" 19 | layout: Center 20 | --- 21 | Example content 22 | ``` 23 | 24 | ```yaml 25 | --- 26 | { 27 | title: "Example title", 28 | layout: Center 29 | } 30 | --- 31 | Example content 32 | ``` 33 | ::: 34 | 35 | Because layouts are React components, i.e. implemented in JavaScript, they can 36 | enable interactive experiences. 37 | -------------------------------------------------------------------------------- /example/slides/005-slides/030-content_type.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: content-type 3 | title: Content types 4 | --- 5 | 6 | Earlier we said the content of a slide can be anything and written in any 7 | markup language. Wherever you can provide free content/text, you can use your 8 | preferred markup language, as long as you have the correct **converter**. Out 9 | of the box, exerslide comes with an **HTML** and a **Markdown** converter which 10 | should satisfy most use cases. 11 | 12 | **Note:** Layouts can require the content to be in a specific format or of a 13 | specific type. 14 | 15 | ## Type inference 16 | 17 | The content type of a slide is determined from the file extension, or can be 18 | explicitly set in the front matter through the `content_type` option: 19 | 20 | ::: a11y 21 | ```yaml 22 | --- 23 | title: "Some slide" 24 | content_type: text/html 25 | --- 26 |

Content

27 | ``` 28 | 29 | ```yaml 30 | --- 31 | { 32 | title: "Some slide", 33 | content_type: text/html, 34 | } 35 | --- 36 |

Content

37 | ``` 38 | ::: 39 | 40 | When inferred from the file extension, exerslide will use the (standardized) 41 | [media type][] name to find the correct converter. For example: 42 | 43 | - `*.md` files: `text/x-markdown` 44 | - `*.html` files: `text/html` 45 | -------------------------------------------------------------------------------- /example/slides/007-building/000-build_presentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building your presentation 3 | --- 4 | 5 | When generating (building) the presentation, exerslide takes all of the slides, 6 | JavaScript, CSS and image files and creates a standalone HTML page from them. 7 | 8 | exerslide uses [webpack][] to generate the final version of the presentation. 9 | 10 | ## `exerslide build` 11 | 12 | This will generate a "production" version of the presentation and store it in 13 | the output folder specified in `exerslide.config.js` (default: `./out`). 14 | 15 | ## `exerslide serve` 16 | 17 | This starts webpack's development server and opens your default browser. 18 | Changes to slide, JavaScript or other files will automatically reload the site. 19 | -------------------------------------------------------------------------------- /example/slides/010-configuration/000-configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: config_file 3 | title: Configuration Files 4 | chapter: Configuration 5 | --- 6 | 7 | `exerslide init` copies a couple of files into the presentation folder, 8 | including configuration files. One of exerslide's goals is it to provide good 9 | defaults "out of the box", but also to allow customization wherever possible. 10 | 11 | The two configuration files are 12 | 13 | - `exerslide.config.js`: exerslide-specific configuration options 14 | - `webpack.config.js`: Webpack configuration, including exerslide specific and 15 | custom loaders to process slide files. 16 | -------------------------------------------------------------------------------- /example/slides/010-configuration/010-exerslide.config.js.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: exerslide.config.js 3 | title: exerslide.config.js 4 | --- 5 | 6 | This file contains configuration options specific to exerslide, such as which 7 | CSS files to load. It is a JavaScript file which exports a configuration 8 | object. 9 | 10 |
11 | 12 | **Note:** Unlike slide headers, these options use [camel case][] as naming 13 | convention. This is more common in JavaScript and is also consistent with how 14 | options are named in `webpack.config.js`. This shouldn't be a big issue since 15 | all the possible options pre-exist in the default config file. 16 | 17 |
18 | 19 | ## Configuration options 20 | 21 | ### `stylesheets` 22 | 23 | ```javascript 24 | stylesheets: [ 25 | 'bootstrap/dist/css/bootstrap.css', 26 | 'font-awesome/css/font-awesome.css', 27 | 'highlight.js/styles/solarized-light.css', 28 | './css/style.css', 29 | ], 30 | ``` 31 | 32 | An array of file or module paths to stylesheets. These files are bundled in the 33 | order they are listed. The easiest way to overwrite default styles or define 34 | your own is to create a new CSS file and add it at the end of this array. 35 | 36 | ### `defaultLayouts` 37 | 38 | ```javascript 39 | defaultLayouts: { 40 | '.center.md': 'Center', 41 | }, 42 | ``` 43 | 44 | An object that maps file extensions to layout names. If you have many slides 45 | that use a specific layout, you can give each slide file the same file 46 | extension and use this option, instead of specifying the layout in the front 47 | matter of each slide. 48 | 49 | ### `plugins` 50 | 51 | ``` 52 | plugins: [ 53 | 'center-layout', 54 | 'twocolumn-layout', 55 | 'html-converter', 56 | 'markdown-converter', 57 | 'shared-urls', 58 | ], 59 | ``` 60 | 61 | An array of plugin names (or paths). These plugins are used to provide 62 | additional [layouts](#/layouts), [content type converters](#/contenttypes), or 63 | other functionally to webpack or exerslide. The convention is to prefix the 64 | name of exerslide specific plugins with `exerslide-plugin-`, but this prefix 65 | can be omitted in this list (i.e. `'center-layout'` will look for a module 66 | named `center-layout` and `exerslide-plugin-center-layout`). 67 | 68 | ### `out` 69 | 70 | ```javascript 71 | out: path.join(__dirname, './out'), 72 | ``` 73 | 74 | Absolute path to the output directory. It is directly passed to webpack. 75 | 76 | ### `slidePaths` 77 | 78 | ```javascript 79 | slidePaths: [ 80 | './slides/*', 81 | './slides/*/*', 82 | ], 83 | ``` 84 | 85 | A list of file patterns. These patterns are primarily used to detect the 86 | addition of new slides and changes to existing slides. By default it matches 87 | any file inside `./slides` and its immediate subfolders. 88 | 89 | ### `processSlides` 90 | 91 | ```javascript 92 | processSlides(paths) { 93 | return paths.filter(isTextPath).sort(); 94 | }, 95 | ``` 96 | 97 | A function that is passed the list of file paths matched by the patterns in 98 | `slidePaths`. This function allows you to further filter the matched paths or 99 | reorder them. 100 | -------------------------------------------------------------------------------- /example/slides/010-configuration/020-webpack.config.js.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: webpack.config.js 3 | --- 4 | 5 | This is a standard webpack configuration file. See the [webpack configuration 6 | documentation][config] for more information. There is one thing worth calling 7 | out though. 8 | 9 | ## `webpackConfig.slideLoader` 10 | 11 | `slideLoader` is an exerslide specific option that is passed to exerslide's own 12 | loader. It allows you to configure custom transforms that should be applied 13 | when processing slides. This is also used by plugins to extend exerslide. 14 | 15 | 23 | 24 | [config]: http://webpack.github.io/docs/configuration.html 25 | -------------------------------------------------------------------------------- /example/slides/020-layouts/000-layouts_explained.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: layouts 3 | title: What are layouts? 4 | chapter: Layouts 5 | --- 6 | 7 | Layouts define how the content of a single slide is **structured**. They are 8 | implemented as [React][] components and can therefore be arbitrarily complex. 9 | 10 | Layouts receive the content of a slide but also any data defined in 11 | `layoutData` in the front matter of the slide. 12 | 13 | Because layouts are independent React components, they can be easily shared via 14 | plugins. Exerslide doesn't include any layouts itself, but it comes 15 | pre-configured with some plugins which provide the following layouts: 16 | 17 | - **Center**: Centers the content vertically and horizontally on the screen 18 | - **Column**: Shows content side be side 19 | - **BulletList**: Renders a list of revealing bullet points 20 | 21 | ## Layout resolution 22 | 23 | Which layout to use for a slide is determined in a couple of ways: 24 | 25 | ### Per slide layout 26 | 27 | Slides can specify in the meta-data section which layout to use, via the 28 | `layout` option. The following example slide source would specify to use the 29 | "Center" layout: 30 | 31 | ::: a11y 32 | ``` 33 | --- 34 | layout: Center 35 | --- 36 | Some content that is going to be centered 37 | ``` 38 | 39 | ``` 40 | --- 41 | { 42 | layout: Center 43 | } 44 | --- 45 | Some content that is going to be centered 46 | ``` 47 | ::: 48 | 49 | ### Default layouts 50 | 51 | Having to specify they layout for every slide explicitly is not convenient. To 52 | make this easier, exerslide uses the **file extension** of the slide source file to 53 | determine the layout. The file extension to layout mapping can be configured in 54 | the [configuration file](#/config_file). For example, we could define the 55 | following map so that files with the extension `.center.md` get rendered with 56 | the `Center` layout: 57 | 58 | ``` 59 | { 60 | "fileTypeToLayoutMapping": { 61 | ".center.md": "Center" 62 | } 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /example/slides/020-layouts/010-center_layout.md: -------------------------------------------------------------------------------- 1 | --- 2 | toc: Center layout 3 | style: | 4 | #exerslide-slide pre > code { 5 | font-size: 0.8em; 6 | } 7 | --- 8 | 9 | @require(!!raw!../../../packages/exerslide-plugin-center-layout/README.md) 10 | -------------------------------------------------------------------------------- /example/slides/020-layouts/014-center_layout_example.md: -------------------------------------------------------------------------------- 1 | --- 2 | toc: "Example: Center layout" 3 | title: Center example 4 | layout: Center 5 | --- 6 | With subtitles 7 | -------------------------------------------------------------------------------- /example/slides/020-layouts/015-image_center_layout_example.md: -------------------------------------------------------------------------------- 1 | --- 2 | toc: "Example: Center layout with image" 3 | layout: Center 4 | layout_data: 5 | image: 6 | src: http://placehold.it/500x300/A8C5FC/?text=+ 7 | alt: Center layout showing a centered image 8 | --- 9 | -------------------------------------------------------------------------------- /example/slides/020-layouts/016-full_screen_center_layout_example.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: Center 3 | toc: "Example: Full screen center layout" 4 | title: Full screen example 5 | hide_toc: true 6 | style: | 7 | .Center-wrapper { 8 | background-size: contain !important; 9 | max-width: 100% !important; 10 | } 11 | .exerslide-slide { 12 | color: white; 13 | padding: 0; 14 | } 15 | .exerslide-slide > * { 16 | max-width: 100% !important; 17 | } 18 | layout_data: 19 | image: http://placehold.it/500x300/A8C5FC/?text=+ 20 | --- 21 | -------------------------------------------------------------------------------- /example/slides/020-layouts/020-column_layout.md: -------------------------------------------------------------------------------- 1 | --- 2 | toc: Column layout 3 | --- 4 | 5 | @require(!!raw!../../../packages/exerslide-plugin-column-layout/README.md) 6 | -------------------------------------------------------------------------------- /example/slides/020-layouts/025-column_layout_example.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: column_example 3 | title: "Example: Column layout" 4 | layout: Column 5 | layout_data: 6 | alignment: [left, center] 7 | position: [top, middle] 8 | --- 9 | This is an example that shows text content on the left and a picture on 10 | the right. 11 | 12 | Example with [Markdown][]: 13 | - For example: 14 | - bullet lists 15 | 16 | Check out the image on the right. 17 | ### 18 | ![Example image on the right hand side](http://placehold.it/500x200/A8C5FC/?text=+) 19 | -------------------------------------------------------------------------------- /example/slides/020-layouts/026-column_layout_example2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example: 3 columns" 3 | layout: Column 4 | layout_data: 5 | alignment: [left, center, right] 6 | position: [top, middle, bottom] 7 | style: | 8 | .Column-2 { 9 | max-width: 100px; 10 | min-width: 100px; 11 | } 12 | --- 13 | This is an example that shows text content on the left a picture in the center 14 | and text on the right. 15 | 16 | Example with [Markdown][]: 17 | - For example: 18 | - bullet lists 19 | 20 | Check out the image in the center. 21 | 22 | ### 23 | ![Example image in the center](http://placehold.it/200x150/A8C5FC/?text=+) 24 | ### 25 | Text in the bottom right 26 | -------------------------------------------------------------------------------- /example/slides/020-layouts/030-bulletlist_layout.md: -------------------------------------------------------------------------------- 1 | --- 2 | toc: Bullet list layout 3 | --- 4 | 5 | @require(!!raw!../../../packages/exerslide-plugin-bulletlist-layout/README.md) 6 | -------------------------------------------------------------------------------- /example/slides/020-layouts/035-bulletlist_layout_example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example: Revealing bullet points" 3 | layout: bulletlist-layout:BulletList 4 | --- 5 | 6 | - Advance the presentation to reveal the next bullet point 7 | - This is the next bullet point 8 | - This is the **last** bullet point 9 | -------------------------------------------------------------------------------- /example/slides/025-customization/000-intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ways to Customize 3 | chapter: Customization 4 | --- 5 | exerslide offers various ways (with varying degrees of complexity) to customize 6 | the look and behavior of your presentation. These are: 7 | 8 | 9 | - [Custom CSS styles](#/custom_cdd) 10 | - [Custom master layout](#/custom_master) 11 | - [Shared or custom layouts](#/custom_layouts) 12 | -------------------------------------------------------------------------------- /example/slides/025-customization/010-custom_master.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom master layout 3 | --- 4 | The **master layout** defines the overall structure of the page / presentation. 5 | The default master layout is copied into the project folder so you can change 6 | it easily. 7 | 8 | For example, this presentation customizes the master layout to include a header 9 | at the top of the page: 10 | 11 | ``` 12 | @require(!!raw!../../js/MasterLayout.js) 13 | ``` 14 | -------------------------------------------------------------------------------- /example/slides/030-advanced/010-custom_layouts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom layouts 3 | chapter: Customization 4 | id: custom_layouts 5 | --- 6 | 7 | You can also create your own layouts. Exerslide was originally developed to 8 | create interactive presentations for teaching web technologies. It comes with 9 | some reusable components to support such use cases. 10 | 11 | The next slide shows a custom layout that renders its content in a text editor 12 | and renders the input as HTML next to it. 13 | If it also implemented validation logic and user feedback, this could be used 14 | to create interactive exercises. 15 | 16 | ## Where to store custom layouts 17 | 18 | For exerslide to be able to find the layout, it has to be stored inside the 19 | `layouts/` directory in your presentation (create the directory if it doesn't 20 | exist): 21 | 22 | ``` 23 | - presentation/ 24 | - layouts/ 25 | - Editor.js 26 | - slides/ 27 | - 00-slide1.md 28 | - 01-slide2.md 29 | - exerslide.config.js 30 | - webpack.config.js 31 | ``` 32 | 33 | ## Example 34 | 35 | The layout used in the next slide is defined as 36 | 37 | ```javascript 38 | @require('!!raw!../../layouts/Editor.js') 39 | ``` 40 | -------------------------------------------------------------------------------- /example/slides/030-advanced/015-custom_layouts_example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Example: Custom Editor layout" 3 | layout: Editor 4 | chapter: Customization 5 | id: custom_layout_example 6 | --- 7 | An example for a layout featuring an editor. 8 | You could use this for interactive demos or 9 | exercises. 10 | -------------------------------------------------------------------------------- /example/slides/050-features/000-image_size.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Maximum Image Size 3 | chapter: Features 4 | --- 5 | The default style sets the maximum width of each image inside the slide (the 6 | `#exerslide-slide` element) to 100%, which makes it scale to the width of its 7 | container. 8 | 9 | Example images: 10 | 11 |

12 | Small example image 13 |

14 | Wide example image 15 | -------------------------------------------------------------------------------- /example/slides/050-features/010-slide_references.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Interslide references 3 | --- 4 | 5 | To make linking between slides easier, every slide can have an ID and you can 6 | use that ID instead of the slide number (which may change as you create the 7 | presentation). 8 | 9 | ## Example 10 | 11 | If a slide has the `id` key in the front matter: 12 | 13 | ::: a11y 14 | ```yaml 15 | --- 16 | id: example 17 | title: "Some title" 18 | --- 19 | Some content 20 | ``` 21 | 22 | ```yaml 23 | --- 24 | { 25 | id: example, 26 | title: "Some title" 27 | } 28 | --- 29 | Some content 30 | ``` 31 | ::: 32 | 33 | Then you can refer to it from any page using the ID in the hash of the URL 34 | instead of the slide number. 35 | 36 | ### Markdown example 37 | 38 | ```markdown 39 | [See this slide](#/example) 40 | ``` 41 | 42 | ### HTML example 43 | 44 | ```html 45 | See this slide 46 | ``` 47 | -------------------------------------------------------------------------------- /example/slides/050-features/020-foundation_fontawesome.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Foundation and Font Awesome 3 | --- 4 | 5 | [Foundation][] (CSS only) and [Font Awesome][] are included as dependencies by 6 | default. Since Markdown allows raw HTML, it is easy to have to readable source 7 | files and still create pleasant info boxes like 8 | 9 |
10 | 11 | This one. 12 |
13 | 14 | [foundation]: http://foundation.zurb.com/sites/docs/ 15 | [font awesome]: http://fortawesome.github.io/ 16 | -------------------------------------------------------------------------------- /example/slides/050-features/030-a11y.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Accessibility 3 | --- 4 | Creating presentations can be difficult for visually impaired people. Most 5 | presentation tools primarily provide visual tools to organize and structure 6 | content. 7 | 8 | Creating text files is usually easier for visually impaired people, but there 9 | is still the issue of how much of the content is actually visible on the screen 10 | when it is presented. 11 | 12 | To help with that, exerslide includes alerts only visible to screen readers 13 | when a slide contains more content than is visibile on the screen, such as the 14 | following one: 15 | 16 | ```html 17 | 18 | Attention: Only 33% of the slide content is visible 19 | 20 | ``` 21 | 22 | This allows authors to adjust the content, browser window size or the font 23 | size. 24 | 25 | --- 26 | 27 | In addition to that we are trying to make the default components that come with 28 | exerslide accessible, such as the text editor component shown in the [custom 29 | layout example](#/custom_layout_example). 30 | -------------------------------------------------------------------------------- /example/slides/050-features/10-error_example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Error slides 3 | style: | 4 | #exerslide-slide pre > code { 5 | font-size: 0.8em; 6 | } 7 | --- 8 | If the build process isn't able to parse the YAML front matter of a slide or 9 | encounters an unknown slide option, exerslide will generate a slide that 10 | contains information about the error, and the original source. 11 | 12 | ## Examples 13 | 14 | The slide 15 | 16 | ::: a11y 17 | ```yaml 18 | --- 19 | title: Broken: example 20 | --- 21 | This examples contains broken YAML to demonstrate how broken slides are 22 | rendered. 23 | ``` 24 | 25 | ```yaml 26 | --- 27 | { 28 | title: Broken: example 29 | } 30 | --- 31 | This examples contains broken YAML to demonstrate how broken slides are 32 | rendered. 33 | ``` 34 | ::: 35 | 36 | would result in a slide showing the error 37 | 38 | ``` 39 | incomplete explicit mapping pair; a key node is missed at line 1, column 14: 40 | title: Broken: example 41 | ^ 42 | ``` 43 | 44 | The slide 45 | 46 | ::: a11y 47 | ```yaml 48 | --- 49 | titel: Some title 50 | --- 51 | Some content 52 | ``` 53 | 54 | ```yaml 55 | --- 56 | { 57 | titel: "Some title" 58 | } 59 | --- 60 | Some content 61 | ``` 62 | ::: 63 | 64 | would result in a slide showing the error 65 | 66 | ``` 67 | Error: Unknown option "titel". Did you mean "title" instead? 68 | ``` 69 | -------------------------------------------------------------------------------- /example/statics/index.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | exerslide 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-root", 3 | "private": true, 4 | "devDependencies": { 5 | "fs-extra": "^0.30.0", 6 | "semver": "^5.3.0" 7 | }, 8 | "scripts": { 9 | "test": "./scripts/check.sh" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | env: 3 | node: true 4 | extends: ["eslint:recommended"] 5 | parserOptions: 6 | ecmaVersion: 6 7 | sourceType: module 8 | ecmaFeatures: 9 | jsx: true 10 | rules: 11 | brace-style: 2 12 | comma-dangle: [1, "always-multiline"] 13 | curly: 2 14 | dot-notation: 1 15 | eqeqeq: [2, "smart"] 16 | no-console: 2 17 | no-debugger: 2 18 | no-extra-bind: 2 19 | no-implicit-coercion: 2 20 | no-trailing-spaces: 2 21 | no-unexpected-multiline: 2 22 | quotes: [1, "single", "avoid-escape"] 23 | keyword-spacing: 2 24 | spaced-comment: 2 25 | no-unused-vars: 26 | - 2 27 | - varsIgnorePattern: "^_" 28 | globals: 29 | Promise: true 30 | Map: true 31 | Set: true 32 | __DEV__: true 33 | -------------------------------------------------------------------------------- /packages/exerslide-cli/bin/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "root": false, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/exerslide-cli/bin/__tests__/exerslide-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const childProcess = require('child_process'); 12 | const expect = require('chai').expect; 13 | const path = require('path'); 14 | const testUtils = require('../../scripts/test-utils'); 15 | 16 | function run(args, cwd) { 17 | return new Promise((resolve, reject) => { 18 | childProcess.exec( 19 | [ 20 | path.join(__dirname, '..', 'exerslide.js'), 21 | ] 22 | .concat(args).concat(['--EXERSLIDE_TEST']).join(' '), 23 | {cwd}, 24 | (error, stdout, stderr) => { 25 | if (error) { 26 | reject(new Error(`Unexpected error: ${error.message}`)); 27 | return; 28 | } 29 | resolve({stdout, stderr}); 30 | } 31 | ); 32 | }); 33 | } 34 | 35 | function prepareWorkingDirectory(commands) { 36 | // Pretend that there is a exerslide installation there. The CLI should 37 | // call our command 38 | const cliModules = Object.keys(commands).reduce((obj, command) => { 39 | obj[command + '.js'] = 'exports.handler = ' + commands[command].toString(); 40 | return obj; 41 | }, {}); 42 | cliModules['index.js'] = [ 43 | 'module.exports = {', 44 | Object.keys(commands).map(c => `c: require("./${c}")`).join(','), 45 | '};', 46 | ].join('\n'); 47 | 48 | return testUtils.makeDirectoryStructure({ 49 | node_modules: { 50 | exerslide: { 51 | cli: cliModules, 52 | 'index.js': '', 53 | 'package.json': JSON.stringify({name: 'exerslide'}), 54 | }, 55 | }, 56 | 'exerslide.config.js': '', 57 | }); 58 | } 59 | 60 | describe('exerslide', () => { 61 | 62 | it('delegates to local commands', () => { 63 | const dir = prepareWorkingDirectory({ 64 | test: () => console.log('called in test'), // eslint-disable-line no-console 65 | }); 66 | return run(['test'], dir) 67 | .then(p => { 68 | expect(p.stdout).to.contain('called in test'); 69 | }); 70 | }); 71 | 72 | }); 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /packages/exerslide-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-cli", 3 | "version": "1.1.0", 4 | "description": "A tool for creating HTML presentations with focus on exercises for web technologies", 5 | "main": "dist/scaffolder.js", 6 | "bin": { 7 | "exerslide": "./bin/exerslide.js" 8 | }, 9 | "scripts": { 10 | "test": "mocha ./bin/__tests__/exerslide-test.js", 11 | "lint": "eslint bin/" 12 | }, 13 | "keywords": [ 14 | "presentation-generator", 15 | "accessible", 16 | "tutorial-generator" 17 | ], 18 | "author": "Felix Kling", 19 | "license": "MIT", 20 | "dependencies": { 21 | "colors": "^1.1.2", 22 | "liftoff": "^2.2.1", 23 | "yargs": "^5.0.0" 24 | }, 25 | "devDependencies": { 26 | "chai": "^3.5.0", 27 | "eslint": "^3.3.1", 28 | "fs-extra": "^0.30.0", 29 | "mocha": "^3.0.2", 30 | "temp": "^0.8.3" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/facebookincubator/exerslide.git" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/facebookincubator/exerslide/issues" 38 | }, 39 | "homepage": "https://github.com/facebookincubator/exerslide#readme" 40 | } 41 | -------------------------------------------------------------------------------- /packages/exerslide-cli/scripts/test-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const fs = require('fs-extra'); 12 | const path = require('path'); 13 | const temp = require('temp').track(); 14 | const expect = require('chai').expect; 15 | 16 | /** 17 | * Helper function to create a directory structure described by the provided 18 | * object at root. If root is not provided, a new temporary directory is created 19 | * (and returned). 20 | * 21 | * Example: 22 | * 23 | * makeDirectoryStructure({ 24 | * x: { 25 | * y: 'abc', 26 | * z: 'def', 27 | * }, 28 | * a: 'ghi' 29 | * }, './foo/bar'); 30 | * 31 | * generates 32 | * 33 | * - ./foo/bar/x/y with content 'abc' 34 | * - ./foo/bar/x/z with content 'def' 35 | * - ./foo/bar/a with content 'ghi' 36 | * 37 | * 38 | * @param {Object} structure 39 | * @param {?string] root Path to root directory 40 | * @return {string} Path to root directory 41 | */ 42 | exports.makeDirectoryStructure = function makeDirectoryStructure(dirs, root) { 43 | if (!root) { 44 | root = temp.mkdirSync(); 45 | } 46 | var subdirs = Object.keys(dirs); 47 | if (subdirs.length === 0) { 48 | fs.ensureDirSync(root); 49 | } else { 50 | subdirs.forEach(name => { 51 | const p = path.join(root, name); 52 | switch (typeof dirs[name]) { 53 | case 'string': 54 | fs.outputFileSync(p, dirs[name]); 55 | break; 56 | case 'object': 57 | makeDirectoryStructure(dirs[name], p); 58 | break; 59 | } 60 | }); 61 | } 62 | return root; 63 | }; 64 | 65 | exports.validateFolderStructure = function validateFolderStructure(root, structure) { 66 | 67 | function normalize(p) { 68 | return p.replace(root, ''); 69 | } 70 | 71 | function validateInternal(dir, structure) { 72 | for (var prop in structure) { 73 | const p = path.join(dir, prop); 74 | expect(() => fs.statSync(p), `${normalize(p)} exists`).to.not.throw(); 75 | const stat = fs.statSync(p); 76 | if (typeof structure[prop] === 'object') { 77 | expect(stat.isDirectory()) 78 | .to.equal(true, `${normalize(p)}) is directory`); 79 | validateFolderStructure(path.join(dir, prop), structure[prop]); 80 | } else { 81 | expect(stat.isFile()).to.equal(true, `${normalize(p)} is file`); 82 | if (structure[prop]) { 83 | switch (typeof structure[prop]) { 84 | case 'function': 85 | structure[prop](fs.readFileSync(p, 'utf-8')); 86 | break; 87 | case 'string': 88 | expect(fs.readFileSync(p, 'utf-8')).to.equal(structure[prop]); 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | validateInternal(root, structure); 97 | }; 98 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-bulletlist-layout/README.md: -------------------------------------------------------------------------------- 1 | # Bullet list layout 2 | 3 | Layout that reveals individual bullet points. It demonstrates how layouts can 4 | react to slide navigation. 5 | 6 | ``` 7 | +-----------------------+ 8 | | | 9 | | - 1. bullet point | 10 | | - 2. bullet point | 11 | | | 12 | | | 13 | +-----------------------+ 14 | ``` 15 | 16 | ## CSS classes 17 | 18 | The content is wrapped in the CSS class `.BulletList-wrapper`. 19 | 20 | ## Example 21 | 22 | ```yaml 23 | --- 24 | toc: Bullet list example 25 | layout: BulletList 26 | --- 27 | - First bullet point 28 | - Second bullet point 29 | ``` 30 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-bulletlist-layout/layouts/BulletList.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import {setSlideCacheData, getSlideCacheData} from 'exerslide/browser'; 10 | import ContentRenderer from 'exerslide/components/ContentRenderer'; 11 | import React from 'react'; 12 | 13 | import './css/bulletList.css'; 14 | 15 | const DEFAULT_CACHE_DATA = {last: 0}; 16 | const CACHE_KEY = 'exerslide-plugin-bulletlist-layout'; 17 | 18 | export default class BulletList extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = getSlideCacheData( 22 | props.slideIndex, 23 | CACHE_KEY, 24 | DEFAULT_CACHE_DATA 25 | ); 26 | } 27 | 28 | componentsWillReceiveProps(nextProps) { 29 | if (this.props.slideIndex !== nextProps.slideIndex) { 30 | this.setState(getSlideCacheData( 31 | nextProps.slideIndex, 32 | CACHE_KEY, 33 | DEFAULT_CACHE_DATA 34 | )); 35 | } 36 | } 37 | 38 | _getBulletPoints() { 39 | return this.props.content.split(/^-\s*/m) 40 | .map(content => content.replace(/\s+$/, '')) 41 | .filter(Boolean); 42 | } 43 | 44 | onForward() { 45 | var bulletPoints = this._getBulletPoints(); 46 | if (this.state.last < bulletPoints.length - 1) { 47 | var data = {last: this.state.last + 1}; 48 | setSlideCacheData(this.props.slideIndex, CACHE_KEY, data); 49 | this.setState(data); 50 | // we handled the event, no need to advance slide 51 | return true; 52 | } 53 | } 54 | 55 | render() { 56 | const bulletPoints = this._getBulletPoints() 57 | .slice(0, this.state.last + 1) 58 | .map((value, i)=>
  • ); 59 | 60 | return ( 61 |
    62 | {this.props.title} 63 |
      64 | {bulletPoints} 65 |
    66 |
    67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-bulletlist-layout/layouts/css/bulletList.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 10 | /** 11 | * Because every bullet point is rendered individually, the markdown renderer 12 | * wraps each point into a div and p element. This introduces unwanted margin 13 | * and padding which we remove here. 14 | */ 15 | .BulletList-wrapper > ul > li > p, 16 | .BulletList-wrapper > ul > li > div > p { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-bulletlist-layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-plugin-bulletlist-layout", 3 | "version": "1.1.0", 4 | "description": "A bullet list layout for exerslide.", 5 | "author": "Felix Kling", 6 | "dependencies": { 7 | "babel-runtime": "^6.11.6" 8 | }, 9 | "peerDependencies": { 10 | "exerslide": "~1.1.0", 11 | "react": "^15.0.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/facebookincubator/exerslide.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/facebookincubator/exerslide/issues" 19 | }, 20 | "homepage": "https://github.com/facebookincubator/exerslide#readme" 21 | } 22 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-center-layout/README.md: -------------------------------------------------------------------------------- 1 | # Center layout 2 | Layout that horizontally and vertically centers its content. 3 | 4 | ``` 5 | +-----------------------+ +-----------------------+ +-----------------------+ 6 | | | | | | | 7 | | | | +---------------+ | | +---------------+ | 8 | | | | | | | | | | | 9 | | Centered Text | | | | | | | Centered Text | | 10 | | | | | | | | | | | 11 | | | | +---------------+ | | +---------------+ | 12 | | | | | | | 13 | +-----------------------+ +-----------------------+ +-----------------------+ 14 | ``` 15 | 16 | ## Layout data 17 | 18 | - `image`: A path/URL to an image or an image object (`{src: ..., alt: ...}`) 19 | 20 | If `image` is specified but no content, the image will be centered. 21 | If both `image` and content are specified, the image becomes the centered 22 | background-image of the slide. 23 | 24 | ## CSS classes 25 | 26 | The content is wrapped in the CSS class `.Center-wrapper`. 27 | 28 | ## Examples 29 | 30 | ### Simple text content 31 | 32 | ```yaml 33 | --- 34 | title: Center layout example 35 | layout: Center 36 | --- 37 | With subtitles 38 | ``` 39 | 40 | ### Centered image 41 | 42 | ```yaml 43 | --- 44 | toc: Center layout image example 45 | layout: Center 46 | layout_data: 47 | image: 48 | src: http://placehold.it/500x300/A8C5FC/?text=+ 49 | alt: Center layout showing a centered image 50 | --- 51 | ``` 52 | 53 | ### Full-screen background image and text 54 | 55 | Exerslide tries to provide a minimal set of styles and to give you the 56 | possibility to customize almost anything. Having a fullscreen slide is 57 | relatively easy to achieve: just write some custom CSS that expands the width 58 | of the slide. If you find yourself doing this often, you can use your own 59 | helper class. 60 | 61 | ```yaml 62 | --- 63 | title: Full screen example 64 | layout: Center 65 | hide_toc: true 66 | style: | 67 | .Center-wrapper { 68 | background-size: contain !important; 69 | max-width: 100% !important; 70 | } 71 | #exerslide-slide { 72 | color: white; 73 | padding: 0; 74 | } 75 | #exerslide-slide > * { 76 | max-width: 100% !important; 77 | } 78 | layout_data: 79 | image: http://placehold.it/500x300/A8C5FC/?text=+ 80 | --- 81 | ``` 82 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-center-layout/layouts/Center.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import React from 'react'; 10 | import ContentRenderer from 'exerslide/components/ContentRenderer'; 11 | import {normalizeImageData} from 'exerslide/browser'; 12 | 13 | import './css/Center.css'; 14 | 15 | /** 16 | * Layout that horizontally and vertically centers its content. 17 | * 18 | * 19 | * Layout data: 20 | * 21 | * - image: A path/URL to an image or an image object ({src: ..., alt: ...}) 22 | * 23 | * If `image` specified but no content, the image will be centered. 24 | * If both, `image` and content are specified, the image becomes the centered 25 | * background-image of the slide. 26 | 27 | * +-----------------------+ +-----------------------+ +-----------------------+ 28 | * | | | | | | 29 | * | | | +---------------+ | | +---------------+ | 30 | * | | | | | | | | | | 31 | * | Centered Text | | | | | | | Centered Text | | 32 | * | | | | | | | | | | 33 | * | | | +---------------+ | | +---------------+ | 34 | * | | | | | | 35 | * +-----------------------+ +-----------------------+ +-----------------------+ 36 | * 37 | */ 38 | export default function Center({title, layoutData, content}) { 39 | let {image} = layoutData; 40 | const style = {}; 41 | let child = 42 | image = normalizeImageData(image); 43 | if (image && (content || title)) { 44 | // image becomes background image 45 | style.background = `no-repeat center url("${image.src}")`; 46 | } else if (image) { 47 | child = {image.alt; 48 | } 49 | return ( 50 |
    51 | {title} 52 | {child} 53 |
    54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-center-layout/layouts/css/Center.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | .Center-wrapper { 10 | align-items: center; 11 | display: flex; 12 | flex-direction: column; 13 | flex: 1; 14 | justify-content: center; 15 | text-align: center; 16 | } 17 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-center-layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-plugin-center-layout", 3 | "version": "1.1.0", 4 | "description": "A center layout for exerslide.", 5 | "author": "Felix Kling", 6 | "dependencies": { 7 | "babel-runtime": "^6.11.6" 8 | }, 9 | "peerDependencies": { 10 | "exerslide": "~1.1.0", 11 | "react": "^15.0.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/facebookincubator/exerslide.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/facebookincubator/exerslide/issues" 19 | }, 20 | "homepage": "https://github.com/facebookincubator/exerslide#readme" 21 | } 22 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-column-layout/layouts/Column.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import React from 'react'; 10 | import ContentRenderer from 'exerslide/components/ContentRenderer'; 11 | 12 | import './css/Column.css'; 13 | 14 | const escapeRegex = (function() { 15 | const PATTERN = /[.*?+[\]{}|\\^$()]/g; 16 | return str => str.replace(PATTERN, '\\$&'); 17 | }()); 18 | 19 | const columnClasses = ['left', 'center', 'right', 'top', 'middle', 'bottom'] 20 | .reduce((obj, name) => ((obj[name] = 'Column-' + name, obj)), {}); 21 | 22 | /** 23 | * This layout renders horizontal columns of text / images. 24 | * 25 | * +-----------------------+ +-----------------------+ 26 | * | | | | 27 | * | | | | 28 | * | | | +------+ | 29 | * | Text Text | | Text | | | 30 | * | | | +------+ | 31 | * | | | | 32 | * | | | | 33 | * +-----------------------+ +-----------------------+ 34 | * 35 | */ 36 | export default function Column({title, layoutData, content}) { 37 | const {divider='###', alignment=[], position=[]} = layoutData; 38 | const columns = content 39 | .split(new RegExp('^' + escapeRegex(divider) + '$', 'm')) 40 | .map((content, i) => { 41 | let classNames = [ 42 | 'Column-column', 43 | 'Column-' + (i+1), 44 | columnClasses[alignment[i] || 'left'], 45 | columnClasses[position[i] || 'top'], 46 | ]; 47 | 48 | return ( 49 |
    50 | 51 |
    52 | ); 53 | }); 54 | 55 | return ( 56 |
    57 | {title} 58 |
    59 | {columns} 60 |
    61 |
    62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-column-layout/layouts/css/Column.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | .Column-wrapper { 10 | display: flex; 11 | flex-shrink: 0; /* have the same height as the children */ 12 | } 13 | 14 | .Column-column { 15 | flex: 1; 16 | padding: 10px; 17 | min-width: 150px; 18 | box-sizing: content-box; 19 | } 20 | 21 | .Column-top { 22 | flex: 1; 23 | align-self: flex-start; 24 | } 25 | 26 | .Column-middle { 27 | flex: 1; 28 | align-self: center; 29 | } 30 | 31 | .Column-bottom { 32 | flex: 1; 33 | align-self: flex-end; 34 | } 35 | 36 | .Column-left { 37 | text-align: left; 38 | } 39 | 40 | .Column-center { 41 | text-align: center; 42 | } 43 | 44 | .Column-right { 45 | text-align: right; 46 | } 47 | 48 | .Column-column > img { 49 | flex-shrink: 0; 50 | } 51 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-column-layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-plugin-column-layout", 3 | "version": "1.1.0", 4 | "description": "A column layout for exerslide.", 5 | "author": "Felix Kling", 6 | "dependencies": { 7 | "babel-runtime": "^6.11.6" 8 | }, 9 | "peerDependencies": { 10 | "exerslide": "~1.1.0", 11 | "react": "^15.0.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/facebookincubator/exerslide.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/facebookincubator/exerslide/issues" 19 | }, 20 | "homepage": "https://github.com/facebookincubator/exerslide#readme" 21 | } 22 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-html-converter/README.md: -------------------------------------------------------------------------------- 1 | # HTML converted 2 | 3 | Converts HTML into a React element. 4 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-html-converter/contentTypes/text_html.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import React from 'react'; // eslint-disable-line no-unused-vars 10 | 11 | /** 12 | * Renders HTML slides as HTML. 13 | */ 14 | export default function convert(content) { 15 | return
    ; 16 | } 17 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-html-converter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-plugin-html-converter", 3 | "version": "1.0.0", 4 | "description": "An HTML to React element converter for exerslide.", 5 | "author": "Felix Kling", 6 | "dependencies": { 7 | "babel-runtime": "^6.11.6" 8 | }, 9 | "peerDependencies": { 10 | "react": "^15.0.0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/facebookincubator/exerslide.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/facebookincubator/exerslide/issues" 18 | }, 19 | "homepage": "https://github.com/facebookincubator/exerslide#readme" 20 | } 21 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-javascriptexercise-layout/README.md: -------------------------------------------------------------------------------- 1 | # JavaScriptExercise layout 2 | 3 | This layout is intended for interactive JavaScript exercises. It shows a text 4 | editor to let the visitor input JavaScript and allows to specify validation 5 | logic to verify the solution. 6 | 7 | ``` 8 | +-----------------------+ 9 | | | 10 | | Description | 11 | | +---------------+ | 12 | | | | | 13 | | |Editor | | 14 | | | | | 15 | | +---------------+ | 16 | | +------+ +-----+ | 17 | | |Submit| |Reset| | 18 | | +------+ +-----+ | 19 | +-----------------------+ 20 | ``` 21 | 22 | Code that is inputted in the text editor can be executed and verified. The 23 | layout makes a special function available to the code in the editor: `log`. 24 | `log` is a wrapper around `console.log` which also records the values that have 25 | been passed to it. These values can later be used to validate solutions. 26 | 27 | ## Layout data 28 | 29 | - `description`: Free form text that will be shown above the text editor. Can 30 | be used to introduce the question / problem. 31 | - `assertion`: JavaScript code to validate the solution. The code has access to 32 | three variables: 33 | 34 | - `assert`, which is the assertion function. It takes a condition as first 35 | argument and a message as second argument. 36 | - `source`: The text editor input. 37 | - `output`: An array of values which have been passed to the `log` function. 38 | 39 | With this you can perform simple checks against the input source and logged 40 | output. 41 | 42 | **Note:** If `assertion` is not present, no "Submit" button is rendered. In 43 | that case the layout can used as interactive demo. 44 | 45 | ## Layout content 46 | 47 | The content of the slide is expected to be JavaScript and is used as the 48 | initial input of the text editor. 49 | 50 | ## Example: 51 | 52 | ``` 53 | --- 54 | title: Exercise 55 | layout_data: 56 | description: | 57 | Create a local variable with name `foo` and value `42`. 58 | Use `log(foo)` to log the value of `foo`. 59 | assertion: | 60 | assert( 61 | /var foo\s*=.+;?$/m.test(source), 62 | "It doesn't look like you have declared a variable (hint: var)." 63 | ); 64 | assert(output[0] === 42, "Don't forget to log the value"); 65 | --- 66 | // Create variable 67 | 68 | // 69 | log(foo); 70 | ``` 71 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-javascriptexercise-layout/js/withoutComments.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This function accepts a piece of JS code and removes all comments it 11 | * contains. 12 | */ 13 | export default function removeComments(code) { 14 | // temporarily remove strings 15 | let token = '%%' + Date.now(); 16 | let strings = {}; 17 | let counter = 0; 18 | code = code.replace(/(['"])(?:(?:\\\1|[^\1])*)\1/g, match => { 19 | let t = token + '_' + counter++; 20 | strings[t] = match; 21 | return t; 22 | }); 23 | 24 | // Remove line comments 25 | code = code.replace(/\/\/.*$/mg, ''); 26 | // Remove block comments 27 | code = code.replace(/\/\*(?:(?!\*\/)[\S\s])+\*\//g, ''); 28 | 29 | // put strings back 30 | return code.replace(new RegExp(token + '_\\d+', 'g'), m => strings[m]); 31 | } 32 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-javascriptexercise-layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-plugin-javascriptexercise-layout", 3 | "version": "1.1.0", 4 | "description": "A JavaScript exercise layout for exerslide.", 5 | "author": "Felix Kling", 6 | "dependencies": { 7 | "babel-runtime": "^6.11.6", 8 | "chai": "^3.4.1", 9 | "classnames": "^2.1.1" 10 | }, 11 | "peerDependencies": { 12 | "codemirror": "^5.10.0", 13 | "exerslide": "~1.1.0", 14 | "react": "^15.0.0" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/facebookincubator/exerslide.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/facebookincubator/exerslide/issues" 22 | }, 23 | "homepage": "https://github.com/facebookincubator/exerslide#readme" 24 | } 25 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/README.md: -------------------------------------------------------------------------------- 1 | # Markdown converter 2 | 3 | Converts Markdown to a React element. It is based on [markdown-it][] but 4 | extends the renderer to provide the following features. 5 | 6 | ## Features 7 | 8 | ### Syntax highlight 9 | 10 | This converter uses [highlight.js][] for code block syntax highlighting. To 11 | keep the JavaScript bundle file size small, this plugin also includes a custom 12 | slide transform which looks for language names in code fences: 13 | 14 | ```html 15 |
    HTML
    16 | ``` 17 | 18 | Only the highlight definitions for the detected languages are included. 19 | 20 | ### Slide title links 21 | 22 | When linking to another slide (via ID (recommended) or slide number), you can 23 | omit the link text: `[](#/intro)`. In that case, the title of slide will be 24 | used as link text. 25 | 26 | ### Shared links / URLs 27 | 28 | The markdown converter uses the `references` config option passed (by default) 29 | to exerslide. This allows you do keep URLs in a single file and use them 30 | across multiple slides. All you need to do is use the name / key of the URL. 31 | 32 | Example: 33 | 34 | `references.yml` 35 | ```yaml 36 | markdown-it: https://github.com/markdown-it/markdown-it 37 | ``` 38 | 39 | `slide.md` 40 | ```markdown 41 | My [markdown parser][markdown-it]. 42 | ``` 43 | 44 | ## Configuration 45 | 46 | You can add plugins to the markdown parser by setting the `markdown-converter` 47 | option of the exerslide config. This is supposed to be a function that gets 48 | passed an instance of the markdown parser. 49 | 50 | Example: 51 | 52 | `js/presentation.js` 53 | ```js 54 | present({ 55 | // ... 56 | 'markdown-converter': function(md) { 57 | md.use(somePlugin); 58 | }, 59 | }); 60 | ``` 61 | 62 | [markdown-it]: https://github.com/markdown-it/markdown-it 63 | [highlight.js]: https://highlightjs.org/ 64 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "root": false, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/__tests__/extractLanguageHighlights-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const transformHelper = require('exerslide').transformHelper; 10 | const extractLanguageHighlights = require('../extractLanguageHighlights'); 11 | const expect = require('chai').expect; 12 | 13 | const helperPath = require.resolve('../utils/registerLanguage'); 14 | 15 | function test(languages, slide, verify) { 16 | extractLanguageHighlights({languagePaths: languages})(transformHelper) 17 | .before(slide, verify); 18 | } 19 | 20 | describe('extractLanguageHighlights', () => { 21 | 22 | it('requires language highlights', () => { 23 | const languages = { 24 | javascript: './foo', 25 | markdown: './bar', 26 | }; 27 | 28 | test( 29 | languages, 30 | '```javascript\ncode\n```\n\n```markdown\ntext\n```', 31 | (errors, output, actions) => { 32 | expect(actions[0].type).to.equal('prefix'); 33 | 34 | expect(actions[0].value).to.contain(`require("${helperPath}")`); 35 | expect(actions[0].value).to.contain('"javascript": require("./foo")'); 36 | expect(actions[0].value).to.contain('"markdown": require("./bar")'); 37 | } 38 | ); 39 | }); 40 | 41 | it('shows an error if language is not found', () => { 42 | const languages = { 43 | javascript: './foo', 44 | markdown: './bar', 45 | }; 46 | 47 | test( 48 | languages, 49 | '```foo\ncode\n```\n\n```markdown\ntext\n```', 50 | (errors, output, actions) => { 51 | expect(errors.join('')).to.contain('foo'); 52 | expect(actions[0].type).to.equal('prefix'); 53 | expect(actions[0].value).to.contain('"markdown": require("./bar")'); 54 | expect(actions[0].value).to.not.contain('"foo": require('); 55 | } 56 | ); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/extractLanguageHighlights.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const path = require('path'); 12 | 13 | const codeFenceRegex = /^\s*[`~]{3} *([a-z]+)/mig; 14 | const helperPath = JSON.stringify( 15 | path.resolve(__dirname, './utils/registerLanguage.js') 16 | ); 17 | 18 | const ignoredLabels = new Set([ 19 | 'react', // React fence code block 20 | ]); 21 | 22 | module.exports = function(config) { 23 | const languagePaths = config.languagePaths || {}; 24 | 25 | return function(transformHelper) { 26 | return { 27 | before(source, next) { 28 | codeFenceRegex.lastIndex = 0; 29 | const actions = []; 30 | const languages = new Set(); 31 | let errors = null; 32 | 33 | let match; 34 | while (match = codeFenceRegex.exec(source)) { // eslint-disable-line no-cond-assign 35 | if (match[1]) { 36 | languages.add(match[1].toLowerCase()); 37 | } 38 | } 39 | const foundLanguages = Array.from(languages).reduce( 40 | (list, name) => { 41 | if (languagePaths[name]) { 42 | list[name] = languagePaths[name]; 43 | } 44 | return list; 45 | }, 46 | {} 47 | ); 48 | errors = Array.from(languages) 49 | .filter(name => !foundLanguages[name] && !ignoredLabels.has(name)) 50 | .map( 51 | name => `Cannot find a syntax highlighter for language "${name}".` 52 | ); 53 | 54 | if (Object.keys(foundLanguages).length > 0) { 55 | const obj = Object.keys(foundLanguages).map( 56 | name => `${JSON.stringify(name)}: ` + 57 | `require(${JSON.stringify(foundLanguages[name])})` 58 | ).join(',\n'); 59 | actions.push( 60 | transformHelper.getPrefixAction(`require(${helperPath})({${obj}});`) 61 | ); 62 | } 63 | next(errors, source, actions); 64 | }, 65 | }; 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const extractLanguageHighlights = require('./extractLanguageHighlights'); 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | 15 | function getLanguages() { 16 | const languageDir = path.join( 17 | path.dirname(require.resolve('highlight.js')), 18 | 'languages' 19 | ); 20 | const hljs = require('highlight.js'); 21 | 22 | return fs.readdirSync(languageDir) 23 | .filter(name => /\.js$/.test(name)) 24 | .reduce( 25 | (list, name) => { 26 | const filePath = path.join(languageDir, name); 27 | // Require language module to get aliases 28 | const aliases = require(filePath)(hljs).aliases; 29 | [path.basename(name, '.js')].concat(aliases).forEach( 30 | n => list[n] = filePath 31 | ); 32 | return list; 33 | }, 34 | {} 35 | ); 36 | } 37 | 38 | module.exports = function(exerslideConfig, webpackConfig) { 39 | webpackConfig.slideLoader.transforms.push( 40 | extractLanguageHighlights({languagePaths: getLanguages()}) 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide-plugin-markdown-converter", 3 | "version": "1.1.1", 4 | "description": "An HTML to React element converter for exerslide.", 5 | "author": "Felix Kling", 6 | "scripts": { 7 | "test": "mocha `find __tests__/ -name *-test.js`" 8 | }, 9 | "dependencies": { 10 | "babel-runtime": "^6.11.6", 11 | "highlight.js": "^9.6.0", 12 | "markdown-it": "^7.0.1" 13 | }, 14 | "peerDependencies": { 15 | "exerslide": "~1.1.0", 16 | "react": "^15.0.0", 17 | "react-dom": "^15.0.0" 18 | }, 19 | "devDependencies": { 20 | "chai": "^3.4.1", 21 | "mocha": "^3.0.2" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/facebookincubator/exerslide.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/facebookincubator/exerslide/issues" 29 | }, 30 | "homepage": "https://github.com/facebookincubator/exerslide#readme" 31 | } 32 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/reactFence.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /** 12 | * This runtime plugin enables authors to replace specific blocks in markdown 13 | * files with custom React components. 14 | * 15 | * Usage example: 16 | * 17 | * exerslide.use(reactFence, (tag, content) { 18 | * if (tag === 'component') { 19 | * return ; 20 | * } 21 | * }); 22 | */ 23 | import reactRenderer, {renderIntoDOM} from './utils/renderIntoDOM'; 24 | import markdownItReactFence from './utils/markdownItReactFence'; 25 | 26 | export default function reactFence(exerslide, factory) { 27 | exerslide.use(reactRenderer); 28 | exerslide.subscribe('CONFIG.SET', config => { 29 | const existingExtension = config['markdown-converter']; 30 | 31 | config['markdown-converter'] = md => { 32 | md.use(markdownItReactFence, (token, env) => { 33 | const component = factory(token.info, token.content, env); 34 | if (component) { 35 | return renderIntoDOM( 36 | env.slideIndex, 37 | component 38 | ); 39 | } 40 | return ''; 41 | }); 42 | existingExtension && existingExtension(md); 43 | }; 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/utils/markdownItBlankLinks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * Render link as _blank, unless it's a local one (starting with `#`) 11 | * Taken straight from the markdown-it example 12 | */ 13 | export default function markdownItBlankLinks(md) { 14 | const defaultRender = md.renderer.rules.link_open || 15 | function(tokens, index, options, env, self) { 16 | return self.renderToken(tokens, index, options); 17 | }; 18 | 19 | md.renderer.rules.link_open = function (tokens, index, options, env, self) { 20 | const token = tokens[index]; 21 | const href = token.attrGet('href'); 22 | 23 | if (href.charAt(0) !== '#') { 24 | token.attrSet('target', '_blank'); 25 | } 26 | return defaultRender(tokens, index, options, env, self); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/utils/markdownItFillSlideTitle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This is a markdown-it plugin to use slide titles if no link text is provided. 11 | */ 12 | export default function markdownItFillSlideTitle(md) { 13 | md.core.ruler.after( 14 | 'inline', 15 | 'link-slide-title', 16 | function (state) { 17 | state.tokens.forEach(function (blockToken) { 18 | if (blockToken.type === 'inline' && blockToken.children) { 19 | const children = blockToken.children; 20 | for (let i = 0; i < children.length; i++) { 21 | const token = children[i]; 22 | if (token.type === 'link_open') { 23 | const nextToken = blockToken.children[i + 1]; 24 | if (nextToken.type === 'link_close') { 25 | // Link without any text 26 | // Get slide options and use the title instead 27 | const href = token.attrGet('href'); 28 | const slide = getSlideFromURL(state.env.slides, href); 29 | if (slide) { 30 | const text = slide.options.title || slide.options.toc; 31 | const textToken = new state.Token('text', '', 0); 32 | textToken.content = text; 33 | children.splice(i + 1, 0, textToken); 34 | } 35 | } 36 | } 37 | } 38 | } 39 | }); 40 | } 41 | ); 42 | } 43 | 44 | function getSlideFromURL(slides, path) { 45 | if (path.indexOf('#/') !== 0) { 46 | return false; 47 | } 48 | const possibleID = path.substr(2); 49 | if (!isNaN(possibleID) && possibleID >= 0 && possibleID < slides.length) { 50 | return slides[possibleID]; 51 | } 52 | for (let i = 0; i < slides.length; i++) { 53 | if (slides[i].options.id === possibleID) { 54 | return slides[i]; 55 | } 56 | } 57 | return false; 58 | } 59 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/utils/markdownItReactFence.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This is a custom render rule to identify react blocks and get access to their 11 | * content. Overwriting the default render is sufficient, there was no need 12 | * for a custom parser rule. 13 | * 14 | * @param {Object} md The markdown-it instance 15 | * @param {function} renderer The custom renderer 16 | */ 17 | export default function reactFenceMD(md, renderer) { 18 | const oldRender = md.renderer.rules.fence || 19 | function(tokens, index, options, env, self) { 20 | return self.renderToken(tokens, index, options); 21 | }; 22 | const PATTERN = /\s*react\b\s*/i; 23 | md.renderer.rules.fence = (tokens, index, options, env, self) => { 24 | const token = tokens[index]; 25 | if (PATTERN.test(token.info)) { 26 | token.info = token.info.replace(PATTERN, ''); 27 | return renderer(token, env); 28 | } 29 | return oldRender(tokens, index, options, env, self); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/utils/registerLanguage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | var hljs = require('highlight.js/lib/highlight'); 10 | 11 | var seen = new Set(); 12 | 13 | module.exports = function(languages) { 14 | for (var lang in languages) { 15 | if (!seen.has(lang)) { 16 | hljs.registerLanguage(lang, languages[lang]); 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/exerslide-plugin-markdown-converter/utils/renderIntoDOM.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import ReactDOM from 'react-dom'; 12 | import scriptStore, {registerScript} from 'exerslide/browser-plugins/scriptStore'; 13 | 14 | let id = 0; 15 | 16 | export default function reactRenderer(exerslide) { 17 | exerslide.use(scriptStore); 18 | } 19 | 20 | export function renderIntoDOM(slideIndex, component) { 21 | const localID = `r${Date.now()}-${id++}`; 22 | const placeholder = `
    `; 23 | registerScript( 24 | slideIndex, 25 | () => setTimeout( 26 | () => ReactDOM.render(component, global.document.getElementById(localID)), 27 | 0 28 | ), 29 | () => { 30 | ReactDOM.unmountComponentAtNode(global.document.getElementById(localID)); 31 | } 32 | ); 33 | return placeholder; 34 | } 35 | -------------------------------------------------------------------------------- /packages/exerslide/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "exerslide-test": { 4 | "presets": ["es2015", "react"], 5 | "plugins": [ 6 | "transform-object-rest-spread", 7 | ["module-resolver", { 8 | "alias": { 9 | "exerslide": "./" 10 | } 11 | }] 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/exerslide/README.md: -------------------------------------------------------------------------------- 1 | See [facebookincubator/exerslide/](https://github.com/facebookincubator/exerslide/blob/master/README.md). 2 | -------------------------------------------------------------------------------- /packages/exerslide/browser-plugins/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - react 3 | parserOptions: 4 | ecmaVersion: 6 5 | sourceType: module 6 | ecmaFeatures: 7 | jsx: true 8 | experimentalObjectRestSpread: true 9 | -------------------------------------------------------------------------------- /packages/exerslide/browser-plugins/__tests__/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | globals: 4 | document: true 5 | -------------------------------------------------------------------------------- /packages/exerslide/browser-plugins/keyboardNavigation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | // ES6 imports don't seem to work properly with this module 10 | const Mousetrap = require('mousetrap'); 11 | 12 | /** 13 | * Allows the visitor to navigate the presentation / tutorial with the keyboard. 14 | * See https://craig.is/killing/mice to learn how to specify key combinations. 15 | * 16 | * The plugin expects an object with `key -> function` mappings. A navigation 17 | * object is passed to the functions. 18 | * 19 | * Usage: 20 | * 21 | * import keyboardNavigation from 'exerslide/browser-plugins/keybordNavigation'; 22 | * exerslide.use( 23 | * keyboardNavigation, 24 | * { 25 | * right: nav => nav.forward() 26 | * left: nav => nav.back() 27 | * } 28 | * ); 29 | */ 30 | export default function keyboardNavigation(exerslide, keyMap) { 31 | const {forward, back} = exerslide; 32 | const navigation = {forward, back}; 33 | 34 | exerslide.subscribe('SITE.LOADED', () => { 35 | Object.keys(keyMap).forEach(prop => { 36 | Mousetrap.bind(prop, () => keyMap[prop](navigation)); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /packages/exerslide/browser-plugins/scriptStore.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This plugin executes functions when a slide was mounted / unmounted. It can 11 | * be used by other plugins to execute code in response to a new slide being 12 | * shown. 13 | * 14 | * Usage: 15 | * 16 | * import scriptStore, {registerScript} from 'exerslide/browser/plugins/scriptStore'; 17 | * 18 | * function plugin(exerslide) { 19 | * exerslide.use(scriptStore); 20 | * // ... 21 | * registerScript( 22 | * 0, 23 | * function() { console.log('first slide shown'); }, 24 | * function() { console.log('goodbye'); } 25 | * ); 26 | * } 27 | */ 28 | const scriptStore = {}; 29 | 30 | export default function plugin(exerslide) { 31 | exerslide.subscribe('SLIDE.DID_MOUNT', ({slideIndex, slide}) => { 32 | if (scriptStore[slideIndex]) { 33 | const scripts = scriptStore[slideIndex]; 34 | for (let i = 0; i < scripts.length; i+=2) { 35 | scripts[i]({slideIndex, slide}); 36 | } 37 | } 38 | }); 39 | 40 | exerslide.subscribe('SLIDE.DID_UPDATE', ({slideIndex, slide}) => { 41 | if (scriptStore[slideIndex]) { 42 | const scripts = scriptStore[slideIndex]; 43 | for (let i = 1; i < scripts.length; i+=2) { 44 | scripts[i+1]({slideIndex, slide}); 45 | scripts[i]({slideIndex, slide}); 46 | } 47 | } 48 | }); 49 | 50 | exerslide.subscribe('SLIDE.WILL_UNMOUNT', ({slideIndex, slide}) => { 51 | if (scriptStore[slideIndex]) { 52 | const scripts = scriptStore[slideIndex]; 53 | for (let i = 1; i < scripts.length; i+=2) { 54 | scripts[i]({slideIndex, slide}); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | /** 61 | * This functions registers functions to be executed when a slide mounted and 62 | * gets unmounted. 63 | * 64 | * @param {number} slideIndex The slide for which these functions should be 65 | * executed. 66 | * 67 | * @param {function} setup The function that should be executed when the slide 68 | * gets mounted. 69 | * 70 | * @param {function} teardown The function that should be executed when the 71 | * slide is unmounted. This should basically undo anything that the setup 72 | * function created. 73 | */ 74 | export function registerScript(slideIndex, setup, teardown) { 75 | if (!scriptStore[slideIndex]) { 76 | scriptStore[slideIndex] = []; 77 | } 78 | scriptStore[slideIndex].push(setup, teardown); 79 | } 80 | -------------------------------------------------------------------------------- /packages/exerslide/browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import present from './browser/present'; 10 | import {IS_MOBILE} from './browser/utils/deviceHelper'; 11 | import {getSlideCacheData, setSlideCacheData} from './browser/slideDataCache'; 12 | import {groupByChapter} from './browser/utils/chapterHelper'; 13 | import {forward, back} from './browser/navigation'; 14 | import {normalizeImageData} from './browser/utils/imageDataHelper'; 15 | import {register} from './browser/pluginManager'; 16 | import {registerExtension} from './browser/extensionManager'; 17 | import {subscribe, subscribeAll} from './browser/pubSub'; 18 | 19 | /** 20 | * This is the API exposed to plugins and other client side code. Client side 21 | * code should only require this file or components in `exerslide/components/` 22 | * but never directly reach into `exerslide/browser/`. 23 | */ 24 | 25 | export { 26 | IS_MOBILE, 27 | back, 28 | forward, 29 | getSlideCacheData, 30 | groupByChapter, 31 | normalizeImageData, 32 | present, 33 | register as use, 34 | registerExtension, 35 | setSlideCacheData, 36 | subscribe, 37 | subscribeAll, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/exerslide/browser/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - react 3 | parserOptions: 4 | ecmaVersion: 6 5 | sourceType: module 6 | ecmaFeatures: 7 | jsx: true 8 | experimentalObjectRestSpread: true 9 | -------------------------------------------------------------------------------- /packages/exerslide/browser/Presentation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import * as navigation from './navigation'; 10 | import React from 'react'; 11 | import Slide from '../components/Slide'; 12 | import {subscribe} from './pubSub'; 13 | 14 | /** 15 | * This component receives the state mutation methods from the store and reacts 16 | * to navigation events. It notifies the current slide about the events and only 17 | * notifies the store if the slide doesn't handle it. 18 | * 19 | * It also passes the configuration object to all descendants via context. 20 | */ 21 | export default class Presentation extends React.Component { 22 | componentDidMount() { 23 | subscribe( 24 | navigation.FORWARD, 25 | () => { 26 | const handled = this._slide && this._slide.onForward(); 27 | if (!handled) { 28 | this.props.nextSlide(); 29 | } 30 | } 31 | ); 32 | subscribe( 33 | navigation.BACK, 34 | () => { 35 | const handled = this._slide && this._slide.onBack(); 36 | if (!handled) { 37 | this.props.previousSlide(); 38 | } 39 | } 40 | ); 41 | subscribe( 42 | navigation.TO_SLIDE, 43 | index => this.props.goToSlide(index) 44 | ); 45 | } 46 | 47 | getChildContext() { 48 | return { 49 | config: this.props.config, 50 | }; 51 | } 52 | 53 | render() { 54 | const { 55 | masterLayout, 56 | slideLayout, 57 | slide, 58 | slideIndex, 59 | slides, 60 | } = this.props; 61 | 62 | const options = slide.options; 63 | const layout = slide.layout; 64 | let classNames = options.classNames ? 65 | [...options.classNames] : 66 | []; 67 | if (layout && layout.getClassNames) { 68 | classNames = classNames.concat( 69 | layout.getClassNames({slides, slideIndex, slide}) 70 | ); 71 | } 72 | 73 | return React.createElement( 74 | masterLayout, 75 | { 76 | className: classNames.join(' '), 77 | }, 78 | this._slide = ref} 81 | slide={slide} 82 | slideIndex={slideIndex} 83 | slideLayout={slideLayout} 84 | /> 85 | ); 86 | } 87 | } 88 | 89 | Presentation.propTypes = { 90 | masterLayout: React.PropTypes.func, 91 | slideLayout: React.PropTypes.func, 92 | slide: React.PropTypes.object, 93 | slideIndex: React.PropTypes.number, 94 | slides: React.PropTypes.arrayOf(React.PropTypes.object), 95 | config: React.PropTypes.object, 96 | nextSlide: React.PropTypes.func, 97 | previousSlide: React.PropTypes.func, 98 | goToSlide: React.PropTypes.func, 99 | }; 100 | 101 | Presentation.childContextTypes = { 102 | config: React.PropTypes.object, 103 | }; 104 | -------------------------------------------------------------------------------- /packages/exerslide/browser/extensionManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import {publish} from './pubSub'; 10 | 11 | /** 12 | * This module is the counter part to the component. The 13 | * registerExtension method is available to plugins so that the can specify 14 | * which components to render at extension points. 15 | */ 16 | 17 | const components = []; 18 | const validModes = ['before', 'after', 'wrap', 'replace']; 19 | 20 | /** 21 | * Returns the list of components that have been registered for the list of 22 | * tags. 23 | * 24 | * @param {Array} tags A list of tag names 25 | * @return {Array<{component: Component, mode: string, tags: Array}>} 26 | */ 27 | export function getRegisteredExtensions(tags) { 28 | const tagMap = tags.reduce((map, t) => (map[t] = true, map), {}); 29 | 30 | return components.filter(component => { 31 | return component.tags.some(t => tagMap[t]); 32 | }); 33 | } 34 | 35 | /** 36 | * Registers a component to be rendered for the specified extension points. 37 | * 38 | * `component` is a React component. 39 | * `mode` is one of "before", "after", "wrap" or "replace" that specifies how 40 | * the component is inserted into the extension point. 41 | * `tags` specifies where the component should be inserted. It will be inserted 42 | * into any extension point that is assigned a tag in that list. 43 | * 44 | * @param {Component} component The component to render 45 | * @param {string} mode How to insert the component 46 | * @param {Array} tags Where to render the component 47 | */ 48 | export function registerExtension(component, mode, tags) { 49 | if (validModes.indexOf(mode) === -1) { 50 | throw new Error(`"${mode}" is not a valid mode for an extension.`); 51 | } 52 | let obj = {component, tags, mode}; 53 | components.push(obj); 54 | 55 | // This informs the instances to rerender 56 | publish('EXTENSIONS.UPDATE'); 57 | return function() { 58 | if (obj) { 59 | components.splice(components.indexOf(obj), 1); 60 | obj = null; 61 | publish('EXTENSIONS.UPDATE'); 62 | } 63 | }; 64 | } 65 | 66 | /** 67 | * This function "unregisters" all components. Only used in test. 68 | */ 69 | export function clearAll_FOR_TESTS() { 70 | components.length = 0; 71 | } 72 | -------------------------------------------------------------------------------- /packages/exerslide/browser/navigation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import {publish} from './pubSub'; 10 | 11 | export const FORWARD = 'NAVIGATION.forward'; 12 | export const BACK = 'NAVIGATION.back'; 13 | export const TO_SLIDE = 'NAVIGATION.TO_SLIDE'; 14 | 15 | export function forward() { 16 | publish(FORWARD); 17 | } 18 | 19 | export function back() { 20 | publish(BACK); 21 | } 22 | 23 | export function gotToSlide(index) { 24 | publish(TO_SLIDE, index); 25 | } 26 | -------------------------------------------------------------------------------- /packages/exerslide/browser/pluginManager.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import * as exerslide from '../browser'; 10 | 11 | /** 12 | * Plugins are simply functions that get passed the exerslide browser API. The 13 | * manager makes sure that a plugin is only executed once. It also passes 14 | * the browser API to them. 15 | */ 16 | 17 | const plugins = []; 18 | 19 | export function register(plugin, ...args) { 20 | if (plugins.indexOf(plugin) === -1) { 21 | plugin(exerslide, ...args); 22 | plugins.push(plugin); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/exerslide/browser/present.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import Presentation from './Presentation'; 10 | import React from 'react'; 11 | import ReactDOM from 'react-dom'; 12 | import Store from './Store'; 13 | import {publish} from './pubSub'; 14 | 15 | /** 16 | * This is the main (internal) entry point. It connects the data store with 17 | * the presentation logic. It receives a configuration object with various 18 | * settings for the presentation. Currently supported are 19 | * 20 | * - masterLayout: A React component that defines the whole page. A default 21 | * version is copied into the project folder upon initialization. 22 | * - slideLayout: A React component that defines the base structure of a slide. 23 | * A default version is also copied into the project folder. 24 | * - slides: An array of slide objects. 25 | * - ...: Other options for plugins, such as content converters can be added. 26 | * 27 | * @param {Object} config Object with various configuration options. 28 | */ 29 | export default function present(config) { 30 | validateConfig(config); 31 | 32 | // This allows plugins to get a reference to the configuration object and 33 | // make changes to it before it used by exerslide. 34 | publish('CONFIG.SET', config); 35 | 36 | // The default index.html shows a loading indicator. If we find it, we remove 37 | // it. 38 | const loader = global.document.getElementById('exerslide-loader'); 39 | if (loader) { 40 | loader.parentNode.removeChild(loader); 41 | } 42 | ReactDOM.render( 43 | 44 | 49 | , 50 | global.document.body.appendChild( 51 | global.document.createElement('div') 52 | ) 53 | ); 54 | } 55 | 56 | const configValidation = { 57 | slides: 'You have to pass an array of slides.', 58 | masterLayout: 'You have to pass a master layout.', 59 | slideLayout: 'You have to pass a slide layout', 60 | }; 61 | 62 | function validateConfig(config) { 63 | Object.keys(configValidation).forEach(prop => { 64 | if (!(prop in config)) { 65 | throw new Error(configValidation[prop]); 66 | } 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /packages/exerslide/browser/pubSub.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import {EventEmitter} from 'events'; 10 | 11 | const emitter = new EventEmitter(); 12 | 13 | // The default limit is 10. There is no reason to restrict the number of 14 | // listeners 15 | emitter.setMaxListeners(Infinity); 16 | 17 | export const publish = emitter.emit.bind(emitter); 18 | 19 | export function subscribe(...args) { 20 | emitter.on(...args); 21 | return unsubscribe.bind(null, ...args); 22 | } 23 | 24 | export function subscribeAll(obj) { 25 | for (const eventName in obj) { 26 | if (obj.hasOwnProperty(eventName)) { 27 | emitter.on(eventName, obj[eventName]); 28 | } 29 | } 30 | return function() { 31 | for (const eventName in obj) { 32 | if (obj.hasOwnProperty(eventName)) { 33 | unsubscribe(eventName, obj[eventName]); 34 | } 35 | } 36 | }; 37 | } 38 | 39 | export const unsubscribe = emitter.removeListener.bind(emitter); 40 | -------------------------------------------------------------------------------- /packages/exerslide/browser/slideDataCache.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | let cache = []; 10 | 11 | /** 12 | * This is a helper module for layouts to persist slide specific data across 13 | * slide transitions. For example, the editor input of the visitor. 14 | */ 15 | 16 | export function getSlideCacheData(slideIndex, key, defaultData) { 17 | let result; 18 | if (cache.hasOwnProperty(slideIndex) && 19 | cache[slideIndex].hasOwnProperty(key)) { 20 | result = cache[slideIndex][key]; 21 | } 22 | if (result == null) { 23 | setSlideCacheData(slideIndex, key, defaultData); 24 | result = defaultData; 25 | } 26 | return result; 27 | } 28 | 29 | export function setSlideCacheData(slideIndex, key, data) { 30 | let slideCache = cache[slideIndex]; 31 | if (!slideCache) { 32 | slideCache = cache[slideIndex] = {}; 33 | } 34 | slideCache[key] = data; 35 | } 36 | 37 | export function getAllCacheData(key) { 38 | return cache.map(cache => cache[key]).filter(data => data); 39 | } 40 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "root": false, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/__tests__/chapterHelper-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import {expect} from 'chai'; 10 | import {groupByChapter} from '../chapterHelper'; 11 | 12 | describe('chapterHelper', () => { 13 | describe('groupByChapter', () => { 14 | 15 | it('groups slide objects by pathHash and chapter property', () => { 16 | const slides = [ 17 | {options: {chapter: 1}, pathHash: 'foo'}, 18 | {pathHash: 'foo', options: {}}, 19 | {pathHash: 'bar', options: {}}, 20 | {options: {chapter: 2}, pathHash: 'baz'}, 21 | {pathHash: 'baz', options: {}}, 22 | ]; 23 | 24 | expect(groupByChapter(slides)).to.deep.equal([ 25 | [{pathHash: 'foo', options: {chapter: 1}}, {pathHash: 'foo', options: {}}], 26 | {pathHash: 'bar', options: {}}, 27 | [{pathHash: 'baz', options: {chapter: 2}}, {pathHash: 'baz', options: {}}], 28 | ]); 29 | }); 30 | 31 | it('does not group slides if they do not have chapter or pathHash', () => { 32 | const slides = [ 33 | {options: {}}, 34 | {options: {}}, 35 | {options: {}}, 36 | ]; 37 | 38 | expect(groupByChapter(slides)).to.deep.equal([ 39 | {options: {}}, 40 | {options: {}}, 41 | {options: {}}, 42 | ]); 43 | }); 44 | 45 | it('groups slides with only `chapter`', () => { 46 | const slides = [ 47 | {options: {chapter: 'A'}}, 48 | {options: {chapter: 'A'}}, 49 | {options: {chapter: 'B'}}, 50 | {options: {}}, 51 | ]; 52 | 53 | expect(groupByChapter(slides)).to.deep.equal([ 54 | [{options: {chapter: 'A'}}, {options: {chapter: 'A'}}], 55 | [{options: {chapter: 'B'}}], 56 | {options: {}}, 57 | ]); 58 | }); 59 | 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/__tests__/imageDataHelper-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import {expect} from 'chai'; 10 | import {normalizeImageData} from '../imageDataHelper'; 11 | 12 | function testAll(tests) { 13 | let called = 0; 14 | tests.forEach(test => { 15 | test(); 16 | called += 1; 17 | }); 18 | expect(called).to.equal(tests.length); 19 | } 20 | 21 | describe('imageDataHelper', () => { 22 | 23 | describe('normalizeImageData', () => { 24 | 25 | it('normalizes file paths', () => { 26 | testAll( 27 | ['foo.png', './foo.png', '/foo.png', 'bar/foo.png'].map( 28 | p => () => expect(normalizeImageData(p)).to.deep.equal({src: p}) 29 | ) 30 | ); 31 | }); 32 | 33 | it('does not accept names without file extension', () => { 34 | testAll( 35 | ['foo', 'foo.', 'foo. bar', './foo.'].map( 36 | p => () => expect(normalizeImageData(p)).to.be.null 37 | ) 38 | ); 39 | }); 40 | 41 | it('accepts HTTP URLs', () => { 42 | testAll( 43 | ['http://foo.bar', 'https://foo.bar'].map( 44 | p => () => expect(normalizeImageData(p)).to.deep.equal({src: p}) 45 | ) 46 | ); 47 | }); 48 | 49 | it('returns image objects as is', () => { 50 | let img = {src: 'foo.png'}; 51 | expect(normalizeImageData(img)).to.equal(img); 52 | }); 53 | 54 | it('does not accept values without src property', () => { 55 | expect(normalizeImageData({bar: 'foo'})).to.be.null; 56 | expect(normalizeImageData('foo')).to.be.null; 57 | expect(normalizeImageData(null)).to.be.null; 58 | }); 59 | 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/__tests__/optionHelper-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import {expect} from 'chai'; 10 | import {normalizeOptions} from '../optionHelper'; 11 | 12 | describe('optionHelper', () => { 13 | 14 | describe('normalizeOptions', () => { 15 | 16 | it('renames snake case to camel case', () => { 17 | const options = { 18 | layout_data: {}, 19 | hide_toc: true, 20 | class_names: [], 21 | content_type: {}, 22 | scale: { 23 | content_width: '', 24 | column_width: '', 25 | max_font_size: '', 26 | }, 27 | }; 28 | 29 | expect(normalizeOptions(options)).to.deep.equal({ 30 | layoutData: {}, 31 | hideTOC: true, 32 | classNames: [], 33 | contentType: {}, 34 | scale: { 35 | contentWidth: '', 36 | columnWidth: '', 37 | maxFontSize: '', 38 | }, 39 | }); 40 | }); 41 | 42 | it('merges class_names', () => { 43 | const options = [ 44 | {title: 'first', class_names: ['foo']}, 45 | {title: 'second', class_names: ['bar']}, 46 | ]; 47 | 48 | expect(normalizeOptions(...options)).to.deep.equal({ 49 | title: 'second', 50 | classNames: ['foo', 'bar'], 51 | }); 52 | }); 53 | 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/chapterHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This does a very simply grouping of slides based on an explicitly set chapter 11 | * name on the slide, or the file path hash of the slide. 12 | * 13 | * It returns something like [slide, slide, [slide, slide slide], slide], where 14 | * a nested array represents a chapter (group). 15 | * 16 | * @param {Array} slides An arrow of slide objects. 17 | */ 18 | export function groupByChapter(slides) { 19 | return slides.reduce( 20 | (groupedSlides, slide) => { 21 | let previousEntry = groupedSlides[groupedSlides.length - 1]; 22 | if (Array.isArray(previousEntry) && 23 | (slide.options.chapter === previousEntry[0].options.chapter || 24 | slide.pathHash && slide.pathHash === previousEntry[0].pathHash) 25 | ) { 26 | previousEntry.push(slide); 27 | } else if (slide.options.chapter) { // new chapter 28 | groupedSlides.push([slide]); 29 | } else { 30 | groupedSlides.push(slide); 31 | } 32 | return groupedSlides; 33 | }, 34 | [] 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/deviceHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const MAX_MOBILE_WIDTH = 768; 10 | const DEVICE_WIDTH = global.document.documentElement.clientWidth; 11 | 12 | export const IS_MOBILE = DEVICE_WIDTH <= MAX_MOBILE_WIDTH; 13 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/imageDataHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * Helper function to normalize various ways of defining a reference to an 11 | * image. 12 | * 13 | * An image can either be defined as simply a path to local or local file, or 14 | * an object with a `src` and (optionally) `alt` property. 15 | * 16 | * This function normalizes the values. It will either return an object or null 17 | * if the input is not a valid image source. 18 | * 19 | * @param {string|{src: string}} imageData An image reference 20 | * 21 | * @return {{src: string}|null} 22 | */ 23 | export function normalizeImageData(imageData) { 24 | if (isPath(imageData)) { 25 | return {src: imageData}; 26 | } else if (isImageObject(imageData)) { 27 | return imageData; 28 | } 29 | return null; 30 | } 31 | 32 | const URL_PATTERN = /^https?:/; 33 | const PATH_PATTERN = /(?:\.{0,2}\/)?(?:[^\/]\/)*[^\/.]+\.\w{2,}/; 34 | 35 | // Only exported for tests 36 | export function isPath(path) { 37 | return typeof path === 'string' && 38 | (URL_PATTERN.test(path) || PATH_PATTERN.test(path)); 39 | } 40 | 41 | // Only exported for tests 42 | export function isImageObject(imageData) { 43 | return Boolean(imageData) && isPath(imageData.src); 44 | } 45 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/optionHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const mergeStrategies = { 10 | class_names(...options) { 11 | return options.reduce((target, classes) => { 12 | target.push(...classes); 13 | return target; 14 | }, []); 15 | }, 16 | }; 17 | 18 | const renameConfig = { 19 | class_names: 'classNames', 20 | hide_toc: 'hideTOC', 21 | layout_data: 'layoutData', 22 | content_type: 'contentType', 23 | scale: value => ([ 24 | 'scale', 25 | renameProperties( 26 | value, 27 | { 28 | content_width: 'contentWidth', 29 | column_width: 'columnWidth', 30 | max_font_size: 'maxFontSize', 31 | } 32 | ), 33 | ]), 34 | }; 35 | 36 | function renameProperties(object, config) { 37 | for (const prop in object) { 38 | if (!config.hasOwnProperty(prop)) { 39 | continue; 40 | } 41 | let value = object[prop]; 42 | let name; 43 | switch (typeof config[prop]) { 44 | case 'function': 45 | ([name, value] = config[prop](value)); 46 | delete object[prop]; 47 | object[name] = value; 48 | break; 49 | case 'string': 50 | delete object[prop]; 51 | object[config[prop]] = value; 52 | break; 53 | } 54 | } 55 | 56 | return object; 57 | } 58 | 59 | /** 60 | * Takes multiple options and merges and renames them based on the above 61 | * settings. 62 | * 63 | * @param {...Object} options Options to normalize. 64 | */ 65 | export function normalizeOptions(...options) { 66 | const target = {}; 67 | options.forEach(option => { 68 | for (const prop in option) { 69 | if (!(prop in target)) { 70 | target[prop] = option[prop]; 71 | } else if (mergeStrategies[prop]) { 72 | target[prop] = mergeStrategies[prop](target[prop], option[prop]); 73 | } else { 74 | target[prop] = option[prop]; 75 | } 76 | } 77 | }); 78 | return renameProperties(target, renameConfig); 79 | } 80 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/scriptHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This helper function evaluates passed in code as CommonJS module. 11 | * 12 | * @param {string} code JavaScript source code. 13 | * @return {?} whatever the code exports 14 | */ 15 | export function evalScript(code) { 16 | const module = {}; 17 | const exports = module.exports = {}; 18 | const Module = new Function('module, exports, global', code); 19 | Module(module, exports, global); 20 | return module.exports; 21 | } 22 | -------------------------------------------------------------------------------- /packages/exerslide/browser/utils/styleHelper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | let document = global.document; 10 | 11 | /** 12 | * Creates a style element from the CSS source passed in and adds it to the DOM. 13 | * It returns a function to remove that element. 14 | * 15 | * @param {string} cssText CSS to add to the page 16 | * @return {function} to remove the element 17 | */ 18 | export function addStyle(cssText) { 19 | let style = document.createElement('style'); 20 | style.innerHTML = cssText; 21 | document.head.appendChild(style); 22 | return { 23 | remove() { 24 | if (style) { 25 | document.head.removeChild(style); 26 | style = null; 27 | } 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/exerslide/cli/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "root": false, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/exerslide/cli/__tests__/init-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const childProcess = require('child_process'); 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | const testUtils = require('../../scripts/test-utils'); 15 | 16 | function run(args, cwd) { 17 | return new Promise((resolve, reject) => { 18 | childProcess.exec( 19 | [ 20 | path.join(__dirname, 'test-cli.js'), 21 | 'init', 22 | ] 23 | .concat(args).concat('--EXERSLIDE_TEST').join(' '), 24 | {cwd}, 25 | (error, stdout, stderr) => { 26 | if (error) { 27 | reject(error); 28 | return; 29 | } 30 | resolve({stdout, stderr}); 31 | } 32 | ); 33 | }); 34 | } 35 | 36 | function prepareWorkingDirectory(dir) { 37 | fs.mkdirSync(path.join(dir, 'node_modules')); 38 | fs.symlinkSync( 39 | path.join(__dirname, '..', '..'), 40 | path.join(dir, 'node_modules', 'exerslide') 41 | ); 42 | } 43 | 44 | describe('exerslide init', () => { 45 | 46 | it('passes "name" to the scaffolder', () => { 47 | // Prepare directory 48 | // It needs to have exerslide linked in, otherwise it will try to install it 49 | // which we don't want. 50 | const dir = testUtils.makeDirectoryStructure({}); 51 | prepareWorkingDirectory(dir); 52 | return run(['foo'], dir) 53 | .then(() => { 54 | testUtils.validateFolderStructure( 55 | dir, 56 | { 57 | 'css': { 58 | 'foo.css': '', 59 | }, 60 | } 61 | ); 62 | }); 63 | }); 64 | 65 | }); 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /packages/exerslide/cli/__tests__/test-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | 10 | const yargs = require('yargs'); 11 | 12 | const cli = yargs 13 | .strict(); 14 | 15 | const commands = require('../'); 16 | Object.keys(commands).forEach(k => cli.command(commands[k])); 17 | 18 | const _argv = cli.argv; 19 | -------------------------------------------------------------------------------- /packages/exerslide/cli/build.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /** 12 | * This command triggers a production build of the presentation. If a path is 13 | * passed as argument, the built files are stored there. 14 | */ 15 | 16 | exports.command = 'build [out]'; 17 | exports.describe = 'create a production-ready version of the presentation'; 18 | exports.builder = function(yargs) { 19 | return yargs 20 | .option({ 21 | verbose: { 22 | alias: ['v'], 23 | boolean: true, 24 | describe: 25 | 'Show more detailed webpack output instead (useful for debugging webpack).', 26 | default: false, 27 | }, 28 | }) 29 | .example('$0 build') 30 | .example('$0 build ./site'); 31 | }; 32 | exports.handler = function(argv) { 33 | // Enforce a production environment. NODE_ENV is read in configuration files 34 | // and needs to be set before they are loaded. This is a bit hacky, but works 35 | // until we find a better solution. 36 | process.env.NODE_ENV = 'production'; 37 | 38 | const utils = require('./utils'); 39 | 40 | utils.launch(env => { 41 | const builder = require('../lib/builder'); 42 | let exerslideConfig = Promise.resolve(require(env.configPath)); 43 | 44 | utils.logEvents(builder); 45 | if (argv.out) { 46 | const mkdirp = require('../lib/fs/mkdirp'); 47 | const path = require('path'); 48 | const out = path.resolve(argv.out); 49 | utils.log(`Building into "${out}"`); 50 | exerslideConfig = exerslideConfig.then(config => ( 51 | mkdirp(out).then(() => { 52 | config.out = path.resolve(out); 53 | return config; 54 | }) 55 | )); 56 | } 57 | 58 | exerslideConfig 59 | .then(config => builder.build( 60 | { 61 | configBase: env.configBase, 62 | config: config, 63 | }, 64 | argv 65 | )) 66 | .catch(error => utils.logError(error)); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /packages/exerslide/cli/copy-defaults.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This command copies all the initial project files into the current working 11 | * directory. The user can choose between multiple options in case the file 12 | * already exists. 13 | */ 14 | 15 | exports.command = 'copy-defaults'; 16 | exports.describe = 17 | 'copy the initial project files into the current directory. Can also be ' + 18 | 'used to update files after (local) exerslide was updated.'; 19 | exports.builder = function(yargs) { 20 | return yargs 21 | .options({ 22 | name: { 23 | describe: 'Presentation name to use in templates.', 24 | nargs: 1, 25 | requiresArg: true, 26 | type: 'string', 27 | }, 28 | 'overwrite-all': { 29 | alias: ['a'], 30 | describe: 'Overwrite existing files (DANGER: Only use this option if you '+ 31 | 'have a backup of your current files or use a version control system', 32 | type: 'boolean', 33 | default: false, 34 | }, 35 | confirm: { 36 | alias: ['c'], 37 | describe: 38 | 'Ask what to do if file already exists. ' + 39 | 'If set to false, keep the existing file.', 40 | type: 'boolean', 41 | default: true, 42 | }, 43 | 'ignore-hash': { 44 | describe: 'Don\'t compare file hashes of existing files. If set to ' + 45 | 'true, you will asked for any existing file that differs from the ' + 46 | 'template file.', 47 | type: 'boolean', 48 | default: false, 49 | }, 50 | }) 51 | .example('$0 copy-files --name myPresentation'); 52 | }; 53 | 54 | exports.handler = function(argv) { 55 | const scaffolder = require('../lib/scaffolder'); 56 | const utils = require('./utils'); 57 | 58 | scaffolder(process.cwd(), argv, error => { 59 | if (error) { 60 | utils.logError(`Unable to copy defaults: ${error.message}`); 61 | process.exit(1); 62 | } 63 | utils.log( 64 | 'All files updated! You may have to run `npm install` if "package.json"' + 65 | ' has changed.' 66 | ); 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /packages/exerslide/cli/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | module.exports = { 10 | init: require('./init'), 11 | copyDefaults: require('./copy-defaults'), 12 | build: require('./build'), 13 | watch: require('./watch'), 14 | serve: require('./serve'), 15 | }; 16 | -------------------------------------------------------------------------------- /packages/exerslide/cli/serve.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This command builds the development version of the presentation, 11 | * starts the webpack development server and opens the browser. Changes to any 12 | * of the runtime files will trigger an automatic reload of the browser. 13 | */ 14 | exports.command = 'serve'; 15 | exports.describe = 'create a developement version of the presentation and serve it via a local webserver'; 16 | exports.builder = function(yargs) { 17 | return yargs 18 | .option({ 19 | port: { 20 | alias: 'p', 21 | describe: 'Port the webserver should listen to', 22 | requiresArg: true, 23 | nargs: 1, 24 | default: 8080, 25 | }, 26 | 'open-browser': { 27 | alias: 'o', 28 | boolean: true, 29 | describe: 'Automatically open presentation in browser.', 30 | default: true, 31 | }, 32 | verbose: { 33 | alias: ['v'], 34 | boolean: true, 35 | describe: 36 | 'Show more detailed webpack output instead (useful for debugging webpack).', 37 | default: false, 38 | }, 39 | }) 40 | .example('$0 serve') 41 | .example('$0 serve --no-open-browser') 42 | .example('$0 serve -p 8000'); 43 | }; 44 | exports.handler = function(argv) { 45 | const utils = require('./utils'); 46 | 47 | utils.launch(env => { 48 | const builder = require('../lib/builder'); 49 | const exerslideConfig = require(env.configPath); 50 | 51 | utils.logEvents(builder); 52 | builder.serve( 53 | { 54 | configBase: env.configBase, 55 | config: exerslideConfig, 56 | }, 57 | argv 58 | ); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/exerslide/cli/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const Liftoff = require('liftoff'); 10 | const colors = require('colors/safe'); 11 | 12 | /** 13 | * Various helper functions for the commands. 14 | */ 15 | 16 | // Used to test exerslide by building the website 17 | const smokeTest = process.argv.some(arg => arg === '--smoke-test'); 18 | 19 | /** 20 | * The different commands need access to the exerslide configuration file. 21 | * We use liftoff to find it. 22 | */ 23 | const liftoff = new Liftoff({ 24 | name: 'exerslide', 25 | configName: 'exerslide.config', 26 | extensions: { 27 | '.js': null, 28 | }, 29 | }); 30 | 31 | /** 32 | * Print an error message and terminate if local configuration file wasn't 33 | * found. 34 | */ 35 | function assertLocalConfig(env) { 36 | if (!env.configPath) { 37 | process.stderr.write( 38 | colors.red( 39 | `Unable to find exerslide.config.js.\nYou need to run "exerslide init" first.\n` 40 | ) 41 | ); 42 | process.exit(1); 43 | } 44 | } 45 | 46 | exports.launch = function(callback) { 47 | liftoff.launch({}, env => { 48 | assertLocalConfig(env); 49 | callback(env); 50 | }); 51 | }; 52 | 53 | exports.logError = smokeTest ? 54 | () => process.exit(1) : 55 | msg => process.stderr.write(colors.red(colors.bold('Error ') + msg + '\n')); 56 | 57 | 58 | function log(msg) { 59 | process.stdout.write(msg + '\n'); 60 | } 61 | exports.log = log; 62 | 63 | /** 64 | * Outputs events from the builder to the console. 65 | */ 66 | exports.logEvents = function logEvents(builder) { 67 | if (smokeTest) { 68 | ['error', 'warning'].forEach( 69 | event => builder.on(event, () => process.exit(1)) 70 | ); 71 | } 72 | ['start', 'stop', 'info', 'error', 'warning'].forEach( 73 | event => builder.on(event, e => log(e.message)) 74 | ); 75 | 76 | builder.on('clear', function clearConsole() { 77 | process.stdout.write('\x1bc'); 78 | }); 79 | }; 80 | 81 | exports.hasFlag = function hasFlag(flag) { 82 | return process.argv.some(x => x === flag); 83 | } 84 | -------------------------------------------------------------------------------- /packages/exerslide/cli/watch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /** 12 | * This command builds the presentation and rebuilds it whenever a dependent 13 | * runtime file changes. 14 | */ 15 | exports.command = 'watch [out]'; 16 | exports.describe = 'create a developement version of the presentation and automatically rebuild on file changes'; 17 | exports.builder = function(yargs) { 18 | return yargs 19 | .option({ 20 | verbose: { 21 | alias: ['v'], 22 | boolean: true, 23 | describe: 24 | 'Show more detailed webpack output instead (useful for debugging webpack).', 25 | default: false, 26 | }, 27 | }) 28 | .example('$0 watch') 29 | .example('$0 watch ./site'); 30 | }; 31 | exports.handler = function(argv) { 32 | const utils = require('./utils'); 33 | 34 | utils.launch(env => { 35 | const builder = require('../lib/builder'); 36 | let exerslideConfig = Promise.resolve(require(env.configPath)); 37 | 38 | utils.logEvents(builder); 39 | if (argv.out) { 40 | const mkdirp = require('../lib/fs/mkdirp'); 41 | const path = require('path'); 42 | const out = path.resolve(argv.out); 43 | utils.log(`Building into "${out}"`); 44 | exerslideConfig = exerslideConfig.then(config => ( 45 | mkdirp(out).then(() => { 46 | config.out = path.resolve(out); 47 | return config; 48 | }) 49 | )); 50 | } 51 | 52 | exerslideConfig 53 | .then(config => builder.watch( 54 | { 55 | configBase: env.configBase, 56 | config: config, 57 | }, 58 | argv 59 | )) 60 | .catch(error => utils.logError(error)); 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /packages/exerslide/components/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaFeatures: 3 | experimentalObjectRestSpread: true 4 | -------------------------------------------------------------------------------- /packages/exerslide/components/ContentRenderer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * This component renders the value it receives via props with the content 11 | * type renderer of the current slide received via context. 12 | */ 13 | 14 | import React from 'react'; 15 | 16 | export default function ContentRenderer({value}, context) { 17 | return context.slide.contentConverter(value, context); 18 | } 19 | 20 | ContentRenderer.propTypes = { 21 | value: React.PropTypes.string, 22 | }; 23 | 24 | ContentRenderer.contextTypes = { 25 | slide: React.PropTypes.object.isRequired, 26 | slideIndex: React.PropTypes.number.isRequired, 27 | slides: React.PropTypes.arrayOf(React.PropTypes.object), 28 | config: React.PropTypes.object.isRequired, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/exerslide/components/Output.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import React from 'react'; 10 | 11 | /** 12 | * A helper component to render the result of something. This makes sure that 13 | * the result is properly indicated for screen readers. 14 | * 15 | * Use this if you e.g. show the rendered output of an HTML snippet as part of 16 | * your presentation / layout. 17 | */ 18 | export default function Output({label, children, ...props}) { 19 | return ( 20 |
    24 | {children} 25 |
    26 | ); 27 | } 28 | 29 | Output.propTypes = { 30 | /** 31 | * Name of the region. Defaults to "Result". Adjust depending on the type of 32 | * content you present. 33 | */ 34 | label: React.PropTypes.string, 35 | 36 | children: React.PropTypes.node, 37 | }; 38 | 39 | Output.defaultProps = { 40 | label: 'Result', 41 | }; 42 | -------------------------------------------------------------------------------- /packages/exerslide/components/__tests__/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /packages/exerslide/components/css/editor.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /* Editor */ 10 | 11 | .editor { 12 | border: 1px solid #EEE; 13 | position: relative; 14 | /* Always stretch to the parent's width, instead of the editor's content */ 15 | /* override for custom width */ 16 | width: 100%; 17 | } 18 | 19 | .editor, 20 | .editor .CodeMirror, 21 | .editor .CodeMirror-gutters { 22 | background-color: #f5f5f5; 23 | } 24 | 25 | .editor > textarea.CodeMirror { 26 | box-sizing: border-box; 27 | width: 100%; 28 | border: none; 29 | resize: none; 30 | overflow: auto; 31 | } 32 | 33 | .editor .CodeMirror-gutters { 34 | } 35 | 36 | .editor .editor-toggle-button { 37 | position: absolute; 38 | left: -10000px; 39 | z-index: 20; 40 | } 41 | 42 | .editor .editor-toggle-button:focus, 43 | .editor:hover .editor-toggle-button { 44 | left: auto; 45 | right: 10px; 46 | top: 10px; 47 | } 48 | -------------------------------------------------------------------------------- /packages/exerslide/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * Exposes build functions / helpers 11 | */ 12 | 13 | 'use strict'; 14 | 15 | exports.builder = require('./lib/builder'); 16 | exports.transforms = require('./lib/slide_transforms'); 17 | exports.transformHelper = require('./lib/slide_transforms/utils/transformHelper'); 18 | -------------------------------------------------------------------------------- /packages/exerslide/layouts/__ExerslideError__.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | import React from 'react'; 10 | 11 | import './css/__exerslideError__.css'; 12 | 13 | /** 14 | * This is a special layout that is used when the front matter of a slide cannot 15 | * be properly parsed. Instead of only showing an error on the command line, we 16 | * also render a slide that contains the error message and the relevant part of 17 | * the slide source. 18 | */ 19 | export default function __ExerslideError__({title, layoutData}) { 20 | const {error, source, filePath} = layoutData; 21 | return ( 22 |
    23 | {title} 24 |

    The slide {filePath} could not be processed:

    25 |
    26 |         {error.message}
    27 |       
    28 | {source ? 29 |
    30 |

    Full source

    31 |
    {source}
    32 |
    : 33 | null 34 | } 35 |
    36 | ); 37 | } 38 | 39 | __ExerslideError__.getClassNames = () => '__exerslide-error__'; 40 | -------------------------------------------------------------------------------- /packages/exerslide/layouts/css/__exerslideError__.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | #exerslide-page.__exerslide-error__ #exerslide-slide-title, 10 | #exerslide-toc-list .exerslide-toc-entry.__exerslide-error__ { 11 | color: red 12 | } 13 | 14 | #exerslide-page.__exerslide-error__ .exerslide-callout { 15 | background-color: #FFEDC2; 16 | font-size: 0.8rem; 17 | padding: 10px; 18 | overflow-x: auto; 19 | } 20 | -------------------------------------------------------------------------------- /packages/exerslide/lib/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "root": false, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/exerslide/lib/__tests__/initPlugins-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const initPlugins = require('../initPlugins'); 12 | const expect = require('chai').expect; 13 | const makeDirectoryStructure = require('../../scripts/test-utils').makeDirectoryStructure; 14 | 15 | global.expect = expect; 16 | 17 | const dir = makeDirectoryStructure({ 18 | node_modules: { 19 | plugin1: { 20 | contentTypes: { 21 | 'myType.js': '', 22 | }, 23 | layouts: { 24 | 'myLayout.js': '', 25 | }, 26 | 'package.json': '{"name": "plugin1"}', 27 | }, 28 | 'exerslide-plugin-plugin2': { 29 | 'package.json': '{"name": "plugin2"}', 30 | 'init.js': ` 31 | module.exports = function(exerslideConfig, webpackConfig) { 32 | expect(exerslideConfig, 'plugin.init[exerslideConfig]').to.not.be.undefined; 33 | expect(webpackConfig, 'plugin.init[webpackConfig]').to.not.be.undefined; 34 | expect(exerslideConfig).to.not.equal(webpackConfig); 35 | }; 36 | `, 37 | }, 38 | }, 39 | }); 40 | 41 | describe('initPlugins', () => { 42 | 43 | it('initializes plugins', () => { 44 | initPlugins( 45 | {plugins: ['plugin2']}, 46 | {context: dir, slideLoader: {transforms: []}} 47 | ); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /packages/exerslide/lib/__tests__/initTransforms-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const initTransforms = require('../initTransforms'); 12 | const expect = require('chai').expect; 13 | const makeDirectoryStructure = require('../../scripts/test-utils').makeDirectoryStructure; 14 | const path = require('path'); 15 | const slideLoader = require('../slide-loader'); 16 | 17 | global.expect = expect; 18 | 19 | const dir = makeDirectoryStructure({ 20 | node_modules: { 21 | plugin1: { 22 | contentTypes: { 23 | 'myType.js': '', 24 | }, 25 | layouts: { 26 | 'myLayout.js': '', 27 | }, 28 | 'package.json': '{"name": "plugin1"}', 29 | }, 30 | }, 31 | }); 32 | 33 | function test(exerslideConfig, webpackConfig, content, verify) { 34 | webpackConfig = Object.assign( 35 | { 36 | context: dir, 37 | slideLoader: { 38 | transforms: [], 39 | }, 40 | }, 41 | webpackConfig 42 | ); 43 | 44 | initTransforms(exerslideConfig, webpackConfig); 45 | slideLoader.call( 46 | { 47 | resourcePath: 'test.md', 48 | options: webpackConfig, 49 | async: () => ( 50 | (error, result) => verify(error, result.replace(/\n+\s*/g, '')) 51 | ), 52 | emitWarning: () => {}, 53 | }, 54 | content 55 | ); 56 | } 57 | 58 | describe('initTransforms', () => { 59 | 60 | it('properly initializes default transforms', done => { 61 | test( 62 | {plugins: ['plugin1']}, 63 | {}, 64 | '---\n layout: myLayout\ncontent_type: myType\n---\n', 65 | (error, result) => { 66 | expect(result).to.contain(path.join(dir, 'node_modules/plugin1/contentTypes/myType.js')); 67 | expect(result).to.contain(path.join(dir, 'node_modules/plugin1/layouts/myLayout.js')); 68 | done(); 69 | } 70 | ); 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /packages/exerslide/lib/__tests__/toSlideObject-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const expect = require('chai').expect; 12 | const toSlideObject = require('../toSlideObject'); 13 | 14 | describe('toSlideObject', () => { 15 | 16 | it('converts slide content to a JS object', () => { 17 | expect(toSlideObject('---\n_foo: bar\n---\nbaz')).to.deep.equal({ 18 | options: {_foo: 'bar'}, 19 | content: 'baz', 20 | }); 21 | }); 22 | 23 | it('accepts slides without content', () => { 24 | expect(toSlideObject('---\n_foo: bar\n---')).to.deep.equal({ 25 | options: {_foo: 'bar'}, 26 | content: '', 27 | }); 28 | }); 29 | 30 | it('accepts slides without fron matter', () => { 31 | expect(toSlideObject('baz')).to.deep.equal({ 32 | options: {}, 33 | content: 'baz', 34 | }); 35 | }); 36 | 37 | it('returns an error slide if the front matter cannot be parsed', () => { 38 | const slide = toSlideObject('---\n_foo: bar: baz\n---', {}); 39 | expect(slide.options.layout).to.equal('__ExerslideError__'); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /packages/exerslide/lib/fs/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "root": false, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/exerslide/lib/fs/__tests__/mkdirp-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const expect = require('chai').expect; 10 | const mkdirp = require('../mkdirp'); 11 | const testUtils = require('../../../scripts/test-utils'); 12 | 13 | describe('mkdirp', () => { 14 | 15 | it('generates nested folders', done => { 16 | const dir = testUtils.makeDirectoryStructure({}); 17 | 18 | mkdirp(dir + '/foo/bar/baz') 19 | .then( 20 | () => { 21 | testUtils.validateFolderStructure(dir, {foo: {bar: {baz: {}}}}); 22 | done(); 23 | }, 24 | () => { 25 | expect(true).to.equal(false); 26 | done(); 27 | } 28 | ) 29 | .then(null, error => done(error)); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /packages/exerslide/lib/fs/mkdirp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | 14 | /** 15 | * Ensures that the directory and all it's parents exists. 16 | * 17 | * @param {string} p Path to the directory 18 | * @return {Promise} 19 | */ 20 | module.exports = function mkdirp(p) { 21 | return new Promise((resolve, reject) => { 22 | fs.mkdir(p, (err) => { 23 | if (!err) { 24 | resolve(); 25 | return; 26 | } 27 | switch (err.code) { 28 | case 'ENOENT': 29 | return mkdirp(path.dirname(p)) 30 | .then(() => mkdirp(p)) 31 | .then(resolve, reject); 32 | case 'EEXIST': 33 | resolve(); 34 | break; 35 | default: 36 | reject(err); 37 | } 38 | }); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/exerslide/lib/fs/pathExists.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const fs = require('fs'); 10 | 11 | /** 12 | * Convenience function to test whether a path exists. 13 | * 14 | * @param {string} path The path to the file / directory. 15 | * @return {bool} 16 | */ 17 | module.exports = function fileExists(path) { 18 | try { 19 | fs.accessSync(path, fs.F_OK); 20 | return true; 21 | } catch (e) { 22 | return false; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/exerslide/lib/initPlugins.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const path = require('path'); 12 | const resolvePlugin = require('./utils/resolvePlugin'); 13 | 14 | /** 15 | * This function goes through all specified plugins and calls there init 16 | * function if it exists. 17 | * 18 | * The init function is a function exported by `init.js` in the plugin's folder. 19 | */ 20 | module.exports = function initPlugins(exerslideConfig, webpackConfig) { 21 | // Initialize plugins 22 | const exerslidePlugins = exerslideConfig.plugins || []; 23 | const context = webpackConfig.context; 24 | 25 | exerslidePlugins.forEach(function(pluginName) { 26 | const plugin = resolvePlugin(pluginName, context); 27 | let init; 28 | try { 29 | init = require(path.join(plugin.path, 'init')); 30 | } catch (err) { 31 | if (err.message.indexOf('Cannot find module') === -1) { 32 | throw err; 33 | } 34 | } 35 | 36 | if (init) { 37 | init(exerslideConfig, webpackConfig); 38 | } 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/exerslide/lib/initTransforms.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const listFilesFromPlugins = require('./utils/fileLookupHelper').listFilesFromPlugins; 12 | 13 | /** 14 | * This function adds the necessary built-in transforms to the webpack config. 15 | * 16 | * We decided to do this internally and not in the default webpack.config.js 17 | * to 18 | * 19 | * - prevent authors from accidentally breaking exerslide by removing one of 20 | * these transforms 21 | * - reduce the complexity of the webpack.config.js file 22 | */ 23 | module.exports = function initTransforms(exerslideConfig, webpackConfig) { 24 | // These transforms are required for exerslide to work properly 25 | let transforms = webpackConfig.slideLoader.transforms; 26 | if (!transforms) { 27 | transforms = webpackConfig.slideLoader.transforms = []; 28 | } 29 | 30 | const plugins = exerslideConfig.plugins; 31 | plugins.push('./'); // look in current project 32 | if (!plugins.indexOf('exerslide') > -1) { 33 | plugins.push('exerslide'); 34 | } 35 | 36 | transforms.push( 37 | require('./slide_transforms/hashPath')(), 38 | require('./slide_transforms/registerContentType')({ 39 | converters: listFilesFromPlugins( 40 | plugins, 41 | 'contentTypes', 42 | webpackConfig.context 43 | ), 44 | }), 45 | require('./slide_transforms/registerLayout')({ 46 | layouts: listFilesFromPlugins(plugins, 'layouts', webpackConfig.context), 47 | defaultLayouts: exerslideConfig.defaultLayouts, 48 | }) 49 | ); 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "root": false, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/__tests__/atRequireExpansion-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const atRequireExpansion = require('../atRequireExpansion'); 10 | const expect = require('chai').expect; 11 | 12 | function test(input, verify) { 13 | atRequireExpansion({}).before(input, verify); 14 | } 15 | 16 | describe('atRequireExpansion', () => { 17 | 18 | it('replaces @require with require calls', () => { 19 | test( 20 | 'This is @require("./a/test") with @require(./paths).', 21 | (errors, output, actions) => { 22 | expect(output).to.equal( 23 | `This is ${actions[0].search} with ${actions[1].search}.` 24 | ); 25 | expect(actions).to.deep.equal([ 26 | { 27 | type: 'interpolate', 28 | search: actions[0].search, 29 | value: 'require(\"./a/test\")', 30 | }, 31 | { 32 | type: 'interpolate', 33 | search: actions[1].search, 34 | value: 'require(\"./paths\")', 35 | }, 36 | ]); 37 | } 38 | ); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/__tests__/hashPath-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const hashPath = require('../hashPath'); 12 | const expect = require('chai').expect; 13 | 14 | describe('hashPath', () => { 15 | 16 | it('assigns the same hash for slides in the same folder', () => { 17 | let hashes = []; 18 | function callback(error, slide) { 19 | hashes.push(slide.pathHash); 20 | } 21 | 22 | const transformer = hashPath({}); 23 | 24 | transformer.after({}, callback, {resourcePath: './foo/bar'}); 25 | transformer.after({}, callback, {resourcePath: './foo/baz'}); 26 | transformer.after({}, callback, {resourcePath: './foo'}); 27 | 28 | expect(hashes[0]).to.equal(hashes[1]); 29 | expect(hashes[1]).to.not.equal(hashes[2]); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/__tests__/requireAssets-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const requireAssets = require('../requireAssets'); 10 | const expect = require('chai').expect; 11 | const temp = require('temp').track(); 12 | const path = require('path'); 13 | 14 | const assetFile = temp.openSync({suffix: '.png'}).path; 15 | const slideFile = temp.openSync({dir: path.dirname(assetFile)}).path; 16 | 17 | function test(input, verify, options) { 18 | requireAssets(options || {}).before( 19 | input, 20 | verify, 21 | {resourcePath: slideFile} 22 | ); 23 | } 24 | 25 | describe('requireAssets', () => { 26 | 27 | it('replaces asset paths with require calls', () => { 28 | test( 29 | `foo: ./${path.basename(assetFile)}`, 30 | (errors, output, actions) => { 31 | expect(output).to.equal(`foo: ${actions[0].search}`); 32 | expect(actions).to.deep.equal([ 33 | { 34 | type: 'interpolate', 35 | search: actions[0].search, 36 | value: `require(\"./${path.basename(assetFile)}\")`, 37 | }, 38 | ]); 39 | } 40 | ); 41 | }); 42 | 43 | it('does not replace paths that do not exist', () => { 44 | test( 45 | 'This is a test: ./foo.png', 46 | (errors, output, replacements) => { 47 | expect(output).to.equal('This is a test: ./foo.png'); 48 | expect(replacements).to.be.empty; 49 | expect(errors.length).to.equal(1); 50 | } 51 | ); 52 | }); 53 | 54 | it('accepts a custom pattern', () => { 55 | test( 56 | `foo: --./${path.basename(assetFile)}--`, 57 | (errors, output, actions) => { 58 | expect(output).to.equal(`foo: ${actions[0].search}`); 59 | expect(actions).to.deep.equal([ 60 | { 61 | type: 'interpolate', 62 | search: actions[0].search, 63 | value: `require(\"./${path.basename(assetFile)}\")`, 64 | }, 65 | ]); 66 | }, 67 | {pattern: /--(.+)--/} 68 | ); 69 | }); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/atRequireExpansion.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const transformHelper = require('./utils/transformHelper'); 12 | 13 | const DEFAULT_REQUIRE_PATTERN = /@require\(([^)]+)\)/ig; 14 | 15 | /** 16 | * This transformer finds occurrences of `@require(path/to/file)` in slide files 17 | * and replaces them with webpack `require` calls so that webpack can load that 18 | * file. 19 | * 20 | * Together with other webpack loaders, such as `raw-loader`, this can be used 21 | * to include the content of another file into the slide. 22 | * 23 | * For example 24 | * 25 | * --- 26 | * ```js 27 | * @require('!!raw!./test.js') 28 | * ``` 29 | * 30 | * is transformed into something like 31 | * 32 | * module.exports = { 33 | * content: "```js\n" + require('!!raw!./test.js) + "\n```", 34 | * }; 35 | * 36 | * which, when executed results in something like 37 | * 38 | * module.exports = { 39 | * content: "```js\n\n```", 40 | * }; 41 | * 42 | * @param {Object} config The following configuration options are available 43 | * - pattern (RegEx): Allows to customize the pattern to search for. The path 44 | * should be return in the first capture group. 45 | */ 46 | module.exports = function(config) { 47 | const REQUIRE_PATTERN = config && config.pattern || DEFAULT_REQUIRE_PATTERN; 48 | 49 | return { 50 | before: function(source, next) { 51 | const actions = []; 52 | source = source.replace(REQUIRE_PATTERN, (match, path) => { 53 | path = path.replace(/^['"]|['"]$/g, ''); 54 | const searchID = transformHelper.getID(); 55 | actions.push(transformHelper.getInterpolateAction( 56 | searchID, 57 | `require(${JSON.stringify(path)})` 58 | )); 59 | return searchID; 60 | }); 61 | next(null, source, actions); 62 | }, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/hashPath.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const crypto = require('crypto'); 12 | const path = require('path'); 13 | 14 | /** 15 | * This is an internal transformer that computes a hash of the parent path of 16 | * the slide. This is used to group slides by folder at runtime. 17 | */ 18 | module.exports = function() { 19 | return { 20 | after: function(slide, next, options) { 21 | slide.pathHash = crypto.createHash('md5') 22 | .update(path.dirname(options.resourcePath)) 23 | .digest('hex'); 24 | next(null, slide); 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | module.exports = { 10 | atRequireExpansion: require('./atRequireExpansion'), 11 | requireAssets: require('./requireAssets'), 12 | }; 13 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/requireAssets.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const transformHelper = require('./utils/transformHelper'); 14 | 15 | const DEFAULT_CONFIG = { 16 | pattern: /(?:\.{1,2}\/)+[-_\/a-z\d.]+\.(?:png|jpe?g|gif|svg)\b/ig, 17 | ignoreMissingFiles: false, 18 | }; 19 | 20 | /** 21 | * This transformer tries to automatically detect paths to asset files and 22 | * converts them to webpack require calls. It also checks whether the path 23 | * exists in the file system. 24 | * 25 | * The advantage of this transformer is that it will pick up files without the 26 | * author having to mark the file path's explicitly. 27 | * The disadvantage is that it might consider something as a file path that 28 | * isn't one. To limit false positives, the default pattern only looks for 29 | * relative paths (something that stats with `./` or `../`) and for paths 30 | * that represent images (end in png, jpg, gif or svg) (see above). 31 | * 32 | * The pattern can be configured in exerslide.config.js depending on the 33 | * author's needs. 34 | * 35 | * @param {Object} config The following configuration options are available: 36 | * - pattern (RegExp): The regex to find file paths. If the pattern has a 37 | * capture group, the value of the first capture group will be used as path 38 | * instead. 39 | * - ignoreMissingFiles (bool): If set to false (default) a warning is shown 40 | * if a detected path doesn't exist in the file system. Otherwise those 41 | * paths are silently ignored. 42 | */ 43 | module.exports = function requireAssets(config) { 44 | config = Object.assign({}, DEFAULT_CONFIG, config); 45 | return { 46 | before: function(source, next, options) { 47 | const actions = []; 48 | const errors = []; 49 | 50 | source = source.replace(config.pattern, (match, p) => { 51 | if (typeof p === 'number') { // not a capture group 52 | p = match; 53 | } 54 | try { 55 | fs.accessSync( 56 | path.join(path.dirname(options.resourcePath), p), 57 | fs.F_OK 58 | ); 59 | } catch (ex) { 60 | if (!config.ignoreMissingFiles) { 61 | errors.push('Unable to find file: ' + p); 62 | } 63 | return match; 64 | } 65 | const searchID = transformHelper.getID(); 66 | actions.push(transformHelper.getInterpolateAction( 67 | searchID, 68 | `require(${JSON.stringify(p)})` 69 | )); 70 | return searchID; 71 | }); 72 | next(errors, source, actions); 73 | }, 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /packages/exerslide/lib/slide_transforms/utils/getErrorSlideObject.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /** 12 | * Helper function to create a slide object representing an error slide. 13 | * 14 | * @param {string} title Title of the file 15 | * @param {string} filePath The (relative) path to the file 16 | * @param {Error} error The error object 17 | * @param {string} source The raw source of the slide 18 | * 19 | * @return {Object} 20 | */ 21 | module.exports = function(title, filePath, error, source) { 22 | return { 23 | options: { 24 | title: 'Slide generation error: ' + title, 25 | toc: 'Slide generation error', 26 | layout: '__ExerslideError__', 27 | style: '', 28 | layout_data: { 29 | filePath, 30 | source, 31 | error, 32 | }, 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/exerslide/lib/toSlideObject.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const frontMatter = require('front-matter'); 12 | const getErrorSlideObject = require('./slide_transforms/utils/getErrorSlideObject'); 13 | const optionHelper = require('./utils/optionHelper'); 14 | 15 | /** 16 | * This function converts the source of a slide file into a JavaScript object. 17 | * That includes parsing the YAML front matter and producing an error slide 18 | * object in case the slide options are not valid. 19 | */ 20 | module.exports = function toSlide(content, options) { 21 | var slide = {}; 22 | if (frontMatter.test(content)) { 23 | try { 24 | let fm = frontMatter(content); 25 | slide.options = fm.attributes; 26 | slide.content = fm.body; 27 | 28 | const optionsToValidate = [slide.options]; 29 | if (slide.options.defaults) { 30 | optionsToValidate.push(slide.options.default); 31 | } 32 | 33 | optionsToValidate.every(o => { 34 | const validationResult = optionHelper.validateOptions(o); 35 | if (!validationResult[0]) { 36 | slide = getErrorSlideObject( 37 | 'Invalid options', 38 | options.resourcePath, 39 | new Error(validationResult[1].map(e => e.message).join('\n')), 40 | content 41 | ); 42 | return false; 43 | } 44 | return true; 45 | }); 46 | } catch (error) { 47 | slide = getErrorSlideObject( 48 | 'YAML parse error', 49 | options.resourcePath, 50 | error, 51 | content 52 | ); 53 | } 54 | } else { 55 | slide.options = {}; 56 | slide.content = content; 57 | } 58 | return slide; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/exerslide/lib/utils/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "root": false, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/exerslide/lib/utils/__tests__/fileLookupHelper-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const expect = require('chai').expect; 10 | const fileLookupHelper = require('../fileLookupHelper'); 11 | const path = require('path'); 12 | const testUtils = require('../../../scripts/test-utils'); 13 | 14 | describe('fileLookupHelper', () => { 15 | 16 | it('lists all files in the provided directory', () => { 17 | const dir = testUtils.makeDirectoryStructure({ 18 | node_modules: { 19 | plugin1: { 20 | dir1: { 21 | file1: '', 22 | dir2: { 23 | file2: '', 24 | }, 25 | __file3__: '', 26 | 'file-04': '', 27 | 'file5.js': '', 28 | 29 | }, 30 | file1: '', 31 | 'package.json': JSON.stringify({name: 'plugin1'}), 32 | }, 33 | }, 34 | }); 35 | 36 | const modulePath = path.resolve( 37 | __dirname, 38 | path.join(dir, 'node_modules', 'plugin1') 39 | ); 40 | 41 | expect(fileLookupHelper.listFilesFromPlugins( 42 | ['plugin1'], 43 | 'dir1', 44 | path.resolve(__dirname, dir) 45 | )) 46 | .to.deep.equal({ 47 | plugin1: { 48 | file1: path.join(modulePath, 'dir1', 'file1'), 49 | __file3__: path.join(modulePath, 'dir1', '__file3__'), 50 | 'file-04': path.join(modulePath, 'dir1', 'file-04'), 51 | file5: path.join(modulePath, 'dir1', 'file5.js'), 52 | }, 53 | }); 54 | }); 55 | 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /packages/exerslide/lib/utils/__tests__/resolvePlugin-test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | const expect = require('chai').expect; 10 | const makeDirectoryStructure = require('../../../scripts/test-utils').makeDirectoryStructure; 11 | const path = require('path'); 12 | const resolvePlugin = require('../resolvePlugin'); 13 | 14 | describe('resolvePlugin', () => { 15 | 16 | it('resolves "exerslide" no matter the context', () => { 17 | expect(resolvePlugin('exerslide', '')).to.deep.equal({ 18 | name: 'exerslide', 19 | path: path.resolve(__dirname, '../../../'), 20 | }); 21 | }); 22 | 23 | it('resolves "." or "./" to the context', () => { 24 | expect(resolvePlugin('.', '/foo')).to.deep.equal({ 25 | name: '.', 26 | path: '/foo', 27 | }); 28 | expect(resolvePlugin('./', '/foo')).to.deep.equal({ 29 | name: '.', 30 | path: '/foo', 31 | }); 32 | }); 33 | 34 | it('resolves local paths', () => { 35 | expect(resolvePlugin('../../../', __dirname)).to.deep.equal({ 36 | name: 'exerslide', 37 | path: path.resolve(__dirname, '../../../'), 38 | }); 39 | }); 40 | 41 | it('resolves module names', () => { 42 | expect(resolvePlugin('chai', __dirname)).to.deep.equal({ 43 | name: 'chai', 44 | path: path.dirname(require.resolve('chai/package.json')), 45 | }); 46 | }); 47 | 48 | it('prepends "exerslide-plugin-" if it cannot find the module', () => { 49 | const dir = makeDirectoryStructure({ 50 | node_modules: { 51 | plugin1: { 52 | 'package.json': '{"name": "plugin1"}', 53 | }, 54 | 'exerslide-plugin-plugin1': { 55 | 'package.json': '{"name": "exerlside-plugin-plugin1"}', 56 | }, 57 | 'exerslide-plugin-plugin2': { 58 | 'package.json': '{"name": "exerlside-plugin-plugin2"}', 59 | }, 60 | }, 61 | }); 62 | 63 | expect(resolvePlugin('plugin1', dir)).to.deep.equal({ 64 | name: 'plugin1', 65 | path: path.resolve(dir, 'node_modules/plugin1/'), 66 | }); 67 | expect(resolvePlugin('plugin2', dir)).to.deep.equal({ 68 | name: 'plugin2', 69 | path: path.resolve(dir, 'node_modules/exerslide-plugin-plugin2/'), 70 | }); 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /packages/exerslide/lib/utils/diff.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const colors = require('colors'); 12 | const diff = require('diff'); 13 | const editor = require('editor'); 14 | const fs = require('fs'); 15 | const path = require('path'); 16 | const temp = require('temp').track(); 17 | 18 | /** 19 | * Print a diff of the existing file and the default file to the console. 20 | */ 21 | exports.printDiff = function(targetPath, oldContent, sourcePath, newContent) { 22 | const patch = diff.structuredPatch( 23 | relative(targetPath), 24 | relative(sourcePath), 25 | oldContent, 26 | newContent 27 | ); 28 | process.stdout.write(colors.red(`--- ${patch.oldFileName}\n`)); 29 | process.stdout.write(colors.green(`+++ ${patch.newFileName}\n`)); 30 | patch.hunks.forEach(h => { 31 | process.stdout.write( 32 | `@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@\n` // eslint-disable-line comma-spacing 33 | ); 34 | h.lines.forEach(line => { 35 | switch (line[0]) { 36 | case '+': 37 | line = colors.green(line); 38 | break; 39 | case '-': 40 | line = colors.red(line); 41 | break; 42 | } 43 | process.stdout.write(line + '\n'); 44 | }); 45 | }); 46 | } 47 | 48 | /** 49 | * Open the author's default editor and edit the diff of the files. 50 | */ 51 | exports.editDiff = function(fileExtension, oldContent, newContent) { 52 | return new Promise((resolve, reject) => { 53 | const file = temp.path({suffix: '.patch'}); 54 | const stream = fs.createWriteStream(file); 55 | diff 56 | .diffLines(oldContent, newContent) 57 | .forEach(function(part){ 58 | const value = part.value.replace( 59 | /^(?=.)/mg, 60 | part.added ? '+' : part.removed ? '-' : ' ' 61 | ); 62 | stream.write(value); 63 | }); 64 | stream.end(); 65 | stream.on('close', () => editor(file, code => { 66 | // Apply the "patch" 67 | const content = fs.readFileSync(file).toString() 68 | .replace(/^-[^\r\n]*\r?\n?/gm, '') 69 | .replace(/^[+ ]/gm, ''); 70 | if (code === 0) { 71 | resolve(content); 72 | } else { 73 | reject(null); 74 | } 75 | })); 76 | }); 77 | } 78 | 79 | function relative(p) { 80 | return path.relative(process.cwd(), p); 81 | } 82 | -------------------------------------------------------------------------------- /packages/exerslide/lib/utils/indent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | /** 10 | * Prepend any line in the string by `indent`. 11 | */ 12 | module.exports = function indent(message, indent, first) { 13 | message = message 14 | .split('\n') 15 | .map(line => indent + line) 16 | .join('\n'); 17 | return first ? message.replace(indent, first) : message; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /packages/exerslide/lib/utils/resolvePlugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const path = require('path'); 12 | const resolve = require('resolve'); 13 | 14 | const RELATIVE_PATH_PATTERN = /\.{1,2}\//; 15 | 16 | /** 17 | * Helper function to correctly resolve plugin references for layout and 18 | * content type transformers. 19 | * 20 | * This also handles the case that a plugin with the name `exerslide-plugin-foo` 21 | * can be specified just as `foo`. 22 | * 23 | * @param {string} pluginName The module name of the plugin 24 | * @param {string} context Folder to resolve the module from 25 | * @return {{name: string, path: string}} 26 | */ 27 | module.exports = function resolvePlugin(pluginName, context) { 28 | // This is to make local development with npm link work 29 | if (pluginName === 'exerslide') { 30 | return { 31 | name: 'exerslide', 32 | path: path.resolve(__dirname, '../../'), 33 | }; 34 | } 35 | // The current project has to be treated in a special way 36 | if (pluginName === '.' || pluginName === './') { 37 | return { 38 | name: '.', 39 | path: context, 40 | }; 41 | } 42 | // local plugins specified by path 43 | if (RELATIVE_PATH_PATTERN.test(pluginName)) { 44 | const pluginPath = path.resolve(context, pluginName); 45 | return { 46 | name: require(path.join(pluginPath, 'package.json')).name, 47 | path: pluginPath, 48 | }; 49 | } 50 | 51 | // node_module / npm plugins 52 | const lookupName = path.join(pluginName, 'package.json'); 53 | try { 54 | return { 55 | name: pluginName, 56 | path: path.dirname(resolve.sync( 57 | lookupName, 58 | {basedir: context} 59 | )), 60 | }; 61 | } catch (err) { 62 | if (err.message.includes('Cannot find module')) { 63 | return { 64 | name: pluginName, 65 | path: path.dirname(resolve.sync( 66 | 'exerslide-plugin-' + lookupName, 67 | {basedir: context} 68 | )), 69 | }; 70 | } 71 | throw err; 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /packages/exerslide/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exerslide", 3 | "version": "1.1.5", 4 | "description": "A tool for creating HTML presentations with focus on exercises for web technologies", 5 | "scripts": { 6 | "test": "npm run test-browser && npm run test-server", 7 | "test-browser": "mocha --require ignore-styles --require ./scripts/browser-test-setup.js --compilers js:babel-register `find browser/ browser-plugins/ components/ scaffolding/ -name *-test.js`", 8 | "test-server": "mocha `find lib/ cli/ -name *-test.js`", 9 | "lint": "eslint index.js browser.js cli/ lib/ components/ browser/ browser-plugins/ scaffolding/", 10 | "prepublish": "npm run lint && npm run test" 11 | }, 12 | "keywords": [ 13 | "presentation" 14 | ], 15 | "author": "Felix Kling", 16 | "license": "MIT", 17 | "dependencies": { 18 | "babel-runtime": "^6.11.6", 19 | "colors": "^1.1.2", 20 | "diff": "^2.2.1", 21 | "editor": "^1.0.0", 22 | "front-matter": "^2.0.5", 23 | "globby": "^6.0.0", 24 | "inquirer": "^1.1.2", 25 | "is-text-path": "^1.0.1", 26 | "leven": "^2.0.0", 27 | "liftoff": "^2.3.0", 28 | "lodash.template": "^4.4.0", 29 | "mime-types": "^2.1.8", 30 | "mocha": "^3.0.2", 31 | "mousetrap": "^1.6.0", 32 | "opener": "^1.4.1", 33 | "resolve": "^1.1.6", 34 | "sane": "^1.4.1", 35 | "temp": "^0.8.3", 36 | "webpack-dev-server": "^1.14.1" 37 | }, 38 | "optionalDependencies": { 39 | "foundation-sites": "^6.3.0", 40 | "font-awesome": "^4.4.0" 41 | }, 42 | "peerDependencies": { 43 | "codemirror": "^5.7.0", 44 | "highlight.js": "^9.6.0", 45 | "react": "^15.0.0", 46 | "react-dom": "^15.0.0", 47 | "webpack": "^1.12.11" 48 | }, 49 | "devDependencies": { 50 | "babel": "^6.5.2", 51 | "babel-plugin-module-resolver": "^2.2.0", 52 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 53 | "babel-preset-es2015": "^6.3.13", 54 | "babel-preset-react": "^6.3.13", 55 | "babel-register": "^6.3.13", 56 | "chai": "^3.4.1", 57 | "enzyme": "^2.4.1", 58 | "eslint": "^3.3.1", 59 | "eslint-import-resolver-webpack": "^0.5.1", 60 | "eslint-plugin-import": "^1.13.0", 61 | "eslint-plugin-react": "^6.1.2", 62 | "fs-extra": "^0.30.0", 63 | "ignore-styles": "^5.0.1", 64 | "jsdom": "^9.4.5", 65 | "react-addons-test-utils": "^15.3.1", 66 | "yargs": "^5.0.0" 67 | }, 68 | "repository": { 69 | "type": "git", 70 | "url": "git+https://github.com/facebookincubator/exerslide.git" 71 | }, 72 | "bugs": { 73 | "url": "https://github.com/facebookincubator/exerslide/issues" 74 | }, 75 | "homepage": "https://github.com/facebookincubator/exerslide#readme", 76 | "eslintConfig": { 77 | "plugins": [ 78 | "react" 79 | ], 80 | "extends": [ 81 | "plugin:react/recommended", 82 | "plugin:import/errors", 83 | "plugin:import/warnings" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaFeatures: 3 | experimentalObjectRestSpread: true 4 | 5 | rules: 6 | import/no-unresolved: [2, { ignore: ['^(exerslide/|!!)'] }] 7 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/.gitignore: -------------------------------------------------------------------------------- 1 | # This hash helps exerslide to determine whether the file needs to be updated 2 | # or not. Please don't remove it. 3 | # @exerslide-file-hash 4 | 5 | /node_modules 6 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/css/style.css: -------------------------------------------------------------------------------- 1 | /* @remove-on-copy-start */ 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | /* @remove-on-copy-end */ 10 | /** 11 | * You can add custom CSS styles here. 12 | */ 13 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/exerslide.config.js: -------------------------------------------------------------------------------- 1 | // @remove-on-copy-start 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | // @remove-on-copy-end 10 | /* 11 | * This hash helps exerslide to determine whether the file needs to be updated 12 | * or not. Please don't remove it. 13 | * @exerslide-file-hash 14 | */ 15 | 16 | 'use strict'; 17 | 18 | const isTextPath = require('is-text-path'); 19 | const path = require('path'); 20 | 21 | module.exports = { 22 | 23 | /** Standard configuration options **/ 24 | 25 | /** 26 | * Paths to stylesheets. Can refer to modules. 27 | */ 28 | stylesheets: [ 29 | 'foundation-sites/dist/css/foundation.css', 30 | 'font-awesome/css/font-awesome.css', 31 | 'highlight.js/styles/solarized-light.css', 32 | './css/exerslide.css', 33 | './css/<%= name %>.css', 34 | ], 35 | 36 | /** 37 | * This map allows you to automatically assign layouts based on file 38 | * extension. 39 | */ 40 | defaultLayouts: { 41 | }, 42 | 43 | /** 44 | * List of plugins to load. Plugins provide layouts, content type converters, 45 | * or other extensions to the exerslide or webpack config. 46 | * 47 | * A list of module names (exerslide-plugin-* can be omitted) or paths. 48 | */ 49 | plugins: [ 50 | 'bulletlist-layout', 51 | 'center-layout', 52 | 'column-layout', 53 | 'html-converter', 54 | 'markdown-converter', 55 | ], 56 | 57 | /** Advanced configuration options **/ 58 | 59 | /** 60 | * Absolute path to save the built presentation. 61 | */ 62 | out: path.join(__dirname, './out'), 63 | 64 | /** 65 | * File path patterns used to watch slides for changes while creating the 66 | * presentation. 67 | */ 68 | slidePaths: [ 69 | './slides/*', 70 | './slides/*/*', 71 | ], 72 | 73 | /** 74 | * A function with which you can filter and reorder the file paths matched by 75 | * the patterns in "slidePaths". 76 | */ 77 | processSlides(paths) { 78 | return paths.filter(isTextPath).sort(); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/index.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | <%= name %> 21 | 22 | 23 | 24 |
    25 | 26 |
    27 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/js/MasterLayout.js: -------------------------------------------------------------------------------- 1 | // @remove-on-copy-start 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | // @remove-on-copy-end 10 | /* 11 | * This hash helps exerslide to determine whether the file needs to be updated 12 | * or not. Please don't remove it. 13 | * @exerslide-file-hash 14 | */ 15 | 16 | import ExtensionPoint from 'exerslide/components/ExtensionPoint'; 17 | import React from 'react'; 18 | import TOC from './components/TOC'; 19 | import Toolbar from './components/Toolbar'; 20 | 21 | /** 22 | * The master layout specifies the overall layout of the page, such as 23 | * the table of contents, a progress indicator and of course the slide itself. 24 | * The current slide component is passed as child to it. 25 | * 26 | * Be default the master layout renders a table of contents, navigation buttons 27 | * and the slide content: 28 | * 29 | * +----------------------------------------+ 30 | * |+---------+ +--------------------------+| 31 | * || | | +-----------------------+|| 32 | * || | | | ||| 33 | * || | | | Slide ||| 34 | * || TOC | | | ||| 35 | * || | | +-----------------------+|| 36 | * || | | +-----------------------+|| 37 | * || | | | Toolbar ||| 38 | * || | | +-----------------------+|| 39 | * |+---------+ +--------------------------+| 40 | * +----------------------------------------+ 41 | * 42 | */ 43 | export default function MasterLayout({className, children}) { 44 | return ( 45 |
    46 | 47 | 48 |
    49 | {children} 50 | 51 |
    52 |
    53 |
    54 | ); 55 | } 56 | 57 | MasterLayout.propTypes = { 58 | /** 59 | * CSS class names to add to the page. 60 | */ 61 | className: React.PropTypes.string, 62 | 63 | /** 64 | * The rendered slide is passed as child to the master layout. 65 | */ 66 | children: React.PropTypes.node, 67 | }; 68 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/js/SlideLayout.js: -------------------------------------------------------------------------------- 1 | // @remove-on-copy-start 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | // @remove-on-copy-end 10 | /* 11 | * This hash helps exerslide to determine whether the file needs to be updated 12 | * or not. Please don't remove it. 13 | * @exerslide-file-hash 14 | */ 15 | 16 | import ExtensionPoint from 'exerslide/components/ExtensionPoint'; 17 | import React from 'react'; 18 | 19 | /** 20 | * The base layout for every slide. This allows you do add additional 21 | * content to all slides before or after the content. 22 | */ 23 | export default function SlideLayout({children}) { 24 | return ( 25 | 26 |
    27 | {children} 28 |
    29 |
    30 | ); 31 | } 32 | 33 | SlideLayout.propTypes = { 34 | /** 35 | * The current slide content and header are passed in as children by exerslide 36 | */ 37 | children: React.PropTypes.node, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/js/components/Toolbar.js: -------------------------------------------------------------------------------- 1 | // @remove-on-copy-start 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | // @remove-on-copy-end 10 | /* 11 | * This hash helps exerslide to determine whether the file needs to be updated 12 | * or not. Please don't remove it. 13 | * @exerslide-file-hash 14 | */ 15 | 16 | import React from 'react'; 17 | import ExtensionPoint from 'exerslide/components/ExtensionPoint'; 18 | import {forward, back} from 'exerslide/browser'; 19 | 20 | import './css/toolbar.css'; 21 | 22 | /** 23 | * This components generates a previous and next buttons (rendered as arrows, 24 | * using Font Awesome) to navigate the presentation. 25 | */ 26 | export default function Toolbar({className}, {slideIndex, slides}) { 27 | const numberOfSlides = slides.length; 28 | 29 | return ( 30 | 31 |
    35 | 43 | 48 | {' ' + (slideIndex + 1) + '/' + numberOfSlides + ' '} 49 | 50 | 58 |
    59 |
    60 | ); 61 | } 62 | 63 | Toolbar.propTypes = { 64 | className: React.PropTypes.string, 65 | }; 66 | 67 | Toolbar.contextTypes = { 68 | /** 69 | * This index of the current slide. 70 | */ 71 | slideIndex: React.PropTypes.number.isRequired, 72 | 73 | /** 74 | * Number of slides. 75 | */ 76 | slides: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, 77 | }; 78 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/js/components/__tests__/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "root": false, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/js/components/css/toc.css: -------------------------------------------------------------------------------- 1 | /* @remove-on-copy-start */ 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | /* @remove-on-copy-end */ 10 | /* 11 | * This hash helps exerslide to determine whether the file needs to be updated 12 | * or not. Please don't remove it. 13 | * @exerslide-file-hash 14 | */ 15 | 16 | .exerslide-toc-container { 17 | background-color: #F4F4F4; 18 | border-right: 1px solid #CCC; 19 | display: flex; 20 | flex-direction: column; 21 | max-width: 30%; 22 | min-width: 20em; 23 | } 24 | 25 | .exerslide-toc-container.collapsed { 26 | background-color: inherit; 27 | border-right: none; 28 | min-width: 0; 29 | position: absolute; 30 | z-index: 100; 31 | } 32 | 33 | ol.exerslide-toc-entries, 34 | ol.exerslide-toc-list { 35 | list-style: none; 36 | counter-reset: item; 37 | overflow-y: auto; 38 | margin: 0; 39 | } 40 | 41 | .exerslide-toc-list { 42 | padding-left: 1em; 43 | padding-right: 1em; 44 | } 45 | 46 | .exerslide-toc-entries { 47 | padding-left: 0; 48 | } 49 | 50 | .exerslide-toc-list > * { 51 | margin-bottom: 1em; 52 | } 53 | 54 | .exerslide-toc-title, 55 | .exerslide-toc-container.collapsed > .exerslide-toc-list { 56 | display: none; 57 | } 58 | 59 | .exerslide-toc-toggleButton { 60 | align-self: flex-end; 61 | background-color: transparent; 62 | border: none; 63 | color: #AAA; 64 | cursor: pointer; 65 | flex-shrink: 0; 66 | font-size: 1em; 67 | outline-width: thin; 68 | padding: 0.5em; 69 | } 70 | 71 | .exerslide-toc-toggleButton:hover { 72 | color: inherit; 73 | } 74 | 75 | .exerslide-toc-list > :first-child { 76 | margin-top: 0; 77 | } 78 | 79 | .exerslide-toc-entry, 80 | .exerslide-toc-chapter { 81 | display: block; 82 | } 83 | 84 | .exerslide-toc-entry::before, 85 | .exerslide-toc-chapter::before { 86 | content: counters(item, '.') ". "; 87 | counter-increment: item; 88 | } 89 | 90 | .exerslide-toc-heading { 91 | display: inline-block; 92 | margin: 0; 93 | margin-bottom: 0.5em; 94 | } 95 | 96 | .exerslide-toc-list > .exerslide-toc-entry::before, 97 | .exerslide-toc-list > .exerslide-toc-entry, 98 | .exerslide-toc-chapter::before, 99 | .exerslide-toc-heading { 100 | font-weight: bold; 101 | font-size: 1em; 102 | } 103 | 104 | .exerslide-toc-entry { 105 | padding: 0.1em 0; 106 | } 107 | 108 | .exerslide-toc-entry > a { 109 | color: inherit; 110 | } 111 | 112 | .exerslide-toc-entry, 113 | .exerslide-toc-entry > a { 114 | text-decoration: none; 115 | outline-width: thin; 116 | } 117 | 118 | .exerslide-toc-entry.active, 119 | .exerslide-toc-entry.active > a, 120 | .exerslide-toc-entry:hover { 121 | color: #428bca; 122 | } 123 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/js/components/css/toolbar.css: -------------------------------------------------------------------------------- 1 | /* @remove-on-copy-start */ 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | /* @remove-on-copy-end */ 10 | /* 11 | * This hash helps exerslide to determine whether the file needs to be updated 12 | * or not. Please don't remove it. 13 | * @exerslide-file-hash 14 | */ 15 | 16 | .exerslide-toolbar { 17 | box-sizing: border-box; 18 | color: #BBB; 19 | padding: 0 1.5em; 20 | text-align: right; 21 | width: 100%; 22 | } 23 | 24 | .exerslide-toolbar-button, 25 | .exerslide-toolbar-text { 26 | font-size: 1.05rem; 27 | } 28 | .exerslide-toolbar-button { 29 | background-color: transparent; 30 | cursor: pointer; 31 | color: #BBB; 32 | border: none; 33 | padding: 10px; 34 | } 35 | 36 | .exerslide-toolbar-button:focus, 37 | .exerslide-toolbar-button:hover { 38 | color: #555; 39 | } 40 | 41 | .exerslide-toolbar-button[disabled] { 42 | visibility: hidden; 43 | } 44 | 45 | @media(max-width: 768px) { 46 | .exerslide-toolbar { 47 | text-align: center; 48 | margin-bottom: 1em; 49 | } 50 | 51 | .exerslide-toolbar-button, 52 | .exerslide-toolbar-text { 53 | font-size: 1.5rem; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= name %>", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "dependencies": { 7 | "codemirror": "^5.7.0", 8 | "exerslide": "~1.1.2", 9 | "exerslide-plugin-bulletlist-layout": "^1.1.0", 10 | "exerslide-plugin-center-layout": "^1.1.0", 11 | "exerslide-plugin-column-layout": "^1.1.0", 12 | "exerslide-plugin-html-converter": "^1.0.0", 13 | "exerslide-plugin-markdown-converter": "^1.1.0", 14 | "font-awesome": "^4.4.0", 15 | "foundation-sites": "^6.3.0", 16 | "highlight.js": "^9.6.0", 17 | "react": "^15.0.0", 18 | "react-dom": "^15.0.0" 19 | }, 20 | "devDependencies": { 21 | "autoprefixer-loader": "^3.1.0", 22 | "babel-core": "^6.2.1", 23 | "babel-loader": "^6.2.0", 24 | "babel-plugin-transform-runtime": "^6.3.13 ", 25 | "babel-preset-es2015": "^6.1.18", 26 | "babel-preset-react": "^6.1.18", 27 | "babel-preset-stage-0": "^6.1.18", 28 | "babel-runtime": "^6.3.13", 29 | "css-loader": "^0.23.0", 30 | "extract-text-webpack-plugin": "^0.9.1", 31 | "file-loader": "^0.8.5", 32 | "html-loader": "^0.4.3", 33 | "html-webpack-plugin": "^2.22.0", 34 | "is-text-path": "^1.0.1", 35 | "json-loader": "^0.5.4", 36 | "style-loader": "^0.13.0", 37 | "webpack": "^1.12.9", 38 | "yaml-loader": "^0.4.0" 39 | }, 40 | "exerslide": "@exerslide-file-hash" 41 | } 42 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/references.yml: -------------------------------------------------------------------------------- 1 | # You can keep links to external sources here. This lets you avoid repeating the 2 | # same URL on different slides. The default markdown parser takes these into 3 | # account.The format is: "name: URL" 4 | # 5 | # Example: 6 | # 7 | # example: http://example.org 8 | -------------------------------------------------------------------------------- /packages/exerslide/scaffolding/slides/00-example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example Slide 3 | --- 4 | 5 | This is an example to demonstrate the basic structure of a slide. 6 | -------------------------------------------------------------------------------- /packages/exerslide/scripts/browser-test-setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | var jsdom = require('jsdom').jsdom; 10 | 11 | global.document = jsdom(''); 12 | global.window = global.document.defaultView; 13 | Object.keys(global.document.defaultView).forEach((property) => { 14 | if (typeof global[property] === 'undefined') { 15 | global[property] = global.document.defaultView[property]; 16 | } 17 | }); 18 | 19 | global.navigator = { 20 | userAgent: 'node.js', 21 | }; 22 | 23 | // The .babelrc file in the parent directory is configured to only be used in 24 | // the "exerslide-test" environment. This prevents Babel from picking up this 25 | // config when running webpack (which results in errors) 26 | process.env.BABEL_ENV = 'exerslide-test'; 27 | -------------------------------------------------------------------------------- /packages/exerslide/scripts/test-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the license found in the LICENSE file in 6 | * the root directory of this source tree. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const fs = require('fs-extra'); 12 | const path = require('path'); 13 | const temp = require('temp').track(); 14 | const expect = require('chai').expect; 15 | 16 | /** 17 | * Helper function to create a directory structure described by the provided 18 | * object at root. If root is not provided, a new temporary directory is created 19 | * (and returned). 20 | * 21 | * Example: 22 | * 23 | * makeDirectoryStructure({ 24 | * x: { 25 | * y: 'abc', 26 | * z: 'def', 27 | * }, 28 | * a: 'ghi' 29 | * }, './foo/bar'); 30 | * 31 | * generates 32 | * 33 | * - ./foo/bar/x/y with content 'abc' 34 | * - ./foo/bar/x/z with content 'def' 35 | * - ./foo/bar/a with content 'ghi' 36 | * 37 | * 38 | * @param {Object} structure 39 | * @param {?string] root Path to root directory 40 | * @return {string} Path to root directory 41 | */ 42 | exports.makeDirectoryStructure = function makeDirectoryStructure(dirs, root) { 43 | if (!root) { 44 | root = temp.mkdirSync(); 45 | } 46 | var subdirs = Object.keys(dirs); 47 | if (subdirs.length === 0) { 48 | fs.ensureDirSync(root); 49 | } else { 50 | subdirs.forEach(name => { 51 | const p = path.join(root, name); 52 | switch (typeof dirs[name]) { 53 | case 'string': 54 | fs.outputFileSync(p, dirs[name]); 55 | break; 56 | case 'object': 57 | makeDirectoryStructure(dirs[name], p); 58 | break; 59 | } 60 | }); 61 | } 62 | return root; 63 | }; 64 | 65 | exports.validateFolderStructure = function validateFolderStructure(root, structure) { 66 | 67 | function normalize(p) { 68 | return p.replace(root, ''); 69 | } 70 | 71 | function validateInternal(dir, structure) { 72 | for (var prop in structure) { 73 | const p = path.join(dir, prop); 74 | expect(() => fs.statSync(p), `${normalize(p)} exists`).to.not.throw(); 75 | const stat = fs.statSync(p); 76 | if (typeof structure[prop] === 'object') { 77 | expect(stat.isDirectory()) 78 | .to.equal(true, `${normalize(p)}) is directory`); 79 | validateFolderStructure(path.join(dir, prop), structure[prop]); 80 | } else { 81 | expect(stat.isFile()).to.equal(true, `${normalize(p)} is file`); 82 | if (structure[prop]) { 83 | switch (typeof structure[prop]) { 84 | case 'function': 85 | structure[prop](fs.readFileSync(p, 'utf-8')); 86 | break; 87 | case 'string': 88 | expect(fs.readFileSync(p, 'utf-8')).to.equal(structure[prop]); 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | validateInternal(root, structure); 97 | }; 98 | -------------------------------------------------------------------------------- /scripts/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2016-present, Facebook, Inc. 3 | # All rights reserved. 4 | # 5 | # This source code is licensed under the BSD-style license found in the 6 | # LICENSE file in the root directory of this source tree. An additional grant 7 | # of patent rights can be found in the PATENTS file in the same directory. 8 | 9 | set -e 10 | 11 | source ./scripts/shared.sh 12 | 13 | for pkg in $PACKAGES; do 14 | pushd "$PACKAGESPATH/$pkg" > /dev/null 15 | 16 | if grep -q '"lint":' ./package.json; then 17 | npm run lint 18 | fi 19 | 20 | if grep -q '"test":' ./package.json; then 21 | npm test 22 | else 23 | echo "Skipping '$pkg' because it doesn't have any tests" 24 | fi 25 | 26 | popd > /dev/null 27 | done 28 | 29 | # Build example as smoke test 30 | pushd ./example 31 | echo "exerslide build" 32 | exerslide build 33 | echo "exerslide watch" 34 | exerslide watch --smoke-test 35 | echo "exerslide serve" 36 | exerslide serve --no-open-browser --smoke-test 37 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) 2016-present, Facebook, Inc. 3 | # All rights reserved. 4 | # 5 | # This source code is licensed under the BSD-style license found in the 6 | # LICENSE file in the root directory of this source tree. An additional grant 7 | # of patent rights can be found in the PATENTS file in the same directory. 8 | 9 | source ./scripts/shared.sh 10 | 11 | for pkg in $PACKAGES; do 12 | pushd "$PACKAGESPATH/$pkg" > /dev/null 13 | 14 | if ! npm publish ; then 15 | echo "Failed to publish $pkg" 16 | fi 17 | 18 | popd > /dev/null 19 | done 20 | -------------------------------------------------------------------------------- /scripts/shared.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright (c) 2016-present, Facebook, Inc. 3 | # All rights reserved. 4 | # 5 | # This source code is licensed under the BSD-style license found in the 6 | # LICENSE file in the root directory of this source tree. An additional grant 7 | # of patent rights can be found in the PATENTS file in the same directory. 8 | 9 | set -e 10 | 11 | SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" 12 | PACKAGESPATH="$SCRIPTPATH/../packages" 13 | 14 | has_package() { 15 | if [ -e "$PACKAGESPATH/$1/package.json" ]; then 16 | return 0; 17 | fi 18 | return 1; 19 | } 20 | 21 | PACKAGES=$( 22 | for pkg in $(ls "$PACKAGESPATH"); do 23 | if has_package "$pkg"; then 24 | echo "$pkg" 25 | fi 26 | done 27 | ); 28 | -------------------------------------------------------------------------------- /scripts/syncPackageVersion.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the license found in the LICENSE file in 7 | * the root directory of this source tree. 8 | */ 9 | 10 | 'use strict'; 11 | 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | const semver = require('semver'); 15 | 16 | const PACKAGES_ROOT = path.join(__dirname, '../packages'); 17 | const FORCE_UPDATE = process.argv.some(x => x === '--force'); 18 | 19 | function mapObject(obj, fn) { 20 | return Object.keys(obj) 21 | .reduce((newObj, key) => ((newObj[key] = fn(obj[key], key)), newObj), {}); 22 | } 23 | 24 | // 0. Get all package.json files 25 | const packages = fs.readdirSync(PACKAGES_ROOT) 26 | .filter(name => !/^\./.test(name)) 27 | .map(name => require(path.join(PACKAGES_ROOT, name, 'package.json'))) 28 | .reduce((obj, pkg) => ((obj[pkg.name] = pkg), obj), {}); 29 | 30 | // 1. Get current version 31 | const versions = mapObject(packages, pkg => pkg.version); 32 | 33 | // 2. Update the dependencies and devDependencies of every package 34 | const newPackages = mapObject( 35 | packages, 36 | pkg => updatePkg(pkg, versions, FORCE_UPDATE) 37 | ); 38 | 39 | // 3. Write package.json objects back to disk 40 | Object.keys(newPackages).forEach(pkgName => { 41 | const pkg = newPackages[pkgName]; 42 | fs.writeFileSync( 43 | path.join(PACKAGES_ROOT, pkgName, 'package.json'), 44 | serializePkg(pkg) 45 | ); 46 | }); 47 | 48 | // 4. Update scaffolding package.json file 49 | const scaffolingPackagePath = path.join( 50 | PACKAGES_ROOT, 51 | 'exerslide/scaffolding/package.json' 52 | ); 53 | fs.writeFileSync( 54 | scaffolingPackagePath, 55 | serializePkg( 56 | updatePkg(require(scaffolingPackagePath), versions, FORCE_UPDATE) 57 | ) 58 | ); 59 | 60 | // ----------- 61 | 62 | function updatePkg(pkg, versions, forceUpdate) { 63 | ['dependencies', 'peerDependencies'].forEach(depsName => { 64 | if (!pkg[depsName]) { 65 | return; 66 | } 67 | pkg[depsName] = mapObject(pkg[depsName], (versionRange, dependency) => { 68 | if (!versions[dependency] || 69 | !forceUpdate && semver.satisfies(versions[dependency], versionRange) 70 | ) { 71 | return versionRange; 72 | } 73 | return (/^\d/.test(versionRange) ? '' : versionRange.substr(0, 1)) + 74 | versions[dependency]; 75 | }); 76 | }); 77 | return pkg; 78 | } 79 | 80 | function serializePkg(pkg) { 81 | return JSON.stringify(pkg, null, 2) + '\n'; 82 | } 83 | -------------------------------------------------------------------------------- /scripts/testNewProject.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2016-present, Facebook, Inc. 3 | # All rights reserved. 4 | # 5 | # This source code is licensed under the BSD-style license found in the 6 | # LICENSE file in the root directory of this source tree. An additional grant 7 | # of patent rights can be found in the PATENTS file in the same directory. 8 | 9 | set -e 10 | 11 | # Simulate installing global exerslide 12 | pushd packages/exerslide-cli 13 | npm link 14 | popd 15 | 16 | 17 | # Create empty project folder 18 | mkdir testProject 19 | dir=$(pwd) 20 | trap 'cd "$dir" && rm -rf testProject' ERR EXIT 21 | pushd testProject 22 | 23 | # Link exerslide (simulates the first step of exerslide init) 24 | mkdir node_modules 25 | npm link ../packages/exerslide/ 26 | 27 | # Initialize and build 28 | # This will install dependencies from 29 | exerslide init --confirm=false 30 | exerslide build 31 | --------------------------------------------------------------------------------