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'deffoo(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'titleHovercrafts 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'