Skip to content

Instantly share code, notes, and snippets.

@JnyJny
Last active March 18, 2020 13:42
Show Gist options
  • Save JnyJny/cef9ee825b261fa8d0eb1a83e369a746 to your computer and use it in GitHub Desktop.
Save JnyJny/cef9ee825b261fa8d0eb1a83e369a746 to your computer and use it in GitHub Desktop.
Generate HTML, PDF, EPUB and MOBI from ASCIIDoctor Source
# Makefile - Generate HTML, PDF, EPUB, MOBI from ASC
# This is how you assign a string to a variable name in 'make'. The
# variable name doesn't have to be all caps and the equal doesn't have
# to be snugged up to the variable name; it's just how I write
# Makefiles
FILENAME= the_document
# $(IDENTIFIER) is how you reference a 'make' variable, you can use
# ${} too, but I prefer $(). If you forget the parantheses or the
# curly braces, eg $INDENTIFER, 'make' will interpret that as $I with
# NDENTFIER appended to it. Probably not what you are expecting.
SOURCE= $(FILENAME).asc
HTML= $(FILENAME).html
PDF= $(FILENAME).pdf
EPUB= $(FILENAME).epub
MOBI= $(FILENAME).mobi
# $(shell shell command ) is how you invoke a command and save the
# results to a variable. It gets kinda tricky since every time you
# reference $(DATE) it will execute the command. The weird assignment
# operator := means just assign it one time.
DATE := $(shell date +%Y-%m-%d)
# Again, calling shell. This time using 'awk' to pull out the revision
# number.
#
# The '$' needs to be doubled in the command string to keep 'make' from
# trying to expand '$2' into something we didn't intend.
# I know there was a grep|cut in the bash version, I prefer to use 'awk'
# for these kinds of 'snip' operations since it's a single process
# invocation. Those are easier to deal with in this context since you
# don't have to worry about any pipe weirdness imposed by 'make'.
REVISION := $(shell awk '/revnumber/ {print $$2}' $(SOURCE))
# Ok this is a "function" definition that we use to build the various
# ASCIIDOCTOR invocations. We could have just written the format
# specific definitions:
#
# ADOC_HTML= bundle exec asciidoctor
# ADOC_PDF= bundle exec asiidoctor-pdf
# ...
#
# The advantage of this technique is you only have to change
# the BUNDLE_EXEC part if the way you invoke asciidoctor
# changes (I don't know why it would change, but the idea
# is to isolate stuff that's repeated so you don't have to
# change it everywhere).
# Macro or 'function' definition
BUNDLE_EXEC= bundle exec $(1)
# Using the macro
ASCIIDOCTOR_HTML= $(call BUNDLE_EXEC,asciidoctor)
ASCIIDOCTOR_PDF= $(call BUNDLE_EXEC,asciidoctor-pdf)
ASCIIDOCTOR_EPUB= $(call BUNDLE_EXEC,asciidoctor-epub3)
ASCIIDOCTOR_MOBI= $(call BUNDLE_EXEC,asciidoctor-epub3)
# Here we build the shared flags used by asciidoctor by all
# invocations. I use the += assignment to show how you can add to a
# variable after it's initial assignment.
ADOC_FLAGS= --attribute revnumber=$(REVISION)
ADOC_FLAGS+= --attribute revdate=$(DATE)
# This next bit is some 'make' magic. The .PHONY rule is how we tell
# 'make' that some of our rules aren't associated directly with a
# file. The 'all' rule below is by default a dependency of .PHONY
#
# We define four rules html, pdf, epub and mobi later on.
.PHONY: html pdf epub mobi
# The default rule that 'make' looks for when invoked without arguments
# is 'all'. To build a rule (also sometimes called a target), 'make'
# builds the dependencies listed after the rulename:
#
# rulename: dep1 dep2 dep3 ... depN
# command_0
# @command_1 # echoing the command is suppressed
# -command_2 # a command whose exit code is ignored
# -@command_3 #
# ...
# command_n
#
# dep1: subdep1 ...
#
# Here, the dependencies for 'all' are $(HTML), $(PDF) and $(EPUB)
# which expand to the names of the files that asciidoctor will
# create. So 'make all' will run the $(HTML), $(PDF) and $(EPUB) rules
# in that order.
all: $(HTML) $(PDF) $(EPUB)
# The $(HTML) rule depends on $(SOURCE), and only executes if the
# source file has changed or the destination file does not exist. $@
# is an alias for the name of the rule to be used in the body of the
# recipe.
#
# By default, make will print the command that is being executed to
# stdout followed by it's output. To suppress printing the command,
# preface the command with an @.
#
# Lastly, the indention is a TAB and not 8 spaces. Make is an
# old-school tool and will complain if it doesn't get tabs.
$(HTML): $(SOURCE)
@echo Converting $(SOURCE) to $@
@$(ASCIIDOCTOR_HTML) $(ADOC_FLAGS) $(SOURCE)
# The html target is one of those .PHONY targets mentioned earlier.
# It creates a file, just indirectly. Things get funky when phony
# targets are dependencies of other rules. I like to group related
# targets together.
html: $(HTML)
# empty recipes are ok
# The $(PDF) target is very similar to $(HTML) execpt the command
# used to generate the file is $(ASCIIDOCTOR_PDF). It's followed
# by the bare pdf target with $(PDF) as it's dependency and an
# empty recipe body.
$(PDF): $(SOURCE)
@echo Converting $(SOURCE) to $@
@$(ASCIIDOCTOR_PDF) $(ADOC_FLAGS) $(SOURCE)
pdf: $(PDF)
# This is starting to be old hat, $(EPUB) mutates the command
# and the rest of the recipe looks the same as $(PDF) and $(HTML).
$(EPUB): $(SOURCE)
@echo Converting $(SOURCE) to $@
@$(ASCIIDOCTOR_EPUB) $(ADOC_FLAGS) $(SOURCE)
epub: $(EPUB)
# Now this is different. The Mobi format is generated by
# $(ASCIIDOCTOR_EPUB), selected by additional arguments to the
# command.
#
# This could be handled two ways, adding the new arguments to the
# definition of $(ASCIIDOCTOR_MOBI) like this:
#
#
# ASCIIDOCTOR_MOBI= $(call BUNDLE_EXEC,asciidoctor-epub3 -a ebook-format=kf8)
#
# This is pretty clean since it groups all the options and commands
# together in one place in the Makefile and to be honest, this is the
# better solution in retrospect.
#
# Another way to accomplish this is mutate $(ADOC_FLAGS) for just this
# target, $(MOBI). Defining a rule multiple times is additive for the
# rule, so the first $(MOBI) updates $(ADOC_FLAGS) the second $(MOBI)
# rule checks to see if the $(MOBI) file exists, checks the state of
# it's dependencies and executes the recipe accordingly with the
# updated flags.
#
# This solution is helpful when you need to mutate flags for specific
# targets and keeps the mutations close to the target definitions.
$(MOBI): ADOC_FLAGS += -a ebook-format=kf8
$(MOBI): $(SOURCE)
@echo Converting $(SOURCE) to $@
@$(ASCIIDOCTOR_MOBI) $(ADOC_FLAGS) $(SOURCE)
mobi: $(MOBI)
# The debug rule is how I checked to make sure all of the
# variables I constructed contained the things I thought they
# should.
debug:
@echo 'Rule -> $@'
@echo ' SOURCE: $(SOURCE)'
@echo ' REVISION: $(REVISION)'
@echo ' HTML: $(HTML)'
@echo ' PDF: $(PDF)'
@echo ' EPUB: $(EPUB)'
@echo ' MOBI: $(MOBI)'
@echo ' ADOC_FLAGS: $(ADOC_FLAGS)'
@echo 'ASCIIDOCTOR HTML: $(ASCIIDOCTOR_HTML)'
@echo ' ASCIIDOCTOR PDF: $(ASCIIDOCTOR_PDF)'
@echo 'ASCIIDOCTOR MOBI: $(ASCIIDOCTOR_MOBI)'
@echo 'ASCIIDOCTOR EPUB: $(ASCIIDOCTOR_EPUB)'
# Often times we want to restart from a known good "clean" state.
# A clean rule is a good place to remove transient files so you ensure
# that all your dependencies in your project are rebuilt. In this
# case we just remove the translated files. We could use wildcards
# in this rule like 'rm *.html' but this can have unintended consequences
# if we have other files in HTML format that we didn't want to smoke.
#
# Always be as explicit as possible in clean rules.
clean:
@/bin/rm -f $(HTML) $(PDF) $(EPUB) $(MOBI)
# Usage
#
# I'll outline some of the ways this makefile can be used:
#
# ## Generate $(HTML), $(PDF), $(PUB) Files
#
# $ make
#
# If all those files exist and the source file hasn't changed
# make will exit without doing anything. Win!
#
#
# ## Generate Just $(PDF)
#
# $ make the_document.pdf
#
#
# ## Specify a Different FILENAME with the 'all' Target
#
# $ make FILENAME=other_doc
#
# This will create files called other_doc.html, other_doc.pdf, and
# other_doc.epub from the source file other_doc.asc.
#
# ## Generate Files with Shortcut Rule Names
#
# $ make pdf
#
# The pdf rule is dependent on the rule $(PDF) so that rule
# is executed if necessary before the empty body of the pdf
# rule is executed.
#
# ## See What Commands 'make' Would Execute for a Target
#
# $ make -n
# $ make -n pdf
# $ make -n the_document.pdf
# $ make -n mobi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment