Import patat_0.4.7.1.orig.tar.gz
authorFélix Sipma <felix+debian@gueux.org>
Mon, 23 Jan 2017 14:20:49 +0000 (14:20 +0000)
committerFélix Sipma <felix+debian@gueux.org>
Mon, 23 Jan 2017 14:20:49 +0000 (14:20 +0000)
[dgit import orig patat_0.4.7.1.orig.tar.gz]

52 files changed:
.gitignore [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
CHANGELOG.md [new file with mode: 0644]
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md [new file with mode: 0644]
Setup.hs [new file with mode: 0644]
extra/make-man.hs [new file with mode: 0644]
extra/screenshot.png [new file with mode: 0644]
patat.cabal [new file with mode: 0644]
src/Data/Aeson/Extended.hs [new file with mode: 0644]
src/Data/Aeson/TH/Extended.hs [new file with mode: 0644]
src/Data/Data/Extended.hs [new file with mode: 0644]
src/Main.hs [new file with mode: 0644]
src/Patat/AutoAdvance.hs [new file with mode: 0644]
src/Patat/Presentation.hs [new file with mode: 0644]
src/Patat/Presentation/Display.hs [new file with mode: 0644]
src/Patat/Presentation/Display/CodeBlock.hs [new file with mode: 0644]
src/Patat/Presentation/Display/Table.hs [new file with mode: 0644]
src/Patat/Presentation/Fragment.hs [new file with mode: 0644]
src/Patat/Presentation/Interactive.hs [new file with mode: 0644]
src/Patat/Presentation/Internal.hs [new file with mode: 0644]
src/Patat/Presentation/Read.hs [new file with mode: 0644]
src/Patat/PrettyPrint.hs [new file with mode: 0644]
src/Patat/Theme.hs [new file with mode: 0644]
src/Text/Pandoc/Extended.hs [new file with mode: 0644]
stack.yaml [new file with mode: 0644]
test.sh [new file with mode: 0644]
tests/01.md [new file with mode: 0644]
tests/01.md.dump [new file with mode: 0644]
tests/02.lhs [new file with mode: 0644]
tests/02.lhs.dump [new file with mode: 0644]
tests/03.md [new file with mode: 0644]
tests/03.md.dump [new file with mode: 0644]
tests/deflist.md [new file with mode: 0644]
tests/deflist.md.dump [new file with mode: 0644]
tests/fragments.md [new file with mode: 0644]
tests/fragments.md.dump [new file with mode: 0644]
tests/links.md [new file with mode: 0644]
tests/links.md.dump [new file with mode: 0644]
tests/lists.md [new file with mode: 0644]
tests/lists.md.dump [new file with mode: 0644]
tests/meta.md [new file with mode: 0644]
tests/meta.md.dump [new file with mode: 0644]
tests/syntax.md [new file with mode: 0644]
tests/syntax.md.dump [new file with mode: 0644]
tests/tables.md [new file with mode: 0644]
tests/tables.md.dump [new file with mode: 0644]
tests/themes.md [new file with mode: 0644]
tests/themes.md.dump [new file with mode: 0644]
tests/wrapping.md [new file with mode: 0644]
tests/wrapping.md.dump [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..da4d999
--- /dev/null
@@ -0,0 +1,7 @@
+*.o
+*.hi
+extra/make-man
+extra/patat.1
+.stack-work
+dist
+tags
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..7759079
--- /dev/null
@@ -0,0 +1,11 @@
+language: haskell
+ghc: '7.8'
+sudo: false
+cache:
+  directories:
+    - '$HOME/.cabal'
+    - '$HOME/.ghc'
+install:
+  - cabal install
+script:
+  - make test
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644 (file)
index 0000000..ea6b6b1
--- /dev/null
@@ -0,0 +1,61 @@
+# Changelog
+
+- 0.4.7.1 (2017-01-22)
+    * Bump `directory-1.3` dependency
+    * Bump `time-1.7` dependency
+
+- 0.4.7.0 (2017-01-20)
+    * Bump `aeson-1.1` dependency
+    * Parse YAML for settings using `yaml` instead of pandoc
+    * Clarify watch & autoAdvance combination in documentation.
+
+- 0.4.6.0 (2016-12-28)
+    * Redraw the screen on unknown commands to prevent accidental typing from
+      showing up.
+    * Make the cursor invisible during the presentation.
+    * Move the footer down one more line to gain some screen real estate.
+
+- 0.4.5.0 (2016-12-05)
+    * Render the date in a locale-independent manner (patch by Daniel
+      Shahaf).
+
+- 0.4.4.0 (2016-12-03)
+    * Force the use of UTF-8 when generating the man page.
+
+- 0.4.3.0 (2016-12-02)
+    * Use `SOURCE_DATE_EPOCH` if it is present instead of getting the date from
+      `git log`.
+
+- 0.4.2.0 (2016-12-01)
+    * Fix issues with man page generation on Travis.
+
+- 0.4.1.0 (2016-12-01)
+    * Fix compatibility with `pandoc-1.18` and `pandoc-1.19`.
+    * Add a man page.
+
+- 0.4.0.0 (2016-11-15)
+    * Add configurable auto advancing.
+    * Support fragmented slides.
+
+- 0.3.3.0 (2016-10-31)
+    * Add a `--version` flag.
+    * Add support for `pandoc-1.18` which includes a new `LineBlock` element.
+
+- 0.3.2.0 (2016-10-20)
+    * Keep running even if errors are encountered during reload.
+
+- 0.3.1.0 (2016-10-18)
+    * Fix compilation with `lts-6.22`.
+
+- 0.3.0.0 (2016-10-17)
+    * Add syntax highlighting support.
+    * Fixed slide clipping after reload.
+
+- 0.2.0.0 (2016-10-13)
+    * Add theming support.
+    * Fix links display.
+    * Add support for wrapping.
+    * Allow org mode as input format.
+
+- 0.1.0.0 (2016-10-02)
+    * Upload first version from hotel wifi in Kalaw.
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..1f53f40
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+             GNU GENERAL PUBLIC LICENSE
+                Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                     Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+             GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                     NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+              END OF TERMS AND CONDITIONS
+
+     How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..68c36b4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+# The minor version is passed to the build.  This is used to do some CPP to
+# solve incompatibilities.
+PANDOC_MINOR_VERSION=$(shell ghc-pkg latest pandoc | sed 's/.*-//' | cut -d. -f2)
+
+# We use `?=` to set SOURCE_DATE_EPOCH only if it is not present.  Unfortunately
+# we can't use `git --date=unix` since only very recent git versions support
+# that, so we need to make a round trip through `date`.
+SOURCE_DATE_EPOCH?=$(shell date '+%s' \
+                                          --date="$(shell git log -1 --format=%cd --date=rfc)")
+
+# Prettify the date.
+SOURCE_DATE=$(shell env LC_ALL=C date '+%B %d, %Y' -d "@${SOURCE_DATE_EPOCH}")
+
+extra/patat.1: README.md extra/make-man
+       SOURCE_DATE="$(SOURCE_DATE)" ./extra/make-man >$@
+
+extra/make-man: extra/make-man.hs
+       ghc -DPANDOC_MINOR_VERSION=${PANDOC_MINOR_VERSION} -Wall -o $@ $<
+
+man: extra/patat.1
+
+# Also check if we can generate the manual.
+test: man
+       bash test.sh
+
+clean:
+       rm -f extra/patat.1
+       rm -f extra/make-man
+
+.PHONY: man test clean
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..c4d52a2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,365 @@
+patat
+=====
+
+[![Build Status](https://img.shields.io/travis/jaspervdj/patat.svg)](https://travis-ci.org/jaspervdj/patat) [![Hackage](https://img.shields.io/hackage/v/patat.svg)](https://hackage.haskell.org/package/patat) [![GitHub tag](https://img.shields.io/github/tag/jaspervdj/patat.svg)]()
+
+`patat` (**P**resentations **A**top **T**he **A**NSI **T**erminal) is a small
+tool that allows you to show presentations using only an ANSI terminal.  It does
+not require `ncurses`.
+
+Features:
+
+- Leverages the great [Pandoc] library to support many input formats including
+  [Literate Haskell].
+- Supports [smart slide splitting](#input-format).
+- Slides can be split up into [multiple fragments](#fragmented-slides)
+- There is a [live reload](#running) mode.
+- [Theming](#theming) support.
+- [Auto advancing](#auto-advancing) with configurable delay.
+- Optionally [re-wrapping](#line-wrapping) text to terminal width with proper
+  indentation.
+- Syntax highlighting for nearly one hundred languages generated from [Kate]
+  syntax files.
+- Written in [Haskell].
+
+![screenshot](extra/screenshot.png?raw=true)
+
+[Kate]: https://kate-editor.org/
+[Haskell]: http://haskell.org/
+[Pandoc]: http://pandoc.org/
+
+Table of Contents
+-----------------
+
+-   [Table of Contents](#table-of-contents)
+-   [Installation](#installation)
+    -   [Pre-built-packages](#pre-built-packages)
+    -   [From source](#from-source)
+-   [Running](#running)
+-   [Options](#options)
+-   [Controls](#controls)
+-   [Input format](#input-format)
+-   [Configuration](#configuration)
+    -   [Line wrapping](#line-wrapping)
+    -   [Auto advancing](#auto-advancing)
+    -   [Fragmented slides](#fragmented-slides)
+    -   [Theming](#theming)
+    -   [Syntax Highlighting](#syntax-highlighting)
+-   [Trivia](#trivia)
+
+Installation
+------------
+
+### Pre-built-packages
+
+There is a pre-built package available for Debian:
+
+- <https://packages.debian.org/unstable/patat>
+
+### From source
+
+Installation from source is very easy.  You can build from source using `stack
+install` or `cabal install`.  `patat` is also available from [Hackage].
+
+[Hackage]: https://hackage.haskell.org/package/patat
+
+For people unfamiliar with the Haskell ecosystem, this means you can do either
+of the following:
+
+#### Using stack
+
+1. Install [stack] for your platform.
+2. Clone this repository.
+3. Run `stack setup` (if you're running stack for the first time) and
+   `stack install`.
+4. Make sure `$HOME/.local/bin` is in your `$PATH`.
+
+[stack]: https://docs.haskellstack.org/en/stable/README/
+
+#### Using cabal
+
+1. Install [cabal] for your platform.
+2. Run `cabal install patat`.
+3. Make sure `$HOME/.cabal/bin` is in your `$PATH`.
+
+[cabal]: https://www.haskell.org/cabal/
+
+Running
+-------
+
+`patat [*options*] file`
+
+Options
+-------
+
+`-w`, `--watch`
+
+:   If you provide the `--watch` flag, `patat` will watch the presentation file
+    for changes and reload automatically.  This is very useful when you are
+    writing the presentation.
+
+`-f`, `--force`
+
+:   Run the presentation even if the terminal claims it does not support ANSI
+    features.
+
+`-d`, `--dump`
+
+:   Just dump all the slides to stdout.  This is useful for debugging.
+
+`--version`
+
+:   Display version information.
+
+Controls
+--------
+
+- **Next slide**: `space`, `enter`, `l`, `→`
+- **Previous slide**: `backspace`, `h`, `←`
+- **Go forward 10 slides**: `j`, `↓`
+- **Go backward 10 slides**: `k`, `↑`
+- **First slide**: `0`
+- **Last slide**: `G`
+- **Reload file**: `r`
+- **Quit**: `q`
+
+The `r` key is very useful since it allows you to preview your slides while you
+are writing them.  You can also use this to fix artifacts when the terminal is
+resized.
+
+Input format
+------------
+
+The input format can be anything that Pandoc supports.  Plain markdown is
+usually the most simple solution:
+
+    ---
+    title: This is my presentation
+    author: Jane Doe
+    ...
+
+    # This is a slide
+
+    Slide contents.  Yay.
+
+    ---
+
+    # Important title
+
+    Things I like:
+
+    - Markdown
+    - Haskell
+    - Pandoc
+
+Horizontal rulers (`---`) are used to split slides.
+
+However, if you prefer not use these since they are a bit intrusive in the
+markdown, you can also start every slide with an `h1` header.  In that case, the
+file should not contain a single horizontal ruler.
+
+This means the following document is equivalent:
+
+    ---
+    title: This is my presentation
+    author: Jane Doe
+    ...
+
+    # This is a slide
+
+    Slide contents.  Yay.
+
+    # Important title
+
+    Things I like:
+
+    - Markdown
+    - Haskell
+    - Pandoc
+
+Configuration
+-------------
+
+`patat` is fairly configurable.  The configuration is done using [YAML].  There
+are two places where you can put your configuration:
+
+1. In the presentation file itself, using the [Pandoc metadata header].
+2. In `$HOME/.patat.yaml`
+
+[YAML]: http://yaml.org/
+[Pandoc metadata header]: http://pandoc.org/MANUAL.html#extension-yaml_metadata_block
+
+For example, we set an option `key` to `val` by using the following file:
+
+    ---
+    title: Presentation with options
+    author: John Doe
+    patat:
+        key: val
+    ...
+
+    Hello world.
+
+Or we can use a normal presentation and have the following `$HOME/.patat.yaml`:
+
+    key: val
+
+### Line wrapping
+
+Line wrapping can be enabled by setting `wrap: true` in the configuration.  This
+will re-wrap all lines to fit the terminal width better.
+
+### Auto advancing
+
+By setting `autoAdvanceDelay` to a number of seconds, `patat` will automatically
+advance to the next slide.
+
+    ---
+    title: Auto-advance, yes please
+    author: John Doe
+    patat:
+        autoAdvanceDelay: 2
+    ...
+
+    Hello World!
+
+    ---
+
+    This slide will be shown two seconds after the presentation starts.
+
+Note that changes to `autoAdvanceDelay` are not picked up automatically if you
+are running `patat --watch`.  This requires restarting `patat`.
+
+### Fragmented slides
+
+By default, slides are always displayed "all at once".  If you want to display
+them fragment by fragment, there are two ways to do that.  The most common
+case is that lists should be displayed incrementally.
+
+This can be configured by settings `incrementalLists` to `true` in the metadata
+block:
+
+    ---
+    title: Presentation with incremental lists
+    author: John Doe
+    patat:
+        incrementalLists: true
+    ...
+
+    - This list
+    - is displayed
+    - item by item
+
+Setting `incrementalLists` works on _all_ lists in the presentation.  To flip
+the setting for a specific list, wrap it in a block quote.  This will make the
+list incremental if `incrementalLists` is not set, and it will display the list
+all at once if `incrementalLists` is set to `true`.
+
+This example contains a sublist which is also displayed incrementally, and then
+a sublist which is displayed all at once (by merit of the block quote).
+
+    ---
+    title: Presentation with incremental lists
+    author: John Doe
+    patat:
+        incrementalLists: true
+    ...
+
+    - This list
+    - is displayed
+
+        * item
+        * by item
+
+    - Or sometimes
+
+        > * all at
+        > * once
+
+Another way to break up slides is to use a pagraph only containing three dots
+separated by spaces.  For example, this slide has two pauses:
+
+    Legen
+
+    . . .
+
+    wait for it
+
+    . . .
+
+    Dary!
+
+### Theming
+
+Colors and other properties can also be changed using this configuration.  For
+example, we can have:
+
+    ---
+    author: 'Jasper Van der Jeugt'
+    title: 'This is a test'
+    patat:
+        wrap: true
+        theme:
+            emph: [vividBlue, onVividBlack, bold]
+            imageTarget: [onDullWhite, vividRed]
+    ...
+
+    # This is a presentation
+
+    This is _emph_ text.
+
+    ![Hello](foo.png)
+
+The properties that can be given a list of styles are:
+
+`blockQuote`, `borders`, `bulletList`, `codeBlock`, `code`, `definitionList`,
+`definitionTerm`, `emph`, `header`, `imageTarget`, `imageText`, `linkTarget`,
+`linkText`, `math`, `orderedList`, `quoted`, `strikeout`, `strong`,
+`tableHeader`, `tableSeparator`
+
+The accepted styles are:
+
+`bold`, `dullBlack`, `dullBlue`, `dullCyan`, `dullGreen`, `dullMagenta`,
+`dullRed`, `dullWhite`, `dullYellow`, `onDullBlack`, `onDullBlue`, `onDullCyan`,
+`onDullGreen`, `onDullMagenta`, `onDullRed`, `onDullWhite`, `onDullYellow`,
+`onVividBlack`, `onVividBlue`, `onVividCyan`, `onVividGreen`, `onVividMagenta`,
+`onVividRed`, `onVividWhite`, `onVividYellow`, `underline`, `vividBlack`,
+`vividBlue`, `vividCyan`, `vividGreen`, `vividMagenta`, `vividRed`,
+`vividWhite`, `vividYellow`
+
+### Syntax Highlighting
+
+As part of theming, syntax highlighting is also configurable.  This can be
+configured like this:
+
+    ---
+    patat:
+      theme:
+        syntaxHighlighting:
+          decVal: [bold, onDullRed]
+    ...
+
+    ...
+
+`decVal` refers to "decimal values".  This is known as a "token type".  For a
+full list of token types, see [this list] -- the names are derived from there in
+an obvious way.
+
+[this list]: https://hackage.haskell.org/package/highlighting-kate-0.6.3/docs/Text-Highlighting-Kate-Types.html#t:TokenType
+
+Trivia
+------
+
+_"Patat"_ is the Flemish word for a simple potato.  Dutch people also use it to
+refer to French Fries but I don't really do that -- in Belgium we just call
+fries _"Frieten"_.
+
+The idea of `patat` is largely based upon [MDP] which is in turn based upon
+[VTMC].  I wanted to write a clone using Pandoc because I ran into a markdown
+parsing bug in MDP which I could not work around.  A second reason to do a
+Pandoc-based tool was that I would be able to use [Literate Haskell] as well.
+Lastly, I also prefer not to install Node.js on my machine if I can avoid it.
+
+[MDP]: https://github.com/visit1985/mdp
+[VTMC]: https://github.com/jclulow/vtmc
+[Literate Haskell]: https://wiki.haskell.org/Literate_programming
diff --git a/Setup.hs b/Setup.hs
new file mode 100644 (file)
index 0000000..9a994af
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/extra/make-man.hs b/extra/make-man.hs
new file mode 100644 (file)
index 0000000..78c01f8
--- /dev/null
@@ -0,0 +1,110 @@
+-- | This script generates a man page for patat.
+{-# LANGUAGE CPP #-}
+import           Control.Applicative ((<$>))
+import           Control.Monad       (guard)
+import           Data.Char           (isSpace, toLower)
+import           Data.List           (isPrefixOf)
+import           Data.Maybe          (isJust)
+import qualified GHC.IO.Encoding     as Encoding
+import           System.Environment  (getEnv)
+import qualified System.IO           as IO
+import qualified Text.Pandoc         as Pandoc
+import qualified Text.Pandoc.Walk    as Pandoc
+import           Prelude
+
+getVersion :: IO String
+getVersion =
+    dropWhile isSpace . drop 1 . dropWhile (/= ':') . head .
+    filter (\l -> "version:" `isPrefixOf` map toLower l) .
+    map (dropWhile isSpace) . lines <$> readFile "patat.cabal"
+
+removeLinks :: Pandoc.Pandoc -> Pandoc.Pandoc
+removeLinks = Pandoc.walk $ \inline -> case inline of
+    Pandoc.Link _ inlines _ -> Pandoc.Emph inlines
+    _                       -> inline
+
+type Sections = [(Int, String, [Pandoc.Block])]
+
+toSections :: Int -> [Pandoc.Block] -> Sections
+toSections level = go
+  where
+    go []       = []
+    go (h : xs) = case toSectionHeader h of
+        Nothing         -> go xs
+        Just (l, title) ->
+            let (section, cont) = break (isJust . toSectionHeader) xs in
+            (l, title, section) : go cont
+
+    toSectionHeader :: Pandoc.Block -> Maybe (Int, String)
+    toSectionHeader (Pandoc.Header l _ inlines) = do
+        guard (l <= level)
+        let doc = Pandoc.Pandoc Pandoc.nullMeta [Pandoc.Plain inlines]
+        return (l, Pandoc.writeMarkdown Pandoc.def doc)
+    toSectionHeader _ = Nothing
+
+fromSections :: Sections -> [Pandoc.Block]
+fromSections = concatMap $ \(level, title, blocks) ->
+    Pandoc.Header level ("", [], []) [Pandoc.Str title] : blocks
+
+reorganizeSections :: Pandoc.Pandoc -> Pandoc.Pandoc
+reorganizeSections (Pandoc.Pandoc meta0 blocks0) =
+    let sections0 = toSections 2 blocks0 in
+    Pandoc.Pandoc meta0 $ fromSections $
+        [ (1, "NAME", nameSection)
+        ] ++
+        [ (1, "SYNOPSIS", s)
+        | (_, _, s) <- lookupSection "Running" sections0
+        ] ++
+        [ (1, "DESCRIPTION", [])
+        ] ++
+            [ (2, n, s)
+            | (_, n, s) <- lookupSection "Controls" sections0
+            ] ++
+            [ (2, n, s)
+            | (_, n, s) <- lookupSection "Input format" sections0
+            ] ++
+            [ (2, n, s)
+            | (_, n, s) <- lookupSection "Configuration" sections0
+            ] ++
+        [ (1, "OPTIONS", s)
+        | (_, _, s) <- lookupSection "Options" sections0
+        ] ++
+        [ (1, "SEE ALSO", seeAlsoSection)
+        ]
+  where
+    nameSection    = mkPara "patat - Presentations Atop The ANSI Terminal"
+    seeAlsoSection = mkPara "pandoc(1)"
+    mkPara str     = [Pandoc.Para [Pandoc.Str str]]
+
+    lookupSection name sections =
+        [section | section@(_, n, _) <- sections, name == n]
+
+main :: IO ()
+main = do
+    Encoding.setLocaleEncoding Encoding.utf8
+    Right pandoc0  <- Pandoc.readMarkdown Pandoc.def <$> readFile "README.md"
+    Right template <- Pandoc.getDefaultTemplate Nothing "man"
+
+    version <- getVersion
+    date    <- getEnv "SOURCE_DATE"
+
+    let writerOptions = Pandoc.def {
+#if PANDOC_MINOR_VERSION >= 19
+              Pandoc.writerTemplate   = Just template
+#else
+              Pandoc.writerStandalone = True
+            , Pandoc.writerTemplate   = template
+#endif
+            , Pandoc.writerVariables  =
+                [ ("author",  "Jasper Van der Jeugt")
+                , ("title",   "patat manual")
+                , ("date",    date)
+                , ("footer",  "patat v" ++ version)
+                , ("section", "1")
+                ]
+            }
+
+    let pandoc1 = reorganizeSections $ removeLinks pandoc0
+
+    putStr $ Pandoc.writeMan writerOptions pandoc1
+    IO.hPutStrLn IO.stderr "Wrote man page."
diff --git a/extra/screenshot.png b/extra/screenshot.png
new file mode 100644 (file)
index 0000000..e20d771
Binary files /dev/null and b/extra/screenshot.png differ
diff --git a/patat.cabal b/patat.cabal
new file mode 100644 (file)
index 0000000..4e23248
--- /dev/null
@@ -0,0 +1,60 @@
+Name:                patat
+Version:             0.4.7.1
+Synopsis:            Terminal-based presentations using Pandoc
+Description:         Terminal-based presentations using Pandoc
+License:             GPL-2
+License-file:        LICENSE
+Author:              Jasper Van der Jeugt <m@jaspervdj.be>
+Maintainer:          Jasper Van der Jeugt <m@jaspervdj.be>
+Homepage:            http://github.com/jaspervdj/patat
+Copyright:           2016 Jasper Van der Jeugt
+Category:            Text
+Build-type:          Simple
+Extra-source-files:  CHANGELOG.md
+Cabal-version:       >=1.10
+
+Source-repository head
+  Type:     git
+  Location: git://github.com/jaspervdj/patat.git
+
+Executable patat
+  Main-is:           Main.hs
+  Ghc-options:       -Wall -threaded -rtsopts "-with-rtsopts=-N"
+  Hs-source-dirs:    src
+  Default-language:  Haskell2010
+
+  Build-depends:
+    aeson                >= 0.9  && < 1.2,
+    ansi-terminal        >= 0.6  && < 0.7,
+    ansi-wl-pprint       >= 0.6  && < 0.7,
+    base                 >= 4.6  && < 4.10,
+    bytestring           >= 0.10 && < 0.11,
+    containers           >= 0.5  && < 0.6,
+    directory            >= 1.2  && < 1.4,
+    filepath             >= 1.4  && < 1.5,
+    highlighting-kate    >= 0.6  && < 0.7,
+    mtl                  >= 2.2  && < 2.3,
+    optparse-applicative >= 0.12 && < 0.14,
+    pandoc               >= 1.16 && < 1.20,
+    terminal-size        >= 0.3  && < 0.4,
+    text                 >= 1.2  && < 1.3,
+    time                 >= 1.4  && < 1.8,
+    unordered-containers >= 0.2  && < 0.3,
+    yaml                 >= 0.7  && < 0.9
+
+  Other-modules:
+    Data.Aeson.Extended
+    Data.Aeson.TH.Extended
+    Data.Data.Extended
+    Patat.AutoAdvance
+    Patat.Presentation
+    Patat.Presentation.Display
+    Patat.Presentation.Display.CodeBlock
+    Patat.Presentation.Display.Table
+    Patat.Presentation.Fragment
+    Patat.Presentation.Interactive
+    Patat.Presentation.Internal
+    Patat.Presentation.Read
+    Patat.PrettyPrint
+    Patat.Theme
+    Text.Pandoc.Extended
diff --git a/src/Data/Aeson/Extended.hs b/src/Data/Aeson/Extended.hs
new file mode 100644 (file)
index 0000000..9b95cec
--- /dev/null
@@ -0,0 +1,22 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+module Data.Aeson.Extended
+    ( module Data.Aeson
+
+    , FlexibleNum (..)
+    ) where
+
+import           Control.Applicative ((<$>))
+import           Data.Aeson
+import qualified Data.Text           as T
+import           Text.Read           (readMaybe)
+import           Prelude
+
+-- | This can be parsed from a JSON string in addition to a JSON number.
+newtype FlexibleNum a = FlexibleNum {unFlexibleNum :: a}
+    deriving (Show, ToJSON)
+
+instance (FromJSON a, Read a) => FromJSON (FlexibleNum a) where
+    parseJSON (String str) = case readMaybe (T.unpack str) of
+        Nothing -> fail $ "Could not parse " ++ T.unpack str ++ " as a number"
+        Just x  -> return (FlexibleNum x)
+    parseJSON val = FlexibleNum <$> parseJSON val
diff --git a/src/Data/Aeson/TH/Extended.hs b/src/Data/Aeson/TH/Extended.hs
new file mode 100644 (file)
index 0000000..0fa5487
--- /dev/null
@@ -0,0 +1,21 @@
+--------------------------------------------------------------------------------
+module Data.Aeson.TH.Extended
+    ( module Data.Aeson.TH
+    , dropPrefixOptions
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Data.Aeson.TH
+import           Data.Char     (isUpper, toLower)
+
+
+--------------------------------------------------------------------------------
+dropPrefixOptions :: Options
+dropPrefixOptions = defaultOptions
+    { fieldLabelModifier = dropPrefix
+    }
+  where
+    dropPrefix str = case break isUpper str of
+        (_, (y : ys)) -> toLower y : ys
+        _             -> str
diff --git a/src/Data/Data/Extended.hs b/src/Data/Data/Extended.hs
new file mode 100644 (file)
index 0000000..636591e
--- /dev/null
@@ -0,0 +1,23 @@
+module Data.Data.Extended
+    ( module Data.Data
+
+    , grecQ
+    , grecT
+    ) where
+
+import           Data.Data
+
+-- | Recursively find all values of a certain type.
+grecQ :: (Data a, Data b) => a -> [b]
+grecQ = concat . gmapQ (\x -> maybe id (:) (cast x) $ grecQ x)
+
+-- | Recursively apply an update to a certain type.
+grecT :: (Data a, Data b) => (a -> a) -> b -> b
+grecT f x = gmapT (grecT f) (castMap f x)
+
+castMap :: (Typeable a, Typeable b) => (a -> a) -> b -> b
+castMap f x = case cast x of
+    Nothing -> x
+    Just y  -> case cast (f y) of
+        Nothing -> x
+        Just z  -> z
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644 (file)
index 0000000..0fccfde
--- /dev/null
@@ -0,0 +1,181 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+module Main where
+
+
+--------------------------------------------------------------------------------
+import           Control.Applicative          ((<$>), (<*>))
+import           Control.Concurrent           (forkIO, threadDelay)
+import qualified Control.Concurrent.Chan      as Chan
+import           Control.Exception            (finally)
+import           Control.Monad                (forever, unless, when)
+import qualified Data.Aeson.Extended          as A
+import           Data.Monoid                  (mempty, (<>))
+import           Data.Time                    (UTCTime)
+import           Data.Version                 (showVersion)
+import qualified Options.Applicative          as OA
+import           Patat.AutoAdvance
+import           Patat.Presentation
+import qualified Paths_patat
+import           Prelude
+import qualified System.Console.ANSI          as Ansi
+import           System.Directory             (doesFileExist,
+                                               getModificationTime)
+import           System.Exit                  (exitFailure, exitSuccess)
+import qualified System.IO                    as IO
+import qualified Text.PrettyPrint.ANSI.Leijen as PP
+
+
+--------------------------------------------------------------------------------
+data Options = Options
+    { oFilePath :: !(Maybe FilePath)
+    , oForce    :: !Bool
+    , oDump     :: !Bool
+    , oWatch    :: !Bool
+    , oVersion  :: !Bool
+    } deriving (Show)
+
+
+--------------------------------------------------------------------------------
+parseOptions :: OA.Parser Options
+parseOptions = Options
+    <$> (OA.optional $ OA.strArgument $
+            OA.metavar "FILENAME" <>
+            OA.help    "Input file")
+    <*> (OA.switch $
+            OA.long    "force" <>
+            OA.short   'f' <>
+            OA.help    "Force ANSI terminal" <>
+            OA.hidden)
+    <*> (OA.switch $
+            OA.long    "dump" <>
+            OA.short   'd' <>
+            OA.help    "Just dump all slides and exit" <>
+            OA.hidden)
+    <*> (OA.switch $
+            OA.long    "watch" <>
+            OA.short   'w' <>
+            OA.help    "Watch file for changes")
+    <*> (OA.switch $
+            OA.long    "version" <>
+            OA.help    "Display version info and exit" <>
+            OA.hidden)
+
+
+--------------------------------------------------------------------------------
+parserInfo :: OA.ParserInfo Options
+parserInfo = OA.info (OA.helper <*> parseOptions) $
+    OA.fullDesc <>
+    OA.header ("patat v" <> showVersion Paths_patat.version) <>
+    OA.progDescDoc (Just desc)
+  where
+    desc = PP.vcat
+        [ "Terminal-based presentations using Pandoc"
+        , ""
+        , "Controls:"
+        , "- Next slide:             space, enter, l, right"
+        , "- Previous slide:         backspace, h, left"
+        , "- Go forward 10 slides:   j, down"
+        , "- Go backward 10 slides:  k, up"
+        , "- First slide:            0"
+        , "- Last slide:             G"
+        , "- Reload file:            r"
+        , "- Quit:                   q"
+        ]
+
+
+--------------------------------------------------------------------------------
+parserPrefs :: OA.ParserPrefs
+parserPrefs = OA.prefs OA.showHelpOnError
+
+
+--------------------------------------------------------------------------------
+errorAndExit :: [String] -> IO a
+errorAndExit msg = do
+    mapM_ (IO.hPutStrLn IO.stderr) msg
+    exitFailure
+
+
+--------------------------------------------------------------------------------
+assertAnsiFeatures :: IO ()
+assertAnsiFeatures = do
+    supports <- Ansi.hSupportsANSI IO.stdout
+    unless supports $ errorAndExit
+        [ "It looks like your terminal does not support ANSI codes."
+        , "If you still want to run the presentation, use `--force`."
+        ]
+
+
+--------------------------------------------------------------------------------
+main :: IO ()
+main = do
+    options <- OA.customExecParser parserPrefs parserInfo
+
+    when (oVersion options) $ do
+        putStrLn (showVersion Paths_patat.version)
+        exitSuccess
+
+    filePath <- case oFilePath options of
+        Just fp -> return fp
+        Nothing -> OA.handleParseResult $ OA.Failure $
+            OA.parserFailure parserPrefs parserInfo OA.ShowHelpText mempty
+
+    errOrPres <- readPresentation filePath
+    pres      <- either (errorAndExit . return) return errOrPres
+
+    unless (oForce options) assertAnsiFeatures
+
+    if oDump options
+        then dumpPresentation pres
+        else interactiveLoop options pres
+  where
+    interactiveLoop :: Options -> Presentation -> IO ()
+    interactiveLoop options pres0 = (`finally` Ansi.showCursor) $ do
+        IO.hSetBuffering IO.stdin IO.NoBuffering
+        Ansi.hideCursor
+
+        -- Spawn the initial channel that gives us commands based on user input.
+        commandChan0 <- Chan.newChan
+        _            <- forkIO $ forever $
+            readPresentationCommand >>= Chan.writeChan commandChan0
+
+        -- If an auto delay is set, use 'autoAdvance' to create a new one.
+        commandChan <- case psAutoAdvanceDelay (pSettings pres0) of
+            Nothing                    -> return commandChan0
+            Just (A.FlexibleNum delay) -> autoAdvance delay commandChan0
+
+        -- Spawn a thread that adds 'Reload' commands based on the file time.
+        mtime0 <- getModificationTime (pFilePath pres0)
+        when (oWatch options) $ do
+            _ <- forkIO $ watcher commandChan (pFilePath pres0) mtime0
+            return ()
+
+        let loop :: Presentation -> Maybe String -> IO ()
+            loop pres mbError = do
+                case mbError of
+                    Nothing  -> displayPresentation pres
+                    Just err -> displayPresentationError pres err
+
+                c      <- Chan.readChan commandChan
+                update <- updatePresentation c pres
+                case update of
+                    ExitedPresentation        -> return ()
+                    UpdatedPresentation pres' -> loop pres' Nothing
+                    ErroredPresentation err   -> loop pres (Just err)
+
+        loop pres0 Nothing
+
+
+--------------------------------------------------------------------------------
+watcher :: Chan.Chan PresentationCommand -> FilePath -> UTCTime -> IO a
+watcher chan filePath mtime0 = do
+    -- The extra exists check helps because some editors temporarily make the
+    -- file dissapear while writing.
+    exists <- doesFileExist filePath
+    mtime1 <- if exists then getModificationTime filePath else return mtime0
+
+    when (mtime1 > mtime0) $ Chan.writeChan chan Reload
+    threadDelay (200 * 1000)
+    watcher chan filePath mtime1
diff --git a/src/Patat/AutoAdvance.hs b/src/Patat/AutoAdvance.hs
new file mode 100644 (file)
index 0000000..236e0cb
--- /dev/null
@@ -0,0 +1,52 @@
+--------------------------------------------------------------------------------
+module Patat.AutoAdvance
+    ( autoAdvance
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Control.Concurrent      (forkIO, threadDelay)
+import qualified Control.Concurrent.Chan as Chan
+import           Control.Monad           (forever)
+import qualified Data.IORef              as IORef
+import           Data.Time               (diffUTCTime, getCurrentTime)
+import           Patat.Presentation      (PresentationCommand (..))
+
+
+--------------------------------------------------------------------------------
+-- | This function takes an existing channel for presentation commands
+-- (presumably coming from human input) and creates a new one that /also/ sends
+-- a 'Forward' command if nothing happens for N seconds.
+autoAdvance
+    :: Int
+    -> Chan.Chan PresentationCommand
+    -> IO (Chan.Chan PresentationCommand)
+autoAdvance delaySeconds existingChan = do
+    let delay = delaySeconds * 1000  -- We are working with ms in this function
+
+    newChan         <- Chan.newChan
+    latestCommandAt <- IORef.newIORef =<< getCurrentTime
+
+    -- This is a thread that copies 'existingChan' to 'newChan', and writes
+    -- whenever the latest command was to 'latestCommandAt'.
+    _ <- forkIO $ forever $ do
+        cmd <- Chan.readChan existingChan
+        getCurrentTime >>= IORef.writeIORef latestCommandAt
+        Chan.writeChan newChan cmd
+
+    -- This is a thread that waits around 'delay' seconds and then checks if
+    -- there's been a more recent command.  If not, we write a 'Forward'.
+    _ <- forkIO $ forever $ do
+        current <- getCurrentTime
+        latest  <- IORef.readIORef latestCommandAt
+        let elapsed = floor $ 1000 * (current `diffUTCTime` latest) :: Int
+        if elapsed >= delay
+            then do
+                Chan.writeChan newChan Forward
+                IORef.writeIORef latestCommandAt current
+                threadDelay (delay * 1000)
+            else do
+                let wait = delay - elapsed
+                threadDelay (wait * 1000)
+
+    return newChan
diff --git a/src/Patat/Presentation.hs b/src/Patat/Presentation.hs
new file mode 100644 (file)
index 0000000..8da5a30
--- /dev/null
@@ -0,0 +1,20 @@
+module Patat.Presentation
+    ( PresentationSettings (..)
+    , defaultPresentationSettings
+
+    , Presentation (..)
+    , readPresentation
+    , displayPresentation
+    , displayPresentationError
+    , dumpPresentation
+
+    , PresentationCommand (..)
+    , readPresentationCommand
+    , UpdatedPresentation (..)
+    , updatePresentation
+    ) where
+
+import           Patat.Presentation.Display
+import           Patat.Presentation.Interactive
+import           Patat.Presentation.Internal
+import           Patat.Presentation.Read
diff --git a/src/Patat/Presentation/Display.hs b/src/Patat/Presentation/Display.hs
new file mode 100644 (file)
index 0000000..cb562d7
--- /dev/null
@@ -0,0 +1,313 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE CPP                        #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+module Patat.Presentation.Display
+    ( displayPresentation
+    , displayPresentationError
+    , dumpPresentation
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Control.Applicative                  ((<$>))
+import           Control.Monad                        (mplus, unless)
+import qualified Data.Aeson.Extended                  as A
+import           Data.Data.Extended                   (grecQ)
+import           Data.List                            (intersperse)
+import           Data.Maybe                           (fromMaybe)
+import           Data.Monoid                          (mconcat, mempty, (<>))
+import qualified Data.Text                            as T
+import           Patat.Presentation.Display.CodeBlock
+import           Patat.Presentation.Display.Table
+import           Patat.Presentation.Internal
+import           Patat.PrettyPrint                    ((<$$>), (<+>))
+import qualified Patat.PrettyPrint                    as PP
+import           Patat.Theme                          (Theme (..))
+import qualified Patat.Theme                          as Theme
+import           Prelude
+import qualified System.Console.ANSI                  as Ansi
+import qualified System.Console.Terminal.Size         as Terminal
+import qualified System.IO                            as IO
+import qualified Text.Pandoc.Extended                 as Pandoc
+
+
+--------------------------------------------------------------------------------
+-- | Display something within the presentation borders that draw the title and
+-- the active slide number and so on.
+displayWithBorders :: Presentation -> (Theme -> PP.Doc) -> IO ()
+displayWithBorders Presentation {..} f = do
+    Ansi.clearScreen
+    Ansi.setCursorPosition 0 0
+
+    -- Get terminal width/title
+    mbWindow <- Terminal.size
+    let columns = fromMaybe 72 $
+            (A.unFlexibleNum <$> psColumns pSettings) `mplus`
+            (Terminal.width  <$> mbWindow)
+        rows    = fromMaybe 24 $
+            (A.unFlexibleNum <$> psRows pSettings) `mplus`
+            (Terminal.height <$> mbWindow)
+
+    let settings    = pSettings {psColumns = Just $ A.FlexibleNum columns}
+        theme       = fromMaybe Theme.defaultTheme (psTheme settings)
+        title       = PP.toString (prettyInlines theme pTitle)
+        titleWidth  = length title
+        titleOffset = (columns - titleWidth) `div` 2
+        borders     = themed (themeBorders theme)
+
+    unless (null title) $ do
+        Ansi.setCursorColumn titleOffset
+        PP.putDoc $ borders $ PP.string title
+        putStrLn ""
+        putStrLn ""
+
+    PP.putDoc $ withWrapSettings settings $ f theme
+    putStrLn ""
+
+    let (sidx, _)   = pActiveFragment
+        active      = show (sidx + 1) ++ " / " ++ show (length pSlides)
+        activeWidth = length active
+
+    Ansi.setCursorPosition (rows - 1) 0
+    PP.putDoc $ " " <> borders (prettyInlines theme pAuthor)
+    Ansi.setCursorColumn (columns - activeWidth - 1)
+    PP.putDoc $ borders $ PP.string active
+    IO.hFlush IO.stdout
+
+
+--------------------------------------------------------------------------------
+displayPresentation :: Presentation -> IO ()
+displayPresentation pres@Presentation {..} = displayWithBorders pres $ \theme ->
+    let fragment = fromMaybe mempty (getActiveFragment pres) in
+    prettyFragment theme fragment
+
+
+--------------------------------------------------------------------------------
+-- | Displays an error in the place of the presentation.  This is useful if we
+-- want to display an error but keep the presentation running.
+displayPresentationError :: Presentation -> String -> IO ()
+displayPresentationError pres err = displayWithBorders pres $ \Theme {..} ->
+    themed themeStrong "Error occurred in the presentation:" <$$>
+    "" <$$>
+    (PP.string err)
+
+
+--------------------------------------------------------------------------------
+dumpPresentation :: Presentation -> IO ()
+dumpPresentation pres =
+    let theme = fromMaybe Theme.defaultTheme (psTheme $ pSettings pres) in
+    PP.putDoc $ withWrapSettings (pSettings pres) $
+        PP.vcat $ intersperse "----------" $ do
+            Slide fragments <- pSlides pres
+            return $ PP.vcat $ intersperse "~~~~~~~~~~" $ do
+                fragment <- fragments
+                return $ prettyFragment theme fragment
+
+
+--------------------------------------------------------------------------------
+withWrapSettings :: PresentationSettings -> PP.Doc -> PP.Doc
+withWrapSettings ps = case (psWrap ps, psColumns ps) of
+    (Just True,  Just (A.FlexibleNum col)) -> PP.wrapAt (Just col)
+    _                                      -> id
+
+
+--------------------------------------------------------------------------------
+prettyFragment :: Theme -> Fragment -> PP.Doc
+prettyFragment theme fragment@(Fragment blocks) =
+    prettyBlocks theme blocks <>
+    case prettyReferences theme fragment of
+        []   -> mempty
+        refs -> PP.hardline <> PP.vcat refs
+
+
+--------------------------------------------------------------------------------
+prettyBlock :: Theme -> Pandoc.Block -> PP.Doc
+
+prettyBlock theme (Pandoc.Plain inlines) = prettyInlines theme inlines
+
+prettyBlock theme (Pandoc.Para inlines) =
+    prettyInlines theme inlines <> PP.hardline
+
+prettyBlock theme@Theme {..} (Pandoc.Header i _ inlines) =
+    themed themeHeader (PP.string (replicate i '#') <+> prettyInlines theme inlines) <>
+    PP.hardline
+
+prettyBlock theme (Pandoc.CodeBlock (_, classes, _) txt) =
+    prettyCodeBlock theme classes txt
+
+prettyBlock theme (Pandoc.BulletList bss) = PP.vcat
+    [ PP.indent
+        (PP.NotTrimmable $ themed (themeBulletList theme) prefix)
+        (PP.Trimmable "    ")
+        (prettyBlocks theme' bs)
+    | bs <- bss
+    ] <> PP.hardline
+  where
+    prefix = "  " <> PP.string [marker] <> " "
+    marker = case T.unpack <$> themeBulletListMarkers theme of
+        Just (x : _) -> x
+        _            -> '-'
+
+    -- Cycle the markers.
+    theme' = theme
+        { themeBulletListMarkers =
+            (\ls -> T.drop 1 ls <> T.take 1 ls) <$> themeBulletListMarkers theme
+        }
+
+prettyBlock theme@Theme {..} (Pandoc.OrderedList _ bss) = PP.vcat
+    [ PP.indent
+        (PP.NotTrimmable $ themed themeOrderedList $ PP.string prefix)
+        (PP.Trimmable "    ")
+        (prettyBlocks theme bs)
+    | (prefix, bs) <- zip padded bss
+    ] <> PP.hardline
+  where
+    padded  = [n ++ replicate (4 - length n) ' ' | n <- numbers]
+    numbers =
+        [ show i ++ "."
+        | i <- [1 .. length bss]
+        ]
+
+prettyBlock _theme (Pandoc.RawBlock _ t) = PP.string t <> PP.hardline
+
+prettyBlock _theme Pandoc.HorizontalRule = "---"
+
+prettyBlock theme@Theme {..} (Pandoc.BlockQuote bs) =
+    let quote = PP.NotTrimmable (themed themeBlockQuote "> ") in
+    PP.indent quote quote (prettyBlocks theme bs)
+
+prettyBlock theme@Theme {..} (Pandoc.DefinitionList terms) =
+    PP.vcat $ map prettyDefinition terms
+  where
+    prettyDefinition (term, definitions) =
+        themed themeDefinitionTerm (prettyInlines theme term) <$$>
+        PP.hardline <> PP.vcat
+        [ PP.indent
+            (PP.NotTrimmable (themed themeDefinitionList ":   "))
+            (PP.Trimmable "    ") $
+            prettyBlocks theme (Pandoc.plainToPara definition)
+        | definition <- definitions
+        ]
+
+prettyBlock theme (Pandoc.Table caption aligns _ headers rows) =
+    PP.wrapAt Nothing $
+    prettyTable theme Table
+        { tCaption = prettyInlines theme caption
+        , tAligns  = map align aligns
+        , tHeaders = map (prettyBlocks theme) headers
+        , tRows    = map (map (prettyBlocks theme)) rows
+        }
+  where
+    align Pandoc.AlignLeft    = PP.AlignLeft
+    align Pandoc.AlignCenter  = PP.AlignCenter
+    align Pandoc.AlignDefault = PP.AlignLeft
+    align Pandoc.AlignRight   = PP.AlignRight
+
+prettyBlock theme (Pandoc.Div _attrs blocks) = prettyBlocks theme blocks
+
+prettyBlock _theme Pandoc.Null = mempty
+
+#if MIN_VERSION_pandoc(1,18,0)
+-- 'LineBlock' elements are new in pandoc-1.18
+prettyBlock theme@Theme {..} (Pandoc.LineBlock inliness) =
+    let ind = PP.NotTrimmable (themed themeLineBlock "| ") in
+    PP.wrapAt Nothing $
+    PP.indent ind ind $
+    PP.vcat $
+    map (prettyInlines theme) inliness
+#endif
+
+
+--------------------------------------------------------------------------------
+prettyBlocks :: Theme -> [Pandoc.Block] -> PP.Doc
+prettyBlocks theme = PP.vcat . map (prettyBlock theme)
+
+
+--------------------------------------------------------------------------------
+prettyInline :: Theme -> Pandoc.Inline -> PP.Doc
+
+prettyInline _theme Pandoc.Space = PP.space
+
+prettyInline _theme (Pandoc.Str str) = PP.string str
+
+prettyInline theme@Theme {..} (Pandoc.Emph inlines) =
+    themed themeEmph $
+    prettyInlines theme inlines
+
+prettyInline theme@Theme {..} (Pandoc.Strong inlines) =
+    themed themeStrong $
+    prettyInlines theme inlines
+
+prettyInline Theme {..} (Pandoc.Code _ txt) =
+    themed themeCode $
+    " " <> PP.string txt <> " "
+
+prettyInline theme@Theme {..} link@(Pandoc.Link _attrs text (target, _title))
+    | isReferenceLink link =
+        "[" <> themed themeLinkText (prettyInlines theme text) <> "]"
+    | otherwise =
+        "<" <> themed themeLinkTarget (PP.string target) <> ">"
+
+prettyInline _theme Pandoc.SoftBreak = PP.softline
+
+prettyInline _theme Pandoc.LineBreak = PP.hardline
+
+prettyInline theme@Theme {..} (Pandoc.Strikeout t) =
+    "~~" <> themed themeStrikeout (prettyInlines theme t) <> "~~"
+
+prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.SingleQuote t) =
+    "'" <> themed themeQuoted (prettyInlines theme t) <> "'"
+prettyInline theme@Theme {..} (Pandoc.Quoted Pandoc.DoubleQuote t) =
+    "'" <> themed themeQuoted (prettyInlines theme t) <> "'"
+
+prettyInline Theme {..} (Pandoc.Math _ t) =
+    themed themeMath (PP.string t)
+
+prettyInline theme@Theme {..} (Pandoc.Image _attrs text (target, _title)) =
+    "![" <> themed themeImageText (prettyInlines theme text) <> "](" <>
+    themed themeImageTarget (PP.string target) <> ")"
+
+-- These elements aren't really supported.
+prettyInline theme  (Pandoc.Cite      _ t) = prettyInlines theme t
+prettyInline theme  (Pandoc.Span      _ t) = prettyInlines theme t
+prettyInline _theme (Pandoc.RawInline _ t) = PP.string t
+prettyInline theme  (Pandoc.Note        t) = prettyBlocks  theme t
+prettyInline theme  (Pandoc.Superscript t) = prettyInlines theme t
+prettyInline theme  (Pandoc.Subscript   t) = prettyInlines theme t
+prettyInline theme  (Pandoc.SmallCaps   t) = prettyInlines theme t
+-- prettyInline unsupported = PP.ondullred $ PP.string $ show unsupported
+
+
+--------------------------------------------------------------------------------
+prettyInlines :: Theme -> [Pandoc.Inline] -> PP.Doc
+prettyInlines theme = mconcat . map (prettyInline theme)
+
+
+--------------------------------------------------------------------------------
+prettyReferences :: Theme -> Fragment -> [PP.Doc]
+prettyReferences theme@Theme {..} =
+    map prettyReference . getReferences . unFragment
+  where
+    getReferences :: [Pandoc.Block] -> [Pandoc.Inline]
+    getReferences = filter isReferenceLink . grecQ
+
+    prettyReference :: Pandoc.Inline -> PP.Doc
+    prettyReference (Pandoc.Link _attrs text (target, title)) =
+        "[" <>
+        themed themeLinkText (prettyInlines theme $ Pandoc.newlineToSpace text) <>
+        "](" <>
+        themed themeLinkTarget (PP.string target) <>
+        (if null title
+            then mempty
+            else PP.space <> "\"" <> PP.string title <> "\"")
+        <> ")"
+    prettyReference _ = mempty
+
+
+--------------------------------------------------------------------------------
+isReferenceLink :: Pandoc.Inline -> Bool
+isReferenceLink (Pandoc.Link _attrs text (target, _)) =
+    [Pandoc.Str target] /= text
+isReferenceLink _ = False
diff --git a/src/Patat/Presentation/Display/CodeBlock.hs b/src/Patat/Presentation/Display/CodeBlock.hs
new file mode 100644 (file)
index 0000000..4888166
--- /dev/null
@@ -0,0 +1,79 @@
+--------------------------------------------------------------------------------
+-- | Displaying code blocks, optionally with syntax highlighting.
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+module Patat.Presentation.Display.CodeBlock
+    ( prettyCodeBlock
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Data.Char                        (toLower)
+import           Data.List                        (find)
+import           Data.Monoid                      (mconcat, (<>))
+import qualified Data.Set                         as S
+import           Patat.Presentation.Display.Table (themed)
+import qualified Patat.PrettyPrint                as PP
+import           Patat.Theme
+import qualified Text.Highlighting.Kate           as Kate
+import           Prelude
+
+
+--------------------------------------------------------------------------------
+lower :: String -> String
+lower = map toLower
+
+
+--------------------------------------------------------------------------------
+supportedLanguages :: S.Set String
+supportedLanguages = S.fromList (map lower Kate.languages)
+
+
+--------------------------------------------------------------------------------
+highlight :: [String] -> String -> [Kate.SourceLine]
+highlight classes rawCodeBlock =
+    case find (\c -> lower c `S.member` supportedLanguages) classes of
+        Nothing   -> zeroHighlight rawCodeBlock
+        Just lang -> Kate.highlightAs lang rawCodeBlock
+
+
+--------------------------------------------------------------------------------
+-- | This does fake highlighting, everything becomes a normal token.  That makes
+-- things a bit easier, since we only need to deal with one cases in the
+-- renderer.
+zeroHighlight :: String -> [Kate.SourceLine]
+zeroHighlight str = [[(Kate.NormalTok, line)] | line <- lines str]
+
+
+--------------------------------------------------------------------------------
+prettyCodeBlock :: Theme -> [String] -> String -> PP.Doc
+prettyCodeBlock theme@Theme {..} classes rawCodeBlock =
+    PP.vcat (map blockified sourceLines) <>
+    PP.hardline
+  where
+    sourceLines :: [Kate.SourceLine]
+    sourceLines =
+        [[]] ++ highlight classes rawCodeBlock ++ [[]]
+
+    prettySourceLine :: Kate.SourceLine -> PP.Doc
+    prettySourceLine = mconcat . map prettyToken
+
+    prettyToken :: Kate.Token -> PP.Doc
+    prettyToken (tokenType, str) =
+        themed (syntaxHighlight theme tokenType) (PP.string str)
+
+    sourceLineLength :: Kate.SourceLine -> Int
+    sourceLineLength line = sum [length str | (_, str) <- line]
+
+    blockWidth :: Int
+    blockWidth = foldr max 0 (map sourceLineLength sourceLines)
+
+    blockified :: Kate.SourceLine -> PP.Doc
+    blockified line =
+        let len    = sourceLineLength line
+            indent = PP.NotTrimmable "   " in
+        PP.indent indent indent $
+        themed themeCodeBlock $
+            " " <>
+            prettySourceLine line <>
+            PP.string (replicate (blockWidth - len) ' ') <> " "
diff --git a/src/Patat/Presentation/Display/Table.hs b/src/Patat/Presentation/Display/Table.hs
new file mode 100644 (file)
index 0000000..fee68c9
--- /dev/null
@@ -0,0 +1,107 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+module Patat.Presentation.Display.Table
+    ( Table (..)
+    , prettyTable
+
+    , themed
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Data.List         (intersperse, transpose)
+import           Data.Monoid       (mconcat, mempty, (<>))
+import           Patat.PrettyPrint ((<$$>))
+import qualified Patat.PrettyPrint as PP
+import           Patat.Theme       (Theme (..))
+import qualified Patat.Theme       as Theme
+import           Prelude
+
+
+--------------------------------------------------------------------------------
+data Table = Table
+    { tCaption :: PP.Doc
+    , tAligns  :: [PP.Alignment]
+    , tHeaders :: [PP.Doc]
+    , tRows    :: [[PP.Doc]]
+    }
+
+
+--------------------------------------------------------------------------------
+prettyTable
+    :: Theme -> Table -> PP.Doc
+prettyTable theme@Theme {..} Table {..} =
+    PP.indent (PP.Trimmable "  ") (PP.Trimmable "  ") $
+        lineIf (not isHeaderLess) (hcat2 headerHeight
+            [ themed themeTableHeader (PP.align w a (vpad headerHeight header))
+            | (w, a, header) <- zip3 columnWidths tAligns tHeaders
+            ]) <>
+        dashedHeaderSeparator theme columnWidths <$$>
+        joinRows
+            [ hcat2 rowHeight
+                [ PP.align w a (vpad rowHeight cell)
+                | (w, a, cell) <- zip3 columnWidths tAligns row
+                ]
+            | (rowHeight, row) <- zip rowHeights tRows
+            ] <$$>
+        lineIf isHeaderLess (dashedHeaderSeparator theme columnWidths) <>
+        lineIf
+            (not $ PP.null tCaption) (PP.hardline <> "Table: " <> tCaption)
+  where
+    lineIf cond line = if cond then line <> PP.hardline else mempty
+
+    joinRows
+        | all (all isSimpleCell) tRows = PP.vcat
+        | otherwise                    = PP.vcat . intersperse ""
+
+    isHeaderLess = all PP.null tHeaders
+
+    headerDimensions = map PP.dimensions tHeaders :: [(Int, Int)]
+    rowDimensions    = map (map PP.dimensions) tRows :: [[(Int, Int)]]
+
+    columnWidths :: [Int]
+    columnWidths =
+        [ safeMax (map snd col)
+        | col <- transpose (headerDimensions : rowDimensions)
+        ]
+
+    rowHeights   = map (safeMax . map fst) rowDimensions :: [Int]
+    headerHeight = safeMax (map fst headerDimensions)    :: Int
+
+    vpad :: Int -> PP.Doc -> PP.Doc
+    vpad height doc =
+        let (actual, _) = PP.dimensions doc in
+        doc <> mconcat (replicate (height - actual) PP.hardline)
+
+    safeMax = foldr max 0
+
+    hcat2 :: Int -> [PP.Doc] -> PP.Doc
+    hcat2 rowHeight = PP.paste . intersperse (spaces2 rowHeight)
+
+    spaces2 :: Int -> PP.Doc
+    spaces2 rowHeight =
+        mconcat $ intersperse PP.hardline $
+        replicate rowHeight (PP.string "  ")
+
+
+--------------------------------------------------------------------------------
+isSimpleCell :: PP.Doc -> Bool
+isSimpleCell = (<= 1) . fst . PP.dimensions
+
+
+--------------------------------------------------------------------------------
+dashedHeaderSeparator :: Theme -> [Int] -> PP.Doc
+dashedHeaderSeparator Theme {..} columnWidths =
+    mconcat $ intersperse (PP.string "  ")
+        [ themed themeTableSeparator (PP.string (replicate w '-'))
+        | w <- columnWidths
+        ]
+
+
+--------------------------------------------------------------------------------
+-- | This does not really belong in the module.
+themed :: Maybe Theme.Style -> PP.Doc -> PP.Doc
+themed Nothing                    = id
+themed (Just (Theme.Style []))    = id
+themed (Just (Theme.Style codes)) = PP.ansi codes
diff --git a/src/Patat/Presentation/Fragment.hs b/src/Patat/Presentation/Fragment.hs
new file mode 100644 (file)
index 0000000..0908381
--- /dev/null
@@ -0,0 +1,134 @@
+-- | For background info on the spec, see the "Incremental lists" section of the
+-- the pandoc manual.
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE DeriveFoldable    #-}
+{-# LANGUAGE DeriveFunctor     #-}
+{-# LANGUAGE DeriveTraversable #-}
+module Patat.Presentation.Fragment
+    ( FragmentSettings (..)
+    , fragmentBlocks
+    , fragmentBlock
+    ) where
+
+import           Data.Foldable    (Foldable)
+import           Data.List        (foldl', intersperse)
+import           Data.Maybe       (fromMaybe)
+import           Data.Traversable (Traversable)
+import           Prelude
+import qualified Text.Pandoc      as Pandoc
+
+data FragmentSettings = FragmentSettings
+    { fsIncrementalLists :: !Bool
+    } deriving (Show)
+
+-- fragmentBlocks :: [Pandoc.Block] -> [[Pandoc.Block]]
+-- fragmentBlocks = NonEmpty.toList . joinFragmentedBlocks . map fragmentBlock
+fragmentBlocks :: FragmentSettings -> [Pandoc.Block] -> [[Pandoc.Block]]
+fragmentBlocks fs blocks0 =
+    case joinFragmentedBlocks (map (fragmentBlock fs) blocks0) of
+        Unfragmented  bs -> [bs]
+        Fragmented xs bs -> map (fromMaybe []) xs ++ [fromMaybe [] bs]
+
+-- | This is all the ways we can "present" a block, after splitting in
+-- fragments.
+--
+-- In the simplest (and most common case) a block can only be presented in a
+-- single way ('Unfragmented').
+--
+-- Alternatively, we might want to show different (partial) versions of the
+-- block first before showing the final complete one.  These partial or complete
+-- versions can be empty, hence the 'Maybe'.
+--
+-- For example, imagine that we display the following bullet list incrementally:
+--
+-- > [1, 2, 3]
+--
+-- Then we would get something like:
+--
+-- > Fragmented [Nothing, Just [1], Just [1, 2]] (Just [1, 2, 3])
+data Fragmented a
+    = Unfragmented a
+    | Fragmented [Maybe a] (Maybe a)
+    deriving (Functor, Foldable, Show, Traversable)
+
+fragmentBlock :: FragmentSettings -> Pandoc.Block -> Fragmented Pandoc.Block
+fragmentBlock _fs block@(Pandoc.Para inlines)
+    | inlines == threeDots = Fragmented [Nothing] Nothing
+    | otherwise            = Unfragmented block
+  where
+    threeDots = intersperse Pandoc.Space $ replicate 3 (Pandoc.Str ".")
+
+fragmentBlock fs (Pandoc.BulletList bs0) =
+    fragmentList fs (fsIncrementalLists fs) Pandoc.BulletList bs0
+
+fragmentBlock fs (Pandoc.OrderedList attr bs0) =
+    fragmentList fs (fsIncrementalLists fs) (Pandoc.OrderedList attr) bs0
+
+fragmentBlock fs (Pandoc.BlockQuote [Pandoc.BulletList bs0]) =
+    fragmentList fs (not $ fsIncrementalLists fs) Pandoc.BulletList bs0
+
+fragmentBlock fs (Pandoc.BlockQuote [Pandoc.OrderedList attr bs0]) =
+    fragmentList fs (not $ fsIncrementalLists fs) (Pandoc.OrderedList attr) bs0
+
+fragmentBlock _ block@(Pandoc.BlockQuote _)     = Unfragmented block
+
+fragmentBlock _ block@(Pandoc.Header _ _ _)     = Unfragmented block
+fragmentBlock _ block@(Pandoc.Plain _)          = Unfragmented block
+fragmentBlock _ block@(Pandoc.CodeBlock _ _)    = Unfragmented block
+fragmentBlock _ block@(Pandoc.RawBlock _ _)     = Unfragmented block
+fragmentBlock _ block@(Pandoc.DefinitionList _) = Unfragmented block
+fragmentBlock _ block@(Pandoc.Table _ _ _ _ _)  = Unfragmented block
+fragmentBlock _ block@(Pandoc.Div _ _)          = Unfragmented block
+fragmentBlock _ block@Pandoc.HorizontalRule     = Unfragmented block
+fragmentBlock _ block@Pandoc.Null               = Unfragmented block
+
+#if MIN_VERSION_pandoc(1,18,0)
+fragmentBlock _ block@(Pandoc.LineBlock _)      = Unfragmented block
+#endif
+
+joinFragmentedBlocks :: [Fragmented block] -> Fragmented [block]
+joinFragmentedBlocks =
+    foldl' append (Unfragmented [])
+  where
+    append (Unfragmented xs) (Unfragmented y) =
+        Unfragmented (xs ++ [y])
+
+    append (Fragmented xs x) (Unfragmented y) =
+        Fragmented xs (appendMaybe x (Just y))
+
+    append (Unfragmented x) (Fragmented ys y) =
+        Fragmented
+            [appendMaybe (Just x) y' | y' <- ys]
+            (appendMaybe (Just x) y)
+
+    append (Fragmented xs x) (Fragmented ys y) =
+        Fragmented
+            (xs ++ [appendMaybe x y' | y' <- ys])
+            (appendMaybe x y)
+
+    appendMaybe :: Maybe [a] -> Maybe a -> Maybe [a]
+    appendMaybe Nothing   Nothing  = Nothing
+    appendMaybe Nothing   (Just x) = Just [x]
+    appendMaybe (Just xs) Nothing  = Just xs
+    appendMaybe (Just xs) (Just x) = Just (xs ++ [x])
+
+fragmentList
+    :: FragmentSettings                    -- ^ Global settings
+    -> Bool                                -- ^ Fragment THIS list?
+    -> ([[Pandoc.Block]] -> Pandoc.Block)  -- ^ List constructor
+    -> [[Pandoc.Block]]                    -- ^ List items
+    -> Fragmented Pandoc.Block             -- ^ Resulting list
+fragmentList fs fragmentThisList constructor blocks0 =
+    fmap constructor fragmented
+  where
+    -- The fragmented list per list item.
+    items :: [Fragmented [Pandoc.Block]]
+    items = map (joinFragmentedBlocks . map (fragmentBlock fs)) blocks0
+
+    fragmented :: Fragmented [[Pandoc.Block]]
+    fragmented = joinFragmentedBlocks $
+        map (if fragmentThisList then insertPause else id) items
+
+    insertPause :: Fragmented a -> Fragmented a
+    insertPause (Unfragmented x)  = Fragmented [Nothing] (Just x)
+    insertPause (Fragmented xs x) = Fragmented (Nothing : xs) x
diff --git a/src/Patat/Presentation/Interactive.hs b/src/Patat/Presentation/Interactive.hs
new file mode 100644 (file)
index 0000000..830f0ff
--- /dev/null
@@ -0,0 +1,122 @@
+--------------------------------------------------------------------------------
+-- | Module that allows the user to interact with the presentation
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE RecordWildCards            #-}
+module Patat.Presentation.Interactive
+    ( PresentationCommand (..)
+    , readPresentationCommand
+
+    , UpdatedPresentation (..)
+    , updatePresentation
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Patat.Presentation.Internal
+import           Patat.Presentation.Read
+
+
+--------------------------------------------------------------------------------
+data PresentationCommand
+    = Exit
+    | Forward
+    | Backward
+    | SkipForward
+    | SkipBackward
+    | First
+    | Last
+    | Reload
+    | UnknownCommand String
+
+
+--------------------------------------------------------------------------------
+readPresentationCommand :: IO PresentationCommand
+readPresentationCommand = do
+    k <- readKey
+    case k of
+        "q"      -> return Exit
+        "\n"     -> return Forward
+        "\DEL"   -> return Backward
+        "h"      -> return Backward
+        "j"      -> return SkipForward
+        "k"      -> return SkipBackward
+        "l"      -> return Forward
+        "\ESC[C" -> return Forward
+        "\ESC[D" -> return Backward
+        "\ESC[B" -> return SkipForward
+        "\ESC[A" -> return SkipBackward
+        "0"      -> return First
+        "G"      -> return Last
+        "r"      -> return Reload
+        _        -> return (UnknownCommand k)
+  where
+    readKey :: IO String
+    readKey = do
+        c0 <- getChar
+        case c0 of
+            '\ESC' -> do
+                c1 <- getChar
+                case c1 of
+                    '[' -> do
+                        c2 <- getChar
+                        return [c0, c1, c2]
+                    _ -> return [c0, c1]
+            _ -> return [c0]
+
+
+--------------------------------------------------------------------------------
+data UpdatedPresentation
+    = UpdatedPresentation !Presentation
+    | ExitedPresentation
+    | ErroredPresentation String
+    deriving (Show)
+
+
+--------------------------------------------------------------------------------
+updatePresentation
+    :: PresentationCommand -> Presentation -> IO UpdatedPresentation
+
+updatePresentation cmd presentation = case cmd of
+    Exit             -> return ExitedPresentation
+    Forward          -> return $ goToSlide $ \(s, f) -> (s, f + 1)
+    Backward         -> return $ goToSlide $ \(s, f) -> (s, f - 1)
+    SkipForward      -> return $ goToSlide $ \(s, _) -> (s + 10, 0)
+    SkipBackward     -> return $ goToSlide $ \(s, _) -> (s - 10, 0)
+    First            -> return $ goToSlide $ \_ -> (0, 0)
+    Last             -> return $ goToSlide $ \_ -> (numSlides presentation, 0)
+    Reload           -> reloadPresentation
+    UnknownCommand _ -> return (UpdatedPresentation presentation)
+  where
+    numSlides :: Presentation -> Int
+    numSlides pres = length (pSlides pres)
+
+    clip :: Index -> Presentation -> Index
+    clip (slide, fragment) pres
+        | slide    >= numSlides pres = (numSlides pres - 1, lastFragments - 1)
+        | slide    <  0              = (0, 0)
+        | fragment >= numFragments slide =
+            if slide + 1 >= numSlides pres
+                then (slide, lastFragments - 1)
+                else (slide + 1, 0)
+        | fragment < 0 =
+            if slide - 1 >= 0
+                then (slide - 1, numFragments (slide - 1) - 1)
+                else (slide, 0)
+        | otherwise                  = (slide, fragment)
+      where
+        numFragments s = maybe 1 (length . unSlide) (getSlide s pres)
+        lastFragments  = numFragments (numSlides pres - 1)
+
+    goToSlide :: (Index -> Index) -> UpdatedPresentation
+    goToSlide f = UpdatedPresentation $ presentation
+        { pActiveFragment = clip (f $ pActiveFragment presentation) presentation
+        }
+
+    reloadPresentation = do
+        errOrPres <- readPresentation (pFilePath presentation)
+        return $ case errOrPres of
+            Left  err  -> ErroredPresentation err
+            Right pres -> UpdatedPresentation $ pres
+                { pActiveFragment = clip (pActiveFragment presentation) pres
+                }
diff --git a/src/Patat/Presentation/Internal.hs b/src/Patat/Presentation/Internal.hs
new file mode 100644 (file)
index 0000000..3554923
--- /dev/null
@@ -0,0 +1,107 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE TemplateHaskell            #-}
+module Patat.Presentation.Internal
+    ( Presentation (..)
+    , PresentationSettings (..)
+    , defaultPresentationSettings
+    , Slide (..)
+    , Fragment (..)
+    , Index
+
+    , getSlide
+    , getActiveFragment
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Control.Monad          (mplus)
+import qualified Data.Aeson.Extended    as A
+import qualified Data.Aeson.TH.Extended as A
+import           Data.Maybe             (listToMaybe)
+import           Data.Monoid            (Monoid (..), (<>))
+import qualified Patat.Theme            as Theme
+import qualified Text.Pandoc            as Pandoc
+import           Prelude
+
+
+--------------------------------------------------------------------------------
+data Presentation = Presentation
+    { pFilePath       :: !FilePath
+    , pTitle          :: ![Pandoc.Inline]
+    , pAuthor         :: ![Pandoc.Inline]
+    , pSettings       :: !PresentationSettings
+    , pSlides         :: [Slide]
+    , pActiveFragment :: !Index
+    } deriving (Show)
+
+
+--------------------------------------------------------------------------------
+-- | These are patat-specific settings.  That is where they differ from more
+-- general metadata (author, title...)
+data PresentationSettings = PresentationSettings
+    { psRows             :: !(Maybe (A.FlexibleNum Int))
+    , psColumns          :: !(Maybe (A.FlexibleNum Int))
+    , psWrap             :: !(Maybe Bool)
+    , psTheme            :: !(Maybe Theme.Theme)
+    , psIncrementalLists :: !(Maybe Bool)
+    , psAutoAdvanceDelay :: !(Maybe (A.FlexibleNum Int))
+    } deriving (Show)
+
+
+--------------------------------------------------------------------------------
+instance Monoid PresentationSettings where
+    mempty      = PresentationSettings
+                    Nothing Nothing Nothing Nothing Nothing Nothing
+    mappend l r = PresentationSettings
+        { psRows             = psRows             l `mplus` psRows             r
+        , psColumns          = psColumns          l `mplus` psColumns          r
+        , psWrap             = psWrap             l `mplus` psWrap             r
+        , psTheme            = psTheme            l <>      psTheme            r
+        , psIncrementalLists = psIncrementalLists l `mplus` psIncrementalLists r
+        , psAutoAdvanceDelay = psAutoAdvanceDelay l `mplus` psAutoAdvanceDelay r
+        }
+
+
+--------------------------------------------------------------------------------
+defaultPresentationSettings :: PresentationSettings
+defaultPresentationSettings = PresentationSettings
+    { psRows             = Nothing
+    , psColumns          = Nothing
+    , psWrap             = Nothing
+    , psTheme            = Just Theme.defaultTheme
+    , psIncrementalLists = Nothing
+    , psAutoAdvanceDelay = Nothing
+    }
+
+
+--------------------------------------------------------------------------------
+newtype Slide = Slide {unSlide :: [Fragment]}
+    deriving (Monoid, Show)
+
+
+--------------------------------------------------------------------------------
+newtype Fragment = Fragment {unFragment :: [Pandoc.Block]}
+    deriving (Monoid, Show)
+
+
+--------------------------------------------------------------------------------
+-- | Active slide, active fragment.
+type Index = (Int, Int)
+
+
+--------------------------------------------------------------------------------
+getSlide :: Int -> Presentation -> Maybe Slide
+getSlide sidx = listToMaybe . drop sidx . pSlides
+
+
+--------------------------------------------------------------------------------
+getActiveFragment :: Presentation -> Maybe Fragment
+getActiveFragment presentation = do
+    let (sidx, fidx) = pActiveFragment presentation
+    Slide fragments <- getSlide sidx presentation
+    listToMaybe $ drop fidx fragments
+
+
+--------------------------------------------------------------------------------
+$(A.deriveJSON A.dropPrefixOptions ''PresentationSettings)
diff --git a/src/Patat/Presentation/Read.hs b/src/Patat/Presentation/Read.hs
new file mode 100644 (file)
index 0000000..19d357d
--- /dev/null
@@ -0,0 +1,156 @@
+-- | Read a presentation from disk.
+{-# LANGUAGE BangPatterns      #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+module Patat.Presentation.Read
+    ( readPresentation
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Control.Monad.Except        (ExceptT (..), runExceptT,
+                                              throwError)
+import           Control.Monad.Trans         (liftIO)
+import qualified Data.Aeson                  as A
+import qualified Data.ByteString             as B
+import qualified Data.HashMap.Strict         as HMS
+import           Data.Maybe                  (fromMaybe)
+import           Data.Monoid                 (mempty, (<>))
+import qualified Data.Set                    as Set
+import qualified Data.Text                   as T
+import qualified Data.Text.Encoding          as T
+import qualified Data.Yaml                   as Yaml
+import           Patat.Presentation.Fragment
+import           Patat.Presentation.Internal
+import           Prelude
+import           System.Directory            (doesFileExist, getHomeDirectory)
+import           System.FilePath             (takeExtension, (</>))
+import qualified Text.Pandoc.Error           as Pandoc
+import qualified Text.Pandoc.Extended        as Pandoc
+
+
+--------------------------------------------------------------------------------
+readPresentation :: FilePath -> IO (Either String Presentation)
+readPresentation filePath = runExceptT $ do
+    src    <- liftIO $ readFile filePath
+    reader <- case readExtension ext of
+        Nothing -> throwError $ "Unknown file extension: " ++ show ext
+        Just x  -> return x
+    doc    <- case reader src of
+        Left  e -> throwError $ "Could not parse document: " ++ show e
+        Right x -> return x
+
+    homeSettings <- ExceptT readHomeSettings
+    metaSettings <- ExceptT $ return $ readMetaSettings src
+    let settings = metaSettings <> homeSettings <> defaultPresentationSettings
+
+    ExceptT $ return $ pandocToPresentation filePath settings doc
+  where
+    ext = takeExtension filePath
+
+
+--------------------------------------------------------------------------------
+readExtension
+    :: String -> Maybe (String -> Either Pandoc.PandocError Pandoc.Pandoc)
+readExtension fileExt = case fileExt of
+    ".md"  -> Just $ Pandoc.readMarkdown Pandoc.def
+    ".lhs" -> Just $ Pandoc.readMarkdown lhsOpts
+    ""     -> Just $ Pandoc.readMarkdown Pandoc.def
+    ".org" -> Just $ Pandoc.readOrg Pandoc.def
+    _      -> Nothing
+
+  where
+    lhsOpts = Pandoc.def
+        { Pandoc.readerExtensions = Set.insert Pandoc.Ext_literate_haskell
+            (Pandoc.readerExtensions Pandoc.def)
+        }
+
+
+--------------------------------------------------------------------------------
+pandocToPresentation
+    :: FilePath -> PresentationSettings -> Pandoc.Pandoc
+    -> Either String Presentation
+pandocToPresentation pFilePath pSettings pandoc@(Pandoc.Pandoc meta _) = do
+    let !pTitle          = Pandoc.docTitle meta
+        !pSlides         = pandocToSlides pSettings pandoc
+        !pActiveFragment = (0, 0)
+        !pAuthor         = concat (Pandoc.docAuthors meta)
+    return Presentation {..}
+
+
+--------------------------------------------------------------------------------
+-- | This re-parses the pandoc metadata block using the YAML library.  This
+-- avoids the problems caused by pandoc involving rendering Markdown.  This
+-- should only be used for settings though, not things like title / authors
+-- since those /can/ contain markdown.
+parseMetadataBlock :: String -> Maybe A.Value
+parseMetadataBlock src = do
+    block <- mbBlock
+    Yaml.decode $! T.encodeUtf8 $! T.pack block
+  where
+    mbBlock = case lines src of
+        ("---" : ls) -> case break (`elem` ["---", "..."]) ls of
+            (_,     [])      -> Nothing
+            (block, (_ : _)) -> Just (unlines block)
+        _            -> Nothing
+
+
+--------------------------------------------------------------------------------
+-- | Read settings from the metadata block in the Pandoc document.
+readMetaSettings :: String -> Either String PresentationSettings
+readMetaSettings src = fromMaybe (Right mempty) $ do
+    A.Object obj <- parseMetadataBlock src
+    val          <- HMS.lookup "patat" obj
+    return $! resultToEither $! A.fromJSON val
+  where
+    resultToEither :: A.Result a -> Either String a
+    resultToEither (A.Success x) = Right x
+    resultToEither (A.Error   e) = Left $!
+        "Error parsing patat settings from metadata: " ++ e
+
+
+--------------------------------------------------------------------------------
+-- | Read settings from "$HOME/.patat.yaml".
+readHomeSettings :: IO (Either String PresentationSettings)
+readHomeSettings = do
+    home <- getHomeDirectory
+    let path = home </> ".patat.yaml"
+    exists <- doesFileExist path
+    if not exists
+        then return (Right mempty)
+        else do
+            contents <- B.readFile path
+            return $! Yaml.decodeEither contents
+
+
+--------------------------------------------------------------------------------
+pandocToSlides :: PresentationSettings -> Pandoc.Pandoc -> [Slide]
+pandocToSlides settings pandoc =
+    let blockss = splitSlides pandoc in
+    map (Slide . map Fragment . (fragmentBlocks fragmentSettings)) blockss
+  where
+    fragmentSettings = FragmentSettings
+        { fsIncrementalLists = fromMaybe False (psIncrementalLists settings)
+        }
+
+
+--------------------------------------------------------------------------------
+-- | Split a pandoc document into slides.  If the document contains horizonal
+-- rules, we use those as slide delimiters.  If there are no horizontal rules,
+-- we split using h1 headers.
+splitSlides :: Pandoc.Pandoc -> [[Pandoc.Block]]
+splitSlides (Pandoc.Pandoc _meta blocks0)
+    | any (== Pandoc.HorizontalRule) blocks0 = splitAtRules blocks0
+    | otherwise                              = splitAtH1s   blocks0
+  where
+    splitAtRules blocks = case break (== Pandoc.HorizontalRule) blocks of
+        (xs, [])           -> [xs]
+        (xs, (_rule : ys)) -> xs : splitAtRules ys
+
+    splitAtH1s []       = []
+    splitAtH1s (b : bs) = case break isH1 bs of
+        (xs, [])       -> [(b : xs)]
+        (xs, (y : ys)) -> (b : xs) : splitAtH1s (y : ys)
+
+    isH1 (Pandoc.Header i _ _) = i == 1
+    isH1 _                     = False
diff --git a/src/Patat/PrettyPrint.hs b/src/Patat/PrettyPrint.hs
new file mode 100644 (file)
index 0000000..7b24b37
--- /dev/null
@@ -0,0 +1,404 @@
+--------------------------------------------------------------------------------
+-- | This is a small pretty-printing library.
+{-# LANGUAGE DeriveFoldable             #-}
+{-# LANGUAGE DeriveFunctor              #-}
+{-# LANGUAGE DeriveTraversable          #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE RecordWildCards            #-}
+module Patat.PrettyPrint
+    ( Doc
+    , toString
+    , dimensions
+    , null
+
+    , hPutDoc
+    , putDoc
+
+    , string
+    , text
+    , space
+    , softline
+    , hardline
+
+    , wrapAt
+
+    , Trimmable (..)
+    , indent
+
+    , ansi
+
+    , (<+>)
+    , (<$$>)
+    , vcat
+
+    -- * Exotic combinators
+    , Alignment (..)
+    , align
+    , paste
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Control.Monad.Reader (asks, local)
+import           Control.Monad.RWS    (RWS, runRWS)
+import           Control.Monad.State  (get, gets, modify)
+import           Control.Monad.Writer (tell)
+import           Data.Foldable        (Foldable)
+import qualified Data.List            as L
+import           Data.Monoid          (Monoid, mconcat, mempty, (<>))
+import           Data.String          (IsString (..))
+import qualified Data.Text            as T
+import           Data.Traversable     (Traversable, traverse)
+import qualified System.Console.ANSI  as Ansi
+import qualified System.IO            as IO
+import           Prelude              hiding (null)
+
+
+--------------------------------------------------------------------------------
+-- | A simple chunk of text.  All ANSI codes are "reset" after printing.
+data Chunk
+    = StringChunk [Ansi.SGR] String
+    | NewlineChunk
+    deriving (Eq)
+
+
+--------------------------------------------------------------------------------
+type Chunks = [Chunk]
+
+
+--------------------------------------------------------------------------------
+hPutChunk :: IO.Handle -> Chunk -> IO ()
+hPutChunk h NewlineChunk            = IO.hPutStrLn h ""
+hPutChunk h (StringChunk codes str) = do
+    Ansi.hSetSGR h (reverse codes)
+    IO.hPutStr h str
+    Ansi.hSetSGR h [Ansi.Reset]
+
+
+--------------------------------------------------------------------------------
+chunkToString :: Chunk -> String
+chunkToString NewlineChunk        = "\n"
+chunkToString (StringChunk _ str) = str
+
+
+--------------------------------------------------------------------------------
+-- | If two neighboring chunks have the same set of ANSI codes, we can group
+-- them together.
+optimizeChunks :: Chunks -> Chunks
+optimizeChunks (StringChunk c1 s1 : StringChunk c2 s2 : chunks)
+    | c1 == c2  = optimizeChunks (StringChunk c1 (s1 <> s2) : chunks)
+    | otherwise =
+        StringChunk c1 s1 : optimizeChunks (StringChunk c2 s2 : chunks)
+optimizeChunks (x : chunks) = x : optimizeChunks chunks
+optimizeChunks [] = []
+
+
+--------------------------------------------------------------------------------
+chunkLines :: Chunks -> [Chunks]
+chunkLines chunks = case break (== NewlineChunk) chunks of
+    (xs, _newline : ys) -> xs : chunkLines ys
+    (xs, [])            -> [xs]
+
+
+--------------------------------------------------------------------------------
+data DocE
+    = String String
+    | Softspace
+    | Hardspace
+    | Softline
+    | Hardline
+    | WrapAt
+        { wrapAtCol :: Maybe Int
+        , wrapDoc   :: Doc
+        }
+    | Ansi
+        { ansiCode :: [Ansi.SGR] -> [Ansi.SGR]  -- ^ Modifies current codes.
+        , ansiDoc  :: Doc
+        }
+    | Indent
+        { indentFirstLine  :: LineBuffer
+        , indentOtherLines :: LineBuffer
+        , indentDoc        :: Doc
+        }
+
+
+--------------------------------------------------------------------------------
+chunkToDocE :: Chunk -> DocE
+chunkToDocE NewlineChunk            = Hardline
+chunkToDocE (StringChunk codes str) = Ansi (\_ -> codes) (Doc [String str])
+
+
+--------------------------------------------------------------------------------
+newtype Doc = Doc {unDoc :: [DocE]}
+    deriving (Monoid)
+
+
+--------------------------------------------------------------------------------
+instance IsString Doc where
+    fromString = string
+
+
+--------------------------------------------------------------------------------
+instance Show Doc where
+    show = toString
+
+
+--------------------------------------------------------------------------------
+data DocEnv = DocEnv
+    { deCodes  :: [Ansi.SGR]  -- ^ Most recent ones first in the list
+    , deIndent :: LineBuffer  -- ^ Don't need to store first-line indent
+    , deWrap   :: Maybe Int   -- ^ Wrap at columns
+    }
+
+
+--------------------------------------------------------------------------------
+type DocM = RWS DocEnv Chunks LineBuffer
+
+
+--------------------------------------------------------------------------------
+data Trimmable a
+    = NotTrimmable !a
+    | Trimmable    !a
+    deriving (Foldable, Functor, Traversable)
+
+
+--------------------------------------------------------------------------------
+-- | Note that this is reversed so we have fast append
+type LineBuffer = [Trimmable Chunk]
+
+
+--------------------------------------------------------------------------------
+bufferToChunks :: LineBuffer -> Chunks
+bufferToChunks = map trimmableToChunk . reverse . dropWhile isTrimmable
+  where
+    isTrimmable (NotTrimmable _) = False
+    isTrimmable (Trimmable    _) = True
+
+    trimmableToChunk (NotTrimmable c) = c
+    trimmableToChunk (Trimmable    c) = c
+
+
+--------------------------------------------------------------------------------
+docToChunks :: Doc -> Chunks
+docToChunks doc0 =
+    let env0        = DocEnv [] [] Nothing
+        ((), b, cs) = runRWS (go $ unDoc doc0) env0 mempty in
+    optimizeChunks (cs <> bufferToChunks b)
+  where
+    go :: [DocE] -> DocM ()
+
+    go [] = return ()
+
+    go (String str : docs) = do
+        chunk <- makeChunk str
+        modify (NotTrimmable chunk :)
+        go docs
+
+    go (Softspace : docs) = do
+        hard <- softConversion Softspace docs
+        go (hard : docs)
+
+    go (Hardspace : docs) = do
+        chunk <- makeChunk " "
+        modify (NotTrimmable chunk :)
+        go docs
+
+    go (Softline : docs) = do
+        hard <- softConversion Softline docs
+        go (hard : docs)
+
+    go (Hardline : docs) = do
+        buffer <- get
+        tell $ bufferToChunks buffer <> [NewlineChunk]
+        indentation <- asks deIndent
+        modify $ \_ -> if L.null docs then [] else indentation
+        go docs
+
+    go (WrapAt {..} : docs) = do
+        local (\env -> env {deWrap = wrapAtCol}) $ go (unDoc wrapDoc)
+        go docs
+
+    go (Ansi {..} : docs) = do
+        local (\env -> env {deCodes = ansiCode (deCodes env)}) $
+            go (unDoc ansiDoc)
+        go docs
+
+    go (Indent {..} : docs) = do
+        local (\env -> env {deIndent = indentOtherLines ++ deIndent env}) $ do
+            modify (indentFirstLine ++)
+            go (unDoc indentDoc)
+        go docs
+
+    makeChunk :: String -> DocM Chunk
+    makeChunk str = do
+        codes <- asks deCodes
+        return $ StringChunk codes str
+
+    -- Convert 'Softspace' or 'Softline' to 'Hardspace' or 'Hardline'
+    softConversion :: DocE -> [DocE] -> DocM DocE
+    softConversion soft docs = do
+        mbWrapCol <- asks deWrap
+        case mbWrapCol of
+            Nothing     -> return hard
+            Just maxCol -> do
+                -- Slow.
+                currentLine <- gets (concatMap chunkToString . bufferToChunks)
+                let currentCol = length currentLine
+                case nextWordLength docs of
+                    Nothing                            -> return hard
+                    Just l
+                        | currentCol + 1 + l <= maxCol -> return Hardspace
+                        | otherwise                    -> return Hardline
+      where
+        hard = case soft of
+            Softspace -> Hardspace
+            Softline  -> Hardline
+            _         -> soft
+
+    nextWordLength :: [DocE] -> Maybe Int
+    nextWordLength []                 = Nothing
+    nextWordLength (String x : xs)
+        | L.null x                    = nextWordLength xs
+        | otherwise                   = Just (length x)
+    nextWordLength (Softspace : xs)   = nextWordLength xs
+    nextWordLength (Hardspace : xs)   = nextWordLength xs
+    nextWordLength (Softline : xs)    = nextWordLength xs
+    nextWordLength (Hardline : _)     = Nothing
+    nextWordLength (WrapAt {..} : xs) = nextWordLength (unDoc wrapDoc   ++ xs)
+    nextWordLength (Ansi   {..} : xs) = nextWordLength (unDoc ansiDoc   ++ xs)
+    nextWordLength (Indent {..} : xs) = nextWordLength (unDoc indentDoc ++ xs)
+
+
+--------------------------------------------------------------------------------
+toString :: Doc -> String
+toString = concat . map chunkToString . docToChunks
+
+
+--------------------------------------------------------------------------------
+-- | Returns the rows and columns necessary to render this document
+dimensions :: Doc -> (Int, Int)
+dimensions doc =
+    let ls = lines (toString doc) in
+    (length ls, foldr max 0 (map length ls))
+
+
+--------------------------------------------------------------------------------
+null :: Doc -> Bool
+null doc = case unDoc doc of [] -> True; _ -> False
+
+
+--------------------------------------------------------------------------------
+hPutDoc :: IO.Handle -> Doc -> IO ()
+hPutDoc h = mapM_ (hPutChunk h) . docToChunks
+
+
+--------------------------------------------------------------------------------
+putDoc :: Doc -> IO ()
+putDoc = hPutDoc IO.stdout
+
+
+--------------------------------------------------------------------------------
+mkDoc :: DocE -> Doc
+mkDoc e = Doc [e]
+
+
+--------------------------------------------------------------------------------
+string :: String -> Doc
+string = mkDoc . String  -- TODO (jaspervdj): Newline conversion
+
+
+--------------------------------------------------------------------------------
+text :: T.Text -> Doc
+text = string . T.unpack
+
+
+--------------------------------------------------------------------------------
+space :: Doc
+space = mkDoc Softspace
+
+
+--------------------------------------------------------------------------------
+softline :: Doc
+softline = mkDoc Softline
+
+
+--------------------------------------------------------------------------------
+hardline :: Doc
+hardline = mkDoc Hardline
+
+
+--------------------------------------------------------------------------------
+wrapAt :: Maybe Int -> Doc -> Doc
+wrapAt wrapAtCol wrapDoc = mkDoc WrapAt {..}
+
+
+--------------------------------------------------------------------------------
+indent :: Trimmable Doc -> Trimmable Doc -> Doc -> Doc
+indent firstLineDoc otherLinesDoc doc = mkDoc $ Indent
+    { indentFirstLine  = traverse docToChunks firstLineDoc
+    , indentOtherLines = traverse docToChunks otherLinesDoc
+    , indentDoc        = doc
+    }
+
+
+--------------------------------------------------------------------------------
+ansi :: [Ansi.SGR] -> Doc -> Doc
+ansi codes =  mkDoc . Ansi (codes ++)
+
+
+--------------------------------------------------------------------------------
+(<+>) :: Doc -> Doc -> Doc
+x <+> y = x <> space <> y
+infixr 6 <+>
+
+
+--------------------------------------------------------------------------------
+(<$$>) :: Doc -> Doc -> Doc
+x <$$> y = x <> hardline <> y
+infixr 5 <$$>
+
+
+--------------------------------------------------------------------------------
+vcat :: [Doc] -> Doc
+vcat = mconcat . L.intersperse hardline
+
+
+--------------------------------------------------------------------------------
+data Alignment = AlignLeft | AlignCenter | AlignRight deriving (Eq, Ord, Show)
+
+
+--------------------------------------------------------------------------------
+align :: Int -> Alignment -> Doc -> Doc
+align width alignment doc0 =
+    let chunks0 = docToChunks doc0
+        lines_  = chunkLines chunks0 in
+    vcat
+        [ Doc (map chunkToDocE (alignLine line))
+        | line <- lines_
+        ]
+  where
+    lineWidth :: [Chunk] -> Int
+    lineWidth = sum . map (length . chunkToString)
+
+    alignLine :: [Chunk] -> [Chunk]
+    alignLine line =
+        let actual   = lineWidth line
+            spaces n = [StringChunk [] (replicate n ' ')] in
+        case alignment of
+            AlignLeft   -> line <> spaces (width - actual)
+            AlignRight  -> spaces (width - actual) <> line
+            AlignCenter ->
+                let r = (width - actual) `div` 2
+                    l = (width - actual) - r in
+                spaces l <> line <> spaces r
+
+
+--------------------------------------------------------------------------------
+-- | Like the unix program 'paste'.
+paste :: [Doc] -> Doc
+paste docs0 =
+    let chunkss = map docToChunks docs0                   :: [Chunks]
+        cols    = map chunkLines chunkss                  :: [[Chunks]]
+        rows0   = L.transpose cols                        :: [[Chunks]]
+        rows1   = map (map (Doc . map chunkToDocE)) rows0 :: [[Doc]] in
+    vcat $ map mconcat rows1
diff --git a/src/Patat/Theme.hs b/src/Patat/Theme.hs
new file mode 100644 (file)
index 0000000..706f825
--- /dev/null
@@ -0,0 +1,286 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE TemplateHaskell            #-}
+module Patat.Theme
+    ( Theme (..)
+    , defaultTheme
+
+    , Style (..)
+
+    , SyntaxHighlighting (..)
+    , defaultSyntaxHighlighting
+    , syntaxHighlight
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Control.Monad          (forM_, mplus)
+import qualified Data.Aeson             as A
+import qualified Data.Aeson.TH.Extended as A
+import           Data.Char              (toLower, toUpper)
+import           Data.List              (intercalate, isSuffixOf)
+import qualified Data.Map               as M
+import           Data.Maybe             (mapMaybe, maybeToList)
+import           Data.Monoid            (Monoid (..), (<>))
+import qualified Data.Text              as T
+import qualified System.Console.ANSI    as Ansi
+import qualified Text.Highlighting.Kate as Kate
+import           Text.Read              (readMaybe)
+import           Prelude
+
+
+--------------------------------------------------------------------------------
+data Theme = Theme
+    { themeBorders            :: !(Maybe Style)
+    , themeHeader             :: !(Maybe Style)
+    , themeCodeBlock          :: !(Maybe Style)
+    , themeBulletList         :: !(Maybe Style)
+    , themeBulletListMarkers  :: !(Maybe T.Text)
+    , themeOrderedList        :: !(Maybe Style)
+    , themeBlockQuote         :: !(Maybe Style)
+    , themeDefinitionTerm     :: !(Maybe Style)
+    , themeDefinitionList     :: !(Maybe Style)
+    , themeTableHeader        :: !(Maybe Style)
+    , themeTableSeparator     :: !(Maybe Style)
+    , themeLineBlock          :: !(Maybe Style)
+    , themeEmph               :: !(Maybe Style)
+    , themeStrong             :: !(Maybe Style)
+    , themeCode               :: !(Maybe Style)
+    , themeLinkText           :: !(Maybe Style)
+    , themeLinkTarget         :: !(Maybe Style)
+    , themeStrikeout          :: !(Maybe Style)
+    , themeQuoted             :: !(Maybe Style)
+    , themeMath               :: !(Maybe Style)
+    , themeImageText          :: !(Maybe Style)
+    , themeImageTarget        :: !(Maybe Style)
+    , themeSyntaxHighlighting :: !(Maybe SyntaxHighlighting)
+    } deriving (Show)
+
+
+--------------------------------------------------------------------------------
+instance Monoid Theme where
+    mempty = Theme
+        Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
+        Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
+        Nothing Nothing Nothing Nothing Nothing
+
+    mappend l r = Theme
+        { themeBorders            = mplusOn   themeBorders
+        , themeHeader             = mplusOn   themeHeader
+        , themeCodeBlock          = mplusOn   themeCodeBlock
+        , themeBulletList         = mplusOn   themeBulletList
+        , themeBulletListMarkers  = mplusOn   themeBulletListMarkers
+        , themeOrderedList        = mplusOn   themeOrderedList
+        , themeBlockQuote         = mplusOn   themeBlockQuote
+        , themeDefinitionTerm     = mplusOn   themeDefinitionTerm
+        , themeDefinitionList     = mplusOn   themeDefinitionList
+        , themeTableHeader        = mplusOn   themeTableHeader
+        , themeTableSeparator     = mplusOn   themeTableSeparator
+        , themeLineBlock          = mplusOn   themeLineBlock
+        , themeEmph               = mplusOn   themeEmph
+        , themeStrong             = mplusOn   themeStrong
+        , themeCode               = mplusOn   themeCode
+        , themeLinkText           = mplusOn   themeLinkText
+        , themeLinkTarget         = mplusOn   themeLinkTarget
+        , themeStrikeout          = mplusOn   themeStrikeout
+        , themeQuoted             = mplusOn   themeQuoted
+        , themeMath               = mplusOn   themeMath
+        , themeImageText          = mplusOn   themeImageText
+        , themeImageTarget        = mplusOn   themeImageTarget
+        , themeSyntaxHighlighting = mappendOn themeSyntaxHighlighting
+        }
+      where
+        mplusOn   f = f l `mplus`   f r
+        mappendOn f = f l `mappend` f r
+
+
+--------------------------------------------------------------------------------
+defaultTheme :: Theme
+defaultTheme = Theme
+    { themeBorders            = dull Ansi.Yellow
+    , themeHeader             = dull Ansi.Blue
+    , themeCodeBlock          = dull Ansi.White <> ondull Ansi.Black
+    , themeBulletList         = dull Ansi.Magenta
+    , themeBulletListMarkers  = Just "-*"
+    , themeOrderedList        = dull Ansi.Magenta
+    , themeBlockQuote         = dull Ansi.Green
+    , themeDefinitionTerm     = dull Ansi.Blue
+    , themeDefinitionList     = dull Ansi.Magenta
+    , themeTableHeader        = dull Ansi.Blue
+    , themeTableSeparator     = dull Ansi.Magenta
+    , themeLineBlock          = dull Ansi.Magenta
+    , themeEmph               = dull Ansi.Green
+    , themeStrong             = dull Ansi.Red <> bold
+    , themeCode               = dull Ansi.White <> ondull Ansi.Black
+    , themeLinkText           = dull Ansi.Green
+    , themeLinkTarget         = dull Ansi.Cyan <> underline
+    , themeStrikeout          = ondull Ansi.Red
+    , themeQuoted             = dull Ansi.Green
+    , themeMath               = dull Ansi.Green
+    , themeImageText          = dull Ansi.Green
+    , themeImageTarget        = dull Ansi.Cyan <> underline
+    , themeSyntaxHighlighting = Just defaultSyntaxHighlighting
+    }
+  where
+    dull   c  = Just $ Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c]
+    ondull c  = Just $ Style [Ansi.SetColor Ansi.Background Ansi.Dull c]
+    bold      = Just $ Style [Ansi.SetConsoleIntensity Ansi.BoldIntensity]
+    underline = Just $ Style [Ansi.SetUnderlining Ansi.SingleUnderline]
+
+
+--------------------------------------------------------------------------------
+newtype Style = Style {unStyle :: [Ansi.SGR]}
+    deriving (Monoid, Show)
+
+
+--------------------------------------------------------------------------------
+instance A.ToJSON Style where
+    toJSON = A.toJSON . mapMaybe nameForSGR . unStyle
+
+
+--------------------------------------------------------------------------------
+instance A.FromJSON Style where
+    parseJSON val = do
+        names <- A.parseJSON val
+        sgrs  <- mapM toSgr names
+        return $! Style sgrs
+      where
+        toSgr name = case M.lookup name sgrsByName of
+            Just sgr -> return sgr
+            Nothing  -> fail $!
+                "Unknown style: " ++ show name ++ ". Known styles are: " ++
+                intercalate ", " (map show $ M.keys sgrsByName)
+
+
+--------------------------------------------------------------------------------
+nameForSGR :: Ansi.SGR -> Maybe String
+nameForSGR (Ansi.SetColor layer intensity color) = Just $
+    (\str -> case layer of
+        Ansi.Foreground -> str
+        Ansi.Background -> "on" ++ capitalize str) $
+    (case intensity of
+        Ansi.Dull  -> "dull"
+        Ansi.Vivid -> "vivid") ++
+    (case color of
+        Ansi.Black   -> "Black"
+        Ansi.Red     -> "Red"
+        Ansi.Green   -> "Green"
+        Ansi.Yellow  -> "Yellow"
+        Ansi.Blue    -> "Blue"
+        Ansi.Magenta -> "Magenta"
+        Ansi.Cyan    -> "Cyan"
+        Ansi.White   -> "White")
+
+nameForSGR (Ansi.SetUnderlining Ansi.SingleUnderline) = Just "underline"
+
+nameForSGR (Ansi.SetConsoleIntensity Ansi.BoldIntensity) = Just "bold"
+
+nameForSGR _ = Nothing
+
+
+--------------------------------------------------------------------------------
+sgrsByName :: M.Map String Ansi.SGR
+sgrsByName = M.fromList
+    [ (name, sgr)
+    | sgr  <- knownSgrs
+    , name <- maybeToList (nameForSGR sgr)
+    ]
+  where
+    -- | It doesn't really matter if we generate "too much" SGRs here since
+    -- 'nameForSGR' will only pick the ones we support.
+    knownSgrs =
+        [ Ansi.SetColor l i c
+        | l <- [minBound .. maxBound]
+        , i <- [minBound .. maxBound]
+        , c <- [minBound .. maxBound]
+        ] ++
+        [Ansi.SetUnderlining      u | u <- [minBound .. maxBound]] ++
+        [Ansi.SetConsoleIntensity c | c <- [minBound .. maxBound]]
+
+
+--------------------------------------------------------------------------------
+newtype SyntaxHighlighting = SyntaxHighlighting
+    { unSyntaxHighlighting :: M.Map String Style
+    } deriving (Monoid, Show, A.ToJSON)
+
+
+--------------------------------------------------------------------------------
+instance A.FromJSON SyntaxHighlighting where
+    parseJSON val = do
+        styleMap <- A.parseJSON val
+        forM_ (M.keys styleMap) $ \k -> case nameToTokenType k of
+            Just _  -> return ()
+            Nothing -> fail $ "Unknown token type: " ++ show k
+        return (SyntaxHighlighting styleMap)
+
+
+--------------------------------------------------------------------------------
+defaultSyntaxHighlighting :: SyntaxHighlighting
+defaultSyntaxHighlighting = mkSyntaxHighlighting
+    [ (Kate.KeywordTok,        dull Ansi.Yellow)
+    , (Kate.ControlFlowTok,    dull Ansi.Yellow)
+
+    , (Kate.DataTypeTok,       dull Ansi.Green)
+
+    , (Kate.DecValTok,         dull Ansi.Red)
+    , (Kate.BaseNTok,          dull Ansi.Red)
+    , (Kate.FloatTok,          dull Ansi.Red)
+    , (Kate.ConstantTok,       dull Ansi.Red)
+    , (Kate.CharTok,           dull Ansi.Red)
+    , (Kate.SpecialCharTok,    dull Ansi.Red)
+    , (Kate.StringTok,         dull Ansi.Red)
+    , (Kate.VerbatimStringTok, dull Ansi.Red)
+    , (Kate.SpecialStringTok,  dull Ansi.Red)
+
+    , (Kate.CommentTok,        dull Ansi.Blue)
+    , (Kate.DocumentationTok,  dull Ansi.Blue)
+    , (Kate.AnnotationTok,     dull Ansi.Blue)
+    , (Kate.CommentVarTok,     dull Ansi.Blue)
+
+    , (Kate.ImportTok,         dull Ansi.Cyan)
+    , (Kate.OperatorTok,       dull Ansi.Cyan)
+    , (Kate.FunctionTok,       dull Ansi.Cyan)
+    , (Kate.PreprocessorTok,   dull Ansi.Cyan)
+    ]
+  where
+    dull c = Style [Ansi.SetColor Ansi.Foreground Ansi.Dull c]
+
+    mkSyntaxHighlighting ls = SyntaxHighlighting $
+        M.fromList [(nameForTokenType tt, s) | (tt, s) <- ls]
+
+
+--------------------------------------------------------------------------------
+nameForTokenType :: Kate.TokenType -> String
+nameForTokenType =
+    unCapitalize . dropTok . show
+  where
+    unCapitalize (x : xs) = toLower x : xs
+    unCapitalize xs       = xs
+
+    dropTok :: String -> String
+    dropTok str
+        | "Tok" `isSuffixOf` str = take (length str - 3) str
+        | otherwise              = str
+
+
+--------------------------------------------------------------------------------
+nameToTokenType :: String -> Maybe Kate.TokenType
+nameToTokenType = readMaybe . capitalize . (++ "Tok")
+
+
+--------------------------------------------------------------------------------
+capitalize :: String -> String
+capitalize ""       = ""
+capitalize (x : xs) = toUpper x : xs
+
+
+--------------------------------------------------------------------------------
+syntaxHighlight :: Theme -> Kate.TokenType -> Maybe Style
+syntaxHighlight theme tokenType = do
+    sh <- themeSyntaxHighlighting theme
+    M.lookup (nameForTokenType tokenType) (unSyntaxHighlighting sh)
+
+
+--------------------------------------------------------------------------------
+$(A.deriveJSON A.dropPrefixOptions ''Theme)
diff --git a/src/Text/Pandoc/Extended.hs b/src/Text/Pandoc/Extended.hs
new file mode 100644 (file)
index 0000000..941d716
--- /dev/null
@@ -0,0 +1,30 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE LambdaCase   #-}
+module Text.Pandoc.Extended
+    ( module Text.Pandoc
+
+    , plainToPara
+    , newlineToSpace
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Data.Data.Extended (grecT)
+import           Text.Pandoc
+import           Prelude
+
+
+--------------------------------------------------------------------------------
+plainToPara :: [Block] -> [Block]
+plainToPara = map $ \case
+    Plain inlines -> Para inlines
+    block         -> block
+
+
+--------------------------------------------------------------------------------
+newlineToSpace :: [Inline] -> [Inline]
+newlineToSpace = grecT $ \case
+    SoftBreak -> Space
+    LineBreak -> Space
+    inline    -> inline
diff --git a/stack.yaml b/stack.yaml
new file mode 100644 (file)
index 0000000..e3c2c1e
--- /dev/null
@@ -0,0 +1,6 @@
+resolver: lts-7.0
+packages:
+- '.'
+extra-deps: []
+flags: {}
+extra-package-dbs: []
diff --git a/test.sh b/test.sh
new file mode 100644 (file)
index 0000000..9f8e48d
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -o nounset -o errexit -o pipefail
+
+srcs=$(find tests -type f ! -name '*.dump')
+stuff_went_wrong=false
+
+for src in $srcs; do
+    expected="$src.dump"
+    echo -n "Testing $src... "
+    actual=$(mktemp)
+    patat --dump --force "$src" >"$actual"
+
+    if [[ $@ == "--fix" ]]; then
+        cp "$actual" "$expected"
+        echo 'Fixed'
+    elif [[ ! -f "$expected" ]]; then
+        echo "missing file: $expected"
+        stuff_went_wrong=true
+    elif [[ "$(cat "$expected")" == "$(cat "$actual")" ]]; then
+        echo 'OK'
+    else
+        echo 'files differ'
+        diff "$actual" "$expected" || true
+        stuff_went_wrong=true
+    fi
+done
+
+if [[ "$stuff_went_wrong" = true ]]; then
+    exit 1
+fi
diff --git a/tests/01.md b/tests/01.md
new file mode 100644 (file)
index 0000000..2fbdde2
--- /dev/null
@@ -0,0 +1,14 @@
+---
+title: This is my presentation
+author: Jasper Van der Jeugt
+...
+
+# This is a test
+
+Hello world
+
+---
+
+# This is a second slide
+
+lololol
diff --git a/tests/01.md.dump b/tests/01.md.dump
new file mode 100644 (file)
index 0000000..1ae41da
--- /dev/null
@@ -0,0 +1,8 @@
+\e[34m# This is a test\e[0m
+
+\e[mHello world\e[0m
+
+\e[m----------\e[0m
+\e[34m# This is a second slide\e[0m
+
+\e[mlololol\e[0m
diff --git a/tests/02.lhs b/tests/02.lhs
new file mode 100644 (file)
index 0000000..e61c2d0
--- /dev/null
@@ -0,0 +1,6 @@
+This is how to do _Hello World_ in Haskell:
+
+> main :: IO ()
+> main = putStrLn "Hello World!"
+
+Cool, right?
diff --git a/tests/02.lhs.dump b/tests/02.lhs.dump
new file mode 100644 (file)
index 0000000..594c1bd
--- /dev/null
@@ -0,0 +1,8 @@
+\e[mThis is how to do \e[0m\e[32mHello World\e[0m\e[m in Haskell:\e[0m
+
+\e[m   \e[0m\e[40;37m                                \e[0m
+\e[m   \e[0m\e[40;37m main :: \e[0m\e[40;37;32mIO\e[0m\e[40;37m ()                  \e[0m
+\e[m   \e[0m\e[40;37m main \e[0m\e[40;37;36m=\e[0m\e[40;37m putStrLn \e[0m\e[40;37;31m"Hello World!"\e[0m\e[40;37m \e[0m
+\e[m   \e[0m\e[40;37m                                \e[0m
+
+\e[mCool, right?\e[0m
diff --git a/tests/03.md b/tests/03.md
new file mode 100644 (file)
index 0000000..6b3ae16
--- /dev/null
@@ -0,0 +1,46 @@
+Inline markups:
+
+- ~~striked out~~
+- <http://example.com>
+
+---
+
+> Some quote
+
+> Quote with embedded list:
+>
+> - Hello
+> - World
+
+---
+
+- List with an embedded quote:
+
+    > Tu quoque
+
+    Wow rad stuff.
+
+- Second item in that list.
+
+---
+
+Code with empty line:
+
+    puts "wow"
+
+    puts "amaze"
+
+---
+
+Code in ordered list:
+
+1. Do you know the coolest codes?
+
+    It's this:
+
+        fire_missiles()
+        cancel()
+
+    Great
+
+2. Also `fib` is pretty cool yeah
diff --git a/tests/03.md.dump b/tests/03.md.dump
new file mode 100644 (file)
index 0000000..e8b6b69
--- /dev/null
@@ -0,0 +1,48 @@
+\e[mInline markups:\e[0m
+
+\e[35m  - \e[0m\e[m~~\e[0m\e[41mstriked out\e[0m\e[m~~\e[0m
+\e[35m  - \e[0m\e[m<\e[0m\e[4;36mhttp://example.com\e[0m\e[m>\e[0m
+
+\e[m----------\e[0m
+\e[32m> \e[0m\e[mSome quote\e[0m
+
+\e[32m> \e[0m\e[mQuote with embedded list:\e[0m
+\e[32m> \e[0m
+\e[32m> \e[0m\e[35m  - \e[0m\e[mHello\e[0m
+\e[32m> \e[0m\e[35m  - \e[0m\e[mWorld\e[0m
+
+\e[m----------\e[0m
+\e[35m  - \e[0m\e[mList with an embedded quote:\e[0m
+
+\e[m    \e[0m\e[32m> \e[0m\e[mTu quoque\e[0m
+
+\e[m    Wow rad stuff.\e[0m
+
+\e[35m  - \e[0m\e[mSecond item in that list.\e[0m
+
+
+\e[m----------\e[0m
+\e[mCode with empty line:\e[0m
+
+\e[m   \e[0m\e[40;37m              \e[0m
+\e[m   \e[0m\e[40;37m puts "wow"   \e[0m
+\e[m   \e[0m\e[40;37m              \e[0m
+\e[m   \e[0m\e[40;37m puts "amaze" \e[0m
+\e[m   \e[0m\e[40;37m              \e[0m
+
+\e[m----------\e[0m
+\e[mCode in ordered list:\e[0m
+
+\e[35m1.  \e[0m\e[mDo you know the coolest codes?\e[0m
+
+\e[m    It's this:\e[0m
+
+\e[m       \e[0m\e[40;37m                 \e[0m
+\e[m       \e[0m\e[40;37m fire_missiles() \e[0m
+\e[m       \e[0m\e[40;37m cancel()        \e[0m
+\e[m       \e[0m\e[40;37m                 \e[0m
+
+\e[m    Great\e[0m
+
+\e[35m2.  \e[0m\e[mAlso \e[0m\e[40;37m fib \e[0m\e[m is pretty cool yeah\e[0m
+
diff --git a/tests/deflist.md b/tests/deflist.md
new file mode 100644 (file)
index 0000000..81aee19
--- /dev/null
@@ -0,0 +1,20 @@
+Term 1
+
+:   Definition 1
+
+Term 2 with *inline markup*
+
+:   Definition 2
+
+        { some code, part of Definition 2 }
+
+    Third paragraph of definition 2.
+
+---
+
+Term 1
+  ~ Definition 1
+
+Term 2
+  ~ Definition 2a
+  ~ Definition 2b
diff --git a/tests/deflist.md.dump b/tests/deflist.md.dump
new file mode 100644 (file)
index 0000000..8089fda
--- /dev/null
@@ -0,0 +1,24 @@
+\e[34mTerm 1\e[0m
+
+\e[35m:   \e[0m\e[mDefinition 1\e[0m
+
+\e[34mTerm 2 with \e[0m\e[34;32minline markup\e[0m
+
+\e[35m:   \e[0m\e[mDefinition 2\e[0m
+
+\e[m       \e[0m\e[40;37m                                     \e[0m
+\e[m       \e[0m\e[40;37m { some code, part of Definition 2 } \e[0m
+\e[m       \e[0m\e[40;37m                                     \e[0m
+
+\e[m    Third paragraph of definition 2.\e[0m
+
+\e[m----------\e[0m
+\e[34mTerm 1\e[0m
+
+\e[35m:   \e[0m\e[mDefinition 1\e[0m
+
+\e[34mTerm 2\e[0m
+
+\e[35m:   \e[0m\e[mDefinition 2a\e[0m
+
+\e[35m:   \e[0m\e[mDefinition 2b\e[0m
diff --git a/tests/fragments.md b/tests/fragments.md
new file mode 100644 (file)
index 0000000..510baa2
--- /dev/null
@@ -0,0 +1,27 @@
+---
+patat:
+  incrementalLists: true
+...
+
+- This list
+- is displayed
+
+    * item
+    * by item
+
+- Or sometimes
+
+    > * all at
+    > * once
+
+---
+
+Legen
+
+. . .
+
+wait for it
+
+. . .
+
+Dary!
diff --git a/tests/fragments.md.dump b/tests/fragments.md.dump
new file mode 100644 (file)
index 0000000..65b7aec
--- /dev/null
@@ -0,0 +1,54 @@
+
+
+\e[m~~~~~~~~~~\e[0m
+\e[35m  - \e[0m\e[mThis list\e[0m
+
+\e[m~~~~~~~~~~\e[0m
+\e[35m  - \e[0m\e[mThis list\e[0m
+\e[35m  - \e[0m\e[mis displayed\e[0m
+
+
+
+
+\e[m~~~~~~~~~~\e[0m
+\e[35m  - \e[0m\e[mThis list\e[0m
+\e[35m  - \e[0m\e[mis displayed\e[0m
+
+\e[m    \e[0m\e[35m  * \e[0m\e[mitem\e[0m
+
+
+\e[m~~~~~~~~~~\e[0m
+\e[35m  - \e[0m\e[mThis list\e[0m
+\e[35m  - \e[0m\e[mis displayed\e[0m
+
+\e[m    \e[0m\e[35m  * \e[0m\e[mitem\e[0m
+\e[m    \e[0m\e[35m  * \e[0m\e[mby item\e[0m
+
+
+\e[m~~~~~~~~~~\e[0m
+\e[35m  - \e[0m\e[mThis list\e[0m
+\e[35m  - \e[0m\e[mis displayed\e[0m
+
+\e[m    \e[0m\e[35m  * \e[0m\e[mitem\e[0m
+\e[m    \e[0m\e[35m  * \e[0m\e[mby item\e[0m
+
+\e[35m  - \e[0m\e[mOr sometimes\e[0m
+
+\e[m    \e[0m\e[35m  * \e[0m\e[mall at\e[0m
+\e[m    \e[0m\e[35m  * \e[0m\e[monce\e[0m
+
+
+\e[m----------\e[0m
+\e[mLegen\e[0m
+
+\e[m~~~~~~~~~~\e[0m
+\e[mLegen\e[0m
+
+\e[mwait for it\e[0m
+
+\e[m~~~~~~~~~~\e[0m
+\e[mLegen\e[0m
+
+\e[mwait for it\e[0m
+
+\e[mDary!\e[0m
diff --git a/tests/links.md b/tests/links.md
new file mode 100644 (file)
index 0000000..153f959
--- /dev/null
@@ -0,0 +1,8 @@
+This is an "automatic link": <https://jaspervdj.be>.
+
+This is an [inline link](/url), and here's [one with
+a title](http://fsf.org "click here for a good time!").
+
+Let's talk about [foo][foosite]
+
+[foosite]: http://foo.com/
diff --git a/tests/links.md.dump b/tests/links.md.dump
new file mode 100644 (file)
index 0000000..2862e9a
--- /dev/null
@@ -0,0 +1,10 @@
+\e[mThis is an "automatic link": <\e[0m\e[4;36mhttps://jaspervdj.be\e[0m\e[m>.\e[0m
+
+\e[mThis is an [\e[0m\e[32minline link\e[0m\e[m], and here's [\e[0m\e[32mone with\e[0m
+\e[32ma title\e[0m\e[m].\e[0m
+
+\e[mLet's talk about [\e[0m\e[32mfoo\e[0m\e[m]\e[0m
+
+\e[m[\e[0m\e[32minline link\e[0m\e[m](\e[0m\e[4;36m/url\e[0m\e[m)\e[0m
+\e[m[\e[0m\e[32mone with a title\e[0m\e[m](\e[0m\e[4;36mhttp://fsf.org\e[0m\e[m "click here for a good time!")\e[0m
+\e[m[\e[0m\e[32mfoo\e[0m\e[m](\e[0m\e[4;36mhttp://foo.com/\e[0m\e[m)\e[0m
\ No newline at end of file
diff --git a/tests/lists.md b/tests/lists.md
new file mode 100644 (file)
index 0000000..d534704
--- /dev/null
@@ -0,0 +1,13 @@
+- This is a nested list.
+
+    * The nested items should have different list markers.
+
+    * I mean, they can be the same, but it doesn't look nice.
+
+        printf("Nested code block!\n")
+
+    * Cool right?
+
+        Definitely super cool
+
+- One final item
diff --git a/tests/lists.md.dump b/tests/lists.md.dump
new file mode 100644 (file)
index 0000000..1305289
--- /dev/null
@@ -0,0 +1,15 @@
+\e[35m  - \e[0m\e[mThis is a nested list.\e[0m
+
+\e[m    \e[0m\e[35m  * \e[0m\e[mThe nested items should have different list markers.\e[0m
+
+\e[m    \e[0m\e[35m  * \e[0m\e[mI mean, they can be the same, but it doesn't look nice.\e[0m
+
+\e[m        printf("Nested code block!\n")\e[0m
+
+\e[m    \e[0m\e[35m  * \e[0m\e[mCool right?\e[0m
+
+\e[m        Definitely super cool\e[0m
+
+
+\e[35m  - \e[0m\e[mOne final item\e[0m
+
diff --git a/tests/meta.md b/tests/meta.md
new file mode 100644 (file)
index 0000000..2ba5db9
--- /dev/null
@@ -0,0 +1,12 @@
+---
+patat:
+    theme:
+        bulletListMarkers: '<>'
+...
+
+- Hello
+- World
+    * How
+    * Are
+    * You
+    * Doing
diff --git a/tests/meta.md.dump b/tests/meta.md.dump
new file mode 100644 (file)
index 0000000..740ed6b
--- /dev/null
@@ -0,0 +1,7 @@
+\e[35m  < \e[0m\e[mHello\e[0m
+\e[35m  < \e[0m\e[mWorld\e[0m
+\e[m    \e[0m\e[35m  > \e[0m\e[mHow\e[0m
+\e[m    \e[0m\e[35m  > \e[0m\e[mAre\e[0m
+\e[m    \e[0m\e[35m  > \e[0m\e[mYou\e[0m
+\e[m    \e[0m\e[35m  > \e[0m\e[mDoing\e[0m
+
diff --git a/tests/syntax.md b/tests/syntax.md
new file mode 100644 (file)
index 0000000..f6c803d
--- /dev/null
@@ -0,0 +1,14 @@
+---
+patat:
+  theme:
+    syntaxHighlighting:
+      decVal: [bold, onDullRed]
+...
+
+Some simple code:
+
+```c
+int main(int argc, char **argv) {
+    return 0;
+}
+```
diff --git a/tests/syntax.md.dump b/tests/syntax.md.dump
new file mode 100644 (file)
index 0000000..eb4893f
--- /dev/null
@@ -0,0 +1,7 @@
+\e[mSome simple code:\e[0m
+
+\e[m   \e[0m\e[40;37m                                   \e[0m
+\e[m   \e[0m\e[40;37m \e[0m\e[40;37;32mint\e[0m\e[40;37m main(\e[0m\e[40;37;32mint\e[0m\e[40;37m argc, \e[0m\e[40;37;32mchar\e[0m\e[40;37m **argv) { \e[0m
+\e[m   \e[0m\e[40;37m     \e[0m\e[40;37;33mreturn\e[0m\e[40;37m \e[0m\e[40;37;41;1m0\e[0m\e[40;37m;                     \e[0m
+\e[m   \e[0m\e[40;37m }                                 \e[0m
+\e[m   \e[0m\e[40;37m                                   \e[0m
diff --git a/tests/tables.md b/tests/tables.md
new file mode 100644 (file)
index 0000000..fe7d72e
--- /dev/null
@@ -0,0 +1,48 @@
+# Normal simple table
+
+  Right     Left     Center     Default
+-------     ------ ----------   -------
+     12     12        12            12
+    123     123       123          123
+      1     1          1             1
+
+Table:  Demonstration of simple table syntax.
+
+
+# Headerless table
+
+-------     ------ ----------   -------
+     12     12        12            12
+    123     123       123          123
+      1     1          1             1
+-------     ------ ----------   -------
+
+# Multiline
+
+-------------------------------------------------------------
+ Centered   Default           Right Left
+  Header    Aligned         Aligned Aligned
+----------- ------- --------------- -------------------------
+   First    row                12.0 Example of a row that
+                                    spans multiple lines.
+
+  Second    row                 5.0 Here's another one. Note
+                                    the blank line between
+                                    rows.
+-------------------------------------------------------------
+
+Table: Here's the caption. It, too, may span
+multiple lines.
+
+# Headerless multiline
+
+----------- ------- --------------- -------------------------
+   First    row                12.0 Example of a row that
+                                    spans multiple lines.
+
+  Second    row                 5.0 Here's another one. Note
+                                    the blank line between
+                                    rows.
+----------- ------- --------------- -------------------------
+
+: Here's a multiline table without headers.
diff --git a/tests/tables.md.dump b/tests/tables.md.dump
new file mode 100644 (file)
index 0000000..0b0a93f
--- /dev/null
@@ -0,0 +1,48 @@
+\e[34m# Normal simple table\e[0m
+
+\e[m  Right  Left  Center  Default\e[0m
+\e[m  \e[0m\e[35m-----\e[0m\e[m  \e[0m\e[35m----\e[0m\e[m  \e[0m\e[35m------\e[0m\e[m  \e[0m\e[35m-------\e[0m
+\e[m     12  12      12    12     \e[0m
+\e[m    123  123     123   123    \e[0m
+\e[m      1  1        1    1      \e[0m
+
+\e[m  Table: Demonstration of simple table syntax.\e[0m
+
+\e[m----------\e[0m
+\e[34m# Headerless table\e[0m
+
+\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m---\e[0m
+\e[m   12  12    12   12\e[0m
+\e[m  123  123  123  123\e[0m
+\e[m    1  1     1    1 \e[0m
+\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m---\e[0m
+
+\e[m----------\e[0m
+\e[34m# Multiline\e[0m
+
+\e[m  Centered  Default    Right  Left                    \e[0m
+\e[m   Header   Aligned  Aligned  Aligned                 \e[0m
+\e[m  \e[0m\e[35m--------\e[0m\e[m  \e[0m\e[35m-------\e[0m\e[m  \e[0m\e[35m-------\e[0m\e[m  \e[0m\e[35m------------------------\e[0m
+\e[m    First   row         12.0  Example of a row that   \e[0m
+\e[m                              spans multiple lines.   \e[0m
+\e[m  \e[0m
+\e[m   Second   row          5.0  Here's another one. Note\e[0m
+\e[m                              the blank line between  \e[0m
+\e[m                              rows.                   \e[0m
+
+\e[m  Table: Here's the caption. It, too, may span\e[0m
+\e[m  multiple lines.\e[0m
+
+\e[m----------\e[0m
+\e[34m# Headerless multiline\e[0m
+
+\e[m  \e[0m\e[35m------\e[0m\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m----\e[0m\e[m  \e[0m\e[35m------------------------\e[0m
+\e[m   First  row  12.0  Example of a row that   \e[0m
+\e[m                     spans multiple lines.   \e[0m
+\e[m  \e[0m
+\e[m  Second  row   5.0  Here's another one. Note\e[0m
+\e[m                     the blank line between  \e[0m
+\e[m                     rows.                   \e[0m
+\e[m  \e[0m\e[35m------\e[0m\e[m  \e[0m\e[35m---\e[0m\e[m  \e[0m\e[35m----\e[0m\e[m  \e[0m\e[35m------------------------\e[0m
+
+\e[m  Table: Here's a multiline table without headers.\e[0m
diff --git a/tests/themes.md b/tests/themes.md
new file mode 100644 (file)
index 0000000..6591ece
--- /dev/null
@@ -0,0 +1,11 @@
+---
+patat:
+  theme:
+    bulletListMarkers: '-+'
+    emph: [onVividRed, underline]
+...
+
+- This is a simple list.
+    * With _nested_ items.
+    * One or two.
+- The list theming is customized a bit.
diff --git a/tests/themes.md.dump b/tests/themes.md.dump
new file mode 100644 (file)
index 0000000..988214f
--- /dev/null
@@ -0,0 +1,5 @@
+\e[35m  - \e[0m\e[mThis is a simple list.\e[0m
+\e[m    \e[0m\e[35m  + \e[0m\e[mWith \e[0m\e[4;101mnested\e[0m\e[m items.\e[0m
+\e[m    \e[0m\e[35m  + \e[0m\e[mOne or two.\e[0m
+
+\e[35m  - \e[0m\e[mThe list theming is customized a bit.\e[0m
diff --git a/tests/wrapping.md b/tests/wrapping.md
new file mode 100644 (file)
index 0000000..15bc088
--- /dev/null
@@ -0,0 +1,23 @@
+---
+patat:
+    wrap: true
+    columns: 40
+...
+
+This is a long
+sentence over multiple
+lines which can be
+re-wrapped.
+
+
+This is a super long sentence over a single line which should also be re-wrapped.
+
+
+  This is  a table  and tables  should not  be wrapped
+  -------  -------  ----------  ----------  ----------
+  1        2        3           4           5
+  6        7        8           9           10
+
+- This is a list
+- This list has a really long sentence in it which should also be wrapped with proper indentation
+- Another item
diff --git a/tests/wrapping.md.dump b/tests/wrapping.md.dump
new file mode 100644 (file)
index 0000000..e23f9e3
--- /dev/null
@@ -0,0 +1,17 @@
+\e[mThis is a long sentence over multiple\e[0m
+\e[mlines which can be re-wrapped.\e[0m
+
+\e[mThis is a super long sentence over a\e[0m
+\e[msingle line which should also be\e[0m
+\e[mre-wrapped.\e[0m
+
+\e[m  This is  a table  and tables  should not  be wrapped\e[0m
+\e[m  \e[0m\e[35m-------\e[0m\e[m  \e[0m\e[35m-------\e[0m\e[m  \e[0m\e[35m----------\e[0m\e[m  \e[0m\e[35m----------\e[0m\e[m  \e[0m\e[35m----------\e[0m
+\e[m  1        2        3           4           5         \e[0m
+\e[m  6        7        8           9           10        \e[0m
+
+\e[35m  - \e[0m\e[mThis is a list\e[0m
+\e[35m  - \e[0m\e[mThis list has a really long sentence\e[0m
+\e[m    in it which should also be wrapped\e[0m
+\e[m    with proper indentation\e[0m
+\e[35m  - \e[0m\e[mAnother item\e[0m