summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile58
-rw-r--r--include/document.tmpl69
-rw-r--r--include/main.css105
-rw-r--r--notes/markdown-vs-multimarkdown.txt23
-rw-r--r--scripts/MarkdownBook/Book.pm264
-rw-r--r--scripts/MarkdownBook/Document.pm245
-rw-r--r--scripts/MarkdownBook/Document/html.pm153
-rw-r--r--scripts/MarkdownBook/Document/txt.pm46
-rw-r--r--scripts/MarkdownBook/HTMLTree.pm44
-rw-r--r--scripts/MarkdownBook/Section.pm67
-rwxr-xr-xscripts/markdownbook.pl32
11 files changed, 1089 insertions, 17 deletions
diff --git a/Makefile b/Makefile
index 0707f0b..61e497c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,23 +1,47 @@
-.SUFFIXES:
-.SUFFIXES: .txt .html
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-SRCS =
-OBJS = $(SRCS:.txt=.html)
+BOOKS = inclusion
+FORMATS = html txt
-.PHONY: all
-all: $(OBJS)
+# Default format in case a book target is called without FORMAT=*
+FORMAT = html
-$(OBJS):
- @printf ' RENDER %s\n' '$@'
- @title=$$(sed -n 's/^ Title: \(.*\)$$/\1/p' $*.txt | \
- head -n 1); \
- sed "s#@TITLE@#$$title#" include/header.html > $@
- @markdown $*.txt >> $@
- @cat include/footer.html >> $@
+all: $(FORMATS)
+
+books: $(BOOKS)
+
+$(FORMATS):
+ @make 'FORMAT=$@' books
+
+$(BOOKS):
+ @printf ' RENDER %s as %s\n' '$@' '$(FORMAT)'
+ @PERL5LIB=scripts scripts/markdownbook.pl '$(FORMAT)' 'policies/$@'
+ @[ '$(FORMAT)' = 'html' ] && \
+ ln -sf '../../include/main.css' 'policies/$@/main.css' || true
-.PHONY: clean
clean:
- @for obj in $(OBJS); do \
- printf ' RM %s\n' "$${obj}"; \
- rm -f $${obj}; \
+ @for book in $(BOOKS); do \
+ printf ' CLEAN %s\n' "$${book}"; \
+ rm -f "policies/$${book}/"*.html; \
+ rm -f "policies/$${book}/"*.txt; \
+ rm -f "policies/$${book}/"main.css; \
done
diff --git a/include/document.tmpl b/include/document.tmpl
new file mode 100644
index 0000000..1f914cd
--- /dev/null
+++ b/include/document.tmpl
@@ -0,0 +1,69 @@
+<!doctype html>
+
+<html>
+ <head>
+ <meta charset="utf-8">
+<TMPL_IF NAME="IS_INDEX">
+ <title><TMPL_VAR NAME="BOOK_TITLE"></title>
+<TMPL_ELSE>
+ <title><TMPL_VAR NAME="BOOK_TITLE"> - <TMPL_VAR NAME="TITLE"></title>
+</TMPL_IF>
+ <link type="text/css" rel="stylesheet" href="main.css">
+ <link rel="start" href="index.html">
+<TMPL_IF NAME="PREV_LINK">
+ <link rel="prev" href="<TMPL_VAR NAME="PREV_LINK">">
+</TMPL_IF>
+<TMPL_IF NAME="NEXT_LINK">
+ <link rel="next" href="<TMPL_VAR NAME="NEXT_LINK">">
+</TMPL_IF>
+ </head>
+ <body>
+ <header>
+ <nav>
+ <ul>
+<TMPL_IF NAME="PREV_LINK">
+ <li class="prev">
+ <a href="<TMPL_VAR NAME="PREV_LINK">">Prev</a>
+ </li>
+</TMPL_IF>
+<TMPL_IF NAME="NEXT_LINK">
+ <li class="next">
+ <a href="<TMPL_VAR NAME="NEXT_LINK">">Next</a>
+ </li>
+</TMPL_IF>
+ <li class="start">
+ <a href="index.html">Home</a>
+ </li>
+ </ul>
+ </nav>
+ <h1><TMPL_VAR NAME="BOOK_TITLE"></h1>
+<TMPL_UNLESS NAME="IS_INDEX">
+ <h2><TMPL_VAR NAME="CHAPT_TITLE"></h2>
+</TMPL_UNLESS>
+ </header>
+ <section>
+<TMPL_VAR NAME="BODY">
+ </section>
+ <footer>
+ <nav>
+ <ul>
+<TMPL_IF NAME="PREV_LINK">
+ <li class="prev">
+ <a href="<TMPL_VAR NAME="PREV_LINK">">Prev</a>
+ <TMPL_VAR NAME="PREV_TITLE">
+ </li>
+</TMPL_IF>
+<TMPL_IF NAME="NEXT_LINK">
+ <li class="next">
+ <a href="<TMPL_VAR NAME="NEXT_LINK">">Next</a>
+ <TMPL_VAR NAME="NEXT_TITLE">
+ </li>
+</TMPL_IF>
+ <li class="start">
+ <a href="index.html">Home</a>
+ </li>
+ </ul>
+ </nav>
+ </footer>
+ </body>
+</html>
diff --git a/include/main.css b/include/main.css
new file mode 100644
index 0000000..6df6c2b
--- /dev/null
+++ b/include/main.css
@@ -0,0 +1,105 @@
+* {
+ margin: 0px;
+ padding: 0px;
+ font-family: serif;
+ color: #000000;
+}
+body {
+ margin: 0px;
+ padding: 0px 12px;
+ background-color: #E0E0E0;
+}
+h1, h2 {
+ font-weight: bold;
+}
+a {
+ color: #0000FF;
+ text-decoration: underline;
+}
+
+nav {
+ margin: 0px;
+ padding: 24px 12px;
+ height: 1%;
+ overflow: auto;
+}
+header nav {
+ border-bottom: 1px solid #666666;
+ margin-bottom: 24px
+}
+footer nav {
+ border-top: 1px solid #666666;
+ margin-top: 24px
+}
+nav ul {
+ display: inline;
+ list-style-type: none;
+}
+nav ul li.prev {
+ float: left;
+ width: 40%;
+ text-align: left;
+}
+nav ul li.next {
+ float: right;
+ width: 40%;
+ text-align: right;
+}
+nav ul li a {
+ display: block;
+}
+footer nav ul li a {
+ margin-bottom: 8px;
+}
+nav ul li.start {
+ text-align: center;
+ width: 20%;
+ margin: 0px auto;
+}
+header h1 {
+ font-size: 20pt;
+ font-weight: bold;
+ margin: 6px 12px;
+}
+header h2 {
+ font-size: 18pt;
+ font-weight: bold;
+ margin: 6px 12px;
+}
+
+section {
+ margin: 0px;
+ margin: 12px 12px 24px 12px;
+}
+
+section h1 {
+ font-size: 16pt;
+ font-weight: bold;
+ margin: 0px 0px 12px 0px;
+}
+section h2 {
+ font-size: 14pt;
+ font-weight: bold;
+ margin: 0px 0px 12px 0px;
+}
+section p {
+ font-size: 12pt;
+ margin: 0px 0px 12px 0px;
+}
+section li {
+ font-size: 12pt;
+}
+section code {
+ background-color: #EEEECC;
+ font-family: monospace;
+}
+section pre {
+ background-color: #EEEECC;
+ margin: 0px 0px 12px 16px;
+}
+section ul, section ol {
+ margin: 0px 0px 12px 24px;
+}
+section ul ul, section ol ol {
+ margin: 0px 0px 0px 24px;
+}
diff --git a/notes/markdown-vs-multimarkdown.txt b/notes/markdown-vs-multimarkdown.txt
new file mode 100644
index 0000000..1d71d57
--- /dev/null
+++ b/notes/markdown-vs-multimarkdown.txt
@@ -0,0 +1,23 @@
+Markdown: <http://daringfireball.net/projects/markdown/>
+MultiMarkdown: <http://fletcherpenney.net/multimarkdown/>
+
+MultiMarkdown seems to provide much of what we currently need and may want in
+the future: cross references, footnotes, tables, and PDF rendering (though this
+can be done with Markdown as well using Pandoc). Why don't we just use that to
+do all our work for us?
+
+The cross references (hyperlinks to sections) only work within a single
+document. We want policy manuals to have a chapter/appendix per document (e.g.
+HTML file). MultiMarkdown doesn't really support hyperlinks to sections in
+other documents. So while MMD might be nice for its other features, it looks
+like we should still handle in-book hyperlinks with our own pre-processing.
+
+Then why don't we just use normal hyperlinks to sections in different chapters?
+This assumes we're outputting documents only in HTML. We would have source text
+like this:
+
+ See [Library Packages](binpkgs.html#librarypackages) for more information.
+
+Rendering a plain text file that refers to other plain text files with a ".html"
+extension makes no sense. Additionally, this fails to work in the case of PDF
+rendering.
diff --git a/scripts/MarkdownBook/Book.pm b/scripts/MarkdownBook/Book.pm
new file mode 100644
index 0000000..4f30afb
--- /dev/null
+++ b/scripts/MarkdownBook/Book.pm
@@ -0,0 +1,264 @@
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+use Carp;
+
+package MarkdownBook::Book;
+
+sub new
+{
+ my ($class, $format, $dir) = @_;
+ my $self;
+ my $control_fh;
+
+ $class = ref($class) || $class;
+ $self = {};
+ bless($self, $class);
+
+ unless ($format =~ m/^[a-z]+/) {
+ Carp::croak('Invalid format "' . $format . '"');
+ }
+ eval {
+ require 'MarkdownBook/Document/' . $format . '.pm';
+ 1;
+ } or Carp::croak('Unsupported format "' . $format . '"');
+ $self->{'format'} = $format;
+
+ $self->{'dir'} = $dir;
+ $self->{'docs'} = [];
+ $self->{'sections'} = [];
+ $self->{'sections_by_id'} = {};
+
+ open($control_fh, '<', $dir . '/control');
+ while (<$control_fh>) {
+ chomp($_);
+ if (m/^Title: /) {
+ s/^Title: (.*)$/$1/;
+ $self->{'title'} = $_;
+ }
+ }
+ close($control_fh);
+
+ $self->create_documents();
+
+ return $self;
+}
+
+sub dir
+{
+ my ($self, $dir) = @_;
+ my $old = $self->{'dir'};
+
+ $self->{'dir'} = $dir if defined($dir);
+
+ return $old;
+}
+
+sub title
+{
+ my ($self, $title) = @_;
+ my $old = $self->{'title'};
+
+ $self->{'title'} = $title if defined($title);
+
+ return $old;
+}
+
+sub documents
+{
+ my ($self, $documents) = @_;
+ my $old = $self->{'docs'};
+
+ $self->{'docs'} = $documents if defined($documents);
+
+ return $old;
+}
+
+sub _get_document_module
+{
+ my ($self) = @_;
+
+ return 'MarkdownBook::Document::' . $self->{'format'};
+}
+
+sub create_documents
+{
+ my ($self) = @_;
+ my $series_fh;
+ my $file;
+ my $title;
+ my $i;
+ my $doc;
+ my $doc_prev;
+ my @letters;
+
+ # Create index document.
+ $doc = $self->_get_document_module()->new($self, 'index', 'index',
+ undef, $self->{'title'});
+ $doc_prev = $doc;
+ push(@{$self->{'docs'}}, $doc);
+
+ # Create chapter documents.
+ $i = 0;
+ open($series_fh, '<', $self->{'dir'} . '/chapters')
+ or Carp::croak('Cannot open chapters file');
+ while (<$series_fh>) {
+ chomp($_);
+ ($file, $title) = split(/[ \t]+/, $_, 2);
+ $doc = $self->_get_document_module()->new($self, 'chapter', $file,
+ ++$i, $title);
+ $doc->prev($doc_prev);
+ $doc_prev->next($doc) if defined $doc_prev;
+ $doc_prev = $doc;
+ push(@{$self->{'docs'}}, $doc);
+ }
+ close($series_fh);
+
+ # Create appendix documents.
+ $i = -1;
+ @letters = ('A' .. 'Z');
+ if (-e $self->{'dir'} . '/appendices') {
+ open($series_fh, '<', $self->{'dir'} . '/apendices')
+ or Carp::croak('Cannot open appendices file');
+ while (<$series_fh>) {
+ chomp($_);
+ ($file, $title) = split(/[ \t]+/, $_, 2);
+ $doc = $self->_get_document_module()->new($self, 'appendix', $file,
+ $letters[++$i], $title);
+ $doc->prev($doc_prev);
+ $doc_prev->next($doc) if defined $doc_prev;
+ $doc_prev = $doc;
+ push(@{$self->{'docs'}}, $doc);
+ }
+ close($series_fh);
+ }
+}
+
+sub add_section
+{
+ my ($self, $section) = @_;
+
+ push(@{$self->{'sections'}}, $section);
+
+ # Index sections (not documents) by ID.
+ if (ref($section) eq 'MarkdownBook::Section') {
+ $self->{'sections_by_id'}->{$section->id()} = $section;
+ }
+}
+
+sub subst_macros
+{
+ my ($self, $text) = @_;
+
+ # Substitute macros with arguments.
+ $text =~ s/
+ \$ # Dollar sign
+ \[ # Left square bracket
+ ([^\]]+) # Macro
+ \] # Right square bracket
+ (
+ \[ # Left square bracket
+ ([^\]]+) # Macro arguments
+ \] # Right square bracket
+ )
+ /$self->_do_subst_macro($1, split(m@[ \t]+@, $3))/exg;
+
+ # Substitute macros without arguments.
+ $text =~ s/
+ \$ # Dollar sign
+ \[ # Left square bracket
+ ([^\]]+) # Macro
+ \] # Right square bracket
+ /$self->_do_subst_macro($1)/exg;
+
+ return $text;
+}
+
+sub _do_subst_macro
+{
+ my ($self, $macro, @args) = @_;
+ my $sec;
+
+ if ($macro eq 'toc') {
+ return $self->_do_gen_toc();
+ } elsif ($macro eq 'sectlink') {
+ if (@args != 1) {
+ Carp::carp('Invalid arguments to "sectlink" macro');
+ } else {
+ $sec = $self->{'sections_by_id'}->{$args[0]};
+ return '[ยง ' . $sec->number() . '][' . $sec->id() . ']';
+ }
+ } else {
+ Carp::carp("Unrecognized macro \"$macro\"");
+ }
+}
+
+sub _do_gen_toc
+{
+ my ($self) = @_;
+ my $section;
+ my $toc = '';
+
+ foreach $section (@{$self->{'sections'}}) {
+ $toc .= "\n" if $toc ne '';
+ if (ref($section) =~ m/^MarkdownBook::Document/) {
+ next if $section->type() eq 'index';
+ $toc .= ' * [';
+ $toc .= $section->id();
+ $toc .= ' ';
+ $toc .= $section->title();
+ $toc .= '][';
+ $toc .= $section->file();
+ $toc .= ']';
+ } elsif (ref($section) eq 'MarkdownBook::Section') {
+ $toc .= ' ' x $section->level();
+ $toc .= ($section->level() == 1 ? ' - [' : ' * [');
+ $toc .= $section->number();
+ $toc .= ' ';
+ $toc .= $section->title();
+ $toc .= '][';
+ $toc .= $section->id();
+ $toc .= ']';
+ }
+ }
+
+ return $toc;
+}
+
+sub parse
+{
+ my ($self) = @_;
+ my $doc;
+
+ foreach $doc (@{$self->{'docs'}}) {
+ $self->add_section($doc);
+ $doc->parse();
+ }
+}
+
+sub output
+{
+ my ($self) = @_;
+ my $doc;
+
+ foreach $doc (@{$self->{'docs'}}) {
+ $doc->output();
+ }
+}
+
+1;
diff --git a/scripts/MarkdownBook/Document.pm b/scripts/MarkdownBook/Document.pm
new file mode 100644
index 0000000..2a30396
--- /dev/null
+++ b/scripts/MarkdownBook/Document.pm
@@ -0,0 +1,245 @@
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+use MarkdownBook::Section;
+use Carp;
+
+package MarkdownBook::Document;
+
+sub new
+{
+ my ($class, $book, $type, $file, $id, $title) = @_;
+ my $self;
+
+ $class = ref($class) || $class;
+ $self = {};
+ bless($self, $class);
+
+ $self->{'book'} = $book;
+ $self->{'type'} = $type;
+ $self->{'file'} = $file;
+ $self->{'id'} = $id;
+ $self->{'title'} = $title;
+
+ return $self;
+}
+
+sub book
+{
+ my ($self, $book) = @_;
+ my $old = $self->{'book'};
+
+ $self->{'book'} = $book if defined($book);
+
+ return $old;
+}
+
+sub type
+{
+ my ($self, $type) = @_;
+ my $old = $self->{'type'};
+
+ $self->{'type'} = $type if defined($type);
+
+ return $old;
+}
+
+sub file
+{
+ my ($self, $file) = @_;
+ my $old = $self->{'file'};
+
+ $self->{'file'} = $file if defined($file);
+
+ return $old;
+}
+
+sub file_path
+{
+ my ($self) = @_;
+
+ return $self->{'book'}->dir() . '/' . $self->{'file'};
+}
+
+sub id
+{
+ my ($self) = @_;
+
+ return $self->{'id'};
+}
+
+sub title
+{
+ my ($self, $title) = @_;
+ my $old = $self->{'title'};
+
+ $self->{'title'} = $title if defined($title);
+
+ return $old;
+}
+
+sub full_title
+{
+ my ($self) = @_;
+
+ if ($self->{'type'} eq 'chapter') {
+ return sprintf('Chapter %d - %s', $self->{'id'}, $self->{'title'});
+ } elsif ($self->{'type'} eq 'appendix') {
+ return sprintf('Appendix %s - %s', $self->{'id'}, $self->{'title'});
+ } else {
+ return undef;
+ }
+}
+
+sub prev
+{
+ my ($self, $other) = @_;
+ my $old = $self->{'prev'};
+
+ $self->{'prev'} = $other if defined($other);
+
+ return $old;
+}
+
+sub next
+{
+ my ($self, $other) = @_;
+ my $old = $self->{'next'};
+
+ $self->{'next'} = $other if defined($other);
+
+ return $old;
+}
+
+sub sections
+{
+ my ($self) = @_;
+
+ # FIXME: Why is this necessary?!
+ foreach my $sec (@{$self->{'sections'}}) {
+ }
+
+ return $self->{'sections'};
+}
+
+sub parse
+{
+ my ($self) = @_;
+ my $source_fh;
+ my $source_text;
+
+ open($source_fh, '<',
+ $self->{'book'}->dir() . '/' . $self->{'file'} . '.mdwn')
+ or Carp::croak('Cannot open "' . $self->{'file'} . '" source document');
+ $source_text = join('', <$source_fh>);
+ close($source_fh);
+
+ # Parse headings of non-index documents.
+ if ($self->{'type'} ne 'index') {
+ $self->{'section_level_numbers'} = [0, 0];
+ $self->{'section_level'} = -1;
+ $source_text =~ s/
+ ^
+ (.+) # Heading text
+ [ \t]* # Optional trailing whitespace
+ \n # Line break
+ (=+|-+) # Underline
+ [ \t]* # Optional trailing whitespace
+ $
+ /$self->_do_heading($1, $2)/mexg;
+ }
+
+ # Store parsed text.
+ $self->{'source_text'} = $source_text;
+}
+
+sub _do_heading
+{
+ my ($self, $text, $underline) = @_;
+ my $level;
+ my $levels;
+ my $section_number;
+ my $section_title;
+ my $section_id;
+ my $section;
+
+ # Shorten underline to one character.
+ $underline =~ s/^([=-]).*$/$1/;
+
+ # Detect heading level.
+ if ($underline eq '=') {
+ $level = 1;
+ } else {
+ $level = 2;
+ }
+
+ # Calculate section number.
+ $levels = $#{$self->{'section_level_numbers'}};
+ if ($level != $self->{'section_level'}) {
+ foreach (@{$self->{'section_level_numbers'}}[$level .. $levels]) {
+ $_ = 0;
+ }
+ }
+ $self->{'section_level'} = $level;
+ ++${$self->{'section_level_numbers'}}[$level - 1];
+ $section_number = join('.', @{$self->{'section_level_numbers'}});
+
+ # Add document ID to section number.
+ $section_number = $self->{'id'} . '.' . $section_number;
+
+ # Trim off unused subsection parts.
+ $section_number =~ s/(?:\.0)*$//;
+
+ # Parse out section title.
+ $section_title = $text;
+ $section_title =~ s/
+ ^
+ ([^\[]+) # Section title
+ [ \t]+ # Whitespace
+ \[ # Left square bracket
+ [^\]]+ # Section ID
+ \] # Right square bracket
+ $
+ /$1/x;
+
+ # Parse out section ID.
+ $section_id = $text;
+ $section_id =~ s/
+ ^
+ [^\[\]]+ # Section title
+ [ \t]+ # Whitespace
+ \[ # Left square bracket
+ ([^\]]+) # Section ID
+ \] # Right square bracket
+ $
+ /$1/x;
+
+ # Create and store section object.
+ $section = MarkdownBook::Section->new($self,
+ $level, $section_number, $section_id, $section_title);
+ push(@{$self->{'sections'}}, $section);
+ $self->{'book'}->add_section($section);
+
+ # Prepend number to section title.
+ $text = $section_number . ' ' . $section_title;
+
+ # Return underlined section title.
+ return $text . "\n" . $underline x length($text);
+}
+
+1;
diff --git a/scripts/MarkdownBook/Document/html.pm b/scripts/MarkdownBook/Document/html.pm
new file mode 100644
index 0000000..60f22c7
--- /dev/null
+++ b/scripts/MarkdownBook/Document/html.pm
@@ -0,0 +1,153 @@
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+use MarkdownBook::Document;
+use MarkdownBook::HTMLTree;
+use Carp;
+use Text::Markdown;
+use HTML::TreeBuilder;
+use HTML::Template;
+
+package MarkdownBook::Document::html;
+
+our @ISA = qw(MarkdownBook::Document);
+
+sub output
+{
+ my ($self) = @_;
+ my $text;
+ my $doc;
+ my $sec;
+
+ # Substitute macros.
+ $text = $self->{'book'}->subst_macros(
+ $self->{'source_text'});
+
+ # Append link definitions.
+ $text .= "\n";
+ foreach $doc (@{$self->{'book'}->documents()}) {
+ $text .= "\n";
+ $text .= '[';
+ $text .= $doc->file();
+ $text .= ']: ';
+ $text .= $doc->file();
+ $text .= '.html';
+ foreach $sec (@{$doc->sections()}) {
+ $text .= "\n";
+ $text .= '[';
+ $text .= $sec->id();
+ $text .= ']: ';
+ $text .= $doc->file();
+ $text .= '.html#';
+ $text .= $sec->id();
+ }
+ }
+
+ # Convert to HTML.
+ $text = Text::Markdown::Markdown($text);
+
+ # Set "id" attributes of headings.
+ $self->_do_set_heading_id_attrs($text);
+
+ # Output the templated HTML.
+ $self->_do_output_template();
+
+ # Clean up.
+ $self->{'tree'}->delete();
+}
+
+sub _do_set_heading_id_attrs
+{
+ my ($self, $text) = @_;
+ my @headings;
+ my $heading;
+ my $i = -1;
+
+ # Parse HTML.
+ $self->{'tree'} = HTML::TreeBuilder->new();
+ $self->{'tree'}->parse($text);
+ $self->{'tree'}->eof($text);
+
+ # Find the "body" element.
+ @{$self->{'tree_body'}} = MarkdownBook::HTMLTree::find_elements_by_tag_names(
+ $self->{'tree'}, ('body'));
+
+ # Don't modify headings of index documents.
+ return if $self->{'type'} eq 'index';
+
+ # Find all headings.
+ @headings = MarkdownBook::HTMLTree::find_elements_by_tag_names(
+ @{$self->{'tree_body'}}[0], ('h1', 'h2'));
+
+ # Set "id" attributes.
+ foreach $heading (@headings) {
+ $heading->attr('id', ${$self->{'sections'}}[++$i]->id());
+ }
+}
+
+sub _do_output_template
+{
+ my ($self) = @_;
+
+ my $doc_tmpl;
+ my %opt_end_tags;
+ my $elem;
+ my $body;
+ my $doc_fh;
+
+ $doc_tmpl = HTML::Template->new(filename => 'include/document.tmpl');
+
+ # Don't omit any end tags.
+ %opt_end_tags = map([$_ => 0], %HTML::Element::optionalEndTag);
+
+ # Get HTML text of all children of the "body" element.
+ foreach $elem (@{$self->{'tree_body'}}[0]->content_list()) {
+ # It's safe to assume (ref($elem) eq 'HTML::Element').
+ $body .= $elem->as_HTML('<>&', '', \%opt_end_tags) . "\n";
+ }
+
+ $doc_tmpl->param(IS_INDEX => ($self->{'type'} eq 'index'));
+
+ $doc_tmpl->param(BOOK_TITLE => $self->{'book'}->title());
+ $doc_tmpl->param(TITLE => $self->{'title'});
+ $doc_tmpl->param(CHAPT_TITLE => $self->full_title());
+
+ if (defined($self->{'prev'})) {
+ $doc_tmpl->param(PREV_LINK => $self->{'prev'}->file() . '.html');
+ $doc_tmpl->param(PREV_TITLE => $self->{'prev'}->title());
+ } else {
+ $doc_tmpl->param(PREV_LINK => undef);
+ $doc_tmpl->param(PREV_TITLE => undef);
+ }
+ if (defined($self->{'next'})) {
+ $doc_tmpl->param(NEXT_LINK => $self->{'next'}->file() . '.html');
+ $doc_tmpl->param(NEXT_TITLE => $self->{'next'}->title());
+ } else {
+ $doc_tmpl->param(NEXT_LINK => undef);
+ $doc_tmpl->param(NEXT_TITLE => undef);
+ }
+
+ $doc_tmpl->param(BODY => $body);
+
+ open($doc_fh, '>', $self->file_path() . '.html')
+ or Carp::croak('Cannot open "' . $self->{'file'} . '" destination document');
+ $doc_tmpl->output(print_to => $doc_fh);
+ close($doc_fh);
+}
+
+1;
diff --git a/scripts/MarkdownBook/Document/txt.pm b/scripts/MarkdownBook/Document/txt.pm
new file mode 100644
index 0000000..f150477
--- /dev/null
+++ b/scripts/MarkdownBook/Document/txt.pm
@@ -0,0 +1,46 @@
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+use MarkdownBook::Document;
+use Carp;
+
+package MarkdownBook::Document::txt;
+
+our @ISA = qw(MarkdownBook::Document);
+
+sub output
+{
+ my ($self) = @_;
+ my $out_fh;
+
+ open($out_fh, '>',
+ $self->{'book'}->dir() . '/' . $self->{'file'} . '.txt')
+ or Carp::croak('Cannot open "' . $self->{'file'} .
+ '" destination document');
+
+ # Print document title.
+ print($out_fh $self->{'title'} . "\n" .
+ '*' x length($self->{'title'}) . "\n\n\n");
+
+ # Print document text with macro substitutions.
+ print($out_fh $self->{'book'}->subst_macros($self->{'source_text'}));
+
+ close($out_fh);
+}
+
+1;
diff --git a/scripts/MarkdownBook/HTMLTree.pm b/scripts/MarkdownBook/HTMLTree.pm
new file mode 100644
index 0000000..e01bab0
--- /dev/null
+++ b/scripts/MarkdownBook/HTMLTree.pm
@@ -0,0 +1,44 @@
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+use HTML::Element;
+
+package MarkdownBook::HTMLTree;
+
+sub find_elements_by_tag_names
+{
+ my ($elem, @tagnames) = @_;
+
+ my @list = $elem->content_list();
+ my $tag;
+ my @retlist = ();
+
+ foreach (@list) {
+ if (ref($_) ne 'HTML::Element') {
+ next;
+ }
+ $tag = $_->tag();
+ if (grep($_ eq $tag, @tagnames)) {
+ push(@retlist, $_);
+ }
+ }
+
+ return @retlist;
+}
+
+1;
diff --git a/scripts/MarkdownBook/Section.pm b/scripts/MarkdownBook/Section.pm
new file mode 100644
index 0000000..82ff897
--- /dev/null
+++ b/scripts/MarkdownBook/Section.pm
@@ -0,0 +1,67 @@
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+package MarkdownBook::Section;
+
+sub new
+{
+ my ($class, $doc, $lev, $num, $id, $title) = @_;
+ my $self;
+
+ $class = ref($class) || $class;
+ $self = {};
+ bless($self, $class);
+
+ $self->{'document'} = $doc;
+ $self->{'level'} = $lev;
+ $self->{'number'} = $num;
+ $self->{'id'} = $id;
+ $self->{'title'} = $title;
+
+ return $self;
+}
+
+sub level
+{
+ my ($self) = @_;
+
+ return $self->{'level'};
+}
+
+sub number
+{
+ my ($self) = @_;
+
+ return $self->{'number'};
+}
+
+sub id
+{
+ my ($self) = @_;
+
+ return $self->{'id'};
+}
+
+sub title
+{
+ my ($self) = @_;
+
+ return $self->{'title'};
+}
+
+1;
diff --git a/scripts/markdownbook.pl b/scripts/markdownbook.pl
new file mode 100755
index 0000000..cceecdb
--- /dev/null
+++ b/scripts/markdownbook.pl
@@ -0,0 +1,32 @@
+#! /usr/bin/perl
+#
+# Copyright (C) 2012 Patrick "P. J." McDermott
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+use strict;
+use warnings;
+
+use MarkdownBook::Book;
+
+my $format;
+my $doc_dir;
+my $book;
+
+($format, $doc_dir) = @ARGV;
+
+$book = MarkdownBook::Book->new($format, $doc_dir);
+
+$book->parse();
+$book->output();