├── tests ├── __init__.py ├── test_data │ ├── maximal │ │ ├── directory │ │ │ ├── .dont-include │ │ │ ├── subdir │ │ │ │ └── afile │ │ │ ├── hovercraft_logo.png │ │ │ └── print.css │ │ ├── js │ │ │ ├── dummy.js │ │ │ └── hovercraft.js │ │ ├── images │ │ │ └── hovercraft_logo.png │ │ ├── fonts │ │ │ ├── texgyreschola-regular-webfont.eot │ │ │ ├── texgyreschola-regular-webfont.ttf │ │ │ └── texgyreschola-regular-webfont.woff │ │ ├── template.cfg │ │ ├── css │ │ │ ├── print.css │ │ │ ├── style.css │ │ │ └── impressConsole.css │ │ └── template.xsl │ ├── minimal │ │ ├── js │ │ │ └── hovercraft-minimal.js │ │ ├── template.cfg │ │ └── template.xsl │ ├── extra.css │ ├── class.rst │ ├── comment.rst │ ├── extra.js │ ├── slide_class.rst │ ├── images │ │ └── hovercraft_logo.png │ ├── css │ │ ├── sub.css │ │ └── sub2.css │ ├── include.rst │ ├── container.rst │ ├── math.rst │ ├── subdir-css.rst │ ├── simple.rst │ ├── table.rst │ ├── presenter-notes.rst │ ├── positioning.rst │ └── advanced.rst ├── test_docs.py ├── test_generator.py ├── test_template.py ├── test_parse.py └── test_hovercraft.py ├── setup.cfg ├── .coveragerc ├── docs ├── examples │ ├── images │ │ └── hovercraft_logo.png │ ├── tutorial.css │ ├── hovercraft.css │ ├── hovercraft.rst │ ├── positions.rst │ └── tutorial.rst ├── index.rst ├── usage.rst ├── designing.rst ├── introduction.rst ├── make.bat ├── templates.rst ├── Makefile └── conf.py ├── hovercraft ├── templates │ ├── simple │ │ ├── template.cfg │ │ ├── js │ │ │ └── hovercraft.js │ │ ├── css │ │ │ ├── hovercraft.css │ │ │ └── highlight.css │ │ └── template.xsl │ ├── default │ │ ├── template.cfg │ │ ├── js │ │ │ ├── hovercraft.js │ │ │ └── gotoSlide.js │ │ ├── css │ │ │ ├── hovercraft.css │ │ │ └── highlight.css │ │ └── template.xsl │ └── reST.xsl ├── __init__.py ├── template.py ├── position.py ├── generate.py └── parse.py ├── Makefile ├── .travis.yml ├── .gitignore ├── MANIFEST.in ├── LICENSE.txt ├── setup.py ├── README.rst └── CHANGES.txt /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_data/maximal/directory/.dont-include: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_data/maximal/directory/subdir/afile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pep8] 2 | max-line-length=100 3 | ignore=E129 -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = hovercraft 3 | omit = tests* 4 | -------------------------------------------------------------------------------- /tests/test_data/minimal/js/hovercraft-minimal.js: -------------------------------------------------------------------------------- 1 | impress().init(); 2 | -------------------------------------------------------------------------------- /tests/test_data/extra.css: -------------------------------------------------------------------------------- 1 | /* An empty css file to test the command line css support. */ -------------------------------------------------------------------------------- /tests/test_data/class.rst: -------------------------------------------------------------------------------- 1 | ---- 2 | 3 | .. class:: my-class 4 | 5 | This is some text in the class 6 | -------------------------------------------------------------------------------- /tests/test_data/comment.rst: -------------------------------------------------------------------------------- 1 | ---- 2 | 3 | This text should appear. 4 | 5 | .. This comment should not. 6 | -------------------------------------------------------------------------------- /tests/test_data/extra.js: -------------------------------------------------------------------------------- 1 | /* An empty javascript file to test the command line javascript support. */ 2 | -------------------------------------------------------------------------------- /tests/test_data/slide_class.rst: -------------------------------------------------------------------------------- 1 | ------------ 2 | 3 | :class: something-else 4 | 5 | This is some text 6 | -------------------------------------------------------------------------------- /docs/examples/images/hovercraft_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramiro/hovercraft/master/docs/examples/images/hovercraft_logo.png -------------------------------------------------------------------------------- /tests/test_data/maximal/js/dummy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dummy.js 3 | * 4 | * This file does nothing, it's only a placeholder. 5 | * 6 | */ 7 | -------------------------------------------------------------------------------- /tests/test_data/images/hovercraft_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramiro/hovercraft/master/tests/test_data/images/hovercraft_logo.png -------------------------------------------------------------------------------- /tests/test_data/minimal/template.cfg: -------------------------------------------------------------------------------- 1 | [hovercraft] 2 | template = template.xsl 3 | 4 | js-body = js/impress.js 5 | js/hovercraft-minimal.js 6 | -------------------------------------------------------------------------------- /tests/test_data/maximal/images/hovercraft_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramiro/hovercraft/master/tests/test_data/maximal/images/hovercraft_logo.png -------------------------------------------------------------------------------- /tests/test_data/maximal/directory/hovercraft_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramiro/hovercraft/master/tests/test_data/maximal/directory/hovercraft_logo.png -------------------------------------------------------------------------------- /tests/test_data/css/sub.css: -------------------------------------------------------------------------------- 1 | /* A CSS file in a subdirectory referencing a resource. */ 2 | html { 3 | background-image: url('../images/hovercraft_logo.png'); 4 | } 5 | -------------------------------------------------------------------------------- /tests/test_data/css/sub2.css: -------------------------------------------------------------------------------- 1 | /* A CSS file in a subdirectory referencing a resource. */ 2 | html { 3 | background-image: url('../images/hovercraft_logo.png'); 4 | } 5 | -------------------------------------------------------------------------------- /tests/test_data/maximal/fonts/texgyreschola-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramiro/hovercraft/master/tests/test_data/maximal/fonts/texgyreschola-regular-webfont.eot -------------------------------------------------------------------------------- /tests/test_data/maximal/fonts/texgyreschola-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramiro/hovercraft/master/tests/test_data/maximal/fonts/texgyreschola-regular-webfont.ttf -------------------------------------------------------------------------------- /tests/test_data/maximal/fonts/texgyreschola-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramiro/hovercraft/master/tests/test_data/maximal/fonts/texgyreschola-regular-webfont.woff -------------------------------------------------------------------------------- /tests/test_data/include.rst: -------------------------------------------------------------------------------- 1 | Presentation with an include 2 | ============================ 3 | 4 | This presentation includes another one. 5 | 6 | ---- 7 | 8 | .. include:: simple.rst 9 | -------------------------------------------------------------------------------- /hovercraft/templates/simple/template.cfg: -------------------------------------------------------------------------------- 1 | [hovercraft] 2 | template = template.xsl 3 | 4 | css = css/hovercraft.css 5 | css/highlight.css 6 | 7 | js-body = js/impress.js 8 | js/hovercraft.js 9 | -------------------------------------------------------------------------------- /tests/test_data/container.rst: -------------------------------------------------------------------------------- 1 | ---- 2 | 3 | .. container:: my-class 4 | 5 | This is some text in the container 6 | 7 | .. container:: 8 | :name: my-thing 9 | 10 | This should have an id 11 | 12 | -------------------------------------------------------------------------------- /tests/test_data/math.rst: -------------------------------------------------------------------------------- 1 | ---- 2 | 3 | Math Presentation 4 | ================= 5 | 6 | First some maths: 7 | 8 | .. math:: 9 | 10 | dS = \frac{dQ}{T} 11 | 12 | Next some more :math:`S = k \log W` 13 | -------------------------------------------------------------------------------- /tests/test_data/subdir-css.rst: -------------------------------------------------------------------------------- 1 | :css: css/sub.css 2 | :css: css/sub2.css 3 | 4 | --- 5 | 6 | This presentation uses a CSS file in a subdirectory which references external 7 | resource files (c.f. issue GH-16). 8 | -------------------------------------------------------------------------------- /hovercraft/templates/default/template.cfg: -------------------------------------------------------------------------------- 1 | [hovercraft] 2 | template = template.xsl 3 | 4 | css = css/hovercraft.css 5 | css/highlight.css 6 | 7 | js-body = js/impress.js 8 | js/gotoSlide.js 9 | js/hovercraft.js 10 | -------------------------------------------------------------------------------- /tests/test_data/simple.rst: -------------------------------------------------------------------------------- 1 | Simple Presentation 2 | =================== 3 | 4 | This presentation has two slides, each with a header and some text. 5 | 6 | ---- 7 | 8 | Second slide 9 | ============ 10 | 11 | There is no positioning or anything fancy. 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | root_dir := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 2 | 3 | all: coverage flake 4 | 5 | flake: 6 | flake8 hovercraft tests 7 | 8 | coverage: 9 | coverage run setup.py test 10 | coverage html 11 | coverage report 12 | 13 | test: 14 | python setup.py test 15 | 16 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Hovercraft! documentation master file. 2 | 3 | Hovercraft! - Merging convenience and cool! 4 | =========================================== 5 | 6 | Contents: 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | introduction.rst 12 | usage.rst 13 | presentations.rst 14 | designing.rst 15 | templates.rst 16 | -------------------------------------------------------------------------------- /tests/test_docs.py: -------------------------------------------------------------------------------- 1 | import manuel.doctest 2 | import manuel.codeblock 3 | import manuel.testing 4 | import unittest 5 | 6 | 7 | def additional_tests(): 8 | m = manuel.doctest.Manuel() 9 | m += manuel.codeblock.Manuel() 10 | return manuel.testing.TestSuite(m, r'../docs/examples/tutorial.rst') 11 | 12 | if __name__ == '__main__': 13 | unittest.TextTestRunner().run(additional_tests()) 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | os: 3 | - linux 4 | matrix: 5 | fast_finish: true 6 | include: 7 | - python: 3.5 8 | - python: 3.6 9 | - python: 3.7 10 | sudo: required 11 | dist: xenial 12 | - python: nightly 13 | sudo: required 14 | dist: xenial 15 | allow_failures: 16 | - python: nightly 17 | # command to run tests 18 | install: python setup.py develop 19 | script: python setup.py test 20 | 21 | -------------------------------------------------------------------------------- /tests/test_data/maximal/template.cfg: -------------------------------------------------------------------------------- 1 | [hovercraft] 2 | template = template.xsl 3 | 4 | css = css/style.css 5 | 6 | css-print = css/print.css 7 | 8 | css-screen,projection = css/impressConsole.css 9 | 10 | js-header = js/dummy.js 11 | 12 | js-body = js/impress.js 13 | js/impressConsole.js 14 | js/hovercraft.js 15 | 16 | resources = images/hovercraft_logo.png 17 | 18 | resource-directories = directory 19 | 20 | doctype = 21 | -------------------------------------------------------------------------------- /tests/test_data/table.rst: -------------------------------------------------------------------------------- 1 | ---- 2 | 3 | .. table:: Truth table for "not" 4 | :class: my-table-class 5 | 6 | +------------+------------+ 7 | | Name | Money Owed | 8 | +============+============+ 9 | | Adam Alpha | 100 | 10 | +------------+------------+ 11 | 12 | 13 | .. table:: 14 | :name: my-table 15 | 16 | +------------+------------+ 17 | | Number | Two | 18 | +============+============+ 19 | | Adam Alpha | 100 | 20 | +------------+------------+ 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | pyvenv.cfg 3 | __pycache__ 4 | *.*~ 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # IDE project files 41 | *.wpr 42 | *.wpu 43 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude __pycache__ 2 | include *.txt 3 | include .coveragerc 4 | include Makefile 5 | exclude tests 6 | recursive-exclude tests * 7 | exclude docs/build 8 | recursive-exclude docs/build * 9 | 10 | recursive-include docs *.bat 11 | recursive-include docs *.css 12 | recursive-include docs *.png 13 | recursive-include docs *.py 14 | recursive-include docs *.rst 15 | recursive-include docs Makefile 16 | recursive-include hovercraft *.cfg 17 | recursive-include hovercraft *.css 18 | recursive-include hovercraft *.js 19 | recursive-include hovercraft *.xsl 20 | -------------------------------------------------------------------------------- /tests/test_data/maximal/css/print.css: -------------------------------------------------------------------------------- 1 | /* This file left intentionally (almost) blank 2 | 3 | Landslide always adds a print.css and a screen.css, but they 4 | are not needed in impress.js, so this theme leaves them blank, 5 | except for hiding some things you want to hide. 6 | 7 | You can modify these files in your own fork, or you can add 8 | css-files in the landslide configuration file. 9 | See https://github.com/adamzap/landslide#presentation-configuration 10 | 11 | */ 12 | 13 | .impress-supported .fallback-message, 14 | .step .notes, 15 | aside.source, 16 | aside.page_number { 17 | display: none; 18 | } 19 | -------------------------------------------------------------------------------- /tests/test_data/maximal/directory/print.css: -------------------------------------------------------------------------------- 1 | /* This file left intentionally (almost) blank 2 | 3 | Landslide always adds a print.css and a screen.css, but they 4 | are not needed in impress.js, so this theme leaves them blank, 5 | except for hiding some things you want to hide. 6 | 7 | You can modify these files in your own fork, or you can add 8 | css-files in the landslide configuration file. 9 | See https://github.com/adamzap/landslide#presentation-configuration 10 | 11 | */ 12 | 13 | .impress-supported .fallback-message, 14 | .step .notes, 15 | aside.source, 16 | aside.page_number { 17 | display: none; 18 | } 19 | -------------------------------------------------------------------------------- /tests/test_data/presenter-notes.rst: -------------------------------------------------------------------------------- 1 | Document title 2 | -------------- 3 | 4 | ---- 5 | 6 | Hovercrafts presenter notes 7 | =========================== 8 | 9 | Hovercraft! supports presenter notes. It does this by taking anything in a 10 | what is calles a "notes-admonition" and making that into presenter notes. 11 | 12 | .. note:: Hence, this will show up as presenter notes. 13 | You have still access to a lot of formatting, like 14 | 15 | * Bullet lists 16 | 17 | * And *all* types of **inline formatting** 18 | 19 | ---- 20 | 21 | .. image:: images/hovercraft_logo.png 22 | 23 | .. note:: 24 | 25 | You don't have to start the text on the same line as 26 | the note, but you can. 27 | 28 | You can also have several paragraphs. You can not have any 29 | headings of any kind though. 30 | 31 | **But you can fake them through bold-text** 32 | 33 | And that's useful enough for presentation notes. 34 | -------------------------------------------------------------------------------- /tests/test_data/positioning.rst: -------------------------------------------------------------------------------- 1 | .. title:: Positioning test 2 | 3 | ---- 4 | 5 | No position 6 | 7 | ---- 8 | 9 | No position 10 | 11 | ---- 12 | 13 | :hovercraft-path: m 100 100 l 200 0 l 0 200 14 | 15 | A Path 16 | 17 | ---- 18 | 19 | No position 20 | 21 | ---- 22 | 23 | No position 24 | 25 | ---- 26 | 27 | :data-x: 0 28 | :data-y: 0 29 | 30 | Explicit position 31 | 32 | ----- 33 | 34 | :data-rotate: 90 35 | 36 | No position change, but rotation 37 | 38 | ---- 39 | 40 | No position 41 | 42 | ---- 43 | 44 | :hovercraft-path: m 100 100 l 200 0 l 0 200 45 | 46 | A Path 47 | 48 | ---- 49 | 50 | No position 51 | 52 | ---- 53 | 54 | :data-rotate-x: 180 55 | :data-z: 1000 56 | 57 | Move in Z and rotate. Path continues to be used. 58 | 59 | ---- 60 | 61 | :data-x: 3000 62 | :data-y: 1000 63 | :id: firstID 64 | 65 | Explicit position 66 | 67 | 68 | ---- 69 | 70 | :id: secondID 71 | :data-x: firstID+1000 72 | :data-y: firstID-500 73 | 74 | ---- 75 | 76 | :data-x: secondID+800 77 | :data-y: 200 78 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 Lennart Regebro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_data/maximal/css/style.css: -------------------------------------------------------------------------------- 1 | /* This file left intentionally (almost) blank 2 | 3 | Landslide always adds a print.css and a screen.css, but they 4 | are not needed in impress.js, so this theme leaves them blank, 5 | except for hiding some things you want to hide. 6 | 7 | You can modify these files in your own fork, or you can add 8 | css-files in the landslide configuration file. 9 | See https://github.com/adamzap/landslide#presentation-configuration 10 | 11 | */ 12 | 13 | .impress-supported .fallback-message, 14 | .step .notes, 15 | aside.source, 16 | aside.page_number { 17 | display: none; 18 | } 19 | 20 | @font-face { 21 | font-family: 'TeXGyreSchola'; 22 | src: url('../fonts/texgyreschola-regular-webfont.eot'); 23 | src: url('../fonts/texgyreschola-regular-webfont.eot?#iefix') format('embedded-opentype'), 24 | url('../fonts/texgyreschola-regular-webfont.woff') format('woff'), 25 | url('../fonts/texgyreschola-regular-webfont.ttf') format('truetype'), 26 | url('../fonts/texgyreschola-regular-webfont.svg#TeXGyreScholaBold') format('svg'); 27 | font-weight: normal; 28 | font-style: normal; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /docs/examples/tutorial.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Libre+Baskerville); 2 | 3 | body { 4 | font-size: 200%; 5 | text-align: center; 6 | font-family: Libre Baskerville; 7 | } 8 | 9 | p { 10 | font-weight: normal; 11 | margin-bottom: 0.3em; 12 | margin-top: 0.3em; 13 | text-align: left; 14 | } 15 | 16 | pre { 17 | text-align: left; 18 | } 19 | 20 | h1, h2, h3, h4 { 21 | font-weight: normal; 22 | margin-bottom: 0.3em; 23 | margin-top: 0.3em; 24 | } 25 | 26 | tt { 27 | font-size: 125; 28 | } 29 | 30 | ul { 31 | list-style-position: inside; 32 | } 33 | 34 | li { 35 | font-family: serif; 36 | font-weight: normal; 37 | margin-bottom: 0.3em; 38 | margin-top: 0.3em; 39 | padding-left: 0.3em; 40 | line-height: 1.8em; 41 | text-align: left; 42 | } 43 | 44 | a { 45 | text-decoration: none; 46 | color: black; 47 | } 48 | 49 | .hidden { 50 | display: none; 51 | } 52 | 53 | .header img { 54 | opacity: 0.2; 55 | width: 25%; 56 | position: absolute; 57 | left: 0; 58 | } 59 | 60 | .footer { 61 | position: absolute; 62 | top: 95%; 63 | font-size: 50%; 64 | } 65 | -------------------------------------------------------------------------------- /hovercraft/templates/simple/js/hovercraft.js: -------------------------------------------------------------------------------- 1 | // Main hovercraft js file 2 | ( function( document ) { 3 | "use strict"; 4 | 5 | document.addEventListener( "impress:init", function( event ) { 6 | 7 | var api = event.detail.api; 8 | var util = api.lib.util; 9 | 10 | 11 | // Set up the help-box 12 | // Overwrite the default navigation help 13 | util.triggerEvent( document, "impress:help:add", { command: "Left, Down, Page Down, Space", 14 | text: "Next step", 15 | row: 1 } ); 16 | util.triggerEvent( document, "impress:help:add", { command: "Right, Up, Page Up", 17 | text: "Previous step", 18 | row: 2 } ); 19 | }); 20 | 21 | // Initialize impress.js 22 | impress().init(); 23 | 24 | }) (document); 25 | 26 | 27 | // Function updating the slide number counter 28 | function update_slide_number(evt) 29 | { 30 | var step = evt.target.attributes['step'].value; 31 | document.getElementById('slide-number').innerText = parseInt(step) + 1; 32 | } 33 | -------------------------------------------------------------------------------- /docs/examples/hovercraft.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Libre+Baskerville|Racing+Sans+One|Satisfy); 2 | 3 | body { 4 | font-size: 200%; 5 | text-align: center; 6 | font-family: Libre Baskerville; 7 | } 8 | 9 | p { 10 | font-size: 125%; 11 | font-weight: normal; 12 | margin-bottom: 0.3em; 13 | margin-top: 0.3em; 14 | } 15 | 16 | em { 17 | font-family: Satisfy; 18 | font-size: 142%; 19 | } 20 | 21 | strong { 22 | font-family: Racing Sans One; 23 | font-size: 127%; 24 | letter-spacing: -2px; 25 | } 26 | 27 | h1, h2, h3, h4 { 28 | font-weight: normal; 29 | margin-bottom: 0.3em; 30 | margin-top: 0.3em; 31 | } 32 | 33 | ul { 34 | list-style-position: inside; 35 | } 36 | 37 | li { 38 | font-size: 125%; 39 | font-family: serif; 40 | font-weight: normal; 41 | margin-bottom: 0.3em; 42 | margin-top: 0.3em; 43 | padding-left: 0.3em; 44 | line-height: 1.8em; 45 | list-style-type: circle; 46 | } 47 | 48 | a { 49 | text-decoration: none; 50 | color: black; 51 | } 52 | 53 | #ThreeD H1 { 54 | font-family: Racing Sans One; 55 | font-size: 300%; 56 | } 57 | 58 | #thequestion p { 59 | font-family: Racing Sans One; 60 | } 61 | -------------------------------------------------------------------------------- /tests/test_data/minimal/template.xsl: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 |
32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | -------------------------------------------------------------------------------- /hovercraft/templates/simple/css/hovercraft.css: -------------------------------------------------------------------------------- 1 | /* This file left intentionally (almost) blank 2 | 3 | Landslide always adds a print.css and a screen.css, but they 4 | are not needed in impress.js, so this theme leaves them blank, 5 | except for hiding some things you want to hide. 6 | 7 | You can modify these files in your own fork, or you can add 8 | css-files in the landslide configuration file. 9 | See https://github.com/adamzap/landslide#presentation-configuration 10 | 11 | */ 12 | 13 | .impress-supported .fallback-message, 14 | .step .notes { 15 | display: none; 16 | } 17 | 18 | .step { 19 | width: 800px; 20 | } 21 | 22 | 23 | /* Help popup */ 24 | 25 | #impress-help { 26 | background: none repeat scroll 0 0 rgba(0, 0, 0, 0.7); 27 | color: #EEEEEE; 28 | font-size: 60%; 29 | left: 2em; 30 | bottom: 2em; 31 | width: 30em; 32 | border-radius: 1em; 33 | padding: 1em; 34 | position: fixed; 35 | right: 0; 36 | text-align: center; 37 | z-index: 100; 38 | display: block; 39 | font-family: Verdana, Arial, Sans; 40 | } 41 | 42 | #impress-help strong { 43 | font-family: inherit; 44 | font-size: 98%; 45 | } 46 | 47 | /* Slide numbers */ 48 | 49 | div#slide-number 50 | { 51 | bottom: 1em; 52 | font-size: 5vh; 53 | position: absolute; 54 | right: 2.5em; 55 | text-align: right; 56 | } 57 | -------------------------------------------------------------------------------- /tests/test_data/advanced.rst: -------------------------------------------------------------------------------- 1 | .. title:: Presentation title 2 | 3 | :data-transition-duration: 2000 4 | :auto-console: True 5 | :css-screen: extra.css 6 | :js-body: extra.js 7 | 8 | 9 | This is an advanced presentation. It doesn't have a section in the first 10 | step, meaning the first step will not be a step at all, but a sort of 11 | introductory comment about the presentation, that will not show up in the 12 | presentation at all. 13 | 14 | It also sets a title and a transition-duration. 15 | 16 | ---- 17 | 18 | :data-x: 1000 19 | :data-y: 1600 20 | 21 | Advanced Presentation 22 | ===================== 23 | 24 | Here we show the positioning feature, where we can explicitly set a position 25 | on one of the steps. 26 | 27 | ---- 28 | 29 | :id: name-this-step 30 | :data-x: r1600 31 | 32 | Formatting 33 | ========== 34 | 35 | Let us also try some basic formatting, like *italic*, and **bold**. 36 | 37 | * We can also 38 | * have a list 39 | * of things. 40 | 41 | ---- 42 | 43 | There should also be possible to have 44 | preformatted text for code. 45 | 46 | .. code:: python 47 | 48 | def foo(bar): 49 | # Comment 50 | a = 1 + "hubbub" 51 | return None 52 | 53 | 54 | ---- 55 | 56 | An image, with attributes: 57 | 58 | .. image:: images/hovercraft_logo.png 59 | :class: imageclass 60 | :width: 50% 61 | 62 | ---- 63 | 64 | Character sets 65 | ============== 66 | 67 | The character set is UTF-8 as of now. Like this: åäö. 68 | -------------------------------------------------------------------------------- /hovercraft/templates/default/js/hovercraft.js: -------------------------------------------------------------------------------- 1 | // Main hovercraft js file 2 | ( function( document ) { 3 | "use strict"; 4 | 5 | document.addEventListener( "impress:init", function( event ) { 6 | 7 | var api = event.detail.api; 8 | var util = api.lib.util; 9 | 10 | 11 | // Set up the help-box 12 | // Overwrite the default navigation help 13 | util.triggerEvent( document, "impress:help:add", { command: "Left, Down, Page Down, Space", 14 | text: "Next step", 15 | row: 1 } ); 16 | util.triggerEvent( document, "impress:help:add", { command: "Right, Up, Page Up", 17 | text: "Previous step", 18 | row: 2 } ); 19 | }); 20 | 21 | // Initialize impress.js 22 | impress().init(); 23 | 24 | var impressattrs = document.getElementById('impress').attributes; 25 | if (impressattrs.hasOwnProperty('auto-console') && impressattrs['auto-console'].value.toLowerCase() === 'true') { 26 | impressConsole().open(); 27 | } 28 | 29 | }) (document); 30 | 31 | 32 | // Function updating the slide number counter 33 | function update_slide_number(evt) 34 | { 35 | var step = evt.target.attributes['step'].value; 36 | document.getElementById('slide-number').innerText = parseInt(step) + 1; 37 | } 38 | -------------------------------------------------------------------------------- /hovercraft/templates/default/js/gotoSlide.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gotoSlide.js 3 | * 4 | * An impress.js plugin to move to a specific slide number 5 | * 6 | * MIT Licensed, see license.txt. 7 | * 8 | * Copyright 2012-2017 impress-console contributors (see README.txt) 9 | * 10 | */ 11 | 12 | (function ( document, window ) { 13 | 14 | var gotoSlide = function (event) { 15 | var target = event.view.prompt("Enter slide number or id"); 16 | 17 | if (isNaN(target)) { 18 | var goto_status = impress().goto(target); 19 | } else if (target === null) { 20 | return; 21 | } else { 22 | // goto(0) goes to step-1, so substract 23 | var goto_status = impress().goto(parseInt(target) - 1); 24 | } 25 | if (goto_status === false) { 26 | event.view.alert("Slide not found: '" + target + "'"); 27 | } 28 | }; 29 | 30 | document.addEventListener( "impress:init", function( event ) { 31 | var api = event.detail.api; 32 | var util = api.lib.util; 33 | 34 | 35 | document.addEventListener( "keyup", function( event ) { 36 | 37 | if ( event.keyCode === 71 ) { // "g" 38 | event.preventDefault(); 39 | gotoSlide( event ); 40 | } 41 | }, false ); 42 | 43 | util.triggerEvent( document, "impress:help:add", 44 | {command: "G", 45 | text: "Go to slide", 46 | row: 3 } 47 | ); 48 | 49 | }); 50 | 51 | })(document, window); 52 | -------------------------------------------------------------------------------- /hovercraft/templates/default/css/hovercraft.css: -------------------------------------------------------------------------------- 1 | /* This file left intentionally (almost) blank 2 | 3 | Landslide always adds a print.css and a screen.css, but they 4 | are not needed in impress.js, so this theme leaves them blank, 5 | except for hiding some things you want to hide. 6 | 7 | You can modify these files in your own fork, or you can add 8 | css-files in the landslide configuration file. 9 | See https://github.com/adamzap/landslide#presentation-configuration 10 | 11 | */ 12 | 13 | .impress-supported .fallback-message, 14 | .step .notes { 15 | display: none; 16 | } 17 | 18 | .step { 19 | width: 800px; 20 | } 21 | 22 | .substep { 23 | opacity: 0; 24 | } 25 | 26 | .substep.substep-visible { 27 | opacity: 1; 28 | transition: opacity 1s; 29 | } 30 | 31 | 32 | /* Help popup */ 33 | 34 | #impress-help { 35 | background: none repeat scroll 0 0 rgba(0, 0, 0, 0.7); 36 | color: #EEEEEE; 37 | font-size: 60%; 38 | left: 2em; 39 | bottom: 2em; 40 | width: 30em; 41 | border-radius: 1em; 42 | padding: 1em; 43 | position: fixed; 44 | right: 0; 45 | text-align: center; 46 | z-index: 100; 47 | display: block; 48 | font-family: Verdana, Arial, Sans; 49 | } 50 | 51 | #impress-help strong { 52 | font-family: inherit; 53 | font-size: 98%; 54 | } 55 | 56 | /* Small hack to prevent the help from showing in previews */ 57 | .impress-console #impress-help { 58 | opacity: 0; 59 | background-color: green; 60 | } 61 | 62 | /* Slide numbers */ 63 | 64 | div#slide-number 65 | { 66 | bottom: 1em; 67 | font-size: 5vh; 68 | position: absolute; 69 | right: 2.5em; 70 | text-align: right; 71 | } 72 | -------------------------------------------------------------------------------- /tests/test_data/maximal/js/hovercraft.js: -------------------------------------------------------------------------------- 1 | // Initialize impress.js 2 | impress().init(); 3 | 4 | // Set up the help-box 5 | var helpdiv = window.document.getElementById('hovercraft-help'); 6 | 7 | if (window.top!=window.self) { 8 | // This is inside an iframe, so don't show the help. 9 | helpdiv.className = "disabled"; 10 | 11 | } else { 12 | // Install a funtion to toggle help on and off. 13 | var help = function() { 14 | if(helpdiv.className == 'hide') 15 | helpdiv.className = 'show'; 16 | else 17 | helpdiv.className = 'hide'; 18 | }; 19 | 20 | // The help is by default shown. Hide it after five seconds. 21 | setTimeout(function () { 22 | var helpdiv = window.document.getElementById('hovercraft-help'); 23 | if(helpdiv.className != 'show') 24 | helpdiv.className = 'hide'; 25 | }, 5000); 26 | } 27 | 28 | 29 | if (impressConsole) { 30 | var impressattrs = document.getElementById('impress').attributes; 31 | var consoleCss = impressattrs['console-css']; 32 | var previewCss = null; 33 | if (impressattrs.hasOwnProperty('preview-css')) { 34 | previewCss = impressattrs['preview-css']; 35 | } 36 | 37 | impressConsole().init(css=consoleCss, cssPreview=previewCss); 38 | 39 | // P to open Console 40 | impressConsole().registerKeyEvent([72], help, window); 41 | 42 | if (impressattrs.hasOwnProperty('auto-console') && impressattrs['auto-console'].value.toLowerCase() === 'true') { 43 | consoleWindow = impressConsole().open(); 44 | } 45 | } 46 | 47 | // Function updating the slide number counter 48 | function update_slide_number(evt) 49 | { 50 | var step = evt.target.attributes['step'].value; 51 | document.getElementById('slide-number').innerText = parseInt(step) + 1; 52 | } 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from io import open 2 | from setuptools import setup, find_packages 3 | from setuptools.command.install import install 4 | import sys 5 | 6 | version = '2.7.dev0' 7 | 8 | with open('README.rst', 'rt', encoding='utf8') as readme: 9 | description = readme.read() 10 | 11 | with open('CHANGES.txt', 'rt', encoding='utf8') as changes: 12 | history = changes.read() 13 | 14 | class CustomInstall(install): 15 | def initialize_options(self): 16 | if sys.version < '3': 17 | print("Hovercraft requires Python 3.5 or higher.") 18 | sys.exit(1) 19 | 20 | return install.initialize_options(self) 21 | 22 | setup(name='hovercraft', 23 | version=version, 24 | description="Makes impress.js presentations from reStructuredText", 25 | long_description=description + '\n' + history, 26 | # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 27 | classifiers=['Development Status :: 5 - Production/Stable', 28 | 'Topic :: Multimedia :: Graphics :: Presentation', 29 | 'Topic :: Text Processing', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3 :: Only', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'License :: OSI Approved :: MIT License', 36 | ], 37 | keywords='presentations restructuredtext', 38 | author='Lennart Regebro', 39 | author_email='regebro@gmail.com', 40 | url='https://github.com/regebro/hovercraft', 41 | license='MIT', 42 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 43 | include_package_data=True, 44 | zip_safe=False, 45 | install_requires=[ 46 | 'setuptools', 47 | 'docutils >= 0.9', 48 | 'lxml>=3.1.0', 49 | 'svg.path', 50 | 'pygments', 51 | 'watchdog', 52 | ], 53 | tests_require=[ 54 | 'manuel', 55 | ], 56 | test_suite='tests', 57 | entry_points={ 58 | 'console_scripts': [ 59 | 'hovercraft = hovercraft:main', 60 | ], 61 | }, 62 | cmdclass={'install': CustomInstall} 63 | ) 64 | -------------------------------------------------------------------------------- /tests/test_data/maximal/template.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | <xsl:value-of select="/document/@title"/> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 |
50 |
51 |
52 |
53 | 54 |
55 | 56 | show 57 | 58 | 59 | hide 60 | 61 | 62 | 63 | 64 | 65 |
Left, Down, Page Down, SpaceNext slide
Right, Up, Page UpPrevious slide
HToggle this help
66 |
67 | 68 | 71 | 72 | 73 | 74 | 75 |
76 | 77 |
78 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Hovercraft! 2 | =========== 3 | 4 | *The merge of convenience and cool!* 5 | 6 | Hovercraft! is a tool to make impress.js_ presentations from 7 | reStructuredText. For a quick explanation, see the demo_. 8 | 9 | Features 10 | -------- 11 | 12 | * Write your presentations in a text markup language. No slow, limiting GUI, no annoying HTML! 13 | 14 | * Pan, rotate and zoom in 3D, with automatic repositioning of slides! 15 | 16 | * A presenter console with notes and slide previews! 17 | 18 | * Support for showing mathematical formulas. 19 | 20 | * Styling is easy with CSS. 21 | 22 | * The slide show generated is in HTML, so you only need a web browser to show it. 23 | 24 | * Easy sharing, as it can be put up on a website for anyone to see! 25 | 26 | Full documentation is available at readthedocs.org_, and also in the 27 | documentation subdirectory. 28 | 29 | Installation 30 | ------------ 31 | 32 | Hovercraft requires Python 3 and can be installed like any Python package. 33 | The easiest way is to install pip_, and then run:: 34 | 35 | $ pip3 install hovercraft 36 | 37 | Hovercraft is untested on Windows, but there is no reason it shouldn't work, at least in theory. 38 | 39 | 40 | Contributors 41 | ------------ 42 | 43 | Hovercraft! was written by Lennart Regebro , and is licensed 44 | as CC-0, except for the following: 45 | 46 | * ``reST.xsl`` is (c) Michael Alyn Miller and 47 | published under a BSD-style license included in reST.xsl itself. 48 | 49 | * ``impress.js`` is (c) Bartek Szopka (@bartaz) released under MIT and GPL 50 | licenses. See the impress.js_ page for more information. 51 | 52 | Other contributors (see CHANGES.txt for details): 53 | 54 | * Carl Meyer [carljm] 55 | 56 | * Chris Withers [cjw296] 57 | 58 | * Fahrzin Hemmati [fahhem] 59 | 60 | * Christophe Labouisse [ggtools] 61 | 62 | * Paul Schoenfelder [bitwalker] 63 | 64 | * Bernhard Weitzhofer [b6d] 65 | 66 | * Russ Ferriday [topiaruss] 67 | 68 | * Henrik Blidh [hbldh] 69 | 70 | * Ian Castleden [arabidopsis] 71 | 72 | * Mario Bodemann [mariobodemann] 73 | 74 | * Jürgen Hermann [jhermann] 75 | 76 | * Adam Johnson [adamchainz] 77 | 78 | * Frederik Möllers [frederikmoellers] 79 | 80 | * David Baum [naraesk] 81 | 82 | * Keith Maxwell [maxwell-k] 83 | 84 | * Tony S Yu [tonysyu] 85 | 86 | * Carlos Cámara [ccamara] 87 | 88 | * Ramiro Morales [ramiro] 89 | 90 | .. _impress.js: http://github.com/bartaz/impress.js 91 | .. _demo: http://regebro.github.com/hovercraft 92 | .. _readthedocs.org: https://hovercraft.readthedocs.io/ 93 | .. _pip: http://www.pip-installer.org/en/latest/ 94 | -------------------------------------------------------------------------------- /tests/test_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from hovercraft.generate import rst2html 5 | from hovercraft.template import Template 6 | from .test_data import HTML_OUTPUTS 7 | 8 | TEST_DATA = os.path.join(os.path.split(__file__)[0], 'test_data') 9 | 10 | 11 | class GeneratorTests(unittest.TestCase): 12 | """Tests that the resulting HTML is correct. 13 | 14 | This tests the whole path except the copying of files.""" 15 | 16 | def test_small(self): 17 | template = Template(os.path.join(TEST_DATA, 'minimal')) 18 | html, deps = rst2html(os.path.join(TEST_DATA, 'simple.rst'), template) 19 | self.assertEqual(html, HTML_OUTPUTS['simple']) 20 | 21 | def test_big(self): 22 | template = Template(os.path.join(TEST_DATA, 'maximal')) 23 | html, deps = rst2html(os.path.join(TEST_DATA, 'advanced.rst'), template) 24 | self.assertEqual(html, HTML_OUTPUTS['advanced']) 25 | 26 | def test_presenter_notes(self): 27 | template = Template(os.path.join(TEST_DATA, 'maximal')) 28 | html, deps = rst2html(os.path.join(TEST_DATA, 'presenter-notes.rst'), template) 29 | self.assertEqual(html, HTML_OUTPUTS['presenter-notes']) 30 | 31 | def test_skip_presenter_notes(self): 32 | template = Template(os.path.join(TEST_DATA, 'maximal')) 33 | html, deps = rst2html(os.path.join(TEST_DATA, 'presenter-notes.rst'), template, skip_notes=True) 34 | self.assertEqual(html, HTML_OUTPUTS['skip-presenter-notes']) 35 | 36 | def test_comments(self): 37 | template = Template(os.path.join(TEST_DATA, 'minimal')) 38 | html, deps = rst2html(os.path.join(TEST_DATA, 'comment.rst'), template) 39 | self.assertEqual(html, HTML_OUTPUTS['comment']) 40 | 41 | def test_slide_with_class(self): 42 | template = Template(os.path.join(TEST_DATA, 'minimal')) 43 | html, deps = rst2html(os.path.join(TEST_DATA, 'slide_class.rst'), template) 44 | self.assertEqual(html, HTML_OUTPUTS['slide_with_class']) 45 | 46 | def test_table(self): 47 | template = Template(os.path.join(TEST_DATA, 'minimal')) 48 | html, deps = rst2html(os.path.join(TEST_DATA, 'table.rst'), template) 49 | self.assertEqual(html, HTML_OUTPUTS['table']) 50 | 51 | def test_class_directive(self): 52 | template = Template(os.path.join(TEST_DATA, 'minimal')) 53 | html, deps = rst2html(os.path.join(TEST_DATA, 'class.rst'), template) 54 | self.assertEqual(html, HTML_OUTPUTS['class_directive']) 55 | 56 | def test_container_directive(self): 57 | template = Template(os.path.join(TEST_DATA, 'minimal')) 58 | html, deps = rst2html(os.path.join(TEST_DATA, 'container.rst'), template) 59 | self.assertEqual(html, HTML_OUTPUTS['container_directive']) 60 | 61 | def test_include(self): 62 | template = Template(os.path.join(TEST_DATA, 'minimal')) 63 | html, deps = rst2html(os.path.join(TEST_DATA, 'include.rst'), template) 64 | self.assertIn(b'Presentation with an include', html) 65 | # Make sure the simple presentation was included: 66 | self.assertIn(b'Simple Presentation', html) 67 | 68 | if __name__ == '__main__': 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /tests/test_data/maximal/css/impressConsole.css: -------------------------------------------------------------------------------- 1 | #impressconsole body { 2 | background-color: rgb(255, 255, 255); 3 | padding: 0; 4 | margin: 0; 5 | font-family: verdana, arial, sans-serif; 6 | font-size: 2vw; 7 | } 8 | 9 | #impressconsole div#console { 10 | position: absolute; 11 | top: 0.5vw; 12 | left: 0.5vw; 13 | right: 0.5vw; 14 | bottom: 3vw; 15 | margin: 0; 16 | } 17 | 18 | #impressconsole div#views, #impressconsole div#notes { 19 | position: absolute; 20 | top: 0; 21 | bottom: 0; 22 | } 23 | 24 | #impressconsole div#views { 25 | left: 0; 26 | right: 50vw; 27 | overflow: hidden; 28 | } 29 | 30 | #impressconsole div#blocker { 31 | position: absolute; 32 | right: 0; 33 | bottom: 0; 34 | } 35 | 36 | #impressconsole div#notes { 37 | left: 50vw; 38 | right: 0; 39 | overflow-x: hidden; 40 | overflow-y: auto; 41 | padding: 0.3ex; 42 | background-color: rgb(255, 255, 255); 43 | border: solid 1px rgb(120, 120, 120); 44 | } 45 | 46 | #impressconsole div#notes .noNotes { 47 | color: rgb(200, 200, 200); 48 | } 49 | 50 | #impressconsole div#notes p { 51 | margin-top: 0; 52 | } 53 | 54 | #impressconsole iframe { 55 | position: absolute; 56 | margin: 0; 57 | padding: 0; 58 | left: 0; 59 | border: solid 1px rgb(120, 120, 120); 60 | } 61 | 62 | #impressconsole iframe#slideView { 63 | top: 0; 64 | width: 49vw; 65 | height: 49vh; 66 | } 67 | 68 | #impressconsole iframe#preView { 69 | opacity: 0.7; 70 | top: 50vh; 71 | width: 30vw; 72 | height: 30vh; 73 | } 74 | 75 | #impressconsole div#controls { 76 | margin: 0; 77 | position: absolute; 78 | bottom: 0.25vw; 79 | left: 0.5vw; 80 | right: 0.5vw; 81 | height: 2.5vw; 82 | background-color: rgb(255, 255, 255); 83 | background-color: rgba(255, 255, 255, 0.6); 84 | } 85 | 86 | #impressconsole div#prev, div#next { 87 | } 88 | 89 | #impressconsole div#prev a, #impressconsole div#next a { 90 | display: block; 91 | border: solid 1px rgb(70, 70, 70); 92 | border-radius: 0.5vw; 93 | font-size: 1.5vw; 94 | padding: 0.25vw; 95 | text-decoration: none; 96 | background-color: rgb(220, 220, 220); 97 | color: rgb(0, 0, 0); 98 | } 99 | 100 | #impressconsole div#prev a:hover, #impressconsole div#next a:hover { 101 | background-color: rgb(245, 245, 245); 102 | } 103 | 104 | #impressconsole div#prev { 105 | float: left; 106 | } 107 | 108 | #impressconsole div#next { 109 | float: right; 110 | } 111 | 112 | #impressconsole div#status { 113 | margin-left: 2em; 114 | margin-right: 2em; 115 | text-align: center; 116 | float: right; 117 | } 118 | 119 | #impressconsole div#clock { 120 | margin-left: 2em; 121 | margin-right: 2em; 122 | text-align: center; 123 | float: left; 124 | } 125 | 126 | #impressconsole div#timer { 127 | margin-left: 2em; 128 | margin-right: 2em; 129 | text-align: center; 130 | float: left; 131 | } 132 | 133 | #impressconsole span.moving { 134 | color: rgb(255, 0, 0); 135 | } 136 | 137 | #impressconsole span.ready { 138 | color: rgb(0, 128, 0); 139 | } 140 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Using Hovercraft! 2 | ================= 3 | 4 | You can either use Hovercraft! to generate the presentation as HTML in a 5 | target directory, or you can let Hovercraft! serve the presentation from 6 | its builtin webserver. 7 | 8 | The latter have several benefits. One is that most webbrowsers will be very 9 | reluctant to open popup-windows from pages served from the file system. 10 | This is a security measure which can be changed, but it's easier to 11 | just point the browser to http://localhost:8000 instead. 12 | 13 | The second benefit is that Hovercraft! will monitor the source files for the 14 | presentation, and if they are modified Hovercraft! will generate the 15 | presentation again automatically. That way you don't have to run Hovercraft! 16 | everytime you save a file, you only need to refresh the browser. 17 | 18 | 19 | Parameters 20 | ---------- 21 | 22 | ``hovercraft [-h] [-t TEMPLATE] [-c CSS] [-j JS] [-a] [-s] [-n] [-p PORT] []`` 23 | 24 | Positional arguments: 25 | 26 | ```` 27 | The path to the reStructuredText presentation file. 28 | 29 | ```` 30 | The directory where the presentation is saved. Will be created if it 31 | does not exist. If you do not specify a targetdir Hovercraft! will 32 | instead start a webserver and serve the presentation from that server. 33 | 34 | Optional arguments: 35 | 36 | ``-h, --help`` 37 | Show this help. 38 | 39 | ``-t TEMPLATE, --template TEMPLATE`` 40 | Specify a template. Must be a .cfg file, or a directory with 41 | a ``template.cfg`` file. If not given it will use a default template. 42 | 43 | ``-c CSS, --css CSS`` 44 | An additional CSS file for the presentation to use. 45 | See also the ``:css:`` settings of the presentation. 46 | 47 | ``-j JS, --js JS`` 48 | An additional Javascript file for the presentation to use. 49 | Added as a js-body script. 50 | See also the ``:js-body:`` settings of the presentation. 51 | 52 | ``-a, --auto-console`` 53 | Open the presenter console automatically. This is useful when you are 54 | rehearsing and making sure the presenter notes are correct. 55 | You can also set this by having ``:auto-console: true`` first in the 56 | presentation. 57 | 58 | ``-s, --skip-help`` 59 | Do not show the initial help popup. You can also set 60 | this by having ``:skip-help: true`` first in the presentation. 61 | 62 | ``-n, --skip-notes`` 63 | Do not include presenter notes in the output. 64 | 65 | ``-p PORT, --port PORT`` 66 | The address and port that the server uses. Ex 8080 or 67 | 127.0.0.1:9000. Defaults to 0.0.0.0:8000. 68 | 69 | ``--mathjax MATHJAX`` 70 | The URL to the mathjax library. (It will only be used 71 | if you have rST ``math::`` in your document) 72 | 73 | ``-N, --slide-numbers`` 74 | Show the current slide number on the slide itself and in the presenter 75 | console. You can also set this by having ``slide-numbers: true`` in 76 | the presentation preamble. 77 | 78 | ``-v, --version`` 79 | Show program's version number and exit 80 | 81 | 82 | Built in templates 83 | ------------------ 84 | 85 | There are two templates that come with Hovercraft! One is called ``default`` 86 | and will be used unless you specify a template. This is the template you will 87 | use most of the time. 88 | 89 | The second is called ``simple`` and right now it only lacks the Goto function. 90 | You can ignore it. 91 | -------------------------------------------------------------------------------- /docs/designing.rst: -------------------------------------------------------------------------------- 1 | Designing your presentations 2 | ============================ 3 | 4 | There are several tricks to making presentations. I certainly do not claim to 5 | be an expert, but here are some beginners hints. 6 | 7 | 8 | Take it easy 9 | ------------ 10 | 11 | Don't go too heavy on the zoom. Having a difference between two slides in 12 | scale of more than 5 is rarely going to look good. It would make for a nice 13 | cool zooming effect if it did, but this is not what browsers are designed 14 | for, so it won't. 15 | 16 | And the 3D effects can be really cool if used well. But not all the time, 17 | it gets tiring for the audience. 18 | 19 | Try, if you can, to use the zoom and 3D effects when they make sense in the 20 | presentation. You can for example mention the main topics on one slide, and 21 | then zoom in on each topic when you discuss it in more detail. That way the 22 | effects help clarify the presentation, rather than distract from it. 23 | 24 | 25 | Custom fonts 26 | ------------ 27 | 28 | Browsers tend to render things subtly differently. 29 | 30 | They also have different default fonts, and different operating systems have 31 | different implementations of the same fonts. So to make sure you have as much 32 | control over the design as possible, you should always include fonts with the 33 | presentation. A good source for free fonts are Google Webfonts_. Those fonts 34 | are free and open source, so you can use them with no cost and no risk of 35 | being sued. They can also be downloaded or included online. 36 | 37 | Online vs Downloaded 38 | ^^^^^^^^^^^^^^^^^^^^ 39 | 40 | If you are making a presentation that is going to run on your computer at a 41 | conference or customer meeting, always download the fonts and have them 42 | as a part of the presentation. Put them in a folder called ``fonts`` 43 | under the folder where your presentation is. 44 | 45 | You also need to define the font-family in your CSS. `Font Squirrel`_'s 46 | webfont generator will provide you with a platform-independent toolkit for 47 | generating both the varius font formats and the CSS. 48 | 49 | If the presentation is online only, you can put an ``@include``-statement in 50 | your CSS to include Googles webfonts directly:: 51 | 52 | @import url(http://fonts.googleapis.com/css?family=Libre+Baskerville|Racing+Sans+One|Satisfy); 53 | 54 | But don't use this for things you need to show on your computer, as it 55 | requires you to have internet access. 56 | 57 | 58 | Test with different browsers 59 | ---------------------------- 60 | 61 | If you are putting the presentation online, test it with a couple of major 62 | browsers, to make sure nothing breaks and that everything still looks good. 63 | Not only are there subtle differences in how things may get positioned, 64 | different browsers are also good at different things. 65 | 66 | I've tested some browsers, all on Ubuntu, and it is likely that they behave 67 | differently on other operating systems, so you have to try for yourself. 68 | 69 | 70 | Firefox 71 | ^^^^^^^ 72 | 73 | Firefox 18 is quite slow to use with impress.js, especially for 3D stuff, so 74 | it can have very jerky movements from slide to slide. It does keep text 75 | looking good no matter how much you zoom. On the other hand, it refuses to 76 | scale text infinitely, so if you scale too much characters will not grow 77 | larger, they will instead start moving around. 78 | 79 | Firefox 19 is better, but for 3D stuff it's still a bit slow. 80 | 81 | Chrome 82 | ^^^^^^ 83 | 84 | Chrome 24 is fast, but will not redraw text in different sizes, but will 85 | instead create an image of them and rescale them, resulting in the previous 86 | slide having a fuzzy pixelated effect. 87 | 88 | Epiphany 89 | ^^^^^^^^ 90 | 91 | Epiphany 3.4.1 is comparable to Firefox 19, possibly a bit smoother, and the 92 | text looks good. But it has bugs in how it handles 3D data, and the location 93 | bar is visible in fullscreen mode, making it less suitable for any sort of 94 | presentation. 95 | 96 | .. _Webfonts: http://www.google.com/webfonts 97 | .. _Font Squirrel: http://www.fontsquirrel.com/ 98 | -------------------------------------------------------------------------------- /hovercraft/templates/default/css/highlight.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 3 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 4 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 5 | .highlight .o { color: #666666 } /* Operator */ 6 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 8 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 10 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 13 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 15 | .highlight .go { color: #303030 } /* Generic.Output */ 16 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 18 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .highlight .gt { color: #0040D0 } /* Generic.Traceback */ 20 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 21 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 22 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 23 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 25 | .highlight .kt { color: #902000 } /* Keyword.Type */ 26 | .highlight .m { color: #208050 } /* Literal.Number */ 27 | .highlight .s { color: #4070a0 } /* Literal.String */ 28 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 29 | .highlight .nb { color: #007020 } /* Name.Builtin */ 30 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 31 | .highlight .no { color: #60add5 } /* Name.Constant */ 32 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 33 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 34 | .highlight .ne { color: #007020 } /* Name.Exception */ 35 | .highlight .nf { color: #06287e } /* Name.Function */ 36 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 37 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 39 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 40 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 48 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 50 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 51 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 54 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 56 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /hovercraft/templates/simple/css/highlight.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 3 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 4 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 5 | .highlight .o { color: #666666 } /* Operator */ 6 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 8 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 10 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 13 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 15 | .highlight .go { color: #303030 } /* Generic.Output */ 16 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 18 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .highlight .gt { color: #0040D0 } /* Generic.Traceback */ 20 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 21 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 22 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 23 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 25 | .highlight .kt { color: #902000 } /* Keyword.Type */ 26 | .highlight .m { color: #208050 } /* Literal.Number */ 27 | .highlight .s { color: #4070a0 } /* Literal.String */ 28 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 29 | .highlight .nb { color: #007020 } /* Name.Builtin */ 30 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 31 | .highlight .no { color: #60add5 } /* Name.Constant */ 32 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 33 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 34 | .highlight .ne { color: #007020 } /* Name.Exception */ 35 | .highlight .nf { color: #06287e } /* Name.Function */ 36 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 37 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 39 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 40 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 48 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 50 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 51 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 54 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 56 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from lxml import etree 4 | 5 | from hovercraft.template import (Template, CSS_RESOURCE, JS_RESOURCE, 6 | JS_POSITION_BODY, JS_POSITION_HEADER) 7 | 8 | TEST_DATA = os.path.join(os.path.split(__file__)[0], 'test_data') 9 | 10 | 11 | class TemplateInfoTests(unittest.TestCase): 12 | """Tests that template information is correctly parsed""" 13 | 14 | def test_template_paths(self): 15 | # You can specify a folder or a cfg file and that's the same thing. 16 | template_info1 = Template(os.path.join(TEST_DATA, 'minimal')) 17 | template_info2 = Template(os.path.join(TEST_DATA, 'minimal', 'template.cfg')) 18 | self.assertEqual(etree.tostring(template_info1.xml_node()), 19 | etree.tostring(template_info2.xml_node())) 20 | 21 | def test_template_minimal(self): 22 | template_info = Template(os.path.join(TEST_DATA, 'minimal')) 23 | with open(os.path.join(TEST_DATA, 'minimal', 'template.xsl'), 'rb') as xslfile: 24 | xsl = xslfile.read() 25 | self.assertEqual(template_info.xsl, xsl) 26 | template_files = [each.filepath for each in template_info.resources] 27 | self.assertIn('js/impress.js', template_files) 28 | self.assertIn('js/hovercraft-minimal.js', template_files) 29 | css_files = list(each.filepath for each in template_info.resources if 30 | each.resource_type == CSS_RESOURCE) 31 | self.assertEqual(len(css_files), 0) 32 | self.assertEqual(template_info.doctype, b'') 33 | 34 | def test_template_maximal(self): 35 | template_info = Template(os.path.join(TEST_DATA, 'maximal')) 36 | with open(os.path.join(TEST_DATA, 'maximal', 'template.xsl'), 'rb') as xslfile: 37 | xsl = xslfile.read() 38 | self.assertEqual(template_info.xsl, xsl) 39 | 40 | template_files = [each.filepath for each in template_info.resources] 41 | self.assertIn('images/hovercraft_logo.png', template_files) 42 | self.assertIn('js/impress.js', template_files) 43 | self.assertIn('js/impressConsole.js', template_files) 44 | self.assertIn('js/hovercraft.js', template_files) 45 | 46 | js_bodies = [each.filepath for each in template_info.resources if 47 | each.resource_type == JS_RESOURCE and 48 | each.extra_info == JS_POSITION_BODY] 49 | self.assertIn('js/impress.js', js_bodies) 50 | self.assertIn('js/impressConsole.js', js_bodies) 51 | self.assertIn('js/hovercraft.js', js_bodies) 52 | 53 | js_headers = [each.filepath for each in template_info.resources if 54 | each.resource_type == JS_RESOURCE and 55 | each.extra_info == JS_POSITION_HEADER] 56 | self.assertIn('js/dummy.js', js_headers) 57 | 58 | self.assertEqual(template_info.resources[0].filepath, 'css/style.css') 59 | self.assertEqual(template_info.resources[0].extra_info, 'all') 60 | self.assertEqual(template_info.resources[1].filepath, 'css/print.css') 61 | self.assertEqual(template_info.resources[1].extra_info, 'print') 62 | self.assertEqual(template_info.resources[2].filepath, 'css/impressConsole.css') 63 | self.assertEqual(template_info.resources[2].extra_info, 'screen,projection') 64 | 65 | self.assertEqual(template_info.doctype, b'') 66 | 67 | 68 | class TemplateInfoNodeTests(unittest.TestCase): 69 | """Tests that template information is correctly made into an xml nodes""" 70 | 71 | def test_minimal_template(self): 72 | template_info = Template(os.path.join(TEST_DATA, 'minimal')) 73 | node = template_info.xml_node() 74 | 75 | self.assertEqual(etree.tostring(node), ( 76 | b'
' 77 | b'' 78 | b'')) 79 | 80 | def test_maximal_template(self): 81 | template_info = Template(os.path.join(TEST_DATA, 'maximal')) 82 | node = template_info.xml_node() 83 | 84 | self.assertEqual(etree.tostring(node), ( 85 | b'
' 86 | b'' 87 | b'' 88 | b'' 89 | b'
' 90 | b'' 91 | b'' 92 | b'
')) 93 | 94 | 95 | if __name__ == '__main__': 96 | unittest.main() 97 | -------------------------------------------------------------------------------- /hovercraft/templates/default/template.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | <xsl:value-of select="/document/@title"/> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | 75 |
76 | 77 |
78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 109 | 110 |
111 |
112 |
113 | 114 | 117 | 118 |
119 | 120 |
121 | 1 122 |
123 |
124 | 125 | 126 | 129 | 130 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /hovercraft/templates/simple/template.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | <xsl:value-of select="/document/@title"/> 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | 75 |
76 | 77 |
78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 109 | 110 |
111 |
112 |
113 | 114 | 117 | 118 |
119 | 120 |
121 | 1 122 |
123 |
124 | 125 | 126 | 129 | 130 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | GUI tools are limiting 5 | ---------------------- 6 | 7 | I used to do presentations with typical slideshow software, such as 8 | OpenOffice/LibreOffice Impress, but these tools felt restricted and limiting. 9 | I need to do a lot of reorganizing and moving around, and that might mean 10 | changing things from bullet lists to headings to text to pictures and back to 11 | bullet lists over again. This happens through the whole process. I might 12 | realize something that was just a bullet point needs to be a slide, or that a 13 | set of slides for time reasons need to be shortened down to bullet points. 14 | Much of the reorganization comes from seeing what fits on one slide and what 15 | does not, and how I need to pace the presentation, and to some extent even 16 | what kinda of pictures I can find to illustrate what I try to say, and if the 17 | pictures are funny or not. 18 | 19 | **Presentation software should give you complete freedom to reorganize your 20 | presentation on every level, not only by reorganizing slides.** 21 | 22 | The solution for me and many others, is to use a text-markup language, like 23 | reStructuredText, Markdown or similar, and then use a tool that generates an 24 | HTML slide show from that. 25 | 26 | **Text-markup gives you the convenience and freedom to quickly move parts 27 | around as you like.** 28 | 29 | I chose reStructuredText_, because I know it and because it has a massive 30 | feature set. When I read the documentations of other text-markup langages it 31 | was not obvious if they has the features I needed or not. 32 | 33 | 34 | Pan, rotate and zoom 35 | -------------------- 36 | 37 | The tools that exist to make presentations from text-markup will make 38 | slideshows that has a sequence of slides from left to right. But the fashion 39 | now is to have presentations that rotate and zoom in and out. One open source 40 | solution for that is impress.js_. 41 | 42 | **With impress.js you can make modern cool presentations.** 43 | 44 | But impress.js requires you to write your presentation as HTML, which is 45 | annoying, and the markup isn't flexible enough to let you quickly reorganize 46 | things from bullet points to headings etc. 47 | 48 | You also have to position each slide separately, and if you insert a new 49 | slide in the middle, you have to reposition all the slides that follow. 50 | 51 | Hovercraft! 52 | ----------- 53 | 54 | So what I want is a tool that takes the power, flexibility and convenience of 55 | reStructuredText and allows me to generate pan, rotate and zoom presentations 56 | with impress.js, without having to manually reposition each slide if I 57 | reorganize a little bit of the presentation. I couldn't find one, so I made 58 | Hovercraft. 59 | 60 | Hovercraft’s power comes from the combination of reStructuredText’s 61 | convenience with the cool of impress.js, together with a flexible and 62 | powerful solution to position the slides. 63 | 64 | There are four ways to position slides: 65 | 66 | #. Absolute positioning: You simply add X and Y coordinates to a slide, 67 | in pixels. Doing only this will not be fun, but someone might need it. 68 | 69 | #. Relative positioning to last slide: By specifying x and/or y with with 70 | a starting r,you specify the distance from the previous slide. By using 71 | this form of positioning you can insert a slide, and the other slides 72 | will just move to make space for the new slide. 73 | 74 | #. Relative positiong to any slide: You can reference any *previous* slide 75 | by its id and specify the position relative to it. This will work for 76 | all positioning fields. However, you should not use ``r`` as a slide id 77 | since the positioning might not behave as you expect. 78 | 79 | #. Automatically: If you don’t specify any position the slide will have the 80 | same settings as the previous slide. With a relative positioning, this 81 | means the slide will move as long as the previous slide moved. This 82 | defaults to moving 1600px to the right, which means that if you supply 83 | no positions at all anywhere in the presentation, you get the standard 84 | slide-to-the-left presentation. 85 | 86 | #. With an SVG path: In this last way of positioning, you can take an 87 | SVG path from an SVG document and stick it into the presentation, and that 88 | slide + all slides following that has no explicit positioning will be 89 | positioned on that path. This can be a bit fiddly to use, but can create 90 | awesome results, such as positioning the slides as snaking Python or 91 | similar. 92 | 93 | Hovercraft! also includes a presenter console that will 94 | show you your notes, slide previews and the time, essential tools for any 95 | presentation. 96 | 97 | Shortcut/Navigation Keys: 98 | ------------------------- 99 | 100 | A help popup appears upon launching a presentation; it shows the keyboard shortcuts. 101 | 102 | * H -> Toggle the help popup 103 | * Right, Down, Page Down, Space -> Next slide 104 | * Left, Up, Page Up -> Previous slide 105 | * G -> Go to slide 106 | * P -> Open presenter console 107 | 108 | .. _reStructuredText: http://docutils.sourceforge.net/docs/index.html 109 | .. _impress.js: http://github.com/bartaz/impress.js 110 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Hovercraft.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Hovercraft.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | Templates 2 | ========= 3 | 4 | Luckily, for most cases you don't need to create your own template, as the 5 | default template is very simple and most things you need to do is doable with 6 | css. However, I don't want Hovercraft! to set up a wall where it isn't 7 | flexible enough for your needs, so I added support to make your own templates. 8 | 9 | You need to create your own template if you are unsatisfied with the HTML 10 | that Hovercraft! generates, for example if you need to use another version of 11 | HTML or if the reStructuredText you are using isn't being rendered in a way 12 | that is useful for you. Although if you aren't happy with the HTML generated 13 | from the reStructuredText that could very well be a bug, so open an issue on 14 | `Github`_ for discussion. 15 | 16 | Hovercraft! generates presentations by converting the reStructuredText into 17 | XML and then using XSLT to translate the XML into HTML. 18 | 19 | Templates are directories with a configuration file, a template XSL file, 20 | and any number of CSS, JS and other resource files. 21 | 22 | 23 | The template configuration file 24 | ------------------------------- 25 | 26 | The configuration file is normally called template.cfg, but if you have 27 | several configuration files in one template directory, you can specify which 28 | one to use by specifying the full path to the configuration file. However, if 29 | you just specify the template directory, ``template.cfg`` will be used. 30 | 31 | Template files are in configparser format, which is an extended ini-style 32 | format. They are very simple, and have only one section, ``[hovercraft]``. Any 33 | other sections will be ignored. Many of the parameters are lists that often 34 | do not fit on one line. In that case you can split the line up over several 35 | lines, but indenting the lines. The amount of indentation doesn't make any 36 | difference, except aesthetically. 37 | 38 | The parameters in the ``[hovercraft]`` section are: 39 | 40 | * ``template`` 41 | The name of the xsl template. 42 | 43 | * ``css`` 44 | A list of CSS filenames separated by whitespace. These files 45 | will get included in the final file with "all" as the media 46 | specification. 47 | 48 | * ``css-`` 49 | A list of CSS filenames separated by whitespace. These files 50 | will get included in the final file with the media given in 51 | the parameter. So the files listed for the parameter 52 | "css-print" will get "print" as their media specification 53 | and a key like "css-screen,print" will return media 54 | "screen,print". 55 | 56 | * ``js-header`` 57 | A list of filenames separated by whitespace. These files 58 | will get included in the target file as header script links. 59 | 60 | * ``js-body`` 61 | A list of filenames separated by whitespace. These files 62 | will get included in the target file as script links at the 63 | end of the file. The files impress.js, impressConsole.js and 64 | hovercraft.js typically need to be included here. 65 | 66 | * ``resources`` 67 | A list of filenames separated by whitespace that will be 68 | copied to the target directory, but nothing else is done 69 | with them. Images and fonts used by CSS will be copied 70 | anyway, but other resources may be added here. 71 | 72 | * ``resource-directories`` 73 | A list of directory names separated by whitespace. These will be treated 74 | like ``resources`` above, ie only copied to the target directory. The 75 | directory contents will be copied recursively, but hidden files (like 76 | files starting with a ``.`` are ignored. 77 | 78 | An example:: 79 | 80 | [hovercraft] 81 | template = template.xsl 82 | 83 | css = css/screen.css 84 | 85 | css-print = css/print.css 86 | 87 | js-header = js/dateinput.js 88 | 89 | js-body = js/impress.js 90 | js/hovercraft.js 91 | 92 | resources = images/back.png 93 | images/forward.png 94 | images/up.png 95 | images/down.png 96 | 97 | 98 | The template file 99 | ----------------- 100 | 101 | The file specified with the ``template`` parameters is the actual XSLT 102 | template that will perform the translation from XML to HTML. 103 | 104 | Most of the time you can just copy the default template file in 105 | ``hovercraft/templates/default/template.xsl`` and modify it. XSLT is very 106 | complex, but modifying the templates HTML is quite straightforward as long as 107 | you don't have to touch any of the ```` tags. 108 | 109 | Also, the HTML that is generated is XHTML compatible and quite 110 | straightforward, so for the most case all you would need to generate another 111 | version of HTML, for example strict XHTML, would be to change the doctype. 112 | 113 | But if you need to add or change the main generated HTML you can add and 114 | change HTML statements in this main file as you like. See for example how the 115 | little help-popup is added to the bottom of the HTML. 116 | 117 | If you want to change the way the reStructuredText is rendered things get 118 | slightly more complex. The XSLT rules that convert the reStructuredText XML 119 | into HTML are contained in a separate file, ``reST.xsl``. For the most part 120 | you can just include it in the template file with the following code:: 121 | 122 | 123 | 124 | The ``resource:`` part here is not a part of XSLT, but a part of Hovercraft! 125 | It tells the XSLT translation that the file specified should not be looked 126 | up on the file system, but as a Python package resource. Currently the 127 | ``templates/reST.xsl`` file is the only XSLT resource import available. 128 | 129 | If you need to change the way reStructuredText is rendered you need to make a 130 | copy of that file and modify it. You then need to make a copy of the main 131 | template and change the reference in it to your modified XSLT file. 132 | 133 | None of the XSLT files need to be copied to the target, and should not be 134 | listed as a resource in the template configuration file. 135 | 136 | 137 | .. _Github: https://github.com/regebro/hovercraft 138 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Hovercraft.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Hovercraft.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Hovercraft" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Hovercraft" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/examples/hovercraft.rst: -------------------------------------------------------------------------------- 1 | :title: Hovercraft! demo 2 | :data-transition-duration: 1500 3 | :css: hovercraft.css 4 | 5 | This is a demo for Hovercraft! You can view it as a finished presentation 6 | at http://regebro.github.com/hovercraft/ 7 | 8 | 9 | It's also useful as an example, in which case it's supposed to be read as 10 | `source code <../_sources/examples/hovercraft.txt>`_. 11 | 12 | You can render this presentation to HTML with the command:: 13 | 14 | hovercraft hovercraft.rst outdir 15 | 16 | And then view the outdir/index.html file to see how it turned out. 17 | 18 | If you are seeing this text, and not reading this as source code, you are 19 | doing it wrong! It's going to be confusing and not very useful. 20 | 21 | Use The Source, Luke! But first you probably want to read through the 22 | official documentation at https://hovercraft.readthedocs.io/ 23 | 24 | ---- 25 | 26 | The problem: 27 | ============ 28 | 29 | Making presentations is no *fun!* 30 | --------------------------------- 31 | 32 | .. note:: 33 | 34 | Welcome to the presenter console! 35 | 36 | ---- 37 | 38 | GUI tools are inflexible 39 | ======================== 40 | 41 | * It's hard to reorganize or import text 42 | 43 | * Slow and memory hungry 44 | 45 | * You get caught up in design early in the process. 46 | 47 | .. note:: 48 | 49 | Here you have a view of the current slide, a preview of the next slide 50 | and your notes. 51 | 52 | ---- 53 | 54 | Use reStructuredText! 55 | ===================== 56 | 57 | * You can use your favorite text-editor! 58 | 59 | * Many tools available: Landslide, S5 60 | 61 | * Convenient (and powerful!) 62 | 63 | .. note:: 64 | 65 | You also have a clock and a timer, so you know how much time you have 66 | left. 67 | 68 | ---- 69 | 70 | But then there was Prezi 71 | ======================== 72 | 73 | Sliding from left to right is no longer enough. 74 | You need to be able to... 75 | 76 | .. note:: 77 | 78 | If you click on the timer it restarts from zero. This is handy when you 79 | are rehearsing the presentation and need to make sure it fits in the time 80 | allocated. 81 | 82 | ---- 83 | 84 | :data-y: r1000 85 | 86 | ...pan... 87 | ========= 88 | 89 | .. note:: 90 | 91 | If you have more notes than fit in the console, you can scroll down, but 92 | more handily, you can scroll the text up by pressing space bar. 93 | 94 | ---- 95 | 96 | :data-rotate: 90 97 | 98 | ...rotate... 99 | ============ 100 | 101 | .. note:: 102 | 103 | If there isn't more text to scroll up, space bar will go to the next 104 | slide. Therefore you, as a presenter, just press space every time you run 105 | out of things to say! 106 | 107 | ---- 108 | 109 | :data-x: r0 110 | :data-y: r500 111 | :data-scale: 0.1 112 | 113 | ...and zoom! 114 | ============ 115 | 116 | .. note:: 117 | 118 | Zooming is cool. But one day it will grow old as well. What will we do 119 | then to make presentations interesting? 120 | 121 | ---- 122 | 123 | :data-x: r-800 124 | :data-scale: 1 125 | 126 | But Prezi is a GUI 127 | ================== 128 | 129 | So we are back to square one. 130 | 131 | (And it is closed source to boot) 132 | 133 | .. note:: 134 | 135 | It's probably back to making bad jokes again. 136 | 137 | ---- 138 | 139 | What about impress.js? 140 | ====================== 141 | 142 | It's open source! 143 | 144 | Supports pan, tilt and zoom! 145 | 146 | 147 | ---- 148 | 149 | :id: ThreeD 150 | :data-y: r1200 151 | :data-rotate-x: 180 152 | 153 | In three dimensions! 154 | ==================== 155 | 156 | *But...* 157 | 158 | .. note:: 159 | 160 | Wow! 3D! You didn't see that one coming, did you? 161 | 162 | ---- 163 | 164 | 165 | It's HTML... 166 | ============ 167 | 168 | Not a friendly format to edit 169 | 170 | ---- 171 | 172 | :data-x: r800 173 | 174 | ...and manual positioning 175 | ========================= 176 | 177 | So inserting a slide means 178 | 179 | repositioning all the following slides! 180 | 181 | 182 | .. note:: 183 | 184 | The endless repositioning of slides is what prompted me to write 185 | Hovercraft! in the first place. 186 | 187 | ---- 188 | 189 | :id: thequestion 190 | :data-x: r0 191 | :data-y: r-1200 192 | 193 | *Is there no solution?* 194 | ======================= 195 | 196 | Of course there is! 197 | 198 | .. note:: 199 | 200 | What would be the point of this slide show if I didn't have a solution? 201 | Duh! 202 | 203 | ---- 204 | 205 | :data-rotate-y: 180 206 | :data-scale: 3 207 | :data-x: r-2500 208 | :data-y: r0 209 | 210 | Introducing **Hovercraft!** 211 | =========================== 212 | 213 | .. note:: 214 | 215 | TADA! 216 | 217 | ---- 218 | 219 | :data-x: r-3000 220 | :data-scale: 1 221 | 222 | reStructuredText 223 | ---------------- 224 | 225 | plus 226 | .... 227 | 228 | impress.js 229 | ---------- 230 | 231 | plus 232 | .... 233 | 234 | positioning! 235 | ------------ 236 | 237 | and 238 | ... 239 | 240 | More! 241 | 242 | ---- 243 | 244 | :data-y: r-1200 245 | 246 | Position slides 247 | =============== 248 | 249 | * Automatically! 250 | * Absolutely! 251 | * Relative to the previous slide! 252 | * Along an SVG path! 253 | 254 | 255 | .. note:: 256 | 257 | That SVG path support was a lot of work. And all I used it for was to 258 | position the slides in circles. 259 | 260 | ---- 261 | 262 | Presenter console! 263 | ================== 264 | 265 | * A view of the current slide 266 | * A view of the next slide 267 | * Your notes 268 | * A clock 269 | * A timer 270 | 271 | .. note:: 272 | 273 | You found the presenter console already! 274 | 275 | ---- 276 | 277 | Mathjax! 278 | ======== 279 | 280 | Beautiful maths! 281 | 282 | .. math:: 283 | 284 | e^{i \pi} + 1 = 0 285 | 286 | dS = \frac{dQ}{T} 287 | 288 | And inline: :math:`S = k \log W` 289 | 290 | ---- 291 | 292 | **Hovercraft!** 293 | =============== 294 | 295 | .. figure:: images/hovercraft_logo.png 296 | 297 | The merge of convenience and cool! 298 | 299 | .. note:: 300 | 301 | A slogan: The ad-mans best friend! 302 | 303 | ---- 304 | 305 | :data-x: 0 306 | :data-y: 2500 307 | :data-z: 4000 308 | :data-rotate-x: 90 309 | 310 | **Hovercraft!** 311 | =============== 312 | 313 | On Github: 314 | 315 | https://github.com/regebro/hovercraft 316 | 317 | .. note:: 318 | 319 | Fork and contribute! 320 | 321 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | 2.7 (unreleased) 5 | ---------------- 6 | 7 | This release moves Hovercraft! over to impress.js 1.0.0. This version of 8 | impress.js has many new features and a new plugin system, which has plenty of 9 | benefits, especially since some features of Hovercraft!, primarily 10 | impressConsole.js, now are plugins to impress.js, so that's less 11 | maintenance burden on me. 12 | 13 | The most obvious changes from the previous version of Hovercraft! are: 14 | 15 | - The ``--skip-help`` argument, and ``:skip-help:`` control now disables 16 | the help popup altogether. This is because impress.js currently has no 17 | way to stop the help from displaying on load except disabling the help 18 | completely. This may change in the future. 19 | 20 | - Also switched the default MathJax to 2.7.5, a minor update. 21 | impress.js MathJax extension is simply just Mathjax, there is no additional 22 | integration, so Hovercraft! doesn't change how Mathjax is integrated. 23 | 24 | - Hovecraft! now supports the new impress.js "substep" plugin, so that 25 | you can show paragraphs lists item by item (see documentation). 26 | 27 | 28 | 2.6 (2018-10-04) 29 | ---------------- 30 | 31 | - The ReStructuredText directive "figure" now is translated into the HTML5 32 | tag "figure", with the caption becoming a figcaption tag. 33 | 34 | - Restored the warning that you need Python 3.5 or higher when trying to 35 | install with Python 2. 36 | 37 | - Simplify in-process execution of Hovercraft! [tonysyu] 38 | 39 | - Document how to make custom directorves. [tonysyu] 40 | 41 | 42 | 2.5 (2017-12-10) 43 | ---------------- 44 | 45 | - Hovercraft! now displays the version number when called with -v or --version. 46 | 47 | - New version of impressConsole that includes styling of the previews and 48 | a goto command . 49 | 50 | - ``:css-console:`` and ``:css-preview:`` added to style the console and 51 | add extra styles in the previews. 52 | 53 | - :auto-console: and -a had stopped working [maxwell-k]. 54 | 55 | 56 | 2.4 (2017-07-18) 57 | ---------------- 58 | 59 | - Option to display slide numbers [frederikmoellers] 60 | 61 | - #51: Positioning relative to other slide [naraesk] 62 | 63 | - Removed the code that uses pkg_util to access included templates. We don't 64 | support installing Hovercraft! as a ZIP file anyway, so it only complicates 65 | things for no good reason. 66 | 67 | - Support for .. header:: and .. footer:: that can be used for static 68 | content. 69 | 70 | - Dropped support for Python 3.3 and 3.4, because I now use recursive glob. 71 | 72 | - Templates can now have a resource-directories statements, to specify extra 73 | directories of resources. This can be used in templates for JS libraries, 74 | like MathJax. 75 | 76 | - The MathJax argument can now be a local copy. 77 | 78 | - Switched the default MathJax URL to https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1 79 | 80 | 81 | 2.3 (2017-04-12) 82 | ---------------- 83 | 84 | - Better implementation of #98 85 | 86 | - #72: Support for adding additional JS files [hbldh] 87 | 88 | - Upgraded impress.js to 0.6.0 89 | 90 | - Support for mathematical formulas with Mathjax [arabidopsis] 91 | 92 | - Default template use UTF-8 [mariobodemann] 93 | 94 | - Added support for Python 3.6 95 | 96 | - readthedocs moved domain [adamchainz] 97 | 98 | 99 | 2.2 (2016-10-15) 100 | ---------------- 101 | 102 | - #98: Presentation not update when using gedit 103 | 104 | 105 | 2.1 (2016-02-27) 106 | ---------------- 107 | 108 | - #87: Support multiple :css: statements. [bitwalker] 109 | 110 | - #86, #88: In-template resources failed for external templates. 111 | 112 | - #89: The file monitoring could make the CPU go to 100%. [b6d] 113 | 114 | - #81: positions.rst example was out of date. 115 | 116 | - Dropped Python 3.2 support, because docutils doesn't seem to 117 | work on Python 3.2 any longer. With docutils 0.9 it probably 118 | still works. 119 | 120 | - Updated tests to work with newer Pygments. 121 | 122 | - #96: Relative paths was not working. 123 | 124 | - #91: When modifying included files the presentation was not updated. 125 | 126 | 127 | 2.0 (2015-06-14) 128 | ---------------- 129 | 130 | - Better support for :class:. [fahhem] 131 | 132 | - Now supports data-perspective. [fahhem] 133 | 134 | - Fixed typos in template.py. [fahhem, ggtools] 135 | 136 | 137 | 2.0b1 (2014-11-27) 138 | ------------------ 139 | 140 | - IMPORTANT! The positioning has been reimplemented. The most important change 141 | is that there is no longer any calculation of relative movement when you use 142 | absolute coordinates. Therefore, if you use absolute coordinates on some slides 143 | and then have no coordinates on other slides, your positioning may no longer 144 | be correct with version 2.0. 145 | 146 | - IMPORTANT! Moved the "note" XML transformation into the templates, as this is an 147 | impress.js feature, and other libraries, such as Reveal.js, will render it 148 | differently. If you make your own templates, you need to update them accordingly! 149 | 150 | - Relative coordinates (starting with r) are now supported for all positioning, 151 | attributes including rotation and scaling. 152 | 153 | - Now includes a server-mode, that will serve the presentation via http and 154 | also re-generate the presentation if the source-files change. 155 | 156 | - Images can now also have a :class: attribute. 157 | 158 | - Added support for multiple levels of slides. This is to make it able 159 | to support for example Reveal.js through external templates. 160 | 161 | 162 | 1.1 (2013-03-15) 163 | ---------------- 164 | 165 | - ReST comments are no longer rendered to HTML. [carljm] 166 | 167 | - Fixed a bug in the path handling for CSS resources. [carljm] 168 | 169 | - Various fixes and improvements in ReST handling. [cjw296] 170 | 171 | 172 | 1.0 (2013-02-22) 173 | ---------------- 174 | 175 | - #1, #2: Add key-binding to pop up the help, a parameter and a presentation 176 | field setting to not show the help at load. 177 | 178 | - Added documentation for #8: Naming steps. 179 | 180 | - #7: You can now define CSS-files to be included with a :css:-field in the 181 | presentation. 182 | 183 | - #3: You can now leave out the presenter notes from the output with the 184 | parameter -n or --skip-notes 185 | 186 | - Added a "simple" template that has no presenter console. 187 | 188 | - Updated to impress-console 1.1, fixing a Firefox bug. 189 | 190 | - Added support for more HTML metadata. 191 | 192 | - Finished documentation and examples. 193 | 194 | 195 | 1.0b2 (2013-02-13) 196 | ------------------ 197 | 198 | - Added syntax highlighting support. 199 | 200 | - #9: All positioning variables except data-x and data-y are now "sticky" so 201 | they will keep their previous value if not defined. 202 | 203 | - Documentation on https://hovercraft.readthedocs.io/ 204 | 205 | 206 | 1.0b1 (2013-02-07) 207 | ------------------ 208 | 209 | - Initial release. 210 | -------------------------------------------------------------------------------- /hovercraft/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import gettext 3 | import os 4 | import sys 5 | import threading 6 | import time 7 | import pkg_resources 8 | from collections import defaultdict 9 | from http.server import HTTPServer, SimpleHTTPRequestHandler 10 | from tempfile import TemporaryDirectory 11 | from watchdog.observers import Observer 12 | from watchdog.events import FileSystemEventHandler 13 | 14 | from .generate import generate 15 | 16 | __version__ = pkg_resources.require("hovercraft")[0].version 17 | 18 | 19 | class HovercraftEventHandler(FileSystemEventHandler): 20 | def __init__(self, filelist): 21 | self.filelist = filelist 22 | self.quit = False 23 | super().__init__() 24 | 25 | def on_modified(self, event): 26 | self._update(event.src_path) 27 | 28 | def on_created(self, event): 29 | self._update(event.src_path) 30 | 31 | def on_moved(self, event): 32 | self._update(event.dest_path) 33 | 34 | def _update(self, src_path): 35 | if self.quit: 36 | return 37 | if src_path in self.filelist: 38 | print("File %s modified, update presentation" % src_path) 39 | self.quit = True 40 | 41 | 42 | def generate_and_observe(args, event): 43 | while event.isSet(): 44 | # Generate the presentation 45 | monitor_list = generate(args) 46 | print("Presentation generated.") 47 | 48 | # Make a list of involved directories 49 | directories = defaultdict(list) 50 | for file in monitor_list: 51 | directory, filename = os.path.split(file) 52 | directories[directory].append(filename) 53 | 54 | observer = Observer() 55 | handler = HovercraftEventHandler(monitor_list) 56 | for directory, files in directories.items(): 57 | observer.schedule(handler, directory, recursive=False) 58 | 59 | observer.start() 60 | while event.wait(1): 61 | time.sleep(0.05) 62 | if handler.quit: 63 | break 64 | 65 | observer.stop() 66 | observer.join() 67 | 68 | 69 | def main(args=None): 70 | parser = create_arg_parser() 71 | args = parser.parse_args(args=args) 72 | serve_presentation(args) 73 | 74 | 75 | def create_arg_parser(): 76 | # That the argparse default strings are lowercase is ugly. 77 | 78 | def my_gettext(s): 79 | return s.capitalize() 80 | 81 | gettext.gettext = my_gettext 82 | 83 | parser = argparse.ArgumentParser( 84 | description='Create impress.js presentations with reStructuredText', 85 | add_help=False) 86 | parser.add_argument( 87 | 'presentation', 88 | metavar='', 89 | help='The path to the reStructuredText presentation file.') 90 | parser.add_argument( 91 | 'targetdir', 92 | metavar='', 93 | nargs='?', 94 | help=('The directory where the presentation is saved. Will be created ' 95 | 'if it does not exist. If you do not specify a targetdir ' 96 | 'Hovercraft! will instead start a webserver and serve the ' 97 | 'presentation from that server.')) 98 | parser.add_argument( 99 | '-h', '--help', 100 | action='help', 101 | help='Show this help.') 102 | parser.add_argument( 103 | '-t', 104 | '--template', 105 | help=('Specify a template. Must be a .cfg file, or a directory with a ' 106 | 'template.cfg file. If not given it will use a default template.')) 107 | parser.add_argument( 108 | '-c', 109 | '--css', 110 | help=('An additional css file for the presentation to use. ' 111 | 'See also the ``:css:`` settings of the presentation.')) 112 | parser.add_argument( 113 | '-j', 114 | '--js', 115 | help=('An additional javascript file for the presentation to use. Added as a js-body script.' 116 | 'See also the ``:js-body:`` settings of the presentation.')) 117 | parser.add_argument( 118 | '-a', 119 | '--auto-console', 120 | action='store_true', 121 | help=('Open the presenter console automatically. This is useful when ' 122 | 'you are rehearsing and making sure the presenter notes are ' 123 | 'correct. You can also set this by having ``:auto-console: ' 124 | 'true`` first in the presentation.')) 125 | parser.add_argument( 126 | '-s', 127 | '--skip-help', 128 | action='store_true', 129 | help=('Do not show the initial help popup.')) 130 | parser.add_argument( 131 | '-n', 132 | '--skip-notes', 133 | action='store_true', 134 | help=('Do not include presenter notes in the output.')) 135 | parser.add_argument( 136 | '-p', 137 | '--port', 138 | default='0.0.0.0:8000', 139 | help=('The address and port that the server uses. ' 140 | 'Ex 8080 or 127.0.0.1:9000. Defaults to 0.0.0.0:8000.')) 141 | parser.add_argument( 142 | '--mathjax', 143 | default=os.environ.get('HOVERCRAFT_MATHJAX', 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML'), 144 | help=('The URL to the mathjax library.' 145 | ' (It will only be used if you have rST ``math::`` in your document)')) 146 | parser.add_argument( 147 | '-N', 148 | '--slide-numbers', 149 | action='store_true', 150 | help=('Show slide numbers during the presentation.')) 151 | parser.add_argument( 152 | '-v', 153 | '--version', 154 | action='version', 155 | #help=('Display version and exit.'), 156 | version="Hovercraft! %s" % __version__ 157 | ) 158 | 159 | return parser 160 | 161 | 162 | def serve_presentation(args): 163 | 164 | # XXX Bit of a hack, clean this up, I check for this twice, also in the template. 165 | if args.template and args.template not in ('simple', 'default'): 166 | args.template = os.path.abspath(args.template) 167 | 168 | if args.targetdir: 169 | # Generate the presentation 170 | generate(args) 171 | else: 172 | # Server mode. Start a server that serves a temporary directory. 173 | 174 | with TemporaryDirectory() as targetdir: 175 | args.targetdir = targetdir 176 | args.presentation = os.path.abspath(args.presentation) 177 | 178 | # Set up watchdog to regenerate presentation if saved. 179 | event = threading.Event() 180 | event.set() 181 | thread = threading.Thread(target=generate_and_observe, args=(args, event)) 182 | try: 183 | # Serve presentation 184 | if ':' in args.port: 185 | bind, port = args.port.split(':') 186 | else: 187 | bind, port = '0.0.0.0', args.port 188 | port = int(port) 189 | 190 | # First create the server. This checks that we can connect to 191 | # the port we want to. 192 | os.chdir(targetdir) 193 | server = HTTPServer((bind, port), SimpleHTTPRequestHandler) 194 | print("Serving HTTP on", bind, "port", port, "...") 195 | 196 | try: 197 | # Now generate the presentation 198 | thread.start() 199 | 200 | try: 201 | # All is good, start the server 202 | server.serve_forever() 203 | except KeyboardInterrupt: 204 | print("\nKeyboard interrupt received, exiting.") 205 | finally: 206 | # Server exited 207 | server.server_close() 208 | 209 | finally: 210 | # Stop the generation thread 211 | event.clear() 212 | # Wait for it to end 213 | thread.join() 214 | 215 | except PermissionError: 216 | print("Can't bind to port %s:%s: No permission" % (bind, port)) 217 | except OSError as e: 218 | if e.errno == 98: 219 | print("Can't bind to port %s:%s: port already in use" % (bind, port)) 220 | else: 221 | raise 222 | -------------------------------------------------------------------------------- /hovercraft/template.py: -------------------------------------------------------------------------------- 1 | import os 2 | import configparser 3 | import shutil 4 | import glob 5 | 6 | from lxml import etree 7 | 8 | RESOURCE_TYPES = range(4) 9 | CSS_RESOURCE, JS_RESOURCE, DIRECTORY_RESOURCE, OTHER_RESOURCE = RESOURCE_TYPES 10 | 11 | JS_POSITIONS = range(2) 12 | JS_POSITION_HEADER, JS_POSITION_BODY = JS_POSITIONS 13 | 14 | HOVERCRAFT_DIR = os.path.split(__file__)[0] 15 | 16 | 17 | class Resource(object): 18 | 19 | def __init__(self, filepath, resource_type, target=None, extra_info=None, is_in_template=False): 20 | self.filepath = filepath 21 | assert resource_type in RESOURCE_TYPES 22 | self.resource_type = resource_type 23 | if resource_type == JS_RESOURCE: 24 | assert extra_info in JS_POSITIONS 25 | 26 | self.target = target 27 | self.extra_info = extra_info 28 | self.is_in_template = is_in_template 29 | 30 | def final_path(self): 31 | if self.target is None: 32 | self.target = self.filepath 33 | if self.target.startswith('..'): 34 | # A path above the current path was given. Treat this as an 35 | # absolute path, unless it's a part of the template. 36 | if self.is_in_template: 37 | return self.target 38 | return os.path.abspath(self.target) 39 | 40 | return self.target 41 | 42 | 43 | class Template(object): 44 | 45 | def __init__(self, template=None): 46 | self.doctype = b'' 47 | self.resources = [] 48 | 49 | if template is None or template in ('default', 'simple'): 50 | self.builtin_template = True 51 | if template is None: 52 | template = 'default' 53 | self.template = '/templates/%s/' % template 54 | else: 55 | self.builtin_template = False 56 | self.template = os.path.abspath(template) 57 | 58 | self._load_template_config() 59 | self._load_template_xsl() 60 | self._load_template_files() 61 | 62 | def add_resource(self, filepath, resource_type, target=None, extra_info=None, 63 | is_in_template=False): 64 | self.resources.append(Resource(filepath, resource_type, target=target, 65 | extra_info=extra_info, is_in_template=is_in_template)) 66 | 67 | def _load_template_config(self): 68 | if self.builtin_template: 69 | self.template_root = os.path.join(HOVERCRAFT_DIR, self.template.strip('/')) 70 | else: 71 | self.template_root = self.template 72 | 73 | if os.path.isdir(self.template_root): 74 | config_file = os.path.join(self.template_root, 'template.cfg') 75 | else: 76 | config_file = self.template_root 77 | self.template_root = os.path.split(self.template)[0] 78 | 79 | config = configparser.ConfigParser() 80 | config.read(config_file) 81 | self.config = config['hovercraft'] 82 | 83 | def _load_template_files(self): 84 | 85 | for key, files in self.config.items(): 86 | # CSS files: 87 | if key.startswith('css'): 88 | # This is a css_file. The media can be specified, and defaults to 'all': 89 | if '-' in key: 90 | css, media = key.split('-') 91 | else: 92 | media = 'all' 93 | for filename in files.split(): 94 | self.add_resource(filename, CSS_RESOURCE, extra_info=media, 95 | is_in_template=True) 96 | 97 | # JS files: 98 | elif key == 'js-header': 99 | for filename in files.split(): 100 | self.add_resource(filename, JS_RESOURCE, extra_info=JS_POSITION_HEADER, 101 | is_in_template=True) 102 | 103 | elif key == 'js-body': 104 | for filename in files.split(): 105 | self.add_resource(filename, JS_RESOURCE, extra_info=JS_POSITION_BODY, 106 | is_in_template=True) 107 | 108 | elif key == 'resource-directories': 109 | for filename in self.config[key].split(): 110 | self.add_resource(filename, DIRECTORY_RESOURCE, is_in_template=True) 111 | 112 | # Other files: 113 | elif key == 'resources': 114 | for filename in self.config[key].split(): 115 | self.add_resource(filename, OTHER_RESOURCE, is_in_template=True) 116 | 117 | # And finally the optional doctype: 118 | elif key == 'doctype': 119 | self.doctype = self.config['doctype'].encode() 120 | 121 | def _load_template_xsl(self): 122 | xsl_template = self.config['template'] 123 | with open(os.path.join(self.template_root, xsl_template), 'rb') as xslfile: 124 | self.xsl = xslfile.read() 125 | 126 | def get_source_path(self, resource): 127 | # Non-template resource (extra css) 128 | if not resource.is_in_template: 129 | return os.path.abspath(resource.filepath) 130 | 131 | # In template 132 | if self.builtin_template: 133 | return os.path.join(HOVERCRAFT_DIR, self.template.strip('/'), resource.filepath) 134 | 135 | # External template 136 | return os.path.join(self.template_root, resource.filepath) 137 | 138 | def read_data(self, resource): 139 | source_path = self.get_source_path(resource) 140 | with open(source_path, 'rb') as infile: 141 | return infile.read() 142 | 143 | def copy_resource(self, resource, targetdir): 144 | """Copies a resource file and returns the source path for monitoring""" 145 | final_path = resource.final_path() 146 | if final_path[0] == '/' or (':' in final_path) or ('?' in final_path): 147 | # Absolute path or URI: Do nothing 148 | return 149 | 150 | source_path = self.get_source_path(resource) 151 | 152 | if resource.resource_type == DIRECTORY_RESOURCE: 153 | for file_path in glob.iglob(os.path.join(source_path, '**'), recursive=True): 154 | if os.path.isdir(file_path): 155 | continue 156 | rest_target_path = file_path[len(source_path)+1:] 157 | target_path = os.path.join(targetdir, final_path, rest_target_path) 158 | # Don't yield the result, we don't monitor these. 159 | self._copy_file(file_path, target_path) 160 | else: 161 | target_path = os.path.join(targetdir, final_path) 162 | yield self._copy_file(source_path, target_path) 163 | 164 | def _copy_file(self, source_path, target_path): 165 | directory_name, filename = os.path.split(target_path) 166 | if not os.path.exists(directory_name): 167 | os.makedirs(directory_name) 168 | 169 | if (os.path.exists(target_path) and 170 | os.path.getmtime(source_path) <= os.path.getmtime(target_path)): 171 | # File has not changed since last copy, so skip. 172 | return source_path # This file should be monitored for changes 173 | 174 | shutil.copy2(source_path, target_path) 175 | return source_path # This file should be monitored for changes 176 | 177 | def copy_resources(self, targetdir): 178 | for resource in self.resources: 179 | yield from self.copy_resource(resource, targetdir) 180 | 181 | def xml_node(self): 182 | node = etree.Element('templateinfo') 183 | header = etree.Element('header') 184 | node.append(header) 185 | body = etree.Element('body') 186 | node.append(body) 187 | 188 | for resource in self.resources: 189 | if resource.resource_type == CSS_RESOURCE: 190 | header.append(etree.Element('css', attrib={'href': resource.final_path(), 191 | 'media': resource.extra_info})) 192 | elif resource.resource_type == JS_RESOURCE: 193 | js_element = etree.Element('js', attrib={'src': resource.final_path()}) 194 | if resource.extra_info == JS_POSITION_BODY: 195 | body.append(js_element) 196 | else: 197 | header.append(js_element) 198 | return node 199 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Hovercraft! documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Feb 7 20:44:36 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | #import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Hovercraft!' 44 | copyright = u'2013, Lennart Regebro' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | #version = '1.0' 52 | # The full version, including alpha/beta/rc tags. 53 | #release = '1.0' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['examples'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | # html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'Hovercraftdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'Hovercraft.tex', u'Hovercraft! Documentation', 187 | u'Lennart Regebro', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'hovercraft', u'Hovercraft! Documentation', 217 | [u'Lennart Regebro'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'Hovercraft', u'Hovercraft! Documentation', 231 | u'Lennart Regebro', 'Hovercraft', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /hovercraft/position.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from svg.path import parse_path 4 | 5 | DEFAULT_MOVEMENT = 1600 # If no other movement is specified, go 1600px to the right. 6 | POSITION_ATTRIBS = ['data-x', 'data-y', 'data-z', 'data-rotate-x', 7 | 'data-rotate-y', 'data-rotate-z', 'data-scale'] 8 | 9 | 10 | def gather_positions(tree): 11 | """Makes a list of positions and position commands from the tree""" 12 | pos = {'data-x': 'r0', 13 | 'data-y': 'r0', 14 | 'data-z': 'r0', 15 | 'data-rotate-x': 'r0', 16 | 'data-rotate-y': 'r0', 17 | 'data-rotate-z': 'r0', 18 | 'data-scale': 'r0', 19 | 'is_path': False 20 | } 21 | 22 | steps = 0 23 | default_movement = True 24 | 25 | for step in tree.findall('step'): 26 | steps += 1 27 | 28 | for key in POSITION_ATTRIBS: 29 | value = step.get(key) 30 | 31 | if value is not None: 32 | # We have a new value 33 | default_movement = False # No longer use the default movement 34 | pos[key] = value 35 | elif pos[key] and not pos[key].startswith('r'): 36 | # The old value was absolute and no new value, so stop 37 | pos[key] = 'r0' 38 | # We had no new value, and the old value was a relative 39 | # movement, so we just keep moving. 40 | 41 | if steps == 1 and pos['data-scale'] == 'r0': 42 | # No scale given for first slide, it needs to start at 1 43 | pos['data-scale'] = '1' 44 | 45 | if default_movement and steps != 1: 46 | # No positioning has been given, use default: 47 | pos['data-x'] = 'r%s' % DEFAULT_MOVEMENT 48 | 49 | if 'data-rotate' in step.attrib: 50 | # data-rotate is an alias for data-rotate-z 51 | pos['data-rotate-z'] = step.get('data-rotate') 52 | del step.attrib['data-rotate'] 53 | 54 | if 'hovercraft-path' in step.attrib: 55 | # Path given x and y will be calculated from the path 56 | default_movement = False # No longer use the default movement 57 | pos['is_path'] = True 58 | # Add the path spec 59 | pos['path'] = step.attrib['hovercraft-path'] 60 | yield pos.copy() 61 | # And get rid of it for the next step 62 | del pos['path'] 63 | else: 64 | if 'data-x' in step.attrib or 'data-y' in step.attrib: 65 | # No longer using a path 66 | pos['is_path'] = False 67 | yield pos.copy() 68 | 69 | 70 | def _coord_to_pos(coord): 71 | return {'data-x': int(coord.real), 'data-y': int(coord.imag)} 72 | 73 | 74 | def _pos_to_cord(coord): 75 | return coord['data-x'] + coord['data-y'] * 1j 76 | 77 | 78 | def _path_angle(path, point): 79 | start = point - 0.01 80 | end = point + 0.01 81 | if start < 0: 82 | start = 0 83 | end += 0.01 84 | elif end > 1: 85 | end = 1 86 | start -= 0.01 87 | 88 | distance = path.point(end) - path.point(start) 89 | hyp = math.hypot(distance.real, distance.imag) 90 | result = math.degrees(math.asin(distance.imag / hyp)) 91 | 92 | if distance.real < 0: 93 | result = -180 - result 94 | 95 | if abs(result) < 0.1: 96 | result = 0 97 | 98 | return result 99 | 100 | 101 | def num(s): 102 | try: 103 | return int(s) 104 | except ValueError: 105 | return float(s) 106 | 107 | 108 | def _update_position(pos1, pos2): 109 | 110 | for key in POSITION_ATTRIBS: 111 | val = pos2.get(key) 112 | if val is not None: 113 | plus = val.find("+") 114 | minus = val.find("-") 115 | if plus > -1: 116 | newval = num(val[plus+1:]) 117 | pos1[key + "-rel"] = val[0:plus] 118 | elif minus > -1 and not val.startswith("r-"): 119 | newval = num(val[minus:]) 120 | pos1[key + "-rel"] = val[0:minus] 121 | else: 122 | if val[0] == 'r': 123 | # Relative movement 124 | newval = pos1[key] + num(val[1:]) 125 | else: 126 | newval = num(val) 127 | pos1.pop(key+"-rel", None) 128 | pos1[key] = newval 129 | 130 | 131 | def calculate_positions(positions): 132 | """Calculates position information""" 133 | current_position = {'data-x': 0, 134 | 'data-y': 0, 135 | 'data-z': 0, 136 | 'data-rotate-x': 0, 137 | 'data-rotate-y': 0, 138 | 'data-rotate-z': 0, 139 | 'data-scale': 1, 140 | } 141 | 142 | positer = iter(positions) 143 | position = next(positer) 144 | _update_position(current_position, position) 145 | 146 | while True: 147 | 148 | if 'path' in position: 149 | # Start of a new path! 150 | path = position['path'] 151 | # Follow the path specification 152 | first_point = _pos_to_cord(current_position) 153 | 154 | # Paths that end in Z or z are closed. 155 | closed_path = path.strip()[-1].upper() == 'Z' 156 | path = parse_path(path) 157 | 158 | # Find out how many positions should be calculated: 159 | count = 1 160 | last = False 161 | deferred_positions = [] 162 | while True: 163 | try: 164 | position = next(positer) 165 | deferred_positions.append(position) 166 | except StopIteration: 167 | last = True # This path goes to the end 168 | break 169 | if not position.get('is_path') or 'path' in position: 170 | # The end of the path, or the start of a new one 171 | break 172 | count += 1 173 | 174 | if count < 2: 175 | raise AssertionError("The path specification is only used for " 176 | "one slide, which makes it pointless.") 177 | 178 | if closed_path: 179 | # This path closes in on itself. Skip the last part, so that 180 | # the first and last step doesn't overlap. 181 | endcount = count + 1 182 | else: 183 | endcount = count 184 | 185 | multiplier = (endcount * DEFAULT_MOVEMENT) / path.length() 186 | offset = path.point(0) 187 | 188 | path_iter = iter(deferred_positions) 189 | for x in range(count): 190 | 191 | point = path.point(x / (endcount - 1)) 192 | point = ((point - offset) * multiplier) + first_point 193 | 194 | current_position.update(_coord_to_pos(point)) 195 | 196 | rotation = _path_angle(path, x / (endcount - 1)) 197 | current_position['data-rotate-z'] = rotation 198 | yield current_position.copy() 199 | try: 200 | position = next(path_iter) 201 | except StopIteration: 202 | last = True 203 | break 204 | _update_position(current_position, position) 205 | 206 | if last: 207 | break 208 | 209 | continue 210 | 211 | yield current_position.copy() 212 | try: 213 | position = next(positer) 214 | except StopIteration: 215 | break 216 | _update_position(current_position, position) 217 | 218 | 219 | def update_positions(tree, positions): 220 | """Updates the tree with new positions""" 221 | 222 | for step, pos in zip(tree.findall('step'), positions): 223 | for key in sorted(pos): 224 | value = pos.get(key) 225 | if key.endswith("-rel"): 226 | abs_key = key[:key.index("-rel")] 227 | if value is not None: 228 | els = tree.findall(".//*[@id='" + value + "']") 229 | for el in els : 230 | pos[abs_key] = num(el.get(abs_key)) + pos.get(abs_key) 231 | step.attrib[abs_key] = str(pos.get(abs_key)) 232 | else: 233 | step.attrib[key] = str(pos[key]) 234 | 235 | if 'hovercraft-path' in step.attrib: 236 | del step.attrib['hovercraft-path'] 237 | 238 | 239 | def position_slides(tree): 240 | """Position the slides in the tree""" 241 | 242 | positions = gather_positions(tree) 243 | positions = calculate_positions(positions) 244 | update_positions(tree, positions) 245 | -------------------------------------------------------------------------------- /tests/test_parse.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pkg_resources import resource_string 3 | from lxml import etree 4 | 5 | from hovercraft.parse import SlideMaker, rst2xml 6 | 7 | 8 | def make_tree(file_name): 9 | """Loads reStructuredText, outputs an lxml tree""" 10 | rst = resource_string(__name__, file_name) 11 | xml, dependencies = rst2xml(rst) 12 | return etree.fromstring(xml) 13 | 14 | 15 | class SlideMakerTests(unittest.TestCase): 16 | """Test the conversion of docutils XML into XML suitable to give to the templates""" 17 | 18 | def test_simple(self): 19 | tree = SlideMaker(make_tree('test_data/simple.rst')).walk() 20 | self.assertEqual(etree.tostring(tree), ( 21 | b'' 22 | b'
' 23 | b'Simple PresentationThis presentation ' 24 | b'has two slides, each with a header and some text.' 25 | b'
Second slide' 27 | b'There is no positioning or anything fancy.' 28 | b'
')) 29 | 30 | def test_advanced(self): 31 | tree = SlideMaker(make_tree('test_data/advanced.rst')).walk() 32 | xml = etree.tostring(tree) 33 | target = ( 34 | b'This is an advanced ' 37 | b'presentation. It doesn\'t have a section in the first\nstep, ' 38 | b'meaning the first step will not be a step at all, but a sort of\n' 39 | b'introductory comment about the presentation, that will not show up ' 40 | b'in the\npresentation at all.It also sets a ' 41 | b'title and a transition-duration.
' 44 | b'Advanced PresentationHere we ' 45 | b'show the positioning feature, where we can explicitly set a ' 46 | b'position\non one of the steps.
' 49 | b'FormattingLet us also try some basic ' 50 | b'formatting, like italic, and bold' 51 | b'.' 52 | b'We can also' 53 | b'have a list' 54 | b'of things.' 55 | b'
' 56 | b'There should also be possible to have\n' 57 | b'preformatted text for code.' 59 | b'def foo(bar):\n # Comment' 62 | b'\n a = 1 + "hubbub"' 65 | b'\n return ' 66 | b'NoneAn image, with attributes:' 69 | b'
Character sets' 71 | b'The character set is UTF-8 as of now. Like this: ' 72 | b'åäö.
') 73 | self.assertEqual(xml, target) 74 | 75 | def test_presenter_notes(self): 76 | tree = SlideMaker(make_tree('test_data/presenter-notes.rst')).walk() 77 | target = ( 78 | b'Document ' 80 | b'title
Hovercrafts presenter ' 83 | b'notesHovercraft! supports presenter notes. It does ' 84 | b'this by taking anything in a\nwhat is calles a "notes-admonition" and ' 85 | b'making that into presenter notes.Hence, ' 86 | b'this will show up as presenter notes.\nYou have still access to a lot ' 87 | b'of formatting, like' 88 | b'Bullet lists' 89 | b'And all types of inline formatting' 90 | b'
' 91 | b'
You ' 93 | b'don\'t have to start the text on the same line as\nthe note, but ' 94 | b'you can.You can also have several paragraphs.' 95 | b' You can not have any\nheadings of any kind ' 96 | b'though.But you can fake them through ' 97 | b'bold-textAnd that\'s useful enough ' 98 | b'for presentation notes.
') 99 | self.assertEqual(etree.tostring(tree), target) 100 | 101 | def test_transition_levels(self): 102 | # Make the XML 103 | xml, deps = rst2xml( 104 | b'Intro\n\n====\n\nLevel 1\n\n====\n\nLevel 1\n\n----\n\nLevel 2\n\n' 105 | b'....\n\nLevel 3\n\n----\n\nLevel 2\n\n....\n\nLevel 3\n\n' 106 | b'====\n\nLevel 1') 107 | 108 | target_start = ( 109 | b'\n\n') 112 | target_end = ( 113 | b'Intro' 114 | b'Level 1' 115 | b'Level 1' 116 | b'Level 2' 117 | b'Level 3' 118 | b'Level 2' 119 | b'Level 3' 120 | b'Level 1' 121 | b'') 122 | self.assertTrue(xml.startswith(target_start)) 123 | self.assertTrue(xml.endswith(target_end)) 124 | 125 | # Make the slides: 126 | tree = SlideMaker(etree.fromstring(xml)).walk() 127 | 128 | target = ( 129 | b'Intro' 130 | b'Level 1' 131 | b'Level 1' 132 | b'Level 2' 133 | b'Level 3' 134 | b'Level 2' 135 | b'Level 3' 136 | b'Level 1' 137 | b'' 138 | ) 139 | self.assertEqual(etree.tostring(tree), target) 140 | 141 | if __name__ == '__main__': 142 | unittest.main() 143 | -------------------------------------------------------------------------------- /hovercraft/generate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import shutil 4 | from lxml import etree, html 5 | from pkg_resources import resource_string 6 | 7 | from .parse import rst2xml, SlideMaker 8 | from .position import position_slides 9 | from .template import (Template, CSS_RESOURCE, JS_RESOURCE, JS_POSITION_HEADER, 10 | JS_POSITION_BODY, OTHER_RESOURCE, DIRECTORY_RESOURCE) 11 | 12 | 13 | class ResourceResolver(etree.Resolver): 14 | 15 | def resolve(self, url, pubid, context): 16 | if url.startswith('resource:'): 17 | prefix, filename = url.split(':', 1) 18 | return self.resolve_string(resource_string(__name__, filename), context) 19 | 20 | 21 | def rst2html(filepath, template_info, auto_console=False, skip_help=False, skip_notes=False, mathjax=False, slide_numbers=False): 22 | # Read the infile 23 | with open(filepath, 'rb') as infile: 24 | rststring = infile.read() 25 | 26 | presentation_dir = os.path.split(filepath)[0] 27 | 28 | # First convert reST to XML 29 | xml, dependencies = rst2xml(rststring, filepath) 30 | tree = etree.fromstring(xml) 31 | 32 | # Fix up the resulting XML so it makes sense 33 | sm = SlideMaker(tree, skip_notes=skip_notes) 34 | tree = sm.walk() 35 | 36 | # Pick up CSS information from the tree: 37 | console_css = None 38 | preview_css = None 39 | for attrib in tree.attrib: 40 | if attrib.startswith('css'): 41 | 42 | if '-' in attrib: 43 | dummy, media = attrib.split('-', 1) 44 | else: 45 | media = 'screen,projection' 46 | css_files = tree.attrib[attrib].split() 47 | for css_file in css_files: 48 | if media in ('console', 'preview'): 49 | # The "console" media is used to style the presenter 50 | # console and does not need to be included in the header, 51 | # but must be copied. So we add it as a non css file, 52 | # even though it's a css-file. 53 | template_info.add_resource( 54 | os.path.abspath(os.path.join(presentation_dir, css_file)), 55 | OTHER_RESOURCE, 56 | target=css_file) 57 | else: 58 | # Add as a css resource: 59 | template_info.add_resource( 60 | os.path.abspath(os.path.join(presentation_dir, css_file)), 61 | CSS_RESOURCE, 62 | target=css_file, 63 | extra_info=media) 64 | 65 | elif attrib.startswith('js'): 66 | if attrib == 'js-header': 67 | media = JS_POSITION_HEADER 68 | else: 69 | # Put javascript in body tag as default. 70 | media = JS_POSITION_BODY 71 | js_files = tree.attrib[attrib].split() 72 | for js_file in js_files: 73 | template_info.add_resource( 74 | os.path.abspath(os.path.join(presentation_dir, js_file)), 75 | JS_RESOURCE, 76 | target=js_file, 77 | extra_info=media) 78 | 79 | if sm.need_mathjax and mathjax: 80 | if mathjax.startswith('http'): 81 | template_info.add_resource(None, JS_RESOURCE, 82 | target=mathjax, 83 | extra_info=JS_POSITION_HEADER) 84 | else: 85 | # Local copy 86 | template_info.add_resource(mathjax, DIRECTORY_RESOURCE, 87 | target='mathjax') 88 | template_info.add_resource(None, JS_RESOURCE, 89 | target='mathjax/MathJax.js?config=TeX-MML-AM_CHTML', 90 | extra_info=JS_POSITION_HEADER) 91 | 92 | # Position all slides 93 | position_slides(tree) 94 | 95 | # Add the template info to the tree: 96 | tree.append(template_info.xml_node()) 97 | 98 | # If the console-should open automatically, set an attribute on the document: 99 | if auto_console: 100 | tree.attrib['auto-console'] = 'True' 101 | 102 | # If the console-should open automatically, set an attribute on the document: 103 | if skip_help: 104 | tree.attrib['skip-help'] = 'True' 105 | 106 | # If the slide numbers should be displayed, set an attribute on the document: 107 | if slide_numbers: 108 | tree.attrib['slide-numbers'] = 'True' 109 | 110 | # We need to set up a resolver for resources, so we can include the 111 | # reST.xsl file if so desired. 112 | parser = etree.XMLParser() 113 | parser.resolvers.add(ResourceResolver()) 114 | 115 | # Transform the tree to HTML 116 | xsl_tree = etree.fromstring(template_info.xsl, parser) 117 | transformer = etree.XSLT(xsl_tree) 118 | tree = transformer(tree) 119 | result = html.tostring(tree) 120 | 121 | return template_info.doctype + result, dependencies 122 | 123 | 124 | def copy_resource(filename, sourcedir, targetdir): 125 | if filename[0] == '/' or ':' in filename: 126 | # Absolute path or URI: Do nothing 127 | return None # No monitoring needed 128 | sourcepath = os.path.join(sourcedir, filename) 129 | targetpath = os.path.join(targetdir, filename) 130 | 131 | if (os.path.exists(targetpath) and 132 | os.path.getmtime(sourcepath) <= os.path.getmtime(targetpath)): 133 | # File has not changed since last copy, so skip. 134 | return sourcepath # Monitor this file 135 | 136 | targetdir = os.path.split(targetpath)[0] 137 | if not os.path.exists(targetdir): 138 | os.makedirs(targetdir) 139 | 140 | shutil.copy2(sourcepath, targetpath) 141 | return sourcepath # Monitor this file 142 | 143 | 144 | def generate(args): 145 | """Generates the presentation and returns a list of files used""" 146 | 147 | source_files = {args.presentation} 148 | 149 | # Parse the template info 150 | template_info = Template(args.template) 151 | if args.css: 152 | presentation_dir = os.path.split(args.presentation)[0] 153 | target_path = os.path.relpath(args.css, presentation_dir) 154 | template_info.add_resource(args.css, CSS_RESOURCE, target=target_path, extra_info='all') 155 | source_files.add(args.css) 156 | if args.js: 157 | presentation_dir = os.path.split(args.presentation)[0] 158 | target_path = os.path.relpath(args.js, presentation_dir) 159 | template_info.add_resource(args.js, JS_RESOURCE, target=target_path, extra_info=JS_POSITION_BODY) 160 | source_files.add(args.js) 161 | 162 | # Make the resulting HTML 163 | htmldata, dependencies = rst2html(args.presentation, template_info, 164 | args.auto_console, args.skip_help, 165 | args.skip_notes, args.mathjax, 166 | args.slide_numbers) 167 | source_files.update(dependencies) 168 | 169 | # Write the HTML out 170 | if not os.path.exists(args.targetdir): 171 | os.makedirs(args.targetdir) 172 | with open(os.path.join(args.targetdir, 'index.html'), 'wb') as outfile: 173 | outfile.write(htmldata) 174 | 175 | # Copy supporting files 176 | source_files.update(template_info.copy_resources(args.targetdir)) 177 | 178 | # Copy images from the source: 179 | sourcedir = os.path.split(os.path.abspath(args.presentation))[0] 180 | tree = html.fromstring(htmldata) 181 | for image in tree.iterdescendants('img'): 182 | filename = image.attrib['src'] 183 | source_files.add(copy_resource(filename, sourcedir, args.targetdir)) 184 | 185 | RE_CSS_URL = re.compile(br"""url\(['"]?(.*?)['"]?[\)\?\#]""") 186 | 187 | # Copy any files referenced by url() in the css-files: 188 | for resource in template_info.resources: 189 | if resource.resource_type != CSS_RESOURCE: 190 | continue 191 | # path in CSS is relative to CSS file; construct source/dest accordingly 192 | css_base = template_info.template_root if resource.is_in_template else sourcedir 193 | css_sourcedir = os.path.dirname(os.path.join(css_base, resource.filepath)) 194 | css_targetdir = os.path.dirname(os.path.join(args.targetdir, resource.final_path())) 195 | uris = RE_CSS_URL.findall(template_info.read_data(resource)) 196 | uris = [uri.decode() for uri in uris] 197 | if resource.is_in_template and template_info.builtin_template: 198 | for filename in uris: 199 | template_info.add_resource(filename, OTHER_RESOURCE, target=css_targetdir, 200 | is_in_template=True) 201 | else: 202 | for filename in uris: 203 | source_files.add(copy_resource(filename, css_sourcedir, css_targetdir)) 204 | 205 | # All done! 206 | 207 | return {os.path.abspath(f) for f in source_files if f} 208 | -------------------------------------------------------------------------------- /docs/examples/positions.rst: -------------------------------------------------------------------------------- 1 | :title: Positioning tutorial 2 | :css: tutorial.css 3 | 4 | This is a tutorial for Hovercraft! positioning. It's meant to be read as 5 | `source code <../_sources/examples/positions.txt>`_. 6 | 7 | You can render this presentation to HTML with the command:: 8 | 9 | hovercraft positions.rst outdir 10 | 11 | And then view the outdir/index.html file to see how it turned out. 12 | 13 | If you are seeing this text, and not reading this as source code, you are 14 | doing it wrong! It's going to be confusing and not very useful. 15 | 16 | Use The Source, Luke! But first you probably want to read through the 17 | official documentation at https://hovercraft.readthedocs.io/ 18 | There are links to the source code in the Examples section. 19 | 20 | ---- 21 | 22 | Positions 23 | ========= 24 | 25 | Each step can be explicitly positioned by putting some ``data-`` fields in 26 | the beginning of the slide. This has to be first in the slide (although you 27 | have to have a blank line beneath the four dashes that start the slide. 28 | 29 | To put the slide at zero pixels to the right and a thousand pixels below the 30 | coordinate centre you add the following:: 31 | 32 | :data-x: 0 33 | :data-y: 1000 34 | 35 | Let's do that for the next slide: 36 | 37 | ---- 38 | 39 | :data-x: 0 40 | :data-y: 1000 41 | 42 | X & Y 43 | ===== 44 | 45 | You don't have to give both X and Y coordinates. They will default to "no 46 | difference from the last slide" if not given. As the first slide ends up at 47 | X=0 and Y=0, the ``:data-x: 0`` above is strictly speaking not necessary. 48 | 49 | ---- 50 | 51 | :data-x: 2000 52 | :data-y: 1000 53 | 54 | Positioning fields 55 | ================== 56 | 57 | Any field starting with ``data-`` will be converted to a ``data-`` attribute 58 | on the impress.js step. There is no filtering done, so if new attributes are 59 | supported by impress.js, they should just work from Hovercraft! as well. 60 | 61 | The ones impress.js currently uses are:: 62 | 63 | data-x Position on the X-axis 64 | data-y Position on the Y-axis 65 | data-z Position on the Z-axis (which means 3D!) 66 | data-rotate Rotation in degrees 67 | data-rotate-z An alias for data-rotate 68 | data-rotate-x Rotation on the X-axis, which agains means 3D effects 69 | data-rotate-y Rotation on the Y-axis 70 | data-scale The size of the slide, which means zooming effects 71 | 72 | Let's do some zoom and rotate! 73 | 74 | ---- 75 | 76 | :data-scale: 5 77 | :data-rotate: 90 78 | :data-x: 3000 79 | :data-y: 1000 80 | 81 | Zoom out! 82 | ========= 83 | 84 | So here we rotated 90 degrees and zoomed out five times. 85 | 86 | ---- 87 | 88 | :data-scale: 1 89 | :data-x: 4000 90 | :data-y: 2000 91 | :id: positions_last_slide 92 | 93 | Relative positions to last slide 94 | ================================ 95 | 96 | One thing that *is* a problem is the absolute positioning. All the positions 97 | we used so far above are in relation to the start of the coordinate system. 98 | But if we now need to insert a slide somewhere in between the slides above, 99 | we need to make room for it, and that means we have to reposition all the 100 | slides that come after. That quickly becomes annoying. 101 | 102 | Hovercraft! therefore supports relative positioning where you just give a 103 | relative coordinate to the last slide. 104 | 105 | ---- 106 | 107 | :data-x: r1000 108 | 109 | ---- 110 | 111 | Like this 112 | ========= 113 | 114 | You just prefix the position with an ``r`` and it becomes relative. That 115 | means that if the previous slide moves, this moves with it. You'll find that 116 | it's generally good practice to use mostly relative positioning if you are 117 | still flexible about what your slides are and what they should say or 118 | in which order. 119 | 120 | For some types of presentation, where typography is important, you need to 121 | decide everything that the slide should say and their position from the 122 | start. Then absolute positioning works fine. But otherwise you probably want 123 | to use relative positioning. 124 | 125 | ---- 126 | 127 | :data-y: positions_last_slide+1000 128 | 129 | Relative positions to any slide 130 | =============================== 131 | 132 | You can reference any *previous* slide by its id and specify the position relative to it. 133 | This will work for all fields. 134 | However, you should not use ``r`` as a slide id since the positioning might not behave as you expect. 135 | 136 | ---- 137 | 138 | :data-rotate: r15 139 | 140 | Automatic positioning 141 | ===================== 142 | 143 | Every field will retain its last value if you don't specify a new one. 144 | In this case, we keep a r1000 value for data-x and introduce a new 145 | r15 value for data-rotate. This and the next slide will therefore 146 | move right 1000 pixels and rotate 15 degrees more for each slide. 147 | 148 | It looks like it moves "up" because we are already rotated 90 degrees. 149 | 150 | ---- 151 | 152 | :data-x: r1000 153 | :data-scale: 0.15 154 | 155 | **A warning!** 156 | ============== 157 | 158 | ---- 159 | 160 | :data-x: r1000 161 | :data-scale: 1 162 | 163 | Didn't that slide look good? 164 | ============================ 165 | 166 | Don't worry, when you make big zooms, different browsers will behave 167 | differently and be good at different things. Some will be slow and jerky on 168 | the 3D effects, and others will show fonts with jagged edges when you zoom. 169 | Older and less common browsers can also have problems with 3D effects. 170 | 171 | ---- 172 | 173 | :hovercraft-path: m275,175 a150,150 0 0,1 -150,150 174 | 175 | SVG paths 176 | ========= 177 | 178 | The field ``:hovercraft-path:`` tells Hovercraft! to place the slides 179 | along a SVG path. This enables you to put slides along a graphical shape. 180 | 181 | ---- 182 | 183 | SVG paths 184 | ========= 185 | 186 | You can design the shape in a vector graphics program like Inkscape 187 | and then lift it out of the SVG file (which are in XML) and use it 188 | in Hovercraft! 189 | 190 | This example is an arc. 191 | 192 | ---- 193 | 194 | SVG paths 195 | ========= 196 | 197 | Using SVG path so is not entirely without it's difficulties and 198 | surprises, and this is discussed more in the documentation, under 199 | the SVG Paths heading. 200 | 201 | ---- 202 | 203 | SVG paths 204 | ========= 205 | 206 | Every following slide will be placed along the path, 207 | and the path will be scaled to fit the slides. 208 | 209 | ---- 210 | 211 | :data-rotate: -180 212 | :data-x: r-1200 213 | 214 | SVG paths 215 | ========= 216 | 217 | And the positioning along the path will end when you get a path that has 218 | explicit positioning, like this one. 219 | 220 | ---- 221 | 222 | :data-rotate-y: -45 223 | :data-y: r-100 224 | :data-x: r-800 225 | 226 | 3D! 227 | === 228 | 229 | Now it get's complicated! 230 | 231 | ---- 232 | 233 | :data-rotate-y: 0 234 | :data-y: r100 235 | :data-x: r-1000 236 | 237 | 3D Rotation 238 | =========== 239 | 240 | We have already seen how we can rotate the slide with ``:data-rotate:``. This is actually rotation 241 | in the Z-axis, so you can use ``:data-rotate-z:`` as well, it's the same thing. 242 | But you can also rotate in the Y-axis. 243 | 244 | ---- 245 | 246 | :data-x: r0 247 | :data-y: r0 248 | :data-rotate-y: 90 249 | 250 | 3D Rotation 251 | =========== 252 | 253 | That was a 90 degree rotation in the Y-axis. 254 | Let's go back. 255 | 256 | ---- 257 | 258 | :data-x: r0 259 | :data-y: r0 260 | :data-rotate-y: 0 261 | 262 | ---- 263 | 264 | :data-x: r-1000 265 | :data-y: r0 266 | :data-rotate-y: 0 267 | 268 | 3D Rotation 269 | =========== 270 | 271 | Notice how the text was invisible before the rotation? 272 | The text is there, but it has no depth, so you can't see it. 273 | Of course, the same happens in the X-axis. 274 | 275 | ---- 276 | 277 | :data-x: r0 278 | :data-y: r0 279 | :data-rotate-x: 90 280 | 281 | 3D Rotation 282 | =========== 283 | 284 | That was a 90 degree rotation in the X-axis. 285 | Let's go back. 286 | 287 | ---- 288 | 289 | :data-x: r0 290 | :data-y: r0 291 | :data-rotate-x: 0 292 | 293 | ---- 294 | 295 | :data-x: r-1000 296 | 297 | 3D Positioning 298 | ============== 299 | 300 | You can not only rotate in all three dimensions, but also position in all 301 | three dimensions. So far we have only used ``:data-x`` and ``:data-y``, but 302 | there is a ``:data-z`` as well. 303 | 304 | ---- 305 | 306 | :data-z: 1000 307 | :data-x: r0 308 | :data-y: r-50 309 | 310 | Z-space 311 | ======= 312 | 313 | ---- 314 | 315 | :data-x: r0 316 | :data-y: r-500 317 | 318 | Z-space 319 | ======= 320 | 321 | This can be used for all sorts of interesting effects. It should be noted 322 | that the depth of the Z-axis is quite limited in some browsers. 323 | 324 | If you set it too high, you'll find the slide appearing low and upside down. 325 | 326 | ---- 327 | 328 | :data-x: r800 329 | :data-y: r0 330 | 331 | Z-space 332 | ======= 333 | 334 | But well used it can give an extra wow-factor, 335 | 336 | ---- 337 | 338 | :data-z: 0 339 | :data-x: r100 340 | :data-y: r-200 341 | :data-scale: 1 342 | 343 | and make text pop! 344 | ================== 345 | 346 | ---- 347 | 348 | :data-x: r3000 349 | :data-y: r-1500 350 | :data-scale: 15 351 | :data-rotate-z: 0 352 | :data-rotate-x: 0 353 | :data-rotate-y: 0 354 | :data-z: 0 355 | 356 | 357 | That's all for now 358 | ================== 359 | 360 | *Have fun!* 361 | 362 | -------------------------------------------------------------------------------- /docs/examples/tutorial.rst: -------------------------------------------------------------------------------- 1 | :title: Slideshow Tutorial 2 | :author: Lennart Regebro 3 | :description: The Hovercraft! tutorial. 4 | :keywords: presentation, restructuredtext, impress.js, tutorial 5 | :css: tutorial.css 6 | 7 | .. header:: 8 | 9 | .. image:: images/hovercraft_logo.png 10 | 11 | .. footer:: 12 | 13 | Hovercraft! Tutorial, https://hovercraft.readthedocs.io 14 | 15 | This slide show is a sort of tutorial of how to use Hovercraft! to make 16 | presentations. It will show the most important features of Hovercraft! with 17 | explanations. 18 | 19 | Hopefully you ended up here by the link from the official documentation at 20 | https://hovercraft.readthedocs.io/ . If not, you probably want to go there 21 | and read through it first. 22 | 23 | This tutorial is meant to be read as source code, not in any HTML form, so if 24 | you can see this text (it won't be visible in the final presentation) and you 25 | aren't seeing the source code, you are doing it wrong. It's going to be 26 | confusing and not very useful. Again, go to the official docs. There are 27 | links to the source code in the Examples section. 28 | 29 | You can render this presentation to HTML with the command:: 30 | 31 | hovercraft positions.rst outdir 32 | 33 | And then view the outdir/index.html file to see how it turned out. 34 | 35 | **Now then, on to the tutorial part!** 36 | 37 | The first thing to note is the special syntax for information about the 38 | presentation that you see above. This is in reStructuredText called "fields" 39 | and it's used all the time in Hovercraft! to change attributes and set data 40 | on the presentation, on slides and on images. The order of the fields is not 41 | important, but you can only have one of each field. 42 | 43 | The fields above are meta-data about the presentation, except for the 44 | :css:-field. This meta data is only useful if you plan to publish the 45 | presentation by putting the HTML online. If you are only going to show this 46 | presentation yourself in a meeting you can skip all of it. 47 | 48 | The title set is the title that is going to be shown in the title bar of the 49 | browser. reStructuredText also has a separate syntax for titles that is also 50 | supported by Hovercraft:: 51 | 52 | .. title:: Slideshow Tutorial 53 | 54 | However that requires an empty line after it, and it looks better to use the 55 | same syntax for all metadata. 56 | 57 | The :css: field will add a custom CSS-file to this presentation. This is 58 | something you almost always want to do, as you otherwise have no control over 59 | how the presentation will look. You can also specify different media for 60 | the CSS, for example "screen,projection":: 61 | 62 | :css-screen,projection: hovercraft.css 63 | 64 | This way you can have different CSS for print and for display. You can only 65 | specify one CSS-file per field, however. If you want to include more you 66 | need to use the @import directive in CSS. 67 | 68 | Once you have added metadata and CSS, it's time to start on the slides. 69 | 70 | You separate slides with a line that consists of four or more dashes. The 71 | first slide will start at the first such line, or at the first heading. Since 72 | none of the text so far has been a heading, it means that the first slide has 73 | not yet started. As a result, all this text will be ignored in the output. 74 | 75 | So lets start the first slide by having a line with four dashes. Since the 76 | first slide starts with a heading, that line is strictly speaking not needed, 77 | but it's good to be explicit. 78 | 79 | ---- 80 | 81 | This is a first slide 82 | ===================== 83 | 84 | Restructured text takes any line that is underlined with punctuation and 85 | makes it into a heading. Each type of underlining will be made into a different 86 | level of heading, but it is not the type that is important, but rather the 87 | order of which each type will be enountered. 88 | 89 | So in this presentation, lines underlined with equal (=) characters will be 90 | made into a first-level (H1) heading. 91 | 92 | ---- 93 | 94 | First header 95 | ============ 96 | 97 | You can choose other punctuation characters as your level 1 heading if you like, 98 | but this is the most common. Any if these character works:: 99 | 100 | = - ` : ' " ~ ^ _ * + # < > . 101 | 102 | Second header 103 | ------------- 104 | 105 | Third header 106 | ............ 107 | 108 | The drawback with reStructuredText is that you can't skip levels. You can't 109 | go directly from level 1 to level 3 without having a level 2 in between. 110 | If you do you get an error:: 111 | 112 | Title level inconsistent 113 | 114 | ---- 115 | 116 | Other formatting 117 | ================ 118 | 119 | All the normal reStructuredText functions are supported in Hovercraft! 120 | 121 | - Such as bulletlists, which start with a dash (-) or an asterisk (*). 122 | You can have many lines of text in one bullet if you indent the 123 | following lines. 124 | 125 | - And you can have many levels of bullets. 126 | 127 | - Like this. 128 | 129 | - There is *Emphasis* and **strong emphasis**, rendered as and . 130 | 131 | ---- 132 | 133 | More formatting 134 | =============== 135 | 136 | #. Numbered lists are of course also supported. 137 | 138 | #. They are automatically numbered. 139 | 140 | #. But only for single-level lists and single rows of text. 141 | 142 | #. ``inline literals``, rendered as and usually shown with a monospace font, which is good for source code. 143 | 144 | #. Hyperlinks, like Python_ 145 | 146 | .. _Python: http://www.python.org 147 | 148 | 149 | ---- 150 | 151 | Images 152 | ====== 153 | 154 | You can insert an image with the .. image:: directive: 155 | 156 | .. image:: images/hovercraft_logo.png 157 | 158 | And you can optionally set width and height: 159 | 160 | .. image:: images/hovercraft_logo.png 161 | :width: 50px 162 | :height: 130px 163 | 164 | Some people like to have slideshows containing only illustrative images. This 165 | works fine with Hovercraft! as well, as you can see on the next slide. 166 | 167 | ---- 168 | 169 | .. image:: images/hovercraft_logo.png 170 | 171 | ---- 172 | 173 | Slides can have presenter notes! 174 | ================================ 175 | 176 | This is the killer-feature of Hovercraft! as very few other tools like this 177 | support a presenter console. You add presenter notes in the slide like this: 178 | 179 | .. note:: 180 | 181 | And then you indent the text afterwards. You can have a lot of formatting 182 | in the presenter notes, like *emphasis* and **strong** emphasis. 183 | 184 | - Even bullet lists! 185 | 186 | - Which can be handy! 187 | 188 | But you can't have any headings. 189 | 190 | 191 | ---- 192 | 193 | Source code 194 | =========== 195 | 196 | You can also have text that is mono spaced, for source code and similar. 197 | There are several syntaxes for that. For code that is a part of a sentence 198 | you use the inline syntax with ``double backticks`` we saw earlier. 199 | 200 | If you want a whole block of preformatted text you can use double colons:: 201 | 202 | And then you 203 | need to indent the block 204 | of text that 205 | should be preformatted 206 | 207 | You can even have the double colons on a line by themselves: 208 | 209 | :: 210 | 211 | And this text will 212 | now be 213 | rendered as 214 | preformatted text 215 | 216 | ---- 217 | 218 | Syntax highlighting 219 | =================== 220 | 221 | But the more interesting syntax for preformatted text is the .. code:: 222 | directive. This enables you to syntax highlight the code. 223 | 224 | .. code:: python 225 | 226 | def day_of_year(month, day): 227 | return (month - 1) * 30 + day_of_month 228 | 229 | def day_of_week(day): 230 | return ((day - 1) % 10) + 1 231 | 232 | def weekno(month, day): 233 | return ((day_of_year(month, day) - 1) // 10) + 1 234 | 235 | ---- 236 | 237 | More code features 238 | ================== 239 | 240 | The syntax highlighting is done via docutils by a module called Pygments_ 241 | which support all popular languages, and a lot of unpopular ones as well. 242 | 243 | The coloring is done by CSS, if you want to change it, copy the CSS in 244 | the highlight.css file and override it in your custom CSS. 245 | 246 | .. _Pygments: http://pygments.org/ 247 | 248 | ---- 249 | 250 | Testing the code 251 | ================ 252 | 253 | If you are including Python-code, then Manuel_ 1.7.0 and later can test the 254 | code for you. This enables you to have code in your presentation and make 255 | sure it works. 256 | 257 | To do this properly you sometimes want setup and teardown code, code that 258 | should be executed as a part of the test, but not shown in the presentation. 259 | 260 | To do that, you can simply set a class on the code block. 261 | 262 | .. code:: python 263 | :class: hidden 264 | 265 | from datetime import datetime 266 | 267 | Add the hidden class in your css: 268 | 269 | .. code:: css 270 | 271 | pre.hidden { 272 | display: none; 273 | } 274 | 275 | ---- 276 | 277 | And your visible code will now be runnable with Manuel: 278 | 279 | .. code:: python 280 | 281 | >>> datetime(2013, 2, 19, 12) 282 | datetime.datetime(2013, 2, 19, 12, 0) 283 | 284 | .. _Manuel: https://pypi.python.org/pypi/manuel 285 | 286 | ---- 287 | 288 | Render mathematics! 289 | =================== 290 | 291 | Mathematical formulas can be rendered with Mathjax! 292 | 293 | .. math:: 294 | 295 | e^{i \pi} + 1 = 0 296 | 297 | dS = \frac{dQ}{T} 298 | 299 | And inline: :math:`S = k \log W` 300 | 301 | .. _Mathjax: https://www.mathjax.org/ 302 | 303 | ---- 304 | 305 | 306 | 307 | That's all folks! 308 | ================= 309 | 310 | That finishes the basic tutorial for Hovercraft! Next you probably want to 311 | take a look at the positioning tutorial, so you can use the pan, rotate and 312 | zoom functionality. 313 | -------------------------------------------------------------------------------- /tests/test_hovercraft.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from tempfile import TemporaryDirectory 4 | import unittest 5 | 6 | from hovercraft import main 7 | from .test_data import HTML_OUTPUTS 8 | 9 | TEST_DATA = os.path.join(os.path.split(__file__)[0], 'test_data') 10 | 11 | 12 | class HTMLTests(unittest.TestCase): 13 | """Test the procedure from rst to html""" 14 | 15 | def test_small(self): 16 | with TemporaryDirectory() as tmpdir: 17 | sys.argv = [ 18 | 'bin/hovercraft', 19 | '-t' + os.path.join(TEST_DATA, 'minimal'), 20 | os.path.join(TEST_DATA, 'simple.rst'), 21 | tmpdir, 22 | ] 23 | 24 | main() 25 | 26 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 27 | self.assertEqual(outfile.read(), HTML_OUTPUTS['simple']) 28 | 29 | js_files = os.listdir(os.path.join(tmpdir, 'js')) 30 | self.assertEqual(set(js_files), {'impress.js', 'hovercraft-minimal.js'}) 31 | 32 | def test_extra_css(self): 33 | with TemporaryDirectory() as tmpdir: 34 | sys.argv = [ 35 | 'bin/hovercraft', 36 | '-t' + os.path.join(TEST_DATA, 'maximal'), 37 | '-c' + os.path.join(TEST_DATA, 'extra.css'), 38 | '-n', 39 | os.path.join(TEST_DATA, 'simple.rst'), 40 | tmpdir, 41 | ] 42 | 43 | main() 44 | 45 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 46 | self.assertEqual(outfile.read(), HTML_OUTPUTS['extra_css']) 47 | 48 | out_files = os.listdir(tmpdir) 49 | self.assertEqual(set(out_files), 50 | {'extra.css', 'index.html', 'js', 'css', 'images', 'fonts', 'directory'}) 51 | 52 | def test_extra_js(self): 53 | with TemporaryDirectory() as tmpdir: 54 | sys.argv = [ 55 | 'bin/hovercraft', 56 | '-t' + os.path.join(TEST_DATA, 'maximal'), 57 | '-j' + os.path.join(TEST_DATA, 'extra.js'), 58 | '-n', 59 | os.path.join(TEST_DATA, 'simple.rst'), 60 | tmpdir, 61 | ] 62 | 63 | main() 64 | 65 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 66 | self.assertEqual(outfile.read(), HTML_OUTPUTS['extra_js']) 67 | 68 | out_files = os.listdir(tmpdir) 69 | self.assertEqual(set(out_files), 70 | {'extra.js', 'index.html', 'js', 'css', 'images', 'fonts', 'directory'}) 71 | 72 | def test_big(self): 73 | with TemporaryDirectory() as tmpdir: 74 | sys.argv = [ 75 | 'bin/hovercraft', 76 | '-t' + os.path.join(TEST_DATA, 'maximal', 'template.cfg'), 77 | os.path.join(TEST_DATA, 'advanced.rst'), 78 | tmpdir, 79 | ] 80 | 81 | main() 82 | 83 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 84 | # We have verified the contents in test_generator.py, let's 85 | # just check that it writes the right thing: 86 | self.assertEqual(outfile.read(), HTML_OUTPUTS['advanced']) 87 | 88 | out_files = os.listdir(tmpdir) 89 | self.assertEqual(set(out_files), 90 | {'extra.css', 'extra.js', 'index.html', 'js', 'css', 'images', 'fonts', 'directory'}) 91 | 92 | # Make sure the whole tree of the directory resource was copies, 93 | # except .dont-include, which should not be included. 94 | out_files = os.listdir(os.path.join(tmpdir, 'directory')) 95 | self.assertEqual(set(out_files), 96 | {'subdir', 'hovercraft_logo.png', 'print.css'}) 97 | out_files = os.listdir(os.path.join(tmpdir, 'directory', 'subdir')) 98 | self.assertEqual(set(out_files), 99 | {'afile'}) 100 | 101 | js_files = os.listdir(os.path.join(tmpdir, 'js')) 102 | self.assertEqual(set(js_files), 103 | {'impress.js', 'hovercraft.js', 'impressConsole.js', 'dummy.js'}) 104 | css_files = os.listdir(os.path.join(tmpdir, 'css')) 105 | self.assertEqual(set(css_files), {'print.css', 'style.css', 'impressConsole.css'}) 106 | image_files = os.listdir(os.path.join(tmpdir, 'images')) 107 | self.assertEqual(set(image_files), {'hovercraft_logo.png'}) 108 | font_files = os.listdir(os.path.join(tmpdir, 'fonts')) 109 | self.assertEqual(set(font_files), { 110 | 'texgyreschola-regular-webfont.ttf', 111 | 'texgyreschola-regular-webfont.eot', 112 | 'texgyreschola-regular-webfont.woff', 113 | 'texgyreschola-regular-webfont.svg', 114 | }) 115 | 116 | def test_skip_notes(self): 117 | with TemporaryDirectory() as tmpdir: 118 | sys.argv = [ 119 | 'bin/hovercraft', 120 | '-t' + os.path.join(TEST_DATA, 'maximal'), 121 | '-n', 122 | os.path.join(TEST_DATA, 'presenter-notes.rst'), 123 | tmpdir, 124 | ] 125 | 126 | main() 127 | 128 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 129 | # We have verified the contents in test_generator.py, let's 130 | # just check that it writes the right thing: 131 | self.assertEqual(outfile.read(), HTML_OUTPUTS['skip-presenter-notes']) 132 | 133 | out_files = os.listdir(tmpdir) 134 | self.assertEqual(set(out_files), {'index.html', 'js', 'css', 'images', 'fonts', 'directory'}) 135 | js_files = os.listdir(os.path.join(tmpdir, 'js')) 136 | self.assertEqual(set(js_files), 137 | {'impress.js', 'hovercraft.js', 'impressConsole.js', 'dummy.js'}) 138 | css_files = os.listdir(os.path.join(tmpdir, 'css')) 139 | self.assertEqual(set(css_files), {'print.css', 'style.css', 'impressConsole.css'}) 140 | image_files = os.listdir(os.path.join(tmpdir, 'images')) 141 | self.assertEqual(set(image_files), {'hovercraft_logo.png'}) 142 | font_files = os.listdir(os.path.join(tmpdir, 'fonts')) 143 | self.assertEqual(set(font_files), { 144 | 'texgyreschola-regular-webfont.ttf', 145 | 'texgyreschola-regular-webfont.eot', 146 | 'texgyreschola-regular-webfont.woff', 147 | 'texgyreschola-regular-webfont.svg', 148 | }) 149 | 150 | def test_default_template(self): 151 | with TemporaryDirectory() as tmpdir: 152 | # Adding a non-existant subdir, to test that it gets created. 153 | tmpdir = os.path.join(tmpdir, 'foo') 154 | 155 | sys.argv = [ 156 | 'bin/hovercraft', 157 | os.path.join(TEST_DATA, 'advanced.rst'), 158 | tmpdir, 159 | ] 160 | main() 161 | 162 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 163 | self.assertEqual(outfile.read(), HTML_OUTPUTS['default-template']) 164 | 165 | js_files = os.listdir(os.path.join(tmpdir, 'js')) 166 | self.assertEqual(set(js_files), {'impress.js', 'hovercraft.js', 167 | 'gotoSlide.js'}) 168 | css_files = os.listdir(os.path.join(tmpdir, 'css')) 169 | self.assertEqual(set(css_files), {'hovercraft.css', 170 | 'highlight.css'}) 171 | image_files = os.listdir(os.path.join(tmpdir, 'images')) 172 | self.assertEqual(set(image_files), {'hovercraft_logo.png'}) 173 | 174 | def test_auto_console(self): 175 | with TemporaryDirectory() as tmpdir: 176 | # Adding a non-existant subdir, to test that it gets created. 177 | tmpdir = os.path.join(tmpdir, 'foo') 178 | 179 | sys.argv = [ 180 | 'bin/hovercraft', 181 | '-a', 182 | os.path.join(TEST_DATA, 'simple.rst'), 183 | tmpdir, 184 | ] 185 | main() 186 | 187 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 188 | result = outfile.read() 189 | self.assertIn(b'auto-console="True"', result) 190 | 191 | def test_subdirectory_css(self): 192 | with TemporaryDirectory() as tmpdir: 193 | sys.argv = [ 194 | 'bin/hovercraft', 195 | os.path.join(TEST_DATA, 'subdir-css.rst'), 196 | tmpdir, 197 | ] 198 | 199 | main() 200 | 201 | out_files = os.listdir(tmpdir) 202 | self.assertEqual(set(out_files), {'index.html', 'js', 'css', 'images'}) 203 | css_files = os.listdir(os.path.join(tmpdir, 'css')) 204 | self.assertEqual(set(css_files), 205 | {'hovercraft.css', 'highlight.css', 'sub.css', 'sub2.css'}) 206 | image_files = os.listdir(os.path.join(tmpdir, 'images')) 207 | self.assertEqual(set(image_files), {'hovercraft_logo.png'}) 208 | 209 | def test_mathjax(self): 210 | with TemporaryDirectory() as tmpdir: 211 | sys.argv = [ 212 | 'bin/hovercraft', 213 | os.path.join(TEST_DATA, 'math.rst'), 214 | tmpdir, 215 | ] 216 | 217 | main() 218 | 219 | with open(os.path.join(tmpdir, 'index.html'), 'rb') as outfile: 220 | result = outfile.read() 221 | self.assertIn(b'